// "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 TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core.Dom
{
///
/// Helps on CSS Layout.
///
internal static class CssLayoutEngine
{
///
/// Measure image box size by the width\height set on the box and the actual rendered image size.
/// If no image exists for the box error icon will be set.
///
/// the image word to measure
public static void MeasureImageSize(CssRectImage imageWord)
{
ArgChecker.AssertArgNotNull(imageWord, "imageWord");
ArgChecker.AssertArgNotNull(imageWord.OwnerBox, "imageWord.OwnerBox");
var width = new CssLength(imageWord.OwnerBox.Width);
var height = new CssLength(imageWord.OwnerBox.Height);
bool hasImageTagWidth = width.Number > 0 && width.Unit == CssUnit.Pixels;
bool hasImageTagHeight = height.Number > 0 && height.Unit == CssUnit.Pixels;
bool scaleImageHeight = false;
if (hasImageTagWidth)
{
imageWord.Width = width.Number;
}
else if (width.Number > 0 && width.IsPercentage)
{
imageWord.Width = width.Number * imageWord.OwnerBox.ContainingBlock.Size.Width;
scaleImageHeight = true;
}
else if (imageWord.Image != null)
{
imageWord.Width = imageWord.ImageRectangle == RRect.Empty ? imageWord.Image.Width : imageWord.ImageRectangle.Width;
}
else
{
imageWord.Width = hasImageTagHeight ? height.Number / 1.14f : 20;
}
var maxWidth = new CssLength(imageWord.OwnerBox.MaxWidth);
if (maxWidth.Number > 0)
{
double maxWidthVal = -1;
if (maxWidth.Unit == CssUnit.Pixels)
{
maxWidthVal = maxWidth.Number;
}
else if (maxWidth.IsPercentage)
{
maxWidthVal = maxWidth.Number * imageWord.OwnerBox.ContainingBlock.Size.Width;
}
if (maxWidthVal > -1 && imageWord.Width > maxWidthVal)
{
imageWord.Width = maxWidthVal;
scaleImageHeight = !hasImageTagHeight;
}
}
if (hasImageTagHeight)
{
imageWord.Height = height.Number;
}
else if (imageWord.Image != null)
{
imageWord.Height = imageWord.ImageRectangle == RRect.Empty ? imageWord.Image.Height : imageWord.ImageRectangle.Height;
}
else
{
imageWord.Height = imageWord.Width > 0 ? imageWord.Width * 1.14f : 22.8f;
}
if (imageWord.Image != null)
{
// If only the width was set in the html tag, ratio the height.
if ((hasImageTagWidth && !hasImageTagHeight) || scaleImageHeight)
{
// Divide the given tag width with the actual image width, to get the ratio.
double ratio = imageWord.Width / imageWord.Image.Width;
imageWord.Height = imageWord.Image.Height * ratio;
}
// If only the height was set in the html tag, ratio the width.
else if (hasImageTagHeight && !hasImageTagWidth)
{
// Divide the given tag height with the actual image height, to get the ratio.
double ratio = imageWord.Height / imageWord.Image.Height;
imageWord.Width = imageWord.Image.Width * ratio;
}
}
imageWord.Height += imageWord.OwnerBox.ActualBorderBottomWidth + imageWord.OwnerBox.ActualBorderTopWidth + imageWord.OwnerBox.ActualPaddingTop + imageWord.OwnerBox.ActualPaddingBottom;
}
///
/// Creates line boxes for the specified blockbox
///
///
///
public static void CreateLineBoxes(RGraphics g, CssBox blockBox)
{
ArgChecker.AssertArgNotNull(g, "g");
ArgChecker.AssertArgNotNull(blockBox, "blockBox");
blockBox.LineBoxes.Clear();
double limitRight = blockBox.ActualRight - blockBox.ActualPaddingRight - blockBox.ActualBorderRightWidth;
//Get the start x and y of the blockBox
double startx = blockBox.Location.X + blockBox.ActualPaddingLeft - 0 + blockBox.ActualBorderLeftWidth;
double starty = blockBox.Location.Y + blockBox.ActualPaddingTop - 0 + blockBox.ActualBorderTopWidth;
double curx = startx + blockBox.ActualTextIndent;
double cury = starty;
//Reminds the maximum bottom reached
double maxRight = startx;
double maxBottom = starty;
//First line box
CssLineBox line = new CssLineBox(blockBox);
//Flow words and boxes
FlowBox(g, blockBox, blockBox, limitRight, 0, startx, ref line, ref curx, ref cury, ref maxRight, ref maxBottom);
// if width is not restricted we need to lower it to the actual width
if (blockBox.ActualRight >= 90999)
{
blockBox.ActualRight = maxRight + blockBox.ActualPaddingRight + blockBox.ActualBorderRightWidth;
}
//Gets the rectangles for each line-box
foreach (var linebox in blockBox.LineBoxes)
{
ApplyAlignment(g, linebox);
ApplyRightToLeft(blockBox, linebox);
BubbleRectangles(blockBox, linebox);
linebox.AssignRectanglesToBoxes();
}
blockBox.ActualBottom = maxBottom + blockBox.ActualPaddingBottom + blockBox.ActualBorderBottomWidth;
// handle limiting block height when overflow is hidden
if (blockBox.Height != null && blockBox.Height != CssConstants.Auto && blockBox.Overflow == CssConstants.Hidden && blockBox.ActualBottom - blockBox.Location.Y > blockBox.ActualHeight)
{
blockBox.ActualBottom = blockBox.Location.Y + blockBox.ActualHeight;
}
}
///
/// Applies special vertical alignment for table-cells
///
///
///
public static void ApplyCellVerticalAlignment(RGraphics g, CssBox cell)
{
ArgChecker.AssertArgNotNull(g, "g");
ArgChecker.AssertArgNotNull(cell, "cell");
if (cell.VerticalAlign == CssConstants.Top || cell.VerticalAlign == CssConstants.Baseline)
return;
double cellbot = cell.ClientBottom;
double bottom = cell.GetMaximumBottom(cell, 0f);
double dist = 0f;
if (cell.VerticalAlign == CssConstants.Bottom)
{
dist = cellbot - bottom;
}
else if (cell.VerticalAlign == CssConstants.Middle)
{
dist = (cellbot - bottom) / 2;
}
foreach (CssBox b in cell.Boxes)
{
b.OffsetTop(dist);
}
//float top = cell.ClientTop;
//float bottom = cell.ClientBottom;
//bool middle = cell.VerticalAlign == CssConstants.Middle;
//foreach (LineBox line in cell.LineBoxes)
//{
// for (int i = 0; i < line.RelatedBoxes.Count; i++)
// {
// double diff = bottom - line.RelatedBoxes[i].Rectangles[line].Bottom;
// if (middle) diff /= 2f;
// RectangleF r = line.RelatedBoxes[i].Rectangles[line];
// line.RelatedBoxes[i].Rectangles[line] = new RectangleF(r.X, r.Y + diff, r.Width, r.Height);
// }
// foreach (BoxWord word in line.Words)
// {
// double gap = word.Top - top;
// word.Top = bottom - gap - word.Height;
// }
//}
}
#region Private methods
///
/// Recursively flows the content of the box using the inline model
///
/// Device Info
/// Blockbox that contains the text flow
/// Current box to flow its content
/// Maximum reached right
/// Space to use between rows of text
/// x starting coordinate for when breaking lines of text
/// Current linebox being used
/// Current x coordinate that will be the left of the next word
/// Current y coordinate that will be the top of the next word
/// Maximum right reached so far
/// Maximum bottom reached so far
private static void FlowBox(RGraphics g, CssBox blockbox, CssBox box, double limitRight, double linespacing, double startx, ref CssLineBox line, ref double curx, ref double cury, ref double maxRight, ref double maxbottom)
{
var startX = curx;
var startY = cury;
box.FirstHostingLineBox = line;
var localCurx = curx;
var localMaxRight = maxRight;
var localmaxbottom = maxbottom;
foreach (CssBox b in box.Boxes)
{
double leftspacing = b.Position != CssConstants.Absolute ? b.ActualMarginLeft + b.ActualBorderLeftWidth + b.ActualPaddingLeft : 0;
double rightspacing = b.Position != CssConstants.Absolute ? b.ActualMarginRight + b.ActualBorderRightWidth + b.ActualPaddingRight : 0;
b.RectanglesReset();
b.MeasureWordsSize(g);
curx += leftspacing;
if (b.Words.Count > 0)
{
bool wrapNoWrapBox = false;
if (b.WhiteSpace == CssConstants.NoWrap && curx > startx)
{
var boxRight = curx;
foreach (var word in b.Words)
boxRight += word.FullWidth;
if (boxRight > limitRight)
wrapNoWrapBox = true;
}
if (DomUtils.IsBoxHasWhitespace(b))
curx += box.ActualWordSpacing;
foreach (var word in b.Words)
{
if (maxbottom - cury < box.ActualLineHeight)
maxbottom += box.ActualLineHeight - (maxbottom - cury);
if ((b.WhiteSpace != CssConstants.NoWrap && b.WhiteSpace != CssConstants.Pre && curx + word.Width + rightspacing > limitRight
&& (b.WhiteSpace != CssConstants.PreWrap || !word.IsSpaces))
|| word.IsLineBreak || wrapNoWrapBox)
{
wrapNoWrapBox = false;
curx = startx;
// handle if line is wrapped for the first text element where parent has left margin\padding
if (b == box.Boxes[0] && !word.IsLineBreak && (word == b.Words[0] || (box.ParentBox != null && box.ParentBox.IsBlock)))
curx += box.ActualMarginLeft + box.ActualBorderLeftWidth + box.ActualPaddingLeft;
cury = maxbottom + linespacing;
line = new CssLineBox(blockbox);
if (word.IsImage || word.Equals(b.FirstWord))
{
curx += leftspacing;
}
}
line.ReportExistanceOf(word);
word.Left = curx;
word.Top = cury;
curx = word.Left + word.FullWidth;
maxRight = Math.Max(maxRight, word.Right);
maxbottom = Math.Max(maxbottom, word.Bottom);
if (b.Position == CssConstants.Absolute)
{
word.Left += box.ActualMarginLeft;
word.Top += box.ActualMarginTop;
}
}
}
else
{
FlowBox(g, blockbox, b, limitRight, linespacing, startx, ref line, ref curx, ref cury, ref maxRight, ref maxbottom);
}
curx += rightspacing;
}
// handle height setting
if (maxbottom - startY < box.ActualHeight)
{
maxbottom += box.ActualHeight - (maxbottom - startY);
}
// handle width setting
if (box.IsInline && 0 <= curx - startX && curx - startX < box.ActualWidth)
{
// hack for actual width handling
curx += box.ActualWidth - (curx - startX);
line.Rectangles.Add(box, new RRect(startX, startY, box.ActualWidth, box.ActualHeight));
}
// handle box that is only a whitespace
if (box.Text != null && box.Text.IsWhitespace() && !box.IsImage && box.IsInline && box.Boxes.Count == 0 && box.Words.Count == 0)
{
curx += box.ActualWordSpacing;
}
// hack to support specific absolute position elements
if (box.Position == CssConstants.Absolute)
{
curx = localCurx;
maxRight = localMaxRight;
maxbottom = localmaxbottom;
AdjustAbsolutePosition(box, 0, 0);
}
box.LastHostingLineBox = line;
}
///
/// Adjust the position of absolute elements by letf and top margins.
///
private static void AdjustAbsolutePosition(CssBox box, double left, double top)
{
left += box.ActualMarginLeft;
top += box.ActualMarginTop;
if (box.Words.Count > 0)
{
foreach (var word in box.Words)
{
word.Left += left;
word.Top += top;
}
}
else
{
foreach (var b in box.Boxes)
AdjustAbsolutePosition(b, left, top);
}
}
///
/// Recursively creates the rectangles of the blockBox, by bubbling from deep to outside of the boxes
/// in the rectangle structure
///
private static void BubbleRectangles(CssBox box, CssLineBox line)
{
if (box.Words.Count > 0)
{
double x = Single.MaxValue, y = Single.MaxValue, r = Single.MinValue, b = Single.MinValue;
List words = line.WordsOf(box);
if (words.Count > 0)
{
foreach (CssRect word in words)
{
// handle if line is wrapped for the first text element where parent has left margin\padding
var left = word.Left;
if (box == box.ParentBox.Boxes[0] && word == box.Words[0] && word == line.Words[0] && line != line.OwnerBox.LineBoxes[0] && !word.IsLineBreak)
left -= box.ParentBox.ActualMarginLeft + box.ParentBox.ActualBorderLeftWidth + box.ParentBox.ActualPaddingLeft;
x = Math.Min(x, left);
r = Math.Max(r, word.Right);
y = Math.Min(y, word.Top);
b = Math.Max(b, word.Bottom);
}
line.UpdateRectangle(box, x, y, r, b);
}
}
else
{
foreach (CssBox b in box.Boxes)
{
BubbleRectangles(b, line);
}
}
}
///
/// Applies vertical and horizontal alignment to words in lineboxes
///
///
///
private static void ApplyAlignment(RGraphics g, CssLineBox lineBox)
{
switch (lineBox.OwnerBox.TextAlign)
{
case CssConstants.Right:
ApplyRightAlignment(g, lineBox);
break;
case CssConstants.Center:
ApplyCenterAlignment(g, lineBox);
break;
case CssConstants.Justify:
ApplyJustifyAlignment(g, lineBox);
break;
default:
ApplyLeftAlignment(g, lineBox);
break;
}
ApplyVerticalAlignment(g, lineBox);
}
///
/// Applies right to left direction to words
///
///
///
private static void ApplyRightToLeft(CssBox blockBox, CssLineBox lineBox)
{
if (blockBox.Direction == CssConstants.Rtl)
{
ApplyRightToLeftOnLine(lineBox);
}
else
{
foreach (var box in lineBox.RelatedBoxes)
{
if (box.Direction == CssConstants.Rtl)
{
ApplyRightToLeftOnSingleBox(lineBox, box);
}
}
}
}
///
/// Applies RTL direction to all the words on the line.
///
/// the line to apply RTL to
private static void ApplyRightToLeftOnLine(CssLineBox line)
{
if (line.Words.Count > 0)
{
double left = line.Words[0].Left;
double right = line.Words[line.Words.Count - 1].Right;
foreach (CssRect word in line.Words)
{
double diff = word.Left - left;
double wright = right - diff;
word.Left = wright - word.Width;
}
}
}
///
/// Applies RTL direction to specific box words on the line.
///
///
///
private static void ApplyRightToLeftOnSingleBox(CssLineBox lineBox, CssBox box)
{
int leftWordIdx = -1;
int rightWordIdx = -1;
for (int i = 0; i < lineBox.Words.Count; i++)
{
if (lineBox.Words[i].OwnerBox == box)
{
if (leftWordIdx < 0)
leftWordIdx = i;
rightWordIdx = i;
}
}
if (leftWordIdx > -1 && rightWordIdx > leftWordIdx)
{
double left = lineBox.Words[leftWordIdx].Left;
double right = lineBox.Words[rightWordIdx].Right;
for (int i = leftWordIdx; i <= rightWordIdx; i++)
{
double diff = lineBox.Words[i].Left - left;
double wright = right - diff;
lineBox.Words[i].Left = wright - lineBox.Words[i].Width;
}
}
}
///
/// Applies vertical alignment to the linebox
///
///
///
private static void ApplyVerticalAlignment(RGraphics g, CssLineBox lineBox)
{
double baseline = Single.MinValue;
foreach (var box in lineBox.Rectangles.Keys)
{
baseline = Math.Max(baseline, lineBox.Rectangles[box].Top);
}
var boxes = new List(lineBox.Rectangles.Keys);
foreach (CssBox box in boxes)
{
//Important notes on http://www.w3.org/TR/CSS21/tables.html#height-layout
switch (box.VerticalAlign)
{
case CssConstants.Sub:
lineBox.SetBaseLine(g, box, baseline + lineBox.Rectangles[box].Height * .2f);
break;
case CssConstants.Super:
lineBox.SetBaseLine(g, box, baseline - lineBox.Rectangles[box].Height * .2f);
break;
case CssConstants.TextTop:
break;
case CssConstants.TextBottom:
break;
case CssConstants.Top:
break;
case CssConstants.Bottom:
break;
case CssConstants.Middle:
break;
default:
//case: baseline
lineBox.SetBaseLine(g, box, baseline);
break;
}
}
}
///
/// Applies centered alignment to the text on the linebox
///
///
///
private static void ApplyJustifyAlignment(RGraphics g, CssLineBox lineBox)
{
if (lineBox.Equals(lineBox.OwnerBox.LineBoxes[lineBox.OwnerBox.LineBoxes.Count - 1]))
return;
double indent = lineBox.Equals(lineBox.OwnerBox.LineBoxes[0]) ? lineBox.OwnerBox.ActualTextIndent : 0f;
double textSum = 0f;
double words = 0f;
double availWidth = lineBox.OwnerBox.ClientRectangle.Width - indent;
// Gather text sum
foreach (CssRect w in lineBox.Words)
{
textSum += w.Width;
words += 1f;
}
if (words <= 0f)
return; //Avoid Zero division
double spacing = (availWidth - textSum) / words; //Spacing that will be used
double curx = lineBox.OwnerBox.ClientLeft + indent;
foreach (CssRect word in lineBox.Words)
{
word.Left = curx;
curx = word.Right + spacing;
if (word == lineBox.Words[lineBox.Words.Count - 1])
{
word.Left = lineBox.OwnerBox.ClientRight - word.Width;
}
}
}
///
/// Applies centered alignment to the text on the linebox
///
///
///
private static void ApplyCenterAlignment(RGraphics g, CssLineBox line)
{
if (line.Words.Count == 0)
return;
CssRect lastWord = line.Words[line.Words.Count - 1];
double right = line.OwnerBox.ActualRight - line.OwnerBox.ActualPaddingRight - line.OwnerBox.ActualBorderRightWidth;
double diff = right - lastWord.Right - lastWord.OwnerBox.ActualBorderRightWidth - lastWord.OwnerBox.ActualPaddingRight;
diff /= 2;
if (diff > 0)
{
foreach (CssRect word in line.Words)
{
word.Left += diff;
}
foreach (CssBox b in line.Rectangles.Keys)
{
RRect r = b.Rectangles[line];
b.Rectangles[line] = new RRect(r.X + diff, r.Y, r.Width, r.Height);
}
}
}
///
/// Applies right alignment to the text on the linebox
///
///
///
private static void ApplyRightAlignment(RGraphics g, CssLineBox line)
{
if (line.Words.Count == 0)
return;
CssRect lastWord = line.Words[line.Words.Count - 1];
double right = line.OwnerBox.ActualRight - line.OwnerBox.ActualPaddingRight - line.OwnerBox.ActualBorderRightWidth;
double diff = right - lastWord.Right - lastWord.OwnerBox.ActualBorderRightWidth - lastWord.OwnerBox.ActualPaddingRight;
if (diff > 0)
{
foreach (CssRect word in line.Words)
{
word.Left += diff;
}
foreach (CssBox b in line.Rectangles.Keys)
{
RRect r = b.Rectangles[line];
b.Rectangles[line] = new RRect(r.X + diff, r.Y, r.Width, r.Height);
}
}
}
///
/// Simplest alignment, just arrange words.
///
///
///
private static void ApplyLeftAlignment(RGraphics g, CssLineBox line)
{
//No alignment needed.
//foreach (LineBoxRectangle r in line.Rectangles)
//{
// double curx = r.Left + (r.Index == 0 ? r.OwnerBox.ActualPaddingLeft + r.OwnerBox.ActualBorderLeftWidth / 2 : 0);
// if (r.SpaceBefore) curx += r.OwnerBox.ActualWordSpacing;
// foreach (BoxWord word in r.Words)
// {
// word.Left = curx;
// word.Top = r.Top;// +r.OwnerBox.ActualPaddingTop + r.OwnerBox.ActualBorderTopWidth / 2;
// curx = word.Right + r.OwnerBox.ActualWordSpacing;
// }
//}
}
#endregion
}
}