// "Therefore those skilled at the unorthodox // are infinite as heaven and earth, // inexhaustible as the great rivers. // When they come to an end, // they begin again, // like the days and months; // they die and are reborn, // like the four seasons." // // - Sun Tsu, // "The Art of War" using System; using System.Collections.Generic; using System.Diagnostics; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.Dom; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Handlers; using TheArtOfDev.HtmlRenderer.Core.Parse; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core { /// /// Low level handling of Html Renderer logic.
/// Allows html layout and rendering without association to actual control, those allowing to handle html rendering on any graphics object.
/// Using this class will require the client to handle all propagation's of mouse/keyboard events, layout/paint calls, scrolling offset, /// location/size/rectangle handling and UI refresh requests.
///
/// /// /// MaxSize and ActualSize:
/// The max width and height of the rendered html.
/// The max width will effect the html layout wrapping lines, resize images and tables where possible.
/// The max height does NOT effect layout, but will not render outside it (clip).
/// can exceed the max size by layout restrictions (unwrap-able line, set image size, etc.).
/// Set zero for unlimited (width/height separately).
///
/// /// ScrollOffset:
/// This will adjust the rendered html by the given offset so the content will be "scrolled".
/// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered /// at -100, therefore outside the client rectangle. ///
/// /// LinkClicked event
/// Raised when the user clicks on a link in the html.
/// Allows canceling the execution of the link to overwrite by custom logic.
/// If error occurred in event handler it will propagate up the stack. ///
/// /// StylesheetLoad event:
/// Raised when a stylesheet is about to be loaded by file path or URL in 'link' element.
/// Allows to overwrite the loaded stylesheet by providing the stylesheet data manually, or different source (file or URL) to load from.
/// Example: The stylesheet 'href' can be non-valid URI string that is interpreted in the overwrite delegate by custom logic to pre-loaded stylesheet object
/// If no alternative data is provided the original source will be used.
///
/// /// ImageLoad event:
/// Raised when an image is about to be loaded by file path, URL or inline data in 'img' element or background-image CSS style.
/// Allows to overwrite the loaded image by providing the image object manually, or different source (file or URL) to load from.
/// Example: image 'src' can be non-valid string that is interpreted in the overwrite delegate by custom logic to resource image object
/// Example: image 'src' in the html is relative - the overwrite intercepts the load and provide full source URL to load the image from
/// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide /// file path to load the image from.
/// If no alternative data is provided the original source will be used.
///
/// /// Refresh event:
/// Raised when html renderer requires refresh of the control hosting (invalidation and re-layout).
/// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. ///
/// /// RenderError event:
/// Raised when an error occurred during html rendering.
///
///
public sealed class HtmlContainerInt : IDisposable { #region Fields and Consts /// /// /// public PageList _pagelist = new PageList(); /// /// /// private readonly RAdapter _adapter; /// /// parser for CSS data /// private readonly CssParser _cssParser; /// /// the root css box of the parsed html /// private CssBox _root; /// /// list of all css boxes that have ":hover" selector on them /// private List _hoverBoxes; /// /// Handler for text selection in the html. /// private SelectionHandler _selectionHandler; /// /// the text fore color use for selected text /// private RColor _selectionForeColor; /// /// the back-color to use for selected text /// private RColor _selectionBackColor; /// /// the parsed stylesheet data used for handling the html /// private CssData _cssData; /// /// Is content selection is enabled for the rendered html (default - true).
/// If set to 'false' the rendered html will be static only with ability to click on links. ///
private bool _isSelectionEnabled = true; /// /// Is the build-in context menu enabled (default - true) /// private bool _isContextMenuEnabled = true; /// /// Gets or sets a value indicating if anti-aliasing should be avoided /// for geometry like backgrounds and borders /// private bool _avoidGeometryAntialias; /// /// Gets or sets a value indicating if image asynchronous loading should be avoided (default - false).
///
private bool _avoidAsyncImagesLoading; /// /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
///
private bool _avoidImagesLateLoading; /// /// the top-left most location of the rendered html /// private RPoint _location; /// /// the max width and height of the rendered html, effects layout, actual size cannot exceed this values.
/// Set zero for unlimited.
///
private RSize _maxSize; /// /// Gets or sets the scroll offset of the document for scroll controls /// private RPoint _scrollOffset; /// /// The actual size of the rendered html (after layout) /// private RSize _actualSize; #endregion /// /// Init. /// public HtmlContainerInt(RAdapter adapter) { ArgChecker.AssertArgNotNull(adapter, "global"); _adapter = adapter; _cssParser = new CssParser(adapter); } /// /// /// internal RAdapter Adapter { get { return _adapter; } } /// /// parser for CSS data /// internal CssParser CssParser { get { return _cssParser; } } /// /// Raised when the user clicks on a link in the html.
/// Allows canceling the execution of the link. ///
public event EventHandler LinkClicked; /// /// Raised when html renderer requires refresh of the control hosting (invalidation and re-layout). /// /// /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. /// public event EventHandler Refresh; /// /// Raised when Html Renderer request scroll to specific location.
/// This can occur on document anchor click. ///
public event EventHandler ScrollChange; /// /// Raised when an error occurred during html rendering.
///
/// /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. /// public event EventHandler RenderError; /// /// Raised when a stylesheet is about to be loaded by file path or URI by link element.
/// This event allows to provide the stylesheet manually or provide new source (file or Uri) to load from.
/// If no alternative data is provided the original source will be used.
///
public event EventHandler StylesheetLoad; /// /// Raised when an image is about to be loaded by file path or URI.
/// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI. ///
public event EventHandler ImageLoad; /// /// the parsed stylesheet data used for handling the html /// public CssData CssData { get { return _cssData; } } /// /// Gets or sets a value indicating if anti-aliasing should be avoided for geometry like backgrounds and borders (default - false). /// public bool AvoidGeometryAntialias { get { return _avoidGeometryAntialias; } set { _avoidGeometryAntialias = value; } } /// /// Gets or sets a value indicating if image asynchronous loading should be avoided (default - false).
/// True - images are loaded synchronously during html parsing.
/// False - images are loaded asynchronously to html parsing when downloaded from URL or loaded from disk.
///
/// /// Asynchronously image loading allows to unblock html rendering while image is downloaded or loaded from disk using IO /// ports to achieve better performance.
/// Asynchronously image loading should be avoided when the full html content must be available during render, like render to image. ///
public bool AvoidAsyncImagesLoading { get { return _avoidAsyncImagesLoading; } set { _avoidAsyncImagesLoading = value; } } /// /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
/// True - images are loaded as soon as the html is parsed.
/// False - images that are not visible because of scroll location are not loaded until they are scrolled to. ///
/// /// Images late loading improve performance if the page contains image outside the visible scroll area, especially if there is large /// amount of images, as all image loading is delayed (downloading and loading into memory).
/// Late image loading may effect the layout and actual size as image without set size will not have actual size until they are loaded /// resulting in layout change during user scroll.
/// Early image loading may also effect the layout if image without known size above the current scroll location are loaded as they /// will push the html elements down. ///
public bool AvoidImagesLateLoading { get { return _avoidImagesLateLoading; } set { _avoidImagesLateLoading = value; } } /// /// Is content selection is enabled for the rendered html (default - true).
/// If set to 'false' the rendered html will be static only with ability to click on links. ///
public bool IsSelectionEnabled { get { return _isSelectionEnabled; } set { _isSelectionEnabled = value; } } /// /// Is the build-in context menu enabled and will be shown on mouse right click (default - true) /// public bool IsContextMenuEnabled { get { return _isContextMenuEnabled; } set { _isContextMenuEnabled = value; } } /// /// The scroll offset of the html.
/// This will adjust the rendered html by the given offset so the content will be "scrolled".
///
/// /// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered as it /// will be at -100 therefore outside the client rectangle. /// public RPoint ScrollOffset { get { return _scrollOffset; } set { _scrollOffset = value; } } /// /// The top-left most location of the rendered html.
/// This will offset the top-left corner of the rendered html. ///
public RPoint Location { get { return _location; } set { _location = value; } } /// /// The max width and height of the rendered html.
/// The max width will effect the html layout wrapping lines, resize images and tables where possible.
/// The max height does NOT effect layout, but will not render outside it (clip).
/// can be exceed the max size by layout restrictions (unwrapable line, set image size, etc.).
/// Set zero for unlimited (width\height separately).
///
public RSize MaxSize { get { return _maxSize; } set { _maxSize = value; } } /// /// The actual size of the rendered html (after layout) /// public RSize ActualSize { get { return _actualSize; } set { _actualSize = value; } } /// /// Get the currently selected text segment in the html. /// public string SelectedText { get { return _selectionHandler.GetSelectedText(); } } /// /// Copy the currently selected html segment with style. /// public string SelectedHtml { get { return _selectionHandler.GetSelectedHtml(); } } /// /// the root css box of the parsed html /// internal CssBox Root { get { return _root; } } /// /// the text fore color use for selected text /// internal RColor SelectionForeColor { get { return _selectionForeColor; } set { _selectionForeColor = value; } } /// /// the back-color to use for selected text /// internal RColor SelectionBackColor { get { return _selectionBackColor; } set { _selectionBackColor = value; } } /// /// Init with optional document and stylesheet. /// /// the html to init with, init empty if not given /// optional: the stylesheet to init with, init default if not given public void SetHtml(string htmlSource, CssData baseCssData = null) { Clear(); if (!string.IsNullOrEmpty(htmlSource)) { _cssData = baseCssData ?? _adapter.DefaultCssData; DomParser parser = new DomParser(_cssParser); _root = parser.GenerateCssTree(htmlSource, this, ref _cssData); if (_root != null) { _selectionHandler = new SelectionHandler(_root); } } } /// /// /// /// public int GetPages() { if (_root != null) { _pagelist.Clear(); _root.GetPages(_pagelist); } return 0; } /// /// Clear the content of the HTML container releasing any resources used to render previously existing content. /// public void Clear() { if (_hoverBoxes != null) { _hoverBoxes.Clear(); } if (_root != null) { _root.Dispose(); _root = null; if (_selectionHandler != null) _selectionHandler.Dispose(); _selectionHandler = null; } } /// /// Get html from the current DOM tree with style if requested. /// /// Optional: controls the way styles are generated when html is generated (default: ) /// generated html public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline) { return DomUtils.GenerateHtml(_root, styleGen); } /// /// Get attribute value of element at the given x,y location by given key.
/// If more than one element exist with the attribute at the location the inner most is returned. ///
/// the location to find the attribute at /// the attribute key to get value by /// found attribute value or null if not found public string GetAttributeAt(RPoint location, string attribute) { ArgChecker.AssertArgNotNullOrEmpty(attribute, "attribute"); var cssBox = DomUtils.GetCssBox(_root, OffsetByScroll(location)); return cssBox != null ? DomUtils.GetAttribute(cssBox, attribute) : null; } /// /// Get all the links in the HTML with the element rectangle and href data. /// /// collection of all the links in the HTML public List> GetLinks() { var linkBoxes = new List(); DomUtils.GetAllLinkBoxes(_root, linkBoxes); var linkElements = new List>(); foreach (var box in linkBoxes) { linkElements.Add(new LinkElementData(box.GetAttribute("id"), box.GetAttribute("href"), CommonUtils.GetFirstValueOrDefault(box.Rectangles, box.Bounds))); } return linkElements; } /// /// Get css link href at the given x,y location. /// /// the location to find the link at /// css link href if exists or null public string GetLinkAt(RPoint location) { var link = DomUtils.GetLinkBox(_root, OffsetByScroll(location)); return link != null ? link.HrefLink : null; } /// /// Get the rectangle of html element as calculated by html layout.
/// Element if found by id (id attribute on the html element).
/// Note: to get the screen rectangle you need to adjust by the hosting control.
///
/// the id of the element to get its rectangle /// the rectangle of the element or null if not found public RRect? GetElementRectangle(string elementId) { ArgChecker.AssertArgNotNullOrEmpty(elementId, "elementId"); var box = DomUtils.GetBoxById(_root, elementId.ToLower()); return box != null ? CommonUtils.GetFirstValueOrDefault(box.Rectangles, box.Bounds) : (RRect?)null; } /// /// Measures the bounds of box and children, recursively. /// /// Device context to draw public void PerformLayout(RGraphics g) { ArgChecker.AssertArgNotNull(g, "g"); _actualSize = RSize.Empty; if (_root != null) { // if width is not restricted we set it to large value to get the actual later _root.Size = new RSize(_maxSize.Width > 0 ? _maxSize.Width : 99999, 0); _root.Location = _location; _root.PerformLayout(g); if (_maxSize.Width <= 0.1) { // in case the width is not restricted we need to double layout, first will find the width so second can layout by it (center alignment) _root.Size = new RSize((int)Math.Ceiling(_actualSize.Width), 0); _actualSize = RSize.Empty; _root.PerformLayout(g); } } } /// /// Render the html using the given device. /// /// the device to use to render public void PerformPaint(RGraphics g) { ArgChecker.AssertArgNotNull(g, "g"); bool pushedClip = false; if (MaxSize.Height > 0) { pushedClip = true; g.PushClip(new RRect(_location, _maxSize)); } if (_root != null) { _root.Paint(g); } if (pushedClip) { g.PopClip(); } } /// /// Render the html using the given device. /// /// the device to use to render /// public void PerformPrint(RGraphics g, int iPage) { ArgChecker.AssertArgNotNull(g, "g"); bool pushedClip = false; if (MaxSize.Height > 0) { pushedClip = true; g.PushClip(new RRect(_location, _maxSize)); } if (_root != null) { _root.Print(g,iPage,_pagelist); } if (pushedClip) { g.PopClip(); } } /// /// Handle mouse down to handle selection. /// /// the control hosting the html to invalidate /// the location of the mouse public void HandleMouseDown(RControl parent, RPoint location) { ArgChecker.AssertArgNotNull(parent, "parent"); try { if (_selectionHandler != null) _selectionHandler.HandleMouseDown(parent, OffsetByScroll(location), IsMouseInContainer(location)); } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse down handle", ex); } } /// /// Handle mouse up to handle selection and link click. /// /// the control hosting the html to invalidate /// the location of the mouse /// the mouse event data public void HandleMouseUp(RControl parent, RPoint location, RMouseEvent e) { ArgChecker.AssertArgNotNull(parent, "parent"); try { if (_selectionHandler != null && IsMouseInContainer(location)) { var ignore = _selectionHandler.HandleMouseUp(parent, e.LeftButton); if (!ignore && e.LeftButton) { var loc = OffsetByScroll(location); var link = DomUtils.GetLinkBox(_root, loc); if (link != null) { HandleLinkClicked(parent, location, link); } } } } catch (HtmlLinkClickedException) { throw; } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse up handle", ex); } } /// /// Handle mouse double click to select word under the mouse. /// /// the control hosting the html to set cursor and invalidate /// the location of the mouse public void HandleMouseDoubleClick(RControl parent, RPoint location) { ArgChecker.AssertArgNotNull(parent, "parent"); try { if (_selectionHandler != null && IsMouseInContainer(location)) _selectionHandler.SelectWord(parent, OffsetByScroll(location)); } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse double click handle", ex); } } /// /// Handle mouse move to handle hover cursor and text selection. /// /// the control hosting the html to set cursor and invalidate /// the location of the mouse public void HandleMouseMove(RControl parent, RPoint location) { ArgChecker.AssertArgNotNull(parent, "parent"); try { var loc = OffsetByScroll(location); if (_selectionHandler != null && IsMouseInContainer(location)) _selectionHandler.HandleMouseMove(parent, loc); /* if( _hoverBoxes != null ) { bool refresh = false; foreach(var hoverBox in _hoverBoxes) { foreach(var rect in hoverBox.Item1.Rectangles.Values) { if( rect.Contains(loc) ) { //hoverBox.Item1.Color = "gold"; refresh = true; } } } if(refresh) RequestRefresh(true); } */ } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse move handle", ex); } } /// /// Handle mouse leave to handle hover cursor. /// /// the control hosting the html to set cursor and invalidate public void HandleMouseLeave(RControl parent) { ArgChecker.AssertArgNotNull(parent, "parent"); try { if (_selectionHandler != null) _selectionHandler.HandleMouseLeave(parent); } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse leave handle", ex); } } /// /// Handle key down event for selection and copy. /// /// the control hosting the html to invalidate /// the pressed key public void HandleKeyDown(RControl parent, RKeyEvent e) { ArgChecker.AssertArgNotNull(parent, "parent"); ArgChecker.AssertArgNotNull(e, "e"); try { if (e.Control && _selectionHandler != null) { // select all if (e.AKeyCode) { _selectionHandler.SelectAll(parent); } // copy currently selected text if (e.CKeyCode) { _selectionHandler.CopySelectedHtml(); } } } catch (Exception ex) { ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed key down handle", ex); } } /// /// Raise the stylesheet load event with the given event args. /// /// the event args internal void RaiseHtmlStylesheetLoadEvent(HtmlStylesheetLoadEventArgs args) { try { if (StylesheetLoad != null) { StylesheetLoad(this, args); } } catch (Exception ex) { ReportError(HtmlRenderErrorType.CssParsing, "Failed stylesheet load event", ex); } } /// /// Raise the image load event with the given event args. /// /// the event args internal void RaiseHtmlImageLoadEvent(HtmlImageLoadEventArgs args) { try { if (ImageLoad != null) { ImageLoad(this, args); } } catch (Exception ex) { ReportError(HtmlRenderErrorType.Image, "Failed image load event", ex); } } /// /// Request invalidation and re-layout of the control hosting the renderer. /// /// is re-layout is required for the refresh public void RequestRefresh(bool layout) { try { if (Refresh != null) { Refresh(this, new HtmlRefreshEventArgs(layout)); } } catch (Exception ex) { ReportError(HtmlRenderErrorType.General, "Failed refresh request", ex); } } /// /// Report error in html render process. /// /// the type of error to report /// the error message /// optional: the exception that occured internal void ReportError(HtmlRenderErrorType type, string message, Exception exception = null) { try { if (RenderError != null) { RenderError(this, new HtmlRenderErrorEventArgs(type, message, exception)); } } catch { } } /// /// Handle link clicked going over event and using if not canceled. /// /// the control hosting the html to invalidate /// the location of the mouse /// the link that was clicked internal void HandleLinkClicked(RControl parent, RPoint location, CssBox link) { if (LinkClicked != null) { var args = new HtmlLinkClickedEventArgs(link.HrefLink, link.HtmlTag.Attributes); try { LinkClicked(this, args); } catch (Exception ex) { throw new HtmlLinkClickedException("Error in link clicked intercept", ex); } if (args.Handled) return; } if (!string.IsNullOrEmpty(link.HrefLink)) { if (link.HrefLink.StartsWith("#") && link.HrefLink.Length > 1) { if (ScrollChange != null) { var rect = GetElementRectangle(link.HrefLink.Substring(1)); if (rect.HasValue) { ScrollChange(this, new HtmlScrollEventArgs(rect.Value.Location)); HandleMouseMove(parent, location); } } } else { var nfo = new ProcessStartInfo(link.HrefLink); nfo.UseShellExecute = true; Process.Start(nfo); } } } /// /// Add css box that has ":hover" selector to be handled on mouse hover. /// /// the box that has the hover selector /// the css block with the css data with the selector internal void AddHoverBox(CssBox box, CssBlock block) { ArgChecker.AssertArgNotNull(box, "box"); ArgChecker.AssertArgNotNull(block, "block"); if (_hoverBoxes == null) _hoverBoxes = new List(); _hoverBoxes.Add(new HoverBoxBlock(box, block)); } ///// ///// Reposition of box and children, recursively to the bounds of pages defined in pagelist . ///// ///// Device context to draw ///// list of RPage definitions //public void RepositionLayoutToPageList(RGraphics g, PageList pagelist, YposAlignedList yal) //{ // ArgChecker.AssertArgNotNull(g, "g"); // _actualSize = RSize.Empty; // if (_root != null) // { // _root.RepositionLayoutToPageList(g, pagelist,yal); // } //} ///// ///// Reposition of box and children, recursively to the bounds of pages defined in pagelist . ///// ///// Device context to draw ///// list of RPage definitions //public void RepositionLayoutToPageList(RGraphics g, PageList pagelist, ref int iPage) //{ // ArgChecker.AssertArgNotNull(g, "g"); // _actualSize = RSize.Empty; // if (_root != null) // { // _root.RepositionLayoutToPageList(g, pagelist, ref iPage); // } //} /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// /// 2 public void Dispose() { Dispose(true); } #region Private methods /// /// Adjust the offset of the given location by the current scroll offset. /// /// the location to adjust /// the adjusted location private RPoint OffsetByScroll(RPoint location) { return new RPoint(location.X - ScrollOffset.X, location.Y - ScrollOffset.Y); } /// /// Check if the mouse is currently on the html container.
/// Relevant if the html container is not filled in the hosted control (location is not zero and the size is not the full size of the control). ///
private bool IsMouseInContainer(RPoint location) { return location.X >= _location.X && location.X <= _location.X + _actualSize.Width && location.Y >= _location.Y + ScrollOffset.Y && location.Y <= _location.Y + ScrollOffset.Y + _actualSize.Height; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// private void Dispose(bool all) { try { if (all) { LinkClicked = null; Refresh = null; RenderError = null; StylesheetLoad = null; ImageLoad = null; } _cssData = null; if (_root != null) _root.Dispose(); _root = null; if (_selectionHandler != null) _selectionHandler.Dispose(); _selectionHandler = null; } catch { } } #endregion } }