// "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.Globalization; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Core.Handlers; using TheArtOfDev.HtmlRenderer.Core.Parse; using TheArtOfDev.HtmlRenderer.Core.Utils; namespace TheArtOfDev.HtmlRenderer.Core.Dom { /// /// Represents a CSS Box of text or replaced elements. /// /// /// The Box can contains other boxes, that's the way that the CSS Tree /// is composed. /// /// To know more about boxes visit CSS spec: /// http://www.w3.org/TR/CSS21/box.html /// internal class CssBox : CssBoxProperties, IDisposable { #region Fields and Consts /// /// the parent css box of this css box in the hierarchy /// private CssBox _parentBox; /// /// the root container for the hierarchy /// protected HtmlContainerInt _htmlContainer; /// /// the html tag that is associated with this css box, null if anonymous box /// private readonly HtmlTag _htmltag; private readonly List _boxWords = new List(); private readonly List _boxes = new List(); private readonly List _lineBoxes = new List(); private readonly List _parentLineBoxes = new List(); private readonly Dictionary _rectangles = new Dictionary(); /// /// the inner text of the box /// private SubString _text; /// /// Do not use or alter this flag /// /// /// Flag that indicates that CssTable algorithm already made fixes on it. /// internal bool _tableFixed; protected bool _wordsSizeMeasured; private CssBox _listItemBox; private CssLineBox _firstHostingLineBox; private CssLineBox _lastHostingLineBox; /// /// handler for loading background image /// private ImageLoadHandler _imageLoadHandler; #endregion /// /// Init. /// /// optional: the parent of this css box in html /// optional: the html tag associated with this css box public CssBox(CssBox parentBox, HtmlTag tag) { if (parentBox != null) { _parentBox = parentBox; _parentBox.Boxes.Add(this); } _htmltag = tag; } /// /// Gets the HtmlContainer of the Box. /// WARNING: May be null. /// public HtmlContainerInt HtmlContainer { get { return _htmlContainer ?? (_htmlContainer = _parentBox != null ? _parentBox.HtmlContainer : null); } set { _htmlContainer = value; } } /// /// Gets or sets the parent box of this box /// public CssBox ParentBox { get { return _parentBox; } set { //Remove from last parent if (_parentBox != null) _parentBox.Boxes.Remove(this); _parentBox = value; //Add to new parent if (value != null) _parentBox.Boxes.Add(this); } } /// /// Gets the children boxes of this box /// public List Boxes { get { return _boxes; } } /// /// Is the box is of "br" element. /// public bool IsBrElement { get { return _htmltag != null && _htmltag.Name.Equals("br", StringComparison.InvariantCultureIgnoreCase); } } /// /// is the box "Display" is "Inline", is this is an inline box and not block. /// public bool IsInline { get { return (Display == CssConstants.Inline || Display == CssConstants.InlineBlock) && !IsBrElement; } } /// /// is the box "Display" is "Block", is this is an block box and not inline. /// public bool IsBlock { get { return Display == CssConstants.Block; } } /// /// Is the css box clickable (by default only "a" element is clickable) /// public virtual bool IsClickable { get { return HtmlTag != null && HtmlTag.Name == HtmlConstants.A && !HtmlTag.HasAttribute("id"); } } /// /// Get the href link of the box (by default get "href" attribute) /// public virtual string HrefLink { get { return GetAttribute(HtmlConstants.Href); } } /// /// Gets the containing block-box of this box. (The nearest parent box with display=block) /// public CssBox ContainingBlock { get { if (ParentBox == null) { return this; //This is the initial containing block. } var box = ParentBox; while (!box.IsBlock && box.Display != CssConstants.ListItem && box.Display != CssConstants.Table && box.Display != CssConstants.TableCell && box.ParentBox != null) { box = box.ParentBox; } //Comment this following line to treat always superior box as block if (box == null) throw new Exception("There's no containing block on the chain"); return box; } } /// /// Gets the HTMLTag that hosts this box /// public HtmlTag HtmlTag { get { return _htmltag; } } /// /// Gets if this box represents an image /// public bool IsImage { get { return Words.Count == 1 && Words[0].IsImage; } } /// /// Tells if the box is empty or contains just blank spaces /// public bool IsSpaceOrEmpty { get { if ((Words.Count != 0 || Boxes.Count != 0) && (Words.Count != 1 || !Words[0].IsSpaces)) { foreach (CssRect word in Words) { if (!word.IsSpaces) { return false; } } } return true; } } /// /// Gets or sets the inner text of the box /// public SubString Text { get { return _text; } set { _text = value; _boxWords.Clear(); } } /// /// Gets the line-boxes of this box (if block box) /// internal List LineBoxes { get { return _lineBoxes; } } /// /// Gets the linebox(es) that contains words of this box (if inline) /// internal List ParentLineBoxes { get { return _parentLineBoxes; } } /// /// Gets the rectangles where this box should be painted /// internal Dictionary Rectangles { get { return _rectangles; } } /// /// Gets the BoxWords of text in the box /// internal List Words { get { return _boxWords; } } /// /// Gets the first word of the box /// internal CssRect FirstWord { get { return Words[0]; } } /// /// Gets or sets the first linebox where content of this box appear /// internal CssLineBox FirstHostingLineBox { get { return _firstHostingLineBox; } set { _firstHostingLineBox = value; } } /// /// Gets or sets the last linebox where content of this box appear /// internal CssLineBox LastHostingLineBox { get { return _lastHostingLineBox; } set { _lastHostingLineBox = value; } } /// /// Create new css box for the given parent with the given html tag.
///
/// the html tag to define the box /// the box to add the new box to it as child /// the new box public static CssBox CreateBox(HtmlTag tag, CssBox parent = null) { ArgChecker.AssertArgNotNull(tag, "tag"); if (tag.Name == HtmlConstants.Img) { return new CssBoxImage(parent, tag); } else if (tag.Name == HtmlConstants.Iframe) { return new CssBoxFrame(parent, tag); } else if (tag.Name == HtmlConstants.Hr) { return new CssBoxHr(parent, tag); } else { return new CssBox(parent, tag); } } /// /// Create new css box for the given parent with the given optional html tag and insert it either /// at the end or before the given optional box.
/// If no html tag is given the box will be anonymous.
/// If no before box is given the new box will be added at the end of parent boxes collection.
/// If before box doesn't exists in parent box exception is thrown.
///
/// /// To learn more about anonymous inline boxes visit: http://www.w3.org/TR/CSS21/visuren.html#anonymous /// /// the box to add the new box to it as child /// optional: the html tag to define the box /// optional: to insert as specific location in parent box /// the new box public static CssBox CreateBox(CssBox parent, HtmlTag tag = null, CssBox before = null) { ArgChecker.AssertArgNotNull(parent, "parent"); var newBox = new CssBox(parent, tag); newBox.InheritStyle(); if (before != null) { newBox.SetBeforeBox(before); } return newBox; } /// /// Create new css block box. /// /// the new block box public static CssBox CreateBlock() { var box = new CssBox(null, null); box.Display = CssConstants.Block; return box; } /// /// Create new css block box for the given parent with the given optional html tag and insert it either /// at the end or before the given optional box.
/// If no html tag is given the box will be anonymous.
/// If no before box is given the new box will be added at the end of parent boxes collection.
/// If before box doesn't exists in parent box exception is thrown.
///
/// /// To learn more about anonymous block boxes visit CSS spec: /// http://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level /// /// the box to add the new block box to it as child /// optional: the html tag to define the box /// optional: to insert as specific location in parent box /// the new block box public static CssBox CreateBlock(CssBox parent, HtmlTag tag = null, CssBox before = null) { ArgChecker.AssertArgNotNull(parent, "parent"); var newBox = CreateBox(parent, tag, before); newBox.Display = CssConstants.Block; return newBox; } /// /// Measures the bounds of box and children, recursively.
/// Performs layout of the DOM structure creating lines by set bounds restrictions. ///
/// Device context to use public void PerformLayout(RGraphics g) { try { PerformLayoutImp(g); } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Layout, "Exception in box layout", ex); } } ///// ///// repostion layout of the DOM structure to match page definitions in PageList. ///// ///// Device context to use ///// List of RPage definitions //public void RepositionLayoutToPageList(RGraphics g,PageList pagelist, YposAlignedList yal) //{ // // private readonly List _boxWords = new List(); // //private readonly List _boxes = new List(); // //private readonly List _lineBoxes = new List(); // //private readonly List _parentLineBoxes = new List(); // //private readonly Dictionary _rectangles = new Dictionary(); // //yal.Add(this); // for (int iCssRect = 0; iCssRect < _boxWords.Count; iCssRect++) // { // _boxWords[iCssRect].RepositionLayoutToPageList(g, pagelist, yal); // } // for (int iCssLineBox = 0; iCssLineBox < _lineBoxes.Count; iCssLineBox++) // { // _lineBoxes[iCssLineBox].RepositionLayoutToPageList(g, pagelist, yal); // } // for (int iCssBox = 0; iCssBox <_boxes.Count;iCssBox++) // { // _boxes[iCssBox].RepositionLayoutToPageList(g, pagelist,yal); // } //} ///// ///// repostion layout of the DOM structure to match page definitions in PageList. ///// ///// Device context to use ///// List of RPage definitions //public void RepositionLayoutToPageList(RGraphics g, PageList pagelist, ref int iPage) //{ // // private readonly List _boxWords = new List(); // //private readonly List _boxes = new List(); // //private readonly List _lineBoxes = new List(); // //private readonly List _parentLineBoxes = new List(); // //private readonly Dictionary _rectangles = new Dictionary(); // //yal.Add(this); // for (int iCssRect = 0; iCssRect < _boxWords.Count; iCssRect++) // { // _boxWords[iCssRect].RepositionLayoutToPageList(g, pagelist, ref iPage); // } // for (int iCssLineBox = 0; iCssLineBox < _lineBoxes.Count; iCssLineBox++) // { // _lineBoxes[iCssLineBox].RepositionLayoutToPageList(g, pagelist, ref iPage); // } // for (int iCssBox = 0; iCssBox < _boxes.Count; iCssBox++) // { // _boxes[iCssBox].RepositionLayoutToPageList(g, pagelist, ref iPage); // } //} internal void GetPages(PageList _pagelist) { //for (int iCssRect = 0; iCssRect < _boxWords.Count; iCssRect++) //{ // _boxWords[iCssRect].RepositionLayoutToPageList(g, pagelist, ref iPage); //} //for (int iCssLineBox = 0; iCssLineBox < _lineBoxes.Count; iCssLineBox++) //{ // _lineBoxes[iCssLineBox].RepositionLayoutToPageList(g, pagelist, ref iPage); //} for (int iCssRect = 0; iCssRect < _boxWords.Count; iCssRect++) { _boxWords[iCssRect].GetPages(_pagelist); } for (int iCssBox = 0; iCssBox < _boxes.Count; iCssBox++) { _boxes[iCssBox].GetPages(_pagelist); } if (this._htmltag != null) { if (this._htmltag.Name.ToLower().Equals("page")) { RPage rp = new RPage(this,this.Location.Y,this.Size.Width, this.Size.Height); _pagelist.Add(rp); } } } /// /// Paints the fragment /// /// Device context to use public void Paint(RGraphics g) { try { if (Display != CssConstants.None && Visibility == CssConstants.Visible) { // don't call paint if the rectangle of the box is not in visible rectangle bool visible = Rectangles.Count == 0; if (!visible) { var clip = g.GetClip(); var rect = ContainingBlock.ClientRectangle; rect.X -= 2; rect.Width += 2; rect.Offset(new RPoint(-HtmlContainer.Location.X, -HtmlContainer.Location.Y)); rect.Offset(HtmlContainer.ScrollOffset); clip.Intersect(rect); if (clip != RRect.Empty) visible = true; } if (visible) PaintImp(g); } } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Paint, "Exception in box paint", ex); } } /// /// Prints the fragment /// /// Device context to use /// /// public void Print(RGraphics g, int iPage,PageList pagelist) { try { if (Display != CssConstants.None && Visibility == CssConstants.Visible) { // don't call paint if the rectangle of the box is not in visible rectangle bool visible = Rectangles.Count == 0; if (!visible) { var clip = g.GetClip(); var rect = ContainingBlock.ClientRectangle; rect.X -= 2; rect.Width += 2; rect.Offset(new RPoint(-HtmlContainer.Location.X, -HtmlContainer.Location.Y)); rect.Offset(HtmlContainer.ScrollOffset); clip.Intersect(rect); if (clip != RRect.Empty) visible = true; } if (visible) PrintImp(g,iPage, pagelist); } } catch (Exception ex) { HtmlContainer.ReportError(HtmlRenderErrorType.Paint, "Exception in box paint", ex); } } /// /// Set this box in /// /// public void SetBeforeBox(CssBox before) { int index = _parentBox.Boxes.IndexOf(before); if (index < 0) throw new Exception("before box doesn't exist on parent"); _parentBox.Boxes.Remove(this); _parentBox.Boxes.Insert(index, this); } /// /// Move all child boxes from to this box. /// /// the box to move all its child boxes from public void SetAllBoxes(CssBox fromBox) { foreach (var childBox in fromBox._boxes) childBox._parentBox = this; _boxes.AddRange(fromBox._boxes); fromBox._boxes.Clear(); } /// /// Splits the text into words and saves the result /// public void ParseToWords() { _boxWords.Clear(); int startIdx = 0; bool preserveSpaces = WhiteSpace == CssConstants.Pre || WhiteSpace == CssConstants.PreWrap; bool respoctNewline = preserveSpaces || WhiteSpace == CssConstants.PreLine; while (startIdx < _text.Length) { while (startIdx < _text.Length && _text[startIdx] == '\r') startIdx++; if (startIdx < _text.Length) { var endIdx = startIdx; while (endIdx < _text.Length && char.IsWhiteSpace(_text[endIdx]) && _text[endIdx] != '\n') endIdx++; if (endIdx > startIdx) { if (preserveSpaces) _boxWords.Add(new CssRectWord(this, HtmlUtils.DecodeHtml(_text.Substring(startIdx, endIdx - startIdx)), false, false)); } else { endIdx = startIdx; while (endIdx < _text.Length && !char.IsWhiteSpace(_text[endIdx]) && _text[endIdx] != '-' && WordBreak != CssConstants.BreakAll && !CommonUtils.IsAsianCharecter(_text[endIdx])) endIdx++; if (endIdx < _text.Length && (_text[endIdx] == '-' || WordBreak == CssConstants.BreakAll || CommonUtils.IsAsianCharecter(_text[endIdx]))) endIdx++; if (endIdx > startIdx) { var hasSpaceBefore = !preserveSpaces && (startIdx > 0 && _boxWords.Count == 0 && char.IsWhiteSpace(_text[startIdx - 1])); var hasSpaceAfter = !preserveSpaces && (endIdx < _text.Length && char.IsWhiteSpace(_text[endIdx])); _boxWords.Add(new CssRectWord(this, HtmlUtils.DecodeHtml(_text.Substring(startIdx, endIdx - startIdx)), hasSpaceBefore, hasSpaceAfter)); } } // create new-line word so it will effect the layout if (endIdx < _text.Length && _text[endIdx] == '\n') { endIdx++; if (respoctNewline) _boxWords.Add(new CssRectWord(this, "\n", false, false)); } startIdx = endIdx; } } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public virtual void Dispose() { if (_imageLoadHandler != null) _imageLoadHandler.Dispose(); foreach (var childBox in Boxes) { childBox.Dispose(); } } #region Private Methods /// /// Measures the bounds of box and children, recursively.
/// Performs layout of the DOM structure creating lines by set bounds restrictions.
///
/// Device context to use protected virtual void PerformLayoutImp(RGraphics g) { if (Display != CssConstants.None) { RectanglesReset(); MeasureWordsSize(g); } if (IsBlock || Display == CssConstants.ListItem || Display == CssConstants.Table || Display == CssConstants.InlineTable || Display == CssConstants.TableCell) { // Because their width and height are set by CssTable if (Display != CssConstants.TableCell && Display != CssConstants.Table) { double width = ContainingBlock.Size.Width - ContainingBlock.ActualPaddingLeft - ContainingBlock.ActualPaddingRight - ContainingBlock.ActualBorderLeftWidth - ContainingBlock.ActualBorderRightWidth; if (Width != CssConstants.Auto && !string.IsNullOrEmpty(Width)) { width = CssValueParser.ParseLength(Width, width, this); } Size = new RSize(width, Size.Height); // must be separate because the margin can be calculated by percentage of the width Size = new RSize(width - ActualMarginLeft - ActualMarginRight, Size.Height); } if (Display != CssConstants.TableCell) { var prevSibling = DomUtils.GetPreviousSibling(this); double left = ContainingBlock.Location.X + ContainingBlock.ActualPaddingLeft + ActualMarginLeft + ContainingBlock.ActualBorderLeftWidth; double top = (prevSibling == null && ParentBox != null ? ParentBox.ClientTop : ParentBox == null ? Location.Y : 0) + MarginTopCollapse(prevSibling) + (prevSibling != null ? prevSibling.ActualBottom + prevSibling.ActualBorderBottomWidth : 0); Location = new RPoint(left, top); ActualBottom = top; } //If we're talking about a table here.. if (Display == CssConstants.Table || Display == CssConstants.InlineTable) { CssLayoutEngineTable.PerformLayout(g, this); } else { //If there's just inline boxes, create LineBoxes if (DomUtils.ContainsInlinesOnly(this)) { ActualBottom = Location.Y; CssLayoutEngine.CreateLineBoxes(g, this); //This will automatically set the bottom of this block } else if (_boxes.Count > 0) { foreach (var childBox in Boxes) { childBox.PerformLayout(g); } ActualRight = CalculateActualRight(); ActualBottom = MarginBottomCollapse(); } } } else { var prevSibling = DomUtils.GetPreviousSibling(this); if (prevSibling != null) { if (Location == RPoint.Empty) Location = prevSibling.Location; ActualBottom = prevSibling.ActualBottom; } } ActualBottom = Math.Max(ActualBottom, Location.Y + ActualHeight); CreateListItemBox(g); var actualWidth = Math.Max(GetMinimumWidth() + GetWidthMarginDeep(this), Size.Width < 90999 ? ActualRight - HtmlContainer.Root.Location.X : 0); HtmlContainer.ActualSize = CommonUtils.Max(HtmlContainer.ActualSize, new RSize(actualWidth, ActualBottom - HtmlContainer.Root.Location.Y)); } /// /// Assigns words its width and height /// /// internal virtual void MeasureWordsSize(RGraphics g) { if (!_wordsSizeMeasured) { if (BackgroundImage != CssConstants.None && _imageLoadHandler == null) { _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnImageLoadComplete); _imageLoadHandler.LoadImage(BackgroundImage, HtmlTag != null ? HtmlTag.Attributes : null); } MeasureWordSpacing(g); if (Words.Count > 0) { foreach (var boxWord in Words) { boxWord.Width = boxWord.Text != "\n" ? g.MeasureString(boxWord.Text, ActualFont).Width : 0; boxWord.Height = ActualFont.Height; } } _wordsSizeMeasured = true; } } /// /// Get the parent of this css properties instance. /// /// protected override sealed CssBoxProperties GetParent() { return _parentBox; } /// /// Gets the index of the box to be used on a (ordered) list /// /// private int GetIndexForList() { bool reversed = !string.IsNullOrEmpty(ParentBox.GetAttribute("reversed")); int index; if (!int.TryParse(ParentBox.GetAttribute("start"), out index)) { if (reversed) { index = 0; foreach (CssBox b in ParentBox.Boxes) { if (b.Display == CssConstants.ListItem) index++; } } else { index = 1; } } foreach (CssBox b in ParentBox.Boxes) { if (b.Equals(this)) return index; if (b.Display == CssConstants.ListItem) index += reversed ? -1 : 1; } return index; } /// /// Creates the /// /// private void CreateListItemBox(RGraphics g) { if (Display == CssConstants.ListItem && ListStyleType != CssConstants.None) { if (_listItemBox == null) { _listItemBox = new CssBox(null, null); _listItemBox.InheritStyle(this); _listItemBox.Display = CssConstants.Inline; _listItemBox.HtmlContainer = HtmlContainer; if (ListStyleType.Equals(CssConstants.Disc, StringComparison.InvariantCultureIgnoreCase)) { _listItemBox.Text = new SubString("•"); } else if (ListStyleType.Equals(CssConstants.Circle, StringComparison.InvariantCultureIgnoreCase)) { _listItemBox.Text = new SubString("o"); } else if (ListStyleType.Equals(CssConstants.Square, StringComparison.InvariantCultureIgnoreCase)) { _listItemBox.Text = new SubString("♠"); } else if (ListStyleType.Equals(CssConstants.Decimal, StringComparison.InvariantCultureIgnoreCase)) { _listItemBox.Text = new SubString(GetIndexForList().ToString(CultureInfo.InvariantCulture) + "."); } else if (ListStyleType.Equals(CssConstants.DecimalLeadingZero, StringComparison.InvariantCultureIgnoreCase)) { _listItemBox.Text = new SubString(GetIndexForList().ToString("00", CultureInfo.InvariantCulture) + "."); } else { _listItemBox.Text = new SubString(CommonUtils.ConvertToAlphaNumber(GetIndexForList(), ListStyleType) + "."); } _listItemBox.ParseToWords(); _listItemBox.PerformLayoutImp(g); _listItemBox.Size = new RSize(_listItemBox.Words[0].Width, _listItemBox.Words[0].Height); } _listItemBox.Words[0].Left = Location.X - _listItemBox.Size.Width - 5; _listItemBox.Words[0].Top = Location.Y + ActualPaddingTop; // +FontAscent; } } /// /// Searches for the first word occurrence inside the box, on the specified linebox /// /// /// /// internal CssRect FirstWordOccourence(CssBox b, CssLineBox line) { if (b.Words.Count == 0 && b.Boxes.Count == 0) { return null; } if (b.Words.Count > 0) { foreach (CssRect word in b.Words) { if (line.Words.Contains(word)) { return word; } } return null; } else { foreach (CssBox bb in b.Boxes) { CssRect w = FirstWordOccourence(bb, line); if (w != null) { return w; } } return null; } } /// /// Gets the specified Attribute, returns string.Empty if no attribute specified /// /// Attribute to retrieve /// Attribute value or string.Empty if no attribute specified internal string GetAttribute(string attribute) { return GetAttribute(attribute, string.Empty); } /// /// Gets the value of the specified attribute of the source HTML tag. /// /// Attribute to retrieve /// Value to return if attribute is not specified /// Attribute value or defaultValue if no attribute specified internal string GetAttribute(string attribute, string defaultValue) { return HtmlTag != null ? HtmlTag.TryGetAttribute(attribute, defaultValue) : defaultValue; } /// /// Gets the minimum width that the box can be.
/// The box can be as thin as the longest word plus padding.
/// The check is deep thru box tree.
///
/// the min width of the box internal double GetMinimumWidth() { double maxWidth = 0; CssRect maxWidthWord = null; GetMinimumWidth_LongestWord(this, ref maxWidth, ref maxWidthWord); double padding = 0f; if (maxWidthWord != null) { var box = maxWidthWord.OwnerBox; while (box != null) { padding += box.ActualBorderRightWidth + box.ActualPaddingRight + box.ActualBorderLeftWidth + box.ActualPaddingLeft; box = box != this ? box.ParentBox : null; } } return maxWidth + padding; } /// /// Gets the longest word (in width) inside the box, deeply. /// /// /// /// /// private static void GetMinimumWidth_LongestWord(CssBox box, ref double maxWidth, ref CssRect maxWidthWord) { if (box.Words.Count > 0) { foreach (CssRect cssRect in box.Words) { if (cssRect.Width > maxWidth) { maxWidth = cssRect.Width; maxWidthWord = cssRect; } } } else { foreach (CssBox childBox in box.Boxes) GetMinimumWidth_LongestWord(childBox, ref maxWidth, ref maxWidthWord); } } /// /// Get the total margin value (left and right) from the given box to the given end box.
///
/// the box to start calculation from. /// the total margin private static double GetWidthMarginDeep(CssBox box) { double sum = 0f; if (box.Size.Width > 90999 || (box.ParentBox != null && box.ParentBox.Size.Width > 90999)) { while (box != null) { sum += box.ActualMarginLeft + box.ActualMarginRight; box = box.ParentBox; } } return sum; } /// /// Gets the maximum bottom of the boxes inside the startBox /// /// /// /// internal double GetMaximumBottom(CssBox startBox, double currentMaxBottom) { foreach (var line in startBox.Rectangles.Keys) { currentMaxBottom = Math.Max(currentMaxBottom, startBox.Rectangles[line].Bottom); } foreach (var b in startBox.Boxes) { currentMaxBottom = Math.Max(currentMaxBottom, GetMaximumBottom(b, currentMaxBottom)); } return currentMaxBottom; } /// /// Get the and width of the box content.
///
/// The minimum width the content must be so it won't overflow (largest word + padding). /// The total width the content can take without line wrapping (with padding). internal void GetMinMaxWidth(out double minWidth, out double maxWidth) { double min = 0f; double maxSum = 0f; double paddingSum = 0f; double marginSum = 0f; GetMinMaxSumWords(this, ref min, ref maxSum, ref paddingSum, ref marginSum); maxWidth = paddingSum + maxSum; minWidth = paddingSum + (min < 90999 ? min : 0); } /// /// Get the and of the box words content and .
///
/// the box to calculate for /// the width that allows for each word to fit (width of the longest word) /// the max width a single line of words can take without wrapping /// the total amount of padding the content has /// /// private static void GetMinMaxSumWords(CssBox box, ref double min, ref double maxSum, ref double paddingSum, ref double marginSum) { double? oldSum = null; // not inline (block) boxes start a new line so we need to reset the max sum if (box.Display != CssConstants.Inline && box.Display != CssConstants.TableCell && box.WhiteSpace != CssConstants.NoWrap) { oldSum = maxSum; maxSum = marginSum; } // add the padding paddingSum += box.ActualBorderLeftWidth + box.ActualBorderRightWidth + box.ActualPaddingRight + box.ActualPaddingLeft; // for tables the padding also contains the spacing between cells if (box.Display == CssConstants.Table) paddingSum += CssLayoutEngineTable.GetTableSpacing(box); if (box.Words.Count > 0) { // calculate the min and max sum for all the words in the box foreach (CssRect word in box.Words) { maxSum += word.FullWidth + (word.HasSpaceBefore ? word.OwnerBox.ActualWordSpacing : 0); min = Math.Max(min, word.Width); } // remove the last word padding if (box.Words.Count > 0 && !box.Words[box.Words.Count - 1].HasSpaceAfter) maxSum -= box.Words[box.Words.Count - 1].ActualWordSpacing; } else { // recursively on all the child boxes for (int i = 0; i < box.Boxes.Count; i++) { CssBox childBox = box.Boxes[i]; marginSum += childBox.ActualMarginLeft + childBox.ActualMarginRight; //maxSum += childBox.ActualMarginLeft + childBox.ActualMarginRight; GetMinMaxSumWords(childBox, ref min, ref maxSum, ref paddingSum, ref marginSum); marginSum -= childBox.ActualMarginLeft + childBox.ActualMarginRight; } } // max sum is max of all the lines in the box if (oldSum.HasValue) { maxSum = Math.Max(maxSum, oldSum.Value); } } /// /// Gets if this box has only inline siblings (including itself) /// /// internal bool HasJustInlineSiblings() { return ParentBox != null && DomUtils.ContainsInlinesOnly(ParentBox); } /// /// Gets the rectangles where inline box will be drawn. See Remarks for more info. /// /// Rectangles where content should be placed /// /// Inline boxes can be split across different LineBoxes, that's why this method /// Delivers a rectangle for each LineBox related to this box, if inline. /// /// /// Inherits inheritable values from parent. /// internal new void InheritStyle(CssBox box = null, bool everything = false) { base.InheritStyle(box ?? ParentBox, everything); } /// /// Gets the result of collapsing the vertical margins of the two boxes /// /// the previous box under the same parent /// Resulting top margin protected double MarginTopCollapse(CssBoxProperties prevSibling) { double value; if (prevSibling != null) { value = Math.Max(prevSibling.ActualMarginBottom, ActualMarginTop); CollapsedMarginTop = value; } else if (_parentBox != null && ActualPaddingTop < 0.1 && ActualPaddingBottom < 0.1 && _parentBox.ActualPaddingTop < 0.1 && _parentBox.ActualPaddingBottom < 0.1) { value = Math.Max(0, ActualMarginTop - Math.Max(_parentBox.ActualMarginTop, _parentBox.CollapsedMarginTop)); } else { value = ActualMarginTop; } // fix for hr tag if (value < 0.1 && HtmlTag != null && HtmlTag.Name == "hr") { value = GetEmHeight() * 1.1f; } return value; } /// /// Calculate the actual right of the box by the actual right of the child boxes if this box actual right is not set. /// /// the calculated actual right value private double CalculateActualRight() { if (ActualRight > 90999) { var maxRight = 0d; foreach (var box in Boxes) { maxRight = Math.Max(maxRight, box.ActualRight + box.ActualMarginRight); } return maxRight + ActualPaddingRight + ActualMarginRight + ActualBorderRightWidth; } else { return ActualRight; } } /// /// Gets the result of collapsing the vertical margins of the two boxes /// /// Resulting bottom margin private double MarginBottomCollapse() { double margin = 0; if (ParentBox != null && ParentBox.Boxes.IndexOf(this) == ParentBox.Boxes.Count - 1 && _parentBox.ActualMarginBottom < 0.1) { var lastChildBottomMargin = _boxes[_boxes.Count - 1].ActualMarginBottom; margin = Height == "auto" ? Math.Max(ActualMarginBottom, lastChildBottomMargin) : lastChildBottomMargin; } return Math.Max(ActualBottom, _boxes[_boxes.Count - 1].ActualBottom + margin + ActualPaddingBottom + ActualBorderBottomWidth); } /// /// Deeply offsets the top of the box and its contents /// /// internal void OffsetTop(double amount) { List lines = new List(); foreach (CssLineBox line in Rectangles.Keys) lines.Add(line); foreach (CssLineBox line in lines) { RRect r = Rectangles[line]; Rectangles[line] = new RRect(r.X, r.Y + amount, r.Width, r.Height); } foreach (CssRect word in Words) { word.Top += amount; } foreach (CssBox b in Boxes) { b.OffsetTop(amount); } if (_listItemBox != null) _listItemBox.OffsetTop(amount); Location = new RPoint(Location.X, Location.Y + amount); } /// /// Paints the fragment /// /// the device to draw to protected virtual void PaintImp(RGraphics g) { if (Display != CssConstants.None && (Display != CssConstants.TableCell || EmptyCells != CssConstants.Hide || !IsSpaceOrEmpty)) { var clipped = RenderUtils.ClipGraphicsByOverflow(g, this); var areas = Rectangles.Count == 0 ? new List(new[] { Bounds }) : new List(Rectangles.Values); RRect[] rects = areas.ToArray(); RPoint offset = HtmlContainer.ScrollOffset; for (int i = 0; i < rects.Length; i++) { var actualRect = rects[i]; actualRect.Offset(offset); PaintBackground(g, actualRect, i == 0, i == rects.Length - 1); BordersDrawHandler.DrawBoxBorders(g, this, actualRect, i == 0, i == rects.Length - 1); } PaintWords(g, offset); for (int i = 0; i < rects.Length; i++) { var actualRect = rects[i]; actualRect.Offset(offset); PaintDecoration(g, actualRect, i == 0, i == rects.Length - 1); } // split paint to handle z-order foreach (CssBox b in Boxes) { if (b.Position != CssConstants.Absolute) b.Paint(g); } foreach (CssBox b in Boxes) { if (b.Position == CssConstants.Absolute) b.Paint(g); } if (clipped) g.PopClip(); if (_listItemBox != null) { _listItemBox.Paint(g); } } } /// /// Prints the fragment /// /// the device to draw to /// /// protected virtual void PrintImp(RGraphics g, int iPage, PageList pagelist) { if (IsPageBlock(iPage, pagelist)) { RPoint rp = HtmlContainer.ScrollOffset; rp.Y = -pagelist.Page(iPage).YOffset; rp.X = -this.Location.X; HtmlContainer.ScrollOffset = rp; PaintImp(g); } else { if (Display != CssConstants.None && (Display != CssConstants.TableCell || EmptyCells != CssConstants.Hide || !IsSpaceOrEmpty)) { var clipped = RenderUtils.ClipGraphicsByOverflow(g, this); var areas = Rectangles.Count == 0 ? new List(new[] { Bounds }) : new List(Rectangles.Values); RRect[] rects = areas.ToArray(); RPoint offset = HtmlContainer.ScrollOffset; //for (int i = 0; i < rects.Length; i++) //{ // var actualRect = rects[i]; // actualRect.Offset(offset); // PaintBackground(g, actualRect, i == 0, i == rects.Length - 1); // BordersDrawHandler.DrawBoxBorders(g, this, actualRect, i == 0, i == rects.Length - 1); //} //PaintWords(g, offset); //for (int i = 0; i < rects.Length; i++) //{ // var actualRect = rects[i]; // actualRect.Offset(offset); // PaintDecoration(g, actualRect, i == 0, i == rects.Length - 1); //} // split paint to handle z-order foreach (CssBox b in Boxes) { if (b.Position != CssConstants.Absolute) b.Print(g, iPage, pagelist); } foreach (CssBox b in Boxes) { if (b.Position == CssConstants.Absolute) b.Print(g, iPage, pagelist); } if (clipped) g.PopClip(); if (_listItemBox != null) { _listItemBox.Print(g, iPage, pagelist); } } } } private bool IsPageBlock(int iPage,PageList pagelist) { if (pagelist != null) { if (pagelist.Count > 0) { if (this._htmltag != null) { if (this._htmltag.Name.ToLower().Equals("page")) { if (this.Location.Y == pagelist.Page(iPage).YOffset) { return true; } } } } } return false; } /// /// Paints the background of the box /// /// the device to draw into /// the bounding rectangle to draw in /// is it the first rectangle of the element /// is it the last rectangle of the element protected void PaintBackground(RGraphics g, RRect rect, bool isFirst, bool isLast) { if (rect.Width > 0 && rect.Height > 0) { RBrush brush = null; if (BackgroundGradient != CssConstants.None) { brush = g.GetLinearGradientBrush(rect, ActualBackgroundColor, ActualBackgroundGradient, ActualBackgroundGradientAngle); } else if (RenderUtils.IsColorVisible(ActualBackgroundColor)) { brush = g.GetSolidBrush(ActualBackgroundColor); } if (brush != null) { // TODO:a handle it correctly (tables background) // if (isLast) // rectangle.Width -= ActualWordSpacing + CssUtils.GetWordEndWhitespace(ActualFont); RGraphicsPath roundrect = null; if (IsRounded) { roundrect = RenderUtils.GetRoundRect(g, rect, ActualCornerNw, ActualCornerNe, ActualCornerSe, ActualCornerSw); } Object prevMode = null; if (HtmlContainer != null && !HtmlContainer.AvoidGeometryAntialias && IsRounded) { prevMode = g.SetAntiAliasSmoothingMode(); } if (roundrect != null) { g.DrawPath(brush, roundrect); } else { g.DrawRectangle(brush, Math.Ceiling(rect.X), Math.Ceiling(rect.Y), rect.Width, rect.Height); } g.ReturnPreviousSmoothingMode(prevMode); if (roundrect != null) roundrect.Dispose(); brush.Dispose(); } if (_imageLoadHandler != null && _imageLoadHandler.Image != null && isFirst) { BackgroundImageDrawHandler.DrawBackgroundImage(g, this, _imageLoadHandler, rect); } } } /// /// Paint all the words in the box. /// /// the device to draw into /// the current scroll offset to offset the words private void PaintWords(RGraphics g, RPoint offset) { if (Width.Length > 0) { var isRtl = Direction == CssConstants.Rtl; foreach (var word in Words) { if (!word.IsLineBreak) { var wordPoint = new RPoint(word.Left + offset.X, word.Top + offset.Y); if (word.Selected) { // handle paint selected word background and with partial word selection var wordLine = DomUtils.GetCssLineBoxByWord(word); var left = word.SelectedStartOffset > -1 ? word.SelectedStartOffset : (wordLine.Words[0] != word && word.HasSpaceBefore ? -ActualWordSpacing : 0); var padWordRight = word.HasSpaceAfter && !wordLine.IsLastSelectedWord(word); var width = word.SelectedEndOffset > -1 ? word.SelectedEndOffset : word.Width + (padWordRight ? ActualWordSpacing : 0); var rect = new RRect(word.Left + offset.X + left, word.Top + offset.Y, width - left, wordLine.LineHeight); g.DrawRectangle(GetSelectionBackBrush(g, false), rect.X, rect.Y, rect.Width, rect.Height); if (HtmlContainer.SelectionForeColor != RColor.Empty && (word.SelectedStartOffset > 0 || word.SelectedEndIndexOffset > -1)) { g.PushClipExclude(rect); g.DrawString(word.Text, ActualFont, ActualColor, wordPoint, new RSize(word.Width, word.Height), isRtl); g.PopClip(); g.PushClip(rect); g.DrawString(word.Text, ActualFont, GetSelectionForeBrush(), wordPoint, new RSize(word.Width, word.Height), isRtl); g.PopClip(); } else { g.DrawString(word.Text, ActualFont, GetSelectionForeBrush(), wordPoint, new RSize(word.Width, word.Height), isRtl); } } else { // g.DrawRectangle(HtmlContainer.Adapter.GetPen(RColor.Black), wordPoint.X, wordPoint.Y, word.Width - 1, word.Height - 1); g.DrawString(word.Text, ActualFont, ActualColor, wordPoint, new RSize(word.Width, word.Height), isRtl); } } } } } /// /// Paints the text decoration (underline/strike-through/over-line) /// /// the device to draw into /// /// /// protected void PaintDecoration(RGraphics g, RRect rectangle, bool isFirst, bool isLast) { if (string.IsNullOrEmpty(TextDecoration) || TextDecoration == CssConstants.None) return; double y = 0f; if (TextDecoration == CssConstants.Underline) { y = Math.Round(rectangle.Top + ActualFont.UnderlineOffset); } else if (TextDecoration == CssConstants.LineThrough) { y = rectangle.Top + rectangle.Height / 2f; } else if (TextDecoration == CssConstants.Overline) { y = rectangle.Top; } y -= ActualPaddingBottom - ActualBorderBottomWidth; double x1 = rectangle.X; if (isFirst) x1 += ActualPaddingLeft + ActualBorderLeftWidth; double x2 = rectangle.Right; if (isLast) x2 -= ActualPaddingRight + ActualBorderRightWidth; var pen = g.GetPen(ActualColor); pen.Width = 1; pen.DashStyle = RDashStyle.Solid; g.DrawLine(pen, x1, y, x2, y); } /// /// Offsets the rectangle of the specified linebox by the specified gap, /// and goes deep for rectangles of children in that linebox. /// /// /// internal void OffsetRectangle(CssLineBox lineBox, double gap) { if (Rectangles.ContainsKey(lineBox)) { var r = Rectangles[lineBox]; Rectangles[lineBox] = new RRect(r.X, r.Y + gap, r.Width, r.Height); } } /// /// Resets the array /// internal void RectanglesReset() { _rectangles.Clear(); } /// /// On image load process complete with image request refresh for it to be painted. /// /// the image loaded or null if failed /// the source rectangle to draw in the image (empty - draw everything) /// is the callback was called async to load image call private void OnImageLoadComplete(RImage image, RRect rectangle, bool async) { if (image != null && async) HtmlContainer.RequestRefresh(false); } /// /// Get brush for the text depending if there is selected text color set. /// protected RColor GetSelectionForeBrush() { return HtmlContainer.SelectionForeColor != RColor.Empty ? HtmlContainer.SelectionForeColor : ActualColor; } /// /// Get brush for selection background depending if it has external and if alpha is required for images. /// /// /// used for images so they will have alpha effect protected RBrush GetSelectionBackBrush(RGraphics g, bool forceAlpha) { var backColor = HtmlContainer.SelectionBackColor; if (backColor != RColor.Empty) { if (forceAlpha && backColor.A > 180) return g.GetSolidBrush(RColor.FromArgb(180, backColor.R, backColor.G, backColor.B)); else return g.GetSolidBrush(backColor); } else { return g.GetSolidBrush(CssUtils.DefaultSelectionBackcolor); } } protected override RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st) { return HtmlContainer.Adapter.GetFont(fontFamily, fsize, st); } protected override RColor GetActualColor(string colorStr) { return HtmlContainer.CssParser.ParseColor(colorStr); } /// /// ToString override. /// /// public override string ToString() { var tag = HtmlTag != null ? string.Format("<{0}>", HtmlTag.Name) : "anon"; if (IsBlock) { return string.Format("{0}{1} Block {2}, Children:{3}", ParentBox == null ? "Root: " : string.Empty, tag, FontSize, Boxes.Count); } else if (Display == CssConstants.None) { return string.Format("{0}{1} None", ParentBox == null ? "Root: " : string.Empty, tag); } else { return string.Format("{0}{1} {2}: {3}", ParentBox == null ? "Root: " : string.Empty, tag, Display, Text); } } #endregion } }