using Inet.Viewer.Data; using Inet.Viewer.WinForms; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Text; namespace Inet.Viewer.Data { /// /// Represents the content of one page and manages the loading and updating of pre-renderings. /// public class PageContent : IPageReceiver { private const int BorderOffset = 2; private const int LoadingSpinDuration = 20000; /// /// Sum of horizontal/vertical borders including any shadows, in pixels. /// public const int BorderTotal = 2 * BorderOffset + 2; private static readonly Brush BgBrush = new SolidBrush(Color.White); private static readonly Brush BgLoadingBrush = new SolidBrush(Color.FromArgb(220, 220, 220)); private static readonly Pen BorderPen = new Pen(Color.FromArgb(190, 190, 190), 1); private static readonly Pen ShadowPen1 = new Pen(Color.FromArgb(160, 160, 160), 2); private static readonly Pen ShadowPen2 = new Pen(Color.FromArgb(80, 80, 80), 2); private static readonly Brush HighlightBrush = new SolidBrush(Color.FromArgb(0x40, 0xff, 0xa0, 0x00)); private int pageNumber; private PageInfo pageInfo; private PageContentData data; private WeakReference cachedData = new WeakReference(null); private List selectedTexts = new List(); private SearchChunk[] highlightedSearchChunks; private Graphics2DPainter painter; private IPageReceiver masterReceiver; private ReportDataCache dataCache; private bool pageInfoReceived; private float imageZoom; /// /// Called after a page was rendered. /// public event EventHandler PageRendered; /// /// Creates a new instance. /// /// the page number /// the main page receiver /// the data cache to read from internal PageContent(int pageNumber, IPageReceiver masterReceiver, ReportDataCache dataCache) { this.pageNumber = pageNumber; this.masterReceiver = masterReceiver; this.dataCache = dataCache; } /// /// Clears any prerendering. /// public void ClearRendering() { data = null; painter = null; } /// /// Updates the pre-rendered image. If required, this method starts the actual loading/rendering in a background thread. /// /// flag indicating that a reload will be forced /// zoom factor, used to check if the zoom factor of the current prerendering is valid public void Update(bool forceReload, float zoomFactor) { if (forceReload) { data = null; painter = null; } else if (data == null) { // try to restore it from weak ref cache data = (PageContentData)cachedData.Target; UpdateSearchHighlighting(); } Graphics2DPainter p = painter; if (p == null && (data == null || Math.Abs(imageZoom - zoomFactor) > 0.001f) || p != null && pageInfoReceived && Math.Abs(painter.ZoomFactor - zoomFactor) > 0.001f) { painter = new Graphics2DPainter(false); pageInfoReceived = false; ThreadManager.RequestPageData(null, dataCache, pageNumber, forceReload, new PageLoader(painter, this), SetImage); } } /// /// Sets the image (after the background thread is finished). /// private void SetImage(Image img, IList links, IList texts, Graphics2DPainter painter) { if (painter == this.painter) { cachedData.Target = this.data = new PageContentData(img, links, texts); this.imageZoom = painter.ZoomFactor; this.painter = null; UpdateSearchHighlighting(); if (PageRendered != null) { PageRendered.Invoke(this, new EventArgs()); } } else { painter.ClearElements(); } } /// /// Gets the page number. /// public int PageNumber { get { return pageNumber; } } /// /// /// public bool WriteReportInfo(ReportInfo info, PageLoader loader) { return masterReceiver.WriteReportInfo(info, loader); } /// /// /// public bool WritePageInfo(PageInfo info, PageLoader loader) { if (loader.Painter != painter || info.PageNr != PageNumber) { return false; } this.pageInfoReceived = true; this.pageInfo = info; return masterReceiver.WritePageInfo(info, loader); } /// /// /// public Font GetEmbeddedFont(int fontID, int fontRevision) { return masterReceiver.GetEmbeddedFont(fontID, fontRevision); } /// /// /// public void PageLoadFailure(Exception exception) { masterReceiver.PageLoadFailure(exception); } /// /// Draws the page. If the pre-rendered image is not available a loading spinner will be painted instead. /// /// the graphics to paint into /// the width of the image to paint /// the height of the image to paint /// if true a loading animation will be shown when the page is not available /// returns true if a repaint is required because of a running animation public bool Paint(Graphics g, int width, int height, bool showLoadingAnim) { PageContentData data = this.data; if (data == null) { if (!showLoadingAnim) { return false; } g.FillRectangle(BgLoadingBrush, 0, 0, width, height); g.DrawRectangle(BorderPen, 0, 0, width, height); Matrix tx = g.Transform; float scale = Math.Min(1f, (float)width / 2 / Images.spinner.Width); g.TranslateTransform(width / 2, height / 2); g.ScaleTransform(scale, scale); PaintLoadingSpinner(g, new Point(0, 0)); g.Transform = tx; return true; } Image image = data.image; // draw image if (width != image.Width || height != image.Height) { g.DrawImage(image, BorderOffset, BorderOffset, width, height); } else { g.DrawImage(image, BorderOffset, BorderOffset); } // draw page border and shadow int x1 = width + BorderOffset; int y1 = height + BorderOffset; g.DrawLine(ShadowPen1, x1, 3, x1, y1); g.DrawLine(ShadowPen1, 3, y1, x1, y1); g.DrawLines(ShadowPen2, new Point[] { new Point(x1, 4), new Point(x1, y1), new Point(4, y1) }); g.DrawRectangle(BorderPen, 0, 0, width, height); // draw highlighted texts lock (selectedTexts) { Brush textBrush = new SolidBrush(Color.Black); g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; foreach (TextBlockRange subTextBlock in selectedTexts) { RectangleF bbox = subTextBlock.BBox; bbox.X -= 1; bbox.Y -= 1; bbox.Width += 4; bbox.Height += 4; g.FillRectangle(HighlightBrush, bbox); } } return false; } /// /// Draws a loading spinner. /// /// the graphics to draw to /// the location of the spinner internal static void PaintLoadingSpinner(Graphics g, Point point) { Matrix tx = g.Transform; { g.TranslateTransform(point.X, point.Y); g.RotateTransform((float)(System.DateTime.Now.Ticks % (360 * LoadingSpinDuration)) / LoadingSpinDuration); g.TranslateTransform(-Images.spinner.Width / 2, -Images.spinner.Height / 2); g.DrawImage(Images.spinner, 0, 0, Images.spinner.Width, Images.spinner.Height); } g.Transform = tx; } /// /// Clears any selected text and hightlighted search chunks. /// public void ClearSelection() { lock (selectedTexts) { selectedTexts.Clear(); highlightedSearchChunks = null; } } /// /// Update the text selection in respect to the search chunks. /// private void UpdateSearchHighlighting() { lock (selectedTexts) { selectedTexts.Clear(); var data = this.data; if (data == null || data.texts == null || highlightedSearchChunks == null) { return; } foreach (SearchChunk chunk in highlightedSearchChunks) { foreach (TextBlock text in data.texts) { if (chunk.Page == pageNumber && text.HasDocumentLocation(chunk.X, chunk.Y)) { selectedTexts.Add(new TextBlockRange(text, chunk.StartIndex, chunk.EndIndex)); } } } } } /// /// Selects the text in the specified rectangle. /// /// public void SelectArea(Rectangle rectangle) { lock (selectedTexts) { if (data == null) { return; } IList texts = data.texts; if (texts == null) { return; } rectangle.X -= 16; rectangle.Y -= 16; foreach (TextBlock textBlock in texts) { TextBlockRange range = textBlock.ComputeRangeForArea(rectangle); if (range != null) { selectedTexts.Add(range); } } } } /// /// Appends any selected text to the specified string builder. /// /// the string builder to append to internal void AppendSelectedText(StringBuilder sb) { lock (selectedTexts) { if (selectedTexts.Count == 0) { return; } selectedTexts.Sort(CompareTextBlockY); float prevY = 0; bool first = true; foreach (TextBlockRange range in selectedTexts) { if (first) { first = false; } else { sb.Append(range.BBox.Y > prevY ? Environment.NewLine : " "); } sb.Append(range.SubString); prevY = range.BBox.Y + range.BBox.Height/2; } sb.Append('\n'); } } /// /// Comparer function to sort text blocks by their x/y positions. /// /// first block /// second block /// order value -1,0 or 1 private static int CompareTextBlockY(TextBlockRange a, TextBlockRange b) { RectangleF abox = a.BBox; RectangleF bbox = b.BBox; if (abox.Y + abox.Height/2 < bbox.Y) { return -1; } else if (abox.Y > bbox.Y + bbox.Height/2 || abox.X < bbox.X) { return 1; } else if (abox.X == bbox.X || abox.Y + abox.Height / 2 == bbox.Y || abox.Y == bbox.Y + bbox.Height / 2) { return 0; } else { return -1; } } /// /// Sets the array of hightlighted search chunks. /// public SearchChunk[] HighlightedSearchChunks { set { highlightedSearchChunks = value; if (data != null) { UpdateSearchHighlighting(); } } } /// /// Gets the links on the page or null if not loaded. /// internal IList Links { get { var data = this.data; return data == null ? null : data.links; } } /// /// Encapsulates the data objects to make them cacheable with one weak reference. /// private class PageContentData { public Image image; public IList links; public IList texts; public PageContentData(Image image, IList links, IList texts) { this.image = image; this.links = links; this.texts = texts; } } } }