diff --git a/.gitignore b/.gitignore
index ce3a150ad..31fb5503a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -107,4 +107,5 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
-*.DotSettings
\ No newline at end of file
+*.DotSettings
+/Source/.vs/
diff --git a/Build/NuGet.exe b/Build/NuGet.exe
index 3ffdd33c6..d56c57880 100644
Binary files a/Build/NuGet.exe and b/Build/NuGet.exe differ
diff --git a/Build/NuGet/HtmlRenderer.PdfSharp.Core.nuspec b/Build/NuGet/HtmlRenderer.PdfSharp.Core.nuspec
new file mode 100644
index 000000000..0c1c60f19
--- /dev/null
+++ b/Build/NuGet/HtmlRenderer.PdfSharp.Core.nuspec
@@ -0,0 +1,30 @@
+
+
+
+ Polybioz.HtmlRenderer.PdfSharp.Core
+ 1.0.0
+ HTML Renderer for PDF using PdfSharp
+ Karel Hajek
+ Karel Hajek
+ LICENSE.txt
+ https://github.com/polybioz/HTML-Renderer-Core
+ false
+
+ HtmlRenderer.PdfSharp for .NET Core
+
+
+ HtmlRenderer.PdfSharp.Core is a partial port of HtmlRenderer.PdfSharp for .NET Core
+
+ José Manuel Menéndez Poo, Arthur Teplitzki
+ html render renderer draw pdfsharp .NETCore
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Build/NuGet/LICENSE.txt b/Build/NuGet/LICENSE.txt
new file mode 100644
index 000000000..ecd7e0153
--- /dev/null
+++ b/Build/NuGet/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2009, José Manuel Menéndez Poo
+Copyright (c) 2013, Arthur Teplitzki
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+ Neither the name of the menendezpoo.com, ArthurHub nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 5a21ca388..7d7fe3055 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,6 @@
-HTML Renderer [](https://ci.appveyor.com/project/ArthurHub/html-renderer)
+HTML Renderer Core
=============
-## Help Wanted
-* Looking for a contributor(s) to take this project forward as I'm unable to continue supporting it.
-* Contribute directly to the repository and update nuget packages.
-
-#### Documentation, Discussion and Issue tracking is on [CodePlex](https://htmlrenderer.codeplex.com/).
-
-**Cross framework** (WinForms/WPF/PDF/Metro/Mono/etc.), **Multipurpose** (UI Controls / Image generation / PDF generation / etc.), **100% managed** (C#), High performance HTML Rendering library.
-
-The library is 100% managed **C#** code without any external dependencies (no WebBrowser control, ActiveX / COM or MSHTML dll), the only requirement is **.NET 2.0 or higher**.
-
-#### [Download](https://htmlrenderer.codeplex.com/releases/) the [Demo application](https://htmlrenderer.codeplex.com/wikipage?title=Demo%20application&referringTitle=Home) to explore HTML Renderer capabilities.
-
-
-
-### Features and Benefits
-* Extensive HTML 4.01 and CSS level 2 specifications support.
-* Support separating CSS from HTML by loading stylesheet code separately.
-* Support text selection, copy-paste and context menu.
-* WinForms controls: HtmlPanel, HtmlLabel and HtmlToolTip.
-* WPF controls: HtmlPanel and HtmlLabel.
-* Works on Mono.
-* Create images/PDFs from HTML snippets.
-* Handles "real world" malformed HTML, it doesn't have to be XHTML.
-* 100% managed code and no external dependencies.
-* Supports .NET 2.0 or higher including Client Profile.
-* Lightweight, just two DLLs (~300K).
-* High performance and low memory footprint.
-* Extendable and configurable.
-* Powerful [Demo application](https://htmlrenderer.codeplex.com/wikipage?title=Demo%20application&referringTitle=Home) to explore and learn the library.
-
-### WinForms/WPF controls
-* *HtmlPanel* - The full power of HTML control build to replace WebBrowser control, accepts HTML, text selection, scrollbars, link click intercept, image load intercept and much more.
-* *HtmlLabel* - As WinForms label but accepts HTML, text selection, auto-size capabilities, transparent background and more.
-* *HtmlToolTip* - As WinForms ToolTip control but accepts HTML and ability to handle links (WinForms only).
-
-### Sample application's
-* Render HTML content generated by rich web editors like forums, blogs, etc.
-* Render Office documents converted to HTML.
-* Create WinForms UI that requires text selection with clipboard support.
-* [Create images from HTML code snippets](https://htmlrenderer.codeplex.com/wikipage?title=Image%20generation&referringTitle=Home).
-* Create PDF document from HTML code snippets.
-
### NuGet packages
-* [HtmlRenderer.WinForms](https://www.nuget.org/packages/HtmlRenderer.WinForms)
-* [HtmlRenderer.WPF](https://www.nuget.org/packages/HtmlRenderer.WPF)
-* [HtmlRenderer.Mono](https://www.nuget.org/packages/HtmlRenderer.Mono)
-* [HtmlRenderer.PdfSharp](https://www.nuget.org/packages/HtmlRenderer.PdfSharp)
-* [HtmlRenderer.Core](https://www.nuget.org/packages/HtmlRenderer.Core)
+* [Polybioz.HtmlRenderer.PdfSharp.Core](https://www.nuget.org/packages/Polybioz.HtmlRenderer.PdfSharp.Core)
-#### HTML Renderer on my blog
-[TheArtOfDev / HTML Renderer](http://theartofdev.com/html-renderer/)
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RColor.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RColor.cs
new file mode 100644
index 000000000..83fb26a12
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RColor.cs
@@ -0,0 +1,273 @@
+// Type: System.Drawing.Color
+// Assembly: System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+// Assembly location: C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll
+
+using System;
+using System.Text;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Represents an ARGB (alpha, red, green, blue) color.
+ ///
+ public struct RColor
+ {
+ #region Fields and Consts
+
+ ///
+ /// Represents a color that is null.
+ ///
+ /// 1
+ public static readonly RColor Empty = new RColor();
+
+ private readonly long _value;
+
+ #endregion
+
+
+ private RColor(long value)
+ {
+ _value = value;
+ }
+
+ ///
+ /// Gets a system-defined color.
+ ///
+ public static RColor Transparent
+ {
+ get { return new RColor(0); }
+ }
+
+ ///
+ /// Gets a system-defined color that has an ARGB value of #FF000000.
+ ///
+ public static RColor Black
+ {
+ get { return FromArgb(0, 0, 0); }
+ }
+
+ ///
+ /// Gets a system-defined color that has an ARGB value of #FFFFFFFF.
+ ///
+ public static RColor White
+ {
+ get { return FromArgb(255, 255, 255); }
+ }
+
+ ///
+ /// Gets a system-defined color that has an ARGB value of #FFF5F5F5.
+ ///
+ public static RColor WhiteSmoke
+ {
+ get { return FromArgb(245, 245, 245); }
+ }
+
+ ///
+ /// Gets a system-defined color that has an ARGB value of #FFD3D3D3.
+ ///
+ public static RColor LightGray
+ {
+ get { return FromArgb(211, 211, 211); }
+ }
+
+ ///
+ /// Gets the red component value of this structure.
+ ///
+ public byte R
+ {
+ get { return (byte)((ulong)(_value >> 16) & byte.MaxValue); }
+ }
+
+ ///
+ /// Gets the green component value of this structure.
+ ///
+ public byte G
+ {
+ get { return (byte)((ulong)(_value >> 8) & byte.MaxValue); }
+ }
+
+ ///
+ /// Gets the blue component value of this structure.
+ ///
+ public byte B
+ {
+ get { return (byte)((ulong)_value & byte.MaxValue); }
+ }
+
+ ///
+ /// Gets the alpha component value of this structure.
+ ///
+ public byte A
+ {
+ get { return (byte)((ulong)(_value >> 24) & byte.MaxValue); }
+ }
+
+ ///
+ /// Specifies whether this structure is uninitialized.
+ ///
+ ///
+ /// This property returns true if this color is uninitialized; otherwise, false.
+ ///
+ /// 1
+ public bool IsEmpty
+ {
+ get { return _value == 0; }
+ }
+
+ ///
+ /// Tests whether two specified structures are equivalent.
+ ///
+ ///
+ /// true if the two structures are equal; otherwise, false.
+ ///
+ ///
+ /// The that is to the left of the equality operator.
+ ///
+ ///
+ /// The that is to the right of the equality operator.
+ ///
+ /// 3
+ public static bool operator ==(RColor left, RColor right)
+ {
+ return left._value == right._value;
+ }
+
+ ///
+ /// Tests whether two specified structures are different.
+ ///
+ ///
+ /// true if the two structures are different; otherwise, false.
+ ///
+ ///
+ /// The that is to the left of the inequality operator.
+ ///
+ ///
+ /// The that is to the right of the inequality operator.
+ ///
+ /// 3
+ public static bool operator !=(RColor left, RColor right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Creates a structure from the four ARGB component (alpha, red, green, and blue) values. Although this method allows a 32-bit value to be passed for each component, the value of each component is limited to 8 bits.
+ ///
+ ///
+ /// The that this method creates.
+ ///
+ /// The alpha component. Valid values are 0 through 255.
+ /// The red component. Valid values are 0 through 255.
+ /// The green component. Valid values are 0 through 255.
+ /// The blue component. Valid values are 0 through 255.
+ ///
+ /// , , , or is less than 0 or greater than 255.
+ ///
+ /// 1
+ public static RColor FromArgb(int alpha, int red, int green, int blue)
+ {
+ CheckByte(alpha);
+ CheckByte(red);
+ CheckByte(green);
+ CheckByte(blue);
+ return new RColor((uint)(red << 16 | green << 8 | blue | alpha << 24) & (long)uint.MaxValue);
+ }
+
+ ///
+ /// Creates a structure from the specified 8-bit color values (red, green, and blue). The alpha value is implicitly 255 (fully opaque). Although this method allows a 32-bit value to be passed for each color component, the value of each component is limited to 8 bits.
+ ///
+ ///
+ /// The that this method creates.
+ ///
+ ///
+ /// The red component value for the new . Valid values are 0 through 255.
+ ///
+ ///
+ /// The green component value for the new . Valid values are 0 through 255.
+ ///
+ ///
+ /// The blue component value for the new . Valid values are 0 through 255.
+ ///
+ ///
+ /// , , or is less than 0 or greater than 255.
+ ///
+ /// 1
+ public static RColor FromArgb(int red, int green, int blue)
+ {
+ return FromArgb(byte.MaxValue, red, green, blue);
+ }
+
+ ///
+ /// Tests whether the specified object is a structure and is equivalent to this
+ ///
+ /// structure.
+ ///
+ ///
+ /// true if is a structure equivalent to this
+ ///
+ /// structure; otherwise, false.
+ ///
+ /// The object to test.
+ /// 1
+ public override bool Equals(object obj)
+ {
+ if (obj is RColor)
+ {
+ var color = (RColor)obj;
+ return _value == color._value;
+ }
+ return false;
+ }
+
+ ///
+ /// Returns a hash code for this structure.
+ ///
+ ///
+ /// An integer value that specifies the hash code for this .
+ ///
+ /// 1
+ public override int GetHashCode()
+ {
+ return _value.GetHashCode();
+ }
+
+ ///
+ /// Converts this structure to a human-readable string.
+ ///
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder(32);
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" [");
+ if (_value != 0)
+ {
+ stringBuilder.Append("A=");
+ stringBuilder.Append(A);
+ stringBuilder.Append(", R=");
+ stringBuilder.Append(R);
+ stringBuilder.Append(", G=");
+ stringBuilder.Append(G);
+ stringBuilder.Append(", B=");
+ stringBuilder.Append(B);
+ }
+ else
+ stringBuilder.Append("Empty");
+ stringBuilder.Append("]");
+ return stringBuilder.ToString();
+ }
+
+
+ #region Private methods
+
+ private static void CheckByte(int value)
+ {
+ if (value >= 0 && value <= byte.MaxValue)
+ return;
+ throw new ArgumentException("InvalidEx2BoundArgument");
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RDashStyle.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RDashStyle.cs
new file mode 100644
index 000000000..d569303eb
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RDashStyle.cs
@@ -0,0 +1,27 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Specifies the style of dashed lines drawn with a object.
+ ///
+ public enum RDashStyle
+ {
+ Solid,
+ Dash,
+ Dot,
+ DashDot,
+ DashDotDot,
+ Custom,
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RFontStyle.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RFontStyle.cs
new file mode 100644
index 000000000..f2c62b29d
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RFontStyle.cs
@@ -0,0 +1,29 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Specifies style information applied to text.
+ ///
+ [Flags]
+ public enum RFontStyle
+ {
+ Regular = 0,
+ Bold = 1,
+ Italic = 2,
+ Underline = 4,
+ Strikeout = 8,
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RKeyEvent.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RKeyEvent.cs
new file mode 100644
index 000000000..3f8f49a68
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RKeyEvent.cs
@@ -0,0 +1,71 @@
+// "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 TheArtOfDev.HtmlRenderer.Core;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Even class for handling keyboard events in .
+ ///
+ public sealed class RKeyEvent
+ {
+ ///
+ /// is control is pressed
+ ///
+ private readonly bool _control;
+
+ ///
+ /// is 'A' key is pressed
+ ///
+ private readonly bool _aKeyCode;
+
+ ///
+ /// is 'C' key is pressed
+ ///
+ private readonly bool _cKeyCode;
+
+ ///
+ /// Init.
+ ///
+ public RKeyEvent(bool control, bool aKeyCode, bool cKeyCode)
+ {
+ _control = control;
+ _aKeyCode = aKeyCode;
+ _cKeyCode = cKeyCode;
+ }
+
+ ///
+ /// is control is pressed
+ ///
+ public bool Control
+ {
+ get { return _control; }
+ }
+
+ ///
+ /// is 'A' key is pressed
+ ///
+ public bool AKeyCode
+ {
+ get { return _aKeyCode; }
+ }
+
+ ///
+ /// is 'C' key is pressed
+ ///
+ public bool CKeyCode
+ {
+ get { return _cKeyCode; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RMouseEvent.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RMouseEvent.cs
new file mode 100644
index 000000000..e0aa88d19
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RMouseEvent.cs
@@ -0,0 +1,43 @@
+// "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 TheArtOfDev.HtmlRenderer.Core;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Even class for handling keyboard events in .
+ ///
+ public sealed class RMouseEvent
+ {
+ ///
+ /// Is the left mouse button participated in the event
+ ///
+ private readonly bool _leftButton;
+
+ ///
+ /// Init.
+ ///
+ public RMouseEvent(bool leftButton)
+ {
+ _leftButton = leftButton;
+ }
+
+ ///
+ /// Is the left mouse button participated in the event
+ ///
+ public bool LeftButton
+ {
+ get { return _leftButton; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RPoint.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RPoint.cs
new file mode 100644
index 000000000..eb36c8b47
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RPoint.cs
@@ -0,0 +1,293 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Represents an ordered pair of floating-point x- and y-coordinates that defines a point in a two-dimensional plane.
+ ///
+ public struct RPoint
+ {
+ ///
+ /// Represents a new instance of the class with member data left uninitialized.
+ ///
+ /// 1
+ public static readonly RPoint Empty = new RPoint();
+
+ private double _x;
+ private double _y;
+
+ static RPoint()
+ { }
+
+ ///
+ /// Initializes a new instance of the class with the specified coordinates.
+ ///
+ /// The horizontal position of the point.
+ /// The vertical position of the point.
+ public RPoint(double x, double y)
+ {
+ _x = x;
+ _y = y;
+ }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ ///
+ /// true if both and
+ ///
+ /// are 0; otherwise, false.
+ ///
+ /// 1
+ public bool IsEmpty
+ {
+ get
+ {
+ if (Math.Abs(_x - 0.0) < 0.001)
+ return Math.Abs(_y - 0.0) < 0.001;
+ else
+ return false;
+ }
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of this .
+ ///
+ ///
+ /// The x-coordinate of this .
+ ///
+ /// 1
+ public double X
+ {
+ get { return _x; }
+ set { _x = value; }
+ }
+
+ ///
+ /// Gets or sets the y-coordinate of this .
+ ///
+ ///
+ /// The y-coordinate of this .
+ ///
+ /// 1
+ public double Y
+ {
+ get { return _y; }
+ set { _y = value; }
+ }
+
+ ///
+ /// Translates the by the specified
+ ///
+ /// .
+ ///
+ ///
+ /// The translated .
+ ///
+ ///
+ /// The to translate.
+ ///
+ ///
+ /// The that specifies the numbers to add to the x- and y-coordinates of the
+ ///
+ /// .
+ ///
+ public static RPoint operator +(RPoint pt, RSize sz)
+ {
+ return Add(pt, sz);
+ }
+
+ ///
+ /// Translates a by the negative of a specified
+ ///
+ /// .
+ ///
+ ///
+ /// The translated .
+ ///
+ ///
+ /// The to translate.
+ ///
+ ///
+ /// The that specifies the numbers to subtract from the coordinates of
+ ///
+ /// .
+ ///
+ public static RPoint operator -(RPoint pt, RSize sz)
+ {
+ return Subtract(pt, sz);
+ }
+
+ ///
+ /// Compares two structures. The result specifies whether the values of the
+ ///
+ /// and properties of the two
+ ///
+ /// structures are equal.
+ ///
+ ///
+ /// true if the and
+ ///
+ /// values of the left and right
+ ///
+ /// structures are equal; otherwise, false.
+ ///
+ ///
+ /// A to compare.
+ ///
+ ///
+ /// A to compare.
+ ///
+ /// 3
+ public static bool operator ==(RPoint left, RPoint right)
+ {
+ if (left.X == right.X)
+ return left.Y == right.Y;
+ else
+ return false;
+ }
+
+ ///
+ /// Determines whether the coordinates of the specified points are not equal.
+ ///
+ ///
+ /// true to indicate the and
+ ///
+ /// values of and
+ ///
+ /// are not equal; otherwise, false.
+ ///
+ ///
+ /// A to compare.
+ ///
+ ///
+ /// A to compare.
+ ///
+ /// 3
+ public static bool operator !=(RPoint left, RPoint right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Translates a given by a specified
+ ///
+ /// .
+ ///
+ ///
+ /// The translated .
+ ///
+ ///
+ /// The to translate.
+ ///
+ ///
+ /// The that specifies the numbers to add to the coordinates of
+ ///
+ /// .
+ ///
+ public static RPoint Add(RPoint pt, RSize sz)
+ {
+ return new RPoint(pt.X + sz.Width, pt.Y + sz.Height);
+ }
+
+ ///
+ /// Translates a by the negative of a specified size.
+ ///
+ ///
+ /// The translated .
+ ///
+ ///
+ /// The to translate.
+ ///
+ ///
+ /// The that specifies the numbers to subtract from the coordinates of
+ ///
+ /// .
+ ///
+ public static RPoint Subtract(RPoint pt, RSize sz)
+ {
+ return new RPoint(pt.X - sz.Width, pt.Y - sz.Height);
+ }
+
+ ///
+ /// Specifies whether this contains the same coordinates as the specified
+ ///
+ /// .
+ ///
+ ///
+ /// This method returns true if is a and has the same coordinates as this
+ ///
+ /// .
+ ///
+ ///
+ /// The to test.
+ ///
+ /// 1
+ public override bool Equals(object obj)
+ {
+ if (!(obj is RPoint))
+ return false;
+ var pointF = (RPoint)obj;
+ if (pointF.X == X && pointF.Y == Y)
+ return pointF.GetType().Equals(GetType());
+ else
+ return false;
+ }
+
+ ///
+ /// Returns a hash code for this structure.
+ ///
+ ///
+ /// An integer value that specifies a hash value for this structure.
+ ///
+ /// 1
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ ///
+ /// Converts this to a human readable string.
+ ///
+ ///
+ /// A string that represents this .
+ ///
+ /// 1
+ public override string ToString()
+ {
+ return string.Format("{{X={0}, Y={1}}}", new object[]
+ {
+ _x,
+ _y
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RRect.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RRect.cs
new file mode 100644
index 000000000..3ae148655
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RRect.cs
@@ -0,0 +1,506 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Stores a set of four floating-point numbers that represent the location and size of a rectangle.
+ ///
+ public struct RRect
+ {
+ #region Fields and Consts
+
+ ///
+ /// Represents an instance of the class with its members uninitialized.
+ ///
+ public static readonly RRect Empty = new RRect();
+
+ private double _height;
+ private double _width;
+
+ private double _x;
+ private double _y;
+
+ #endregion
+
+
+ ///
+ /// Initializes a new instance of the class with the specified location and size.
+ ///
+ /// The x-coordinate of the upper-left corner of the rectangle.
+ /// The y-coordinate of the upper-left corner of the rectangle.
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RRect(double x, double y, double width, double height)
+ {
+ _x = x;
+ _y = y;
+ _width = width;
+ _height = height;
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified location and size.
+ ///
+ /// A that represents the upper-left corner of the rectangular region.
+ /// A that represents the width and height of the rectangular region.
+ public RRect(RPoint location, RSize size)
+ {
+ _x = location.X;
+ _y = location.Y;
+ _width = size.Width;
+ _height = size.Height;
+ }
+
+ ///
+ /// Gets or sets the coordinates of the upper-left corner of this structure.
+ ///
+ /// A that represents the upper-left corner of this structure.
+ public RPoint Location
+ {
+ get { return new RPoint(X, Y); }
+ set
+ {
+ X = value.X;
+ Y = value.Y;
+ }
+ }
+
+ ///
+ /// Gets or sets the size of this .
+ ///
+ /// A that represents the width and height of this structure.
+ public RSize Size
+ {
+ get { return new RSize(Width, Height); }
+ set
+ {
+ Width = value.Width;
+ Height = value.Height;
+ }
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of the upper-left corner of this structure.
+ ///
+ ///
+ /// The x-coordinate of the upper-left corner of this structure.
+ ///
+ public double X
+ {
+ get { return _x; }
+ set { _x = value; }
+ }
+
+ ///
+ /// Gets or sets the y-coordinate of the upper-left corner of this structure.
+ ///
+ ///
+ /// The y-coordinate of the upper-left corner of this structure.
+ ///
+ public double Y
+ {
+ get { return _y; }
+ set { _y = value; }
+ }
+
+ ///
+ /// Gets or sets the width of this structure.
+ ///
+ ///
+ /// The width of this structure.
+ ///
+ public double Width
+ {
+ get { return _width; }
+ set { _width = value; }
+ }
+
+ ///
+ /// Gets or sets the height of this structure.
+ ///
+ ///
+ /// The height of this structure.
+ ///
+ public double Height
+ {
+ get { return _height; }
+ set { _height = value; }
+ }
+
+ ///
+ /// Gets the x-coordinate of the left edge of this structure.
+ ///
+ ///
+ /// The x-coordinate of the left edge of this structure.
+ ///
+ public double Left
+ {
+ get { return X; }
+ }
+
+ ///
+ /// Gets the y-coordinate of the top edge of this structure.
+ ///
+ ///
+ /// The y-coordinate of the top edge of this structure.
+ ///
+ public double Top
+ {
+ get { return Y; }
+ }
+
+ ///
+ /// Gets the x-coordinate that is the sum of and
+ ///
+ /// of this structure.
+ ///
+ ///
+ /// The x-coordinate that is the sum of and
+ ///
+ /// of this structure.
+ ///
+ public double Right
+ {
+ get { return X + Width; }
+ }
+
+ ///
+ /// Gets the y-coordinate that is the sum of and
+ ///
+ /// of this structure.
+ ///
+ ///
+ /// The y-coordinate that is the sum of and
+ ///
+ /// of this structure.
+ ///
+ public double Bottom
+ {
+ get { return Y + Height; }
+ }
+
+ ///
+ /// Tests whether the or
+ ///
+ /// property of this has a value of zero.
+ ///
+ ///
+ /// This property returns true if the or
+ ///
+ /// property of this has a value of zero; otherwise, false.
+ ///
+ public bool IsEmpty
+ {
+ get
+ {
+ if (Width > 0.0)
+ return Height <= 0.0;
+ else
+ return true;
+ }
+ }
+
+ ///
+ /// Tests whether two structures have equal location and size.
+ ///
+ ///
+ /// This operator returns true if the two specified structures have equal
+ /// , , , and properties.
+ ///
+ ///
+ /// The structure that is to the left of the equality operator.
+ ///
+ ///
+ /// The structure that is to the right of the equality operator.
+ ///
+ public static bool operator ==(RRect left, RRect right)
+ {
+ if (Math.Abs(left.X - right.X) < 0.001 && Math.Abs(left.Y - right.Y) < 0.001 && Math.Abs(left.Width - right.Width) < 0.001)
+ return Math.Abs(left.Height - right.Height) < 0.001;
+ else
+ return false;
+ }
+
+ ///
+ /// Tests whether two structures differ in location or size.
+ ///
+ ///
+ /// This operator returns true if any of the ,
+ /// , , or
+ /// properties of the two structures are unequal; otherwise false.
+ ///
+ ///
+ /// The structure that is to the left of the inequality operator.
+ ///
+ ///
+ /// The structure that is to the right of the inequality operator.
+ ///
+ public static bool operator !=(RRect left, RRect right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Creates a structure with upper-left corner and lower-right corner at the specified locations.
+ ///
+ ///
+ /// The new that this method creates.
+ ///
+ /// The x-coordinate of the upper-left corner of the rectangular region.
+ /// The y-coordinate of the upper-left corner of the rectangular region.
+ /// The x-coordinate of the lower-right corner of the rectangular region.
+ /// The y-coordinate of the lower-right corner of the rectangular region.
+ public static RRect FromLTRB(double left, double top, double right, double bottom)
+ {
+ return new RRect(left, top, right - left, bottom - top);
+ }
+
+ ///
+ /// Tests whether is a with the same location and size of this
+ /// .
+ ///
+ ///
+ /// This method returns true if is a and its X, Y, Width, and Height properties are equal to the corresponding properties of this
+ /// ; otherwise, false.
+ ///
+ ///
+ /// The to test.
+ ///
+ public override bool Equals(object obj)
+ {
+ if (!(obj is RRect))
+ return false;
+ var rectangleF = (RRect)obj;
+ if (Math.Abs(rectangleF.X - X) < 0.001 && Math.Abs(rectangleF.Y - Y) < 0.001 && Math.Abs(rectangleF.Width - Width) < 0.001)
+ return Math.Abs(rectangleF.Height - Height) < 0.001;
+ else
+ return false;
+ }
+
+ ///
+ /// Determines if the specified point is contained within this structure.
+ ///
+ ///
+ /// This method returns true if the point defined by and is contained within this
+ ///
+ /// structure; otherwise false.
+ ///
+ /// The x-coordinate of the point to test.
+ /// The y-coordinate of the point to test.
+ public bool Contains(double x, double y)
+ {
+ if (X <= x && x < X + Width && Y <= y)
+ return y < Y + Height;
+ else
+ return false;
+ }
+
+ ///
+ /// Determines if the specified point is contained within this structure.
+ ///
+ ///
+ /// This method returns true if the point represented by the parameter is contained within this
+ ///
+ /// structure; otherwise false.
+ ///
+ /// The to test.
+ public bool Contains(RPoint pt)
+ {
+ return Contains(pt.X, pt.Y);
+ }
+
+ ///
+ /// Determines if the rectangular region represented by is entirely contained within this
+ ///
+ /// structure.
+ ///
+ ///
+ /// This method returns true if the rectangular region represented by is entirely contained within the rectangular region represented by this
+ ///
+ /// ; otherwise false.
+ ///
+ ///
+ /// The to test.
+ ///
+ public bool Contains(RRect rect)
+ {
+ if (X <= rect.X && rect.X + rect.Width <= X + Width && Y <= rect.Y)
+ return rect.Y + rect.Height <= Y + Height;
+ else
+ return false;
+ }
+
+ ///
+ /// Inflates this structure by the specified amount.
+ ///
+ ///
+ /// The amount to inflate this structure horizontally.
+ ///
+ ///
+ /// The amount to inflate this structure vertically.
+ ///
+ public void Inflate(double x, double y)
+ {
+ X -= x;
+ Y -= y;
+ Width += 2f * x;
+ Height += 2f * y;
+ }
+
+ ///
+ /// Inflates this by the specified amount.
+ ///
+ /// The amount to inflate this rectangle.
+ public void Inflate(RSize size)
+ {
+ Inflate(size.Width, size.Height);
+ }
+
+ ///
+ /// Creates and returns an inflated copy of the specified structure. The copy is inflated by the specified amount. The original rectangle remains unmodified.
+ ///
+ ///
+ /// The inflated .
+ ///
+ ///
+ /// The to be copied. This rectangle is not modified.
+ ///
+ /// The amount to inflate the copy of the rectangle horizontally.
+ /// The amount to inflate the copy of the rectangle vertically.
+ public static RRect Inflate(RRect rect, double x, double y)
+ {
+ RRect rectangleF = rect;
+ rectangleF.Inflate(x, y);
+ return rectangleF;
+ }
+
+ ///
+ /// Replaces this structure with the intersection of itself and the specified
+ ///
+ /// structure.
+ ///
+ /// The rectangle to intersect.
+ public void Intersect(RRect rect)
+ {
+ RRect rectangleF = Intersect(rect, this);
+ X = rectangleF.X;
+ Y = rectangleF.Y;
+ Width = rectangleF.Width;
+ Height = rectangleF.Height;
+ }
+
+ ///
+ /// Returns a structure that represents the intersection of two rectangles. If there is no intersection, and empty
+ ///
+ /// is returned.
+ ///
+ ///
+ /// A third structure the size of which represents the overlapped area of the two specified rectangles.
+ ///
+ /// A rectangle to intersect.
+ /// A rectangle to intersect.
+ public static RRect Intersect(RRect a, RRect b)
+ {
+ double x = Math.Max(a.X, b.X);
+ double num1 = Math.Min(a.X + a.Width, b.X + b.Width);
+ double y = Math.Max(a.Y, b.Y);
+ double num2 = Math.Min(a.Y + a.Height, b.Y + b.Height);
+ if (num1 >= x && num2 >= y)
+ return new RRect(x, y, num1 - x, num2 - y);
+ else
+ return Empty;
+ }
+
+ ///
+ /// Determines if this rectangle intersects with .
+ ///
+ ///
+ /// This method returns true if there is any intersection.
+ ///
+ /// The rectangle to test.
+ public bool IntersectsWith(RRect rect)
+ {
+ if (rect.X < X + Width && X < rect.X + rect.Width && rect.Y < Y + Height)
+ return Y < rect.Y + rect.Height;
+ else
+ return false;
+ }
+
+ ///
+ /// Creates the smallest possible third rectangle that can contain both of two rectangles that form a union.
+ ///
+ ///
+ /// A third structure that contains both of the two rectangles that form the union.
+ ///
+ /// A rectangle to union.
+ /// A rectangle to union.
+ public static RRect Union(RRect a, RRect b)
+ {
+ double x = Math.Min(a.X, b.X);
+ double num1 = Math.Max(a.X + a.Width, b.X + b.Width);
+ double y = Math.Min(a.Y, b.Y);
+ double num2 = Math.Max(a.Y + a.Height, b.Y + b.Height);
+ return new RRect(x, y, num1 - x, num2 - y);
+ }
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The amount to offset the location.
+ public void Offset(RPoint pos)
+ {
+ Offset(pos.X, pos.Y);
+ }
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The amount to offset the location horizontally.
+ /// The amount to offset the location vertically.
+ public void Offset(double x, double y)
+ {
+ X += x;
+ Y += y;
+ }
+
+ ///
+ /// Gets the hash code for this structure. For information about the use of hash codes, see Object.GetHashCode.
+ ///
+ /// The hash code for this
+ public override int GetHashCode()
+ {
+ return (int)(uint)X ^ ((int)(uint)Y << 13 | (int)((uint)Y >> 19)) ^ ((int)(uint)Width << 26 | (int)((uint)Width >> 6)) ^ ((int)(uint)Height << 7 | (int)((uint)Height >> 25));
+ }
+
+ ///
+ /// Converts the Location and Size of this to a human-readable string.
+ ///
+ ///
+ /// A string that contains the position, width, and height of this structure for example, "{X=20, Y=20, Width=100, Height=50}".
+ ///
+ public override string ToString()
+ {
+ return "{X=" + X + ",Y=" + Y + ",Width=" + Width + ",Height=" + Height + "}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/Entities/RSize.cs b/Source/HtmlRenderer.Core/Adapters/Entities/RSize.cs
new file mode 100644
index 000000000..521bce308
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/Entities/RSize.cs
@@ -0,0 +1,341 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters.Entities
+{
+ ///
+ /// Stores an ordered pair of floating-point numbers, typically the width and height of a rectangle.
+ ///
+ public struct RSize
+ {
+ #region Fields and Consts
+
+ ///
+ /// Gets a structure that has a
+ ///
+ /// and
+ ///
+ /// value of 0.
+ ///
+ ///
+ /// A structure that has a
+ ///
+ /// and
+ ///
+ /// value of 0.
+ ///
+ /// 1
+ public static readonly RSize Empty = new RSize();
+
+ private double _height;
+ private double _width;
+
+ #endregion
+
+
+ ///
+ /// Initializes a new instance of the structure from the specified existing
+ ///
+ /// structure.
+ ///
+ ///
+ /// The structure from which to create the new
+ ///
+ /// structure.
+ ///
+ public RSize(RSize size)
+ {
+ _width = size._width;
+ _height = size._height;
+ }
+
+ ///
+ /// Initializes a new instance of the structure from the specified structure.
+ ///
+ /// The structure from which to initialize this structure.
+ public RSize(RPoint pt)
+ {
+ _width = pt.X;
+ _height = pt.Y;
+ }
+
+ ///
+ /// Initializes a new instance of the structure from the specified dimensions.
+ ///
+ ///
+ /// The width component of the new structure.
+ ///
+ ///
+ /// The height component of the new structure.
+ ///
+ public RSize(double width, double height)
+ {
+ _width = width;
+ _height = height;
+ }
+
+ ///
+ /// Gets a value that indicates whether this structure has zero width and height.
+ ///
+ ///
+ /// This property returns true when this structure has both a width and height of zero; otherwise, false.
+ ///
+ /// 1
+ public bool IsEmpty
+ {
+ get
+ {
+ if (Math.Abs(_width) < 0.0001)
+ return Math.Abs(_height) < 0.0001;
+ else
+ return false;
+ }
+ }
+
+ ///
+ /// Gets or sets the horizontal component of this structure.
+ ///
+ ///
+ /// The horizontal component of this structure, typically measured in pixels.
+ ///
+ /// 1
+ public double Width
+ {
+ get { return _width; }
+ set { _width = value; }
+ }
+
+ ///
+ /// Gets or sets the vertical component of this structure.
+ ///
+ ///
+ /// The vertical component of this structure, typically measured in pixels.
+ ///
+ /// 1
+ public double Height
+ {
+ get { return _height; }
+ set { _height = value; }
+ }
+
+ ///
+ /// Converts the specified structure to a
+ /// structure.
+ ///
+ /// The structure to which this operator converts.
+ /// The structure to be converted
+ ///
+ public static explicit operator RPoint(RSize size)
+ {
+ return new RPoint(size.Width, size.Height);
+ }
+
+ ///
+ /// Adds the width and height of one structure to the width and height of another
+ ///
+ /// structure.
+ ///
+ ///
+ /// A structure that is the result of the addition operation.
+ ///
+ ///
+ /// The first structure to add.
+ ///
+ ///
+ /// The second structure to add.
+ ///
+ /// 3
+ public static RSize operator +(RSize sz1, RSize sz2)
+ {
+ return Add(sz1, sz2);
+ }
+
+ ///
+ /// Subtracts the width and height of one structure from the width and height of another
+ ///
+ /// structure.
+ ///
+ ///
+ /// A that is the result of the subtraction operation.
+ ///
+ ///
+ /// The structure on the left side of the subtraction operator.
+ ///
+ ///
+ /// The structure on the right side of the subtraction operator.
+ ///
+ /// 3
+ public static RSize operator -(RSize sz1, RSize sz2)
+ {
+ return Subtract(sz1, sz2);
+ }
+
+ ///
+ /// Tests whether two structures are equal.
+ ///
+ ///
+ /// This operator returns true if and have equal width and height; otherwise, false.
+ ///
+ ///
+ /// The structure on the left side of the equality operator.
+ ///
+ ///
+ /// The structure on the right of the equality operator.
+ ///
+ /// 3
+ public static bool operator ==(RSize sz1, RSize sz2)
+ {
+ if (Math.Abs(sz1.Width - sz2.Width) < 0.001)
+ return Math.Abs(sz1.Height - sz2.Height) < 0.001;
+ else
+ return false;
+ }
+
+ ///
+ /// Tests whether two structures are different.
+ ///
+ ///
+ /// This operator returns true if and differ either in width or height; false if
+ ///
+ /// and are equal.
+ ///
+ ///
+ /// The structure on the left of the inequality operator.
+ ///
+ ///
+ /// The structure on the right of the inequality operator.
+ ///
+ /// 3
+ public static bool operator !=(RSize sz1, RSize sz2)
+ {
+ return !(sz1 == sz2);
+ }
+
+ ///
+ /// Adds the width and height of one structure to the width and height of another
+ ///
+ /// structure.
+ ///
+ ///
+ /// A structure that is the result of the addition operation.
+ ///
+ ///
+ /// The first structure to add.
+ ///
+ ///
+ /// The second structure to add.
+ ///
+ public static RSize Add(RSize sz1, RSize sz2)
+ {
+ return new RSize(sz1.Width + sz2.Width, sz1.Height + sz2.Height);
+ }
+
+ ///
+ /// Subtracts the width and height of one structure from the width and height of another
+ ///
+ /// structure.
+ ///
+ ///
+ /// A structure that is a result of the subtraction operation.
+ ///
+ ///
+ /// The structure on the left side of the subtraction operator.
+ ///
+ ///
+ /// The structure on the right side of the subtraction operator.
+ ///
+ public static RSize Subtract(RSize sz1, RSize sz2)
+ {
+ return new RSize(sz1.Width - sz2.Width, sz1.Height - sz2.Height);
+ }
+
+ ///
+ /// Tests to see whether the specified object is a structure with the same dimensions as this
+ ///
+ /// structure.
+ ///
+ ///
+ /// This method returns true if is a and has the same width and height as this
+ ///
+ /// ; otherwise, false.
+ ///
+ ///
+ /// The to test.
+ ///
+ /// 1
+ public override bool Equals(object obj)
+ {
+ if (!(obj is RSize))
+ return false;
+ var sizeF = (RSize)obj;
+ if (Math.Abs(sizeF.Width - Width) < 0.001 && Math.Abs(sizeF.Height - Height) < 0.001)
+ return sizeF.GetType() == GetType();
+ else
+ return false;
+ }
+
+ ///
+ /// Returns a hash code for this structure.
+ ///
+ ///
+ /// An integer value that specifies a hash value for this structure.
+ ///
+ /// 1
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ ///
+ /// Converts a structure to a structure.
+ ///
+ ///
+ /// Returns a structure.
+ ///
+ public RPoint ToPointF()
+ {
+ return (RPoint)this;
+ }
+
+ ///
+ /// Creates a human-readable string that represents this structure.
+ ///
+ ///
+ /// A string that represents this structure.
+ ///
+ /// 1
+ ///
+ ///
+ ///
+ public override string ToString()
+ {
+ return "{Width=" + _width + ", Height=" + _height + "}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RAdapter.cs b/Source/HtmlRenderer.Core/Adapters/RAdapter.cs
new file mode 100644
index 000000000..6b13459c1
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RAdapter.cs
@@ -0,0 +1,459 @@
+// "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.IO;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Handlers;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Platform adapter to bridge platform specific objects to HTML Renderer core library.
+ /// Core uses abstract renderer objects (RAdapter/RControl/REtc...) to access platform specific functionality, the concrete platforms
+ /// implements those objects to provide concrete platform implementation. Those allowing the core library to be platform agnostic.
+ ///
+ /// Platforms: WinForms, WPF, Metro, PDF renders, etc.
+ /// Objects: UI elements(Controls), Graphics(Render context), Colors, Brushes, Pens, Fonts, Images, Clipboard, etc.
+ ///
+ ///
+ ///
+ /// It is best to have a singleton instance of this class for concrete implementation!
+ /// This is because it holds caches of default CssData, Images, Fonts and Brushes.
+ ///
+ public abstract class RAdapter
+ {
+ #region Fields/Consts
+
+ ///
+ /// cache of brush color to brush instance
+ ///
+ private readonly Dictionary _brushesCache = new Dictionary();
+
+ ///
+ /// cache of pen color to pen instance
+ ///
+ private readonly Dictionary _penCache = new Dictionary();
+
+ ///
+ /// cache of all the font used not to create same font again and again
+ ///
+ private readonly FontsHandler _fontsHandler;
+
+ ///
+ /// default CSS parsed data singleton
+ ///
+ private CssData _defaultCssData;
+
+ ///
+ /// image used to draw loading image icon
+ ///
+ private RImage _loadImage;
+
+ ///
+ /// image used to draw error image icon
+ ///
+ private RImage _errorImage;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ protected RAdapter()
+ {
+ _fontsHandler = new FontsHandler(this);
+ }
+
+ ///
+ /// Get the default CSS stylesheet data.
+ ///
+ public CssData DefaultCssData
+ {
+ get { return _defaultCssData ?? (_defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false)); }
+ }
+
+ ///
+ /// Resolve color value from given color name.
+ ///
+ /// the color name
+ /// color value
+ public RColor GetColor(string colorName)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(colorName, "colorName");
+ return GetColorInt(colorName);
+ }
+
+ ///
+ /// Get cached pen instance for the given color.
+ ///
+ /// the color to get pen for
+ /// pen instance
+ public RPen GetPen(RColor color)
+ {
+ RPen pen;
+ if (!_penCache.TryGetValue(color, out pen))
+ {
+ _penCache[color] = pen = CreatePen(color);
+ }
+ return pen;
+ }
+
+ ///
+ /// Get cached solid brush instance for the given color.
+ ///
+ /// the color to get brush for
+ /// brush instance
+ public RBrush GetSolidBrush(RColor color)
+ {
+ RBrush brush;
+ if (!_brushesCache.TryGetValue(color, out brush))
+ {
+ _brushesCache[color] = brush = CreateSolidBrush(color);
+ }
+ return brush;
+ }
+
+ ///
+ /// Get linear gradient color brush from to .
+ ///
+ /// the rectangle to get the brush for
+ /// the start color of the gradient
+ /// the end color of the gradient
+ /// the angle to move the gradient from start color to end color in the rectangle
+ /// linear gradient color brush instance
+ public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ {
+ return CreateLinearGradientBrush(rect, color1, color2, angle);
+ }
+
+ ///
+ /// Convert image object returned from to .
+ ///
+ /// the image returned from load event
+ /// converted image or null
+ public RImage ConvertImage(object image)
+ {
+ // TODO:a remove this by creating better API.
+ return ConvertImageInt(image);
+ }
+
+ ///
+ /// Create an object from the given stream.
+ ///
+ /// the stream to create image from
+ /// new image instance
+ public RImage ImageFromStream(Stream memoryStream)
+ {
+ return ImageFromStreamInt(memoryStream);
+ }
+
+ ///
+ /// Check if the given font exists in the system by font family name.
+ ///
+ /// the font name to check
+ /// true - font exists by given family name, false - otherwise
+ public bool IsFontExists(string font)
+ {
+ return _fontsHandler.IsFontExists(font);
+ }
+
+ ///
+ /// Adds a font family to be used.
+ ///
+ /// The font family to add.
+ public void AddFontFamily(RFontFamily fontFamily)
+ {
+ _fontsHandler.AddFontFamily(fontFamily);
+ }
+
+ ///
+ /// Adds a font mapping from to iff the is not found.
+ /// When the font is used in rendered html and is not found in existing
+ /// fonts (installed or added) it will be replaced by .
+ ///
+ /// the font family to replace
+ /// the font family to replace with
+ public void AddFontFamilyMapping(string fromFamily, string toFamily)
+ {
+ _fontsHandler.AddFontFamilyMapping(fromFamily, toFamily);
+ }
+
+ ///
+ /// Get font instance by given font family name, size and style.
+ ///
+ /// the font family name
+ /// font size
+ /// font style
+ /// font instance
+ public RFont GetFont(string family, double size, RFontStyle style)
+ {
+ return _fontsHandler.GetCachedFont(family, size, style);
+ }
+
+ ///
+ /// Get image to be used while HTML image is loading.
+ ///
+ public RImage GetLoadingImage()
+ {
+ if (_loadImage == null)
+ {
+ var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("TheArtOfDev.HtmlRenderer.Core.Utils.ImageLoad.png");
+ if (stream != null)
+ _loadImage = ImageFromStream(stream);
+ }
+ return _loadImage;
+ }
+
+ ///
+ /// Get image to be used if HTML image load failed.
+ ///
+ public RImage GetLoadingFailedImage()
+ {
+ if (_errorImage == null)
+ {
+ var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("TheArtOfDev.HtmlRenderer.Core.Utils.ImageError.png");
+ if (stream != null)
+ _errorImage = ImageFromStream(stream);
+ }
+ return _errorImage;
+ }
+
+ ///
+ /// Get data object for the given html and plain text data.
+ /// The data object can be used for clipboard or drag-drop operation.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the html data
+ /// the plain text data
+ /// drag-drop data object
+ public object GetClipboardDataObject(string html, string plainText)
+ {
+ return GetClipboardDataObjectInt(html, plainText);
+ }
+
+ ///
+ /// Set the given text to the clipboard
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the text to set
+ public void SetToClipboard(string text)
+ {
+ SetToClipboardInt(text);
+ }
+
+ ///
+ /// Set the given html and plain text data to clipboard.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the html data
+ /// the plain text data
+ public void SetToClipboard(string html, string plainText)
+ {
+ SetToClipboardInt(html, plainText);
+ }
+
+ ///
+ /// Set the given image to clipboard.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the image object to set to clipboard
+ public void SetToClipboard(RImage image)
+ {
+ SetToClipboardInt(image);
+ }
+
+ ///
+ /// Create a context menu that can be used on the control
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// new context menu
+ public RContextMenu GetContextMenu()
+ {
+ return CreateContextMenuInt();
+ }
+
+ ///
+ /// Save the given image to file by showing save dialog to the client.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the image to save
+ /// the name of the image for save dialog
+ /// the extension of the image for save dialog
+ /// optional: the control to show the dialog on
+ public void SaveToFile(RImage image, string name, string extension, RControl control = null)
+ {
+ SaveToFileInt(image, name, extension, control);
+ }
+
+ ///
+ /// Get font instance by given font family name, size and style.
+ ///
+ /// the font family name
+ /// font size
+ /// font style
+ /// font instance
+ internal RFont CreateFont(string family, double size, RFontStyle style)
+ {
+ return CreateFontInt(family, size, style);
+ }
+
+ ///
+ /// Get font instance by given font family instance, size and style.
+ /// Used to support custom fonts that require explicit font family instance to be created.
+ ///
+ /// the font family instance
+ /// font size
+ /// font style
+ /// font instance
+ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style)
+ {
+ return CreateFontInt(family, size, style);
+ }
+
+
+ #region Private/Protected methods
+
+ ///
+ /// Resolve color value from given color name.
+ ///
+ /// the color name
+ /// color value
+ protected abstract RColor GetColorInt(string colorName);
+
+ ///
+ /// Get cached pen instance for the given color.
+ ///
+ /// the color to get pen for
+ /// pen instance
+ protected abstract RPen CreatePen(RColor color);
+
+ ///
+ /// Get cached solid brush instance for the given color.
+ ///
+ /// the color to get brush for
+ /// brush instance
+ protected abstract RBrush CreateSolidBrush(RColor color);
+
+ ///
+ /// Get linear gradient color brush from to .
+ ///
+ /// the rectangle to get the brush for
+ /// the start color of the gradient
+ /// the end color of the gradient
+ /// the angle to move the gradient from start color to end color in the rectangle
+ /// linear gradient color brush instance
+ protected abstract RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle);
+
+ ///
+ /// Convert image object returned from to .
+ ///
+ /// the image returned from load event
+ /// converted image or null
+ protected abstract RImage ConvertImageInt(object image);
+
+ ///
+ /// Create an object from the given stream.
+ ///
+ /// the stream to create image from
+ /// new image instance
+ protected abstract RImage ImageFromStreamInt(Stream memoryStream);
+
+ ///
+ /// Get font instance by given font family name, size and style.
+ ///
+ /// the font family name
+ /// font size
+ /// font style
+ /// font instance
+ protected abstract RFont CreateFontInt(string family, double size, RFontStyle style);
+
+ ///
+ /// Get font instance by given font family instance, size and style.
+ /// Used to support custom fonts that require explicit font family instance to be created.
+ ///
+ /// the font family instance
+ /// font size
+ /// font style
+ /// font instance
+ protected abstract RFont CreateFontInt(RFontFamily family, double size, RFontStyle style);
+
+ ///
+ /// Get data object for the given html and plain text data.
+ /// The data object can be used for clipboard or drag-drop operation.
+ ///
+ /// the html data
+ /// the plain text data
+ /// drag-drop data object
+ protected virtual object GetClipboardDataObjectInt(string html, string plainText)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Set the given text to the clipboard
+ ///
+ /// the text to set
+ protected virtual void SetToClipboardInt(string text)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Set the given html and plain text data to clipboard.
+ ///
+ /// the html data
+ /// the plain text data
+ protected virtual void SetToClipboardInt(string html, string plainText)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Set the given image to clipboard.
+ ///
+ ///
+ protected virtual void SetToClipboardInt(RImage image)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Create a context menu that can be used on the control
+ ///
+ /// new context menu
+ protected virtual RContextMenu CreateContextMenuInt()
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Save the given image to file by showing save dialog to the client.
+ ///
+ /// the image to save
+ /// the name of the image for save dialog
+ /// the extension of the image for save dialog
+ /// optional: the control to show the dialog on
+ protected virtual void SaveToFileInt(RImage image, string name, string extension, RControl control = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RBrush.cs b/Source/HtmlRenderer.Core/Adapters/RBrush.cs
new file mode 100644
index 000000000..439eb4e7b
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RBrush.cs
@@ -0,0 +1,25 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific brush objects - used to fill graphics (rectangles, polygons and paths).
+ /// The brush can be solid color, gradient or image.
+ ///
+ public abstract class RBrush : IDisposable
+ {
+ public abstract void Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RContextMenu.cs b/Source/HtmlRenderer.Core/Adapters/RContextMenu.cs
new file mode 100644
index 000000000..53c61f36c
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RContextMenu.cs
@@ -0,0 +1,52 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific context menu - used to create and show context menu at specific location.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ public abstract class RContextMenu : IDisposable
+ {
+ ///
+ /// The total number of items in the context menu
+ ///
+ public abstract int ItemsCount { get; }
+
+ ///
+ /// Add divider item to the context menu.
+ /// The divider is a non clickable place holder used to separate items.
+ ///
+ public abstract void AddDivider();
+
+ ///
+ /// Add item to the context menu with the given text that will raise the given event when clicked.
+ /// the text to set on the new context menu itemif to set the item as enabled or disabledthe event to raise when the item is clicked
+ public abstract void AddItem(string text, bool enabled, EventHandler onClick);
+
+ ///
+ /// Remove the last item from the context menu iff it is a divider
+ ///
+ public abstract void RemoveLastDivider();
+
+ ///
+ /// Show the context menu in the given parent control at the given location.
+ /// the parent control to show inthe location to show at relative to the parent control
+ public abstract void Show(RControl parent, RPoint location);
+
+ public abstract void Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RControl.cs b/Source/HtmlRenderer.Core/Adapters/RControl.cs
new file mode 100644
index 000000000..40acdeeaa
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RControl.cs
@@ -0,0 +1,97 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific control object - used to handle updating the control that the html is rendered on.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ public abstract class RControl
+ {
+ ///
+ /// The platform adapter.
+ ///
+ private readonly RAdapter _adapter;
+
+ ///
+ /// Init control with platform adapter.
+ ///
+ protected RControl(RAdapter adapter)
+ {
+ ArgChecker.AssertArgNotNull(adapter, "adapter");
+ _adapter = adapter;
+ }
+
+ ///
+ /// The platform adapter.
+ ///
+ public RAdapter Adapter
+ {
+ get { return _adapter; }
+ }
+
+ ///
+ /// Is the left mouse button is currently in pressed state
+ ///
+ public abstract bool LeftMouseButton { get; }
+
+ ///
+ /// Is the right mouse button is currently in pressed state
+ ///
+ public abstract bool RightMouseButton { get; }
+
+ ///
+ /// Get the current location of the mouse relative to the control
+ ///
+ public abstract RPoint MouseLocation { get; }
+
+ ///
+ /// Set the cursor over the control to default cursor
+ ///
+ public abstract void SetCursorDefault();
+
+ ///
+ /// Set the cursor over the control to hand cursor
+ ///
+ public abstract void SetCursorHand();
+
+ ///
+ /// Set the cursor over the control to I beam cursor
+ ///
+ public abstract void SetCursorIBeam();
+
+ ///
+ /// Do drag-drop copy operation for the given data object.
+ ///
+ /// the drag-drop data object
+ public abstract void DoDragDropCopy(object dragDropData);
+
+ ///
+ /// Measure the width of string under max width restriction calculating the number of characters that can fit and the width those characters take.
+ ///
+ /// the string to measure
+ /// the font to measure string with
+ /// the max width to calculate fit characters
+ /// the number of characters that will fit under restriction
+ /// the width that only the characters that fit into max width take
+ public abstract void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth);
+
+ ///
+ /// Invalidates the entire surface of the control and causes the control to be redrawn.
+ ///
+ public abstract void Invalidate();
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RFont.cs b/Source/HtmlRenderer.Core/Adapters/RFont.cs
new file mode 100644
index 000000000..43f171d80
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RFont.cs
@@ -0,0 +1,42 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific font object - used to render text using specific font.
+ ///
+ public abstract class RFont
+ {
+ ///
+ /// Gets the em-size of this Font measured in the units specified by the Unit property.
+ ///
+ public abstract double Size { get; }
+
+ ///
+ /// The line spacing, in pixels, of this font.
+ ///
+ public abstract double Height { get; }
+
+ ///
+ /// Get the vertical offset of the font underline location from the top of the font.
+ ///
+ public abstract double UnderlineOffset { get; }
+
+ ///
+ /// Get the left padding, in pixels, of the font.
+ ///
+ public abstract double LeftPadding { get; }
+
+ public abstract double GetWhitespaceWidth(RGraphics graphics);
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RFontFamily.cs b/Source/HtmlRenderer.Core/Adapters/RFontFamily.cs
new file mode 100644
index 000000000..724d9d96f
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RFontFamily.cs
@@ -0,0 +1,26 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific font family object - define the available font families to use.
+ /// Required for custom fonts handling: fonts that are not installed on the system.
+ ///
+ public abstract class RFontFamily
+ {
+ ///
+ /// Gets the name of this Font Family.
+ ///
+ public abstract string Name { get; }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RGraphics.cs b/Source/HtmlRenderer.Core/Adapters/RGraphics.cs
new file mode 100644
index 000000000..af54f2ae4
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RGraphics.cs
@@ -0,0 +1,272 @@
+// "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.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific graphics rendering object - used to render graphics and text in platform specific context.
+ /// The core HTML Renderer components use this class for rendering logic, extending this
+ /// class in different platform: WinForms, WPF, Metro, PDF, etc.
+ ///
+ public abstract class RGraphics : IDisposable
+ {
+ #region Fields/Consts
+
+ ///
+ /// the global adapter
+ ///
+ protected readonly RAdapter _adapter;
+
+ ///
+ /// The clipping bound stack as clips are pushed/poped to/from the graphics
+ ///
+ protected readonly Stack _clipStack = new Stack();
+
+ ///
+ /// The suspended clips
+ ///
+ private Stack _suspendedClips = new Stack();
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ protected RGraphics(RAdapter adapter, RRect initialClip)
+ {
+ ArgChecker.AssertArgNotNull(adapter, "global");
+
+ _adapter = adapter;
+ _clipStack.Push(initialClip);
+ }
+
+ ///
+ /// Get color pen.
+ ///
+ /// the color to get the pen for
+ /// pen instance
+ public RPen GetPen(RColor color)
+ {
+ return _adapter.GetPen(color);
+ }
+
+ ///
+ /// Get solid color brush.
+ ///
+ /// the color to get the brush for
+ /// solid color brush instance
+ public RBrush GetSolidBrush(RColor color)
+ {
+ return _adapter.GetSolidBrush(color);
+ }
+
+ ///
+ /// Get linear gradient color brush from to .
+ ///
+ /// the rectangle to get the brush for
+ /// the start color of the gradient
+ /// the end color of the gradient
+ /// the angle to move the gradient from start color to end color in the rectangle
+ /// linear gradient color brush instance
+ public RBrush GetLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle)
+ {
+ return _adapter.GetLinearGradientBrush(rect, color1, color2, angle);
+ }
+
+ ///
+ /// Gets a Rectangle structure that bounds the clipping region of this Graphics.
+ ///
+ /// A rectangle structure that represents a bounding rectangle for the clipping region of this Graphics.
+ public RRect GetClip()
+ {
+ return _clipStack.Peek();
+ }
+
+ ///
+ /// Pop the latest clip push.
+ ///
+ public abstract void PopClip();
+
+ ///
+ /// Push the clipping region of this Graphics to interception of current clipping rectangle and the given rectangle.
+ ///
+ /// Rectangle to clip to.
+ public abstract void PushClip(RRect rect);
+
+ ///
+ /// Push the clipping region of this Graphics to exclude the given rectangle from the current clipping rectangle.
+ ///
+ /// Rectangle to exclude clipping in.
+ public abstract void PushClipExclude(RRect rect);
+
+
+ ///
+ /// Restore the clipping region to the initial clip.
+ ///
+ public void SuspendClipping()
+ {
+ while (_clipStack.Count > 1)
+ {
+ var clip = GetClip();
+ _suspendedClips.Push(clip);
+ PopClip();
+ }
+ }
+
+ ///
+ /// Resumes the suspended clips.
+ ///
+ public void ResumeClipping()
+ {
+ while (_suspendedClips.Count > 0)
+ {
+ var clip = _suspendedClips.Pop();
+ PushClip(clip);
+ }
+ }
+
+ ///
+ /// Set the graphics smooth mode to use anti-alias.
+ /// Use to return back the mode used.
+ ///
+ /// the previous smooth mode before the change
+ public abstract Object SetAntiAliasSmoothingMode();
+
+ ///
+ /// Return to previous smooth mode before anti-alias was set as returned from .
+ ///
+ /// the previous mode to set
+ public abstract void ReturnPreviousSmoothingMode(Object prevMode);
+
+ ///
+ /// Get TextureBrush object that uses the specified image and bounding rectangle.
+ ///
+ /// The Image object with which this TextureBrush object fills interiors.
+ /// A Rectangle structure that represents the bounding rectangle for this TextureBrush object.
+ /// The dimension by which to translate the transformation
+ public abstract RBrush GetTextureBrush(RImage image, RRect dstRect, RPoint translateTransformLocation);
+
+ ///
+ /// Get GraphicsPath object.
+ ///
+ /// graphics path instance
+ public abstract RGraphicsPath GetGraphicsPath();
+
+ ///
+ /// Measure the width and height of string when drawn on device context HDC
+ /// using the given font .
+ ///
+ /// the string to measure
+ /// the font to measure string with
+ /// the size of the string
+ public abstract RSize MeasureString(string str, RFont font);
+
+ ///
+ /// Measure the width of string under max width restriction calculating the number of characters that can fit and the width those characters take.
+ /// Not relevant for platforms that don't render HTML on UI element.
+ ///
+ /// the string to measure
+ /// the font to measure string with
+ /// the max width to calculate fit characters
+ /// the number of characters that will fit under restriction
+ /// the width that only the characters that fit into max width take
+ public abstract void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth);
+
+ ///
+ /// Draw the given string using the given font and foreground color at given location.
+ ///
+ /// the string to draw
+ /// the font to use to draw the string
+ /// the text color to set
+ /// the location to start string draw (top-left)
+ /// used to know the size of the rendered text for transparent text support
+ /// is to render the string right-to-left (true - RTL, false - LTR)
+ public abstract void DrawString(String str, RFont font, RColor color, RPoint point, RSize size, bool rtl);
+
+ ///
+ /// Draws a line connecting the two points specified by the coordinate pairs.
+ ///
+ /// Pen that determines the color, width, and style of the line.
+ /// The x-coordinate of the first point.
+ /// The y-coordinate of the first point.
+ /// The x-coordinate of the second point.
+ /// The y-coordinate of the second point.
+ public abstract void DrawLine(RPen pen, double x1, double y1, double x2, double y2);
+
+ ///
+ /// Draws a rectangle specified by a coordinate pair, a width, and a height.
+ ///
+ /// A Pen that determines the color, width, and style of the rectangle.
+ /// The x-coordinate of the upper-left corner of the rectangle to draw.
+ /// The y-coordinate of the upper-left corner of the rectangle to draw.
+ /// The width of the rectangle to draw.
+ /// The height of the rectangle to draw.
+ public abstract void DrawRectangle(RPen pen, double x, double y, double width, double height);
+
+ ///
+ /// Fills the interior of a rectangle specified by a pair of coordinates, a width, and a height.
+ ///
+ /// Brush that determines the characteristics of the fill.
+ /// The x-coordinate of the upper-left corner of the rectangle to fill.
+ /// The y-coordinate of the upper-left corner of the rectangle to fill.
+ /// Width of the rectangle to fill.
+ /// Height of the rectangle to fill.
+ public abstract void DrawRectangle(RBrush brush, double x, double y, double width, double height);
+
+ ///
+ /// Draws the specified portion of the specified at the specified location and with the specified size.
+ ///
+ /// Image to draw.
+ /// Rectangle structure that specifies the location and size of the drawn image. The image is scaled to fit the rectangle.
+ /// Rectangle structure that specifies the portion of the object to draw.
+ public abstract void DrawImage(RImage image, RRect destRect, RRect srcRect);
+
+ ///
+ /// Draws the specified Image at the specified location and with the specified size.
+ ///
+ /// Image to draw.
+ /// Rectangle structure that specifies the location and size of the drawn image.
+ public abstract void DrawImage(RImage image, RRect destRect);
+
+ ///
+ /// Draws a GraphicsPath.
+ ///
+ /// Pen that determines the color, width, and style of the path.
+ /// GraphicsPath to draw.
+ public abstract void DrawPath(RPen pen, RGraphicsPath path);
+
+ ///
+ /// Fills the interior of a GraphicsPath.
+ ///
+ /// Brush that determines the characteristics of the fill.
+ /// GraphicsPath that represents the path to fill.
+ public abstract void DrawPath(RBrush brush, RGraphicsPath path);
+
+ ///
+ /// Fills the interior of a polygon defined by an array of points specified by Point structures.
+ ///
+ /// Brush that determines the characteristics of the fill.
+ /// Array of Point structures that represent the vertices of the polygon to fill.
+ public abstract void DrawPolygon(RBrush brush, RPoint[] points);
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public abstract void Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RGraphicsPath.cs b/Source/HtmlRenderer.Core/Adapters/RGraphicsPath.cs
new file mode 100644
index 000000000..21c86bc1a
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RGraphicsPath.cs
@@ -0,0 +1,53 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific graphics path object - used to render (draw/fill) path shape.
+ ///
+ public abstract class RGraphicsPath : IDisposable
+ {
+ ///
+ /// Start path at the given point.
+ ///
+ public abstract void Start(double x, double y);
+
+ ///
+ /// Add stright line to the given point from te last point.
+ ///
+ public abstract void LineTo(double x, double y);
+
+ ///
+ /// Add circular arc of the given size to the given point from the last point.
+ ///
+ public abstract void ArcTo(double x, double y, double size, Corner corner);
+
+ ///
+ /// Release path resources.
+ ///
+ public abstract void Dispose();
+
+ ///
+ /// The 4 corners that are handled in arc rendering.
+ ///
+ public enum Corner
+ {
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight,
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RImage.cs b/Source/HtmlRenderer.Core/Adapters/RImage.cs
new file mode 100644
index 000000000..6c184e2d1
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RImage.cs
@@ -0,0 +1,34 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific image object - used to render images.
+ ///
+ public abstract class RImage : IDisposable
+ {
+ ///
+ /// Get the width, in pixels, of the image.
+ ///
+ public abstract double Width { get; }
+
+ ///
+ /// Get the height, in pixels, of the image.
+ ///
+ public abstract double Height { get; }
+
+ public abstract void Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Adapters/RPen.cs b/Source/HtmlRenderer.Core/Adapters/RPen.cs
new file mode 100644
index 000000000..5804ab895
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Adapters/RPen.cs
@@ -0,0 +1,32 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Adapters
+{
+ ///
+ /// Adapter for platform specific pen objects - used to draw graphics (lines, rectangles and paths)
+ ///
+ public abstract class RPen
+ {
+ ///
+ /// Gets or sets the width of this Pen, in units of the Graphics object used for drawing.
+ ///
+ public abstract double Width { get; set; }
+
+ ///
+ /// Gets or sets the style used for dashed lines drawn with this Pen.
+ ///
+ public abstract RDashStyle DashStyle { set; }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/CssData.cs b/Source/HtmlRenderer.Core/Core/CssData.cs
new file mode 100644
index 000000000..f0e4a8106
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/CssData.cs
@@ -0,0 +1,214 @@
+// "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.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core
+{
+ ///
+ /// Holds parsed stylesheet css blocks arranged by media and classes.
+ ///
+ ///
+ ///
+ /// To learn more about CSS blocks visit CSS spec: http://www.w3.org/TR/CSS21/syndata.html#block
+ ///
+ public sealed class CssData
+ {
+ #region Fields and Consts
+
+ ///
+ /// used to return empty array
+ ///
+ private static readonly List _emptyArray = new List();
+
+ ///
+ /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data.
+ ///
+ private readonly Dictionary>> _mediaBlocks = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase);
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ internal CssData()
+ {
+ _mediaBlocks.Add("all", new Dictionary>(StringComparer.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ /// Parse the given stylesheet to object.
+ /// If is true the parsed css blocks are added to the
+ /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned.
+ ///
+ ///
+ /// Platform adapter
+ /// the stylesheet source to parse
+ /// true - combine the parsed css data with default css data, false - return only the parsed css data
+ /// the parsed css data
+ public static CssData Parse(RAdapter adapter, string stylesheet, bool combineWithDefault = true)
+ {
+ CssParser parser = new CssParser(adapter);
+ return parser.ParseStyleSheet(stylesheet, combineWithDefault);
+ }
+
+ ///
+ /// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data
+ ///
+ internal IDictionary>> MediaBlocks
+ {
+ get { return _mediaBlocks; }
+ }
+
+ ///
+ /// Check if there are css blocks for the given class selector.
+ ///
+ /// the class selector to check for css blocks by
+ /// optional: the css media type (default - all)
+ /// true - has css blocks for the class, false - otherwise
+ public bool ContainsCssBlock(string className, string media = "all")
+ {
+ Dictionary> mid;
+ return _mediaBlocks.TryGetValue(media, out mid) && mid.ContainsKey(className);
+ }
+
+ ///
+ /// Get collection of css blocks for the requested class selector.
+ /// the can be: class name, html element name, html element and
+ /// class name (elm.class), hash tag with element id (#id).
+ /// returned all the blocks that word on the requested class selector, it can contain simple
+ /// selector or hierarchy selector.
+ ///
+ /// the class selector to get css blocks by
+ /// optional: the css media type (default - all)
+ /// collection of css blocks, empty collection if no blocks exists (never null)
+ public IEnumerable GetCssBlock(string className, string media = "all")
+ {
+ List block = null;
+ Dictionary> mid;
+ if (_mediaBlocks.TryGetValue(media, out mid))
+ {
+ mid.TryGetValue(className, out block);
+ }
+ return block ?? _emptyArray;
+ }
+
+ ///
+ /// Add the given css block to the css data, merging to existing block if required.
+ ///
+ ///
+ /// If there is no css blocks for the same class it will be added to data collection.
+ /// If there is already css blocks for the same class it will check for each existing block
+ /// if the hierarchical selectors match (or not exists). if do the two css blocks will be merged into
+ /// one where the new block properties overwrite existing if needed. if the new block doesn't mach any
+ /// existing it will be added either to the beginning of the list if it has no hierarchical selectors or at the end.
+ /// Css block without hierarchical selectors must be added to the beginning of the list so more specific block
+ /// can overwrite it when the style is applied.
+ ///
+ /// the media type to add the CSS to
+ /// the css block to add
+ public void AddCssBlock(string media, CssBlock cssBlock)
+ {
+ Dictionary> mid;
+ if (!_mediaBlocks.TryGetValue(media, out mid))
+ {
+ mid = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
+ _mediaBlocks.Add(media, mid);
+ }
+
+ if (!mid.ContainsKey(cssBlock.Class))
+ {
+ var list = new List();
+ list.Add(cssBlock);
+ mid[cssBlock.Class] = list;
+ }
+ else
+ {
+ bool merged = false;
+ var list = mid[cssBlock.Class];
+ foreach (var block in list)
+ {
+ if (block.EqualsSelector(cssBlock))
+ {
+ merged = true;
+ block.Merge(cssBlock);
+ break;
+ }
+ }
+
+ if (!merged)
+ {
+ // general block must be first
+ if (cssBlock.Selectors == null)
+ list.Insert(0, cssBlock);
+ else
+ list.Add(cssBlock);
+ }
+ }
+ }
+
+ ///
+ /// Combine this CSS data blocks with CSS blocks for each media.
+ /// Merge blocks if exists in both.
+ ///
+ /// the CSS data to combine with
+ public void Combine(CssData other)
+ {
+ ArgChecker.AssertArgNotNull(other, "other");
+
+ // for each media block
+ foreach (var mediaBlock in other.MediaBlocks)
+ {
+ // for each css class in the media block
+ foreach (var bla in mediaBlock.Value)
+ {
+ // for each css block of the css class
+ foreach (var cssBlock in bla.Value)
+ {
+ // combine with this
+ AddCssBlock(mediaBlock.Key, cssBlock);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Create deep copy of the css data with cloned css blocks.
+ ///
+ /// cloned object
+ public CssData Clone()
+ {
+ var clone = new CssData();
+ foreach (var mid in _mediaBlocks)
+ {
+ var cloneMid = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
+ foreach (var blocks in mid.Value)
+ {
+ var cloneList = new List();
+ foreach (var cssBlock in blocks.Value)
+ {
+ cloneList.Add(cssBlock.Clone());
+ }
+ cloneMid[blocks.Key] = cloneList;
+ }
+ clone._mediaBlocks[mid.Key] = cloneMid;
+ }
+ return clone;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/CssDefaults.cs b/Source/HtmlRenderer.Core/Core/CssDefaults.cs
new file mode 100644
index 000000000..bb313c223
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/CssDefaults.cs
@@ -0,0 +1,128 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core
+{
+ internal static class CssDefaults
+ {
+ ///
+ /// CSS Specification's Default Style Sheet for HTML 4
+ ///
+ ///
+ /// http://www.w3.org/TR/CSS21/sample.html
+ ///
+ public const string DefaultStyleSheet = @"
+ html, address,
+ blockquote,
+ body, dd, div,
+ dl, dt, fieldset, form,
+ frame, frameset,
+ h1, h2, h3, h4,
+ h5, h6, noframes,
+ ol, p, ul, center,
+ dir, menu, pre { display: block }
+ li { display: list-item }
+ head { display: none }
+ table { display: table }
+ tr { display: table-row }
+ thead { display: table-header-group }
+ tbody { display: table-row-group }
+ tfoot { display: table-footer-group }
+ col { display: table-column }
+ colgroup { display: table-column-group }
+ td, th { display: table-cell }
+ caption { display: table-caption }
+ th { font-weight: bolder; text-align: center }
+ caption { text-align: center }
+ body { margin: 8px }
+ h1 { font-size: 2em; margin: .67em 0 }
+ h2 { font-size: 1.5em; margin: .75em 0 }
+ h3 { font-size: 1.17em; margin: .83em 0 }
+ h4, p,
+ blockquote, ul,
+ fieldset, form,
+ ol, dl, dir,
+ menu { margin: 1.12em 0 }
+ h5 { font-size: .83em; margin: 1.5em 0 }
+ h6 { font-size: .75em; margin: 1.67em 0 }
+ h1, h2, h3, h4,
+ h5, h6, b,
+ strong { font-weight: bolder; }
+ blockquote { margin-left: 40px; margin-right: 40px }
+ i, cite, em,
+ var, address { font-style: italic }
+ pre, tt, code,
+ kbd, samp { font-family: monospace }
+ pre { white-space: pre }
+ button, textarea,
+ input, select { display: inline-block }
+ big { font-size: 1.17em }
+ small, sub, sup { font-size: .83em }
+ sub { vertical-align: sub }
+ sup { vertical-align: super }
+ table { border-spacing: 2px; }
+ thead, tbody,
+ tfoot, tr { vertical-align: middle }
+ td, th { vertical-align: inherit }
+ s, strike, del { text-decoration: line-through }
+ hr { border: 1px inset; }
+ ol, ul, dir,
+ menu, dd { margin-left: 40px }
+ ol { list-style-type: decimal }
+ ol ul, ul ol,
+ ul ul, ol ol { margin-top: 0; margin-bottom: 0 }
+ ol ul, ul ul { list-style-type: circle }
+ ul ul ul,
+ ol ul ul,
+ ul ol ul { list-style-type: square }
+ u, ins { text-decoration: underline }
+ br:before { content: ""\A"" }
+ :before, :after { white-space: pre-line }
+ center { text-align: center }
+ :link, :visited { text-decoration: underline }
+ :focus { outline: thin dotted invert }
+
+ /* Begin bidirectionality settings (do not change) */
+ BDO[DIR=""ltr""] { direction: ltr; unicode-bidi: bidi-override }
+ BDO[DIR=""rtl""] { direction: rtl; unicode-bidi: bidi-override }
+
+ *[DIR=""ltr""] { direction: ltr; unicode-bidi: embed }
+ *[DIR=""rtl""] { direction: rtl; unicode-bidi: embed }
+
+ @media print {
+ h1 { page-break-before: always }
+ h1, h2, h3,
+ h4, h5, h6 { page-break-after: avoid }
+ ul, ol, dl { page-break-before: avoid }
+ }
+
+ /* Not in the specification but necessary */
+ a { color: #0055BB; text-decoration:underline }
+ table { border-color:#dfdfdf; }
+ td, th { border-color:#dfdfdf; overflow: hidden; }
+ style, title,
+ script, link,
+ meta, area,
+ base, param { display:none }
+ hr { border-top-color: #9A9A9A; border-left-color: #9A9A9A; border-bottom-color: #EEEEEE; border-right-color: #EEEEEE; }
+ pre { font-size: 10pt; margin-top: 15px; }
+
+ /*This is the background of the HtmlToolTip*/
+ .htmltooltip {
+ border:solid 1px #767676;
+ background-color:white;
+ background-gradient:#E4E5F0;
+ padding: 8px;
+ Font: 9pt Tahoma;
+ }";
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/Border.cs b/Source/HtmlRenderer.Core/Core/Dom/Border.cs
new file mode 100644
index 000000000..c4ee107eb
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/Border.cs
@@ -0,0 +1,25 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Border types
+ ///
+ internal enum Border
+ {
+ Top,
+ Right,
+ Bottom,
+ Left
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssBox.cs b/Source/HtmlRenderer.Core/Core/Dom/CssBox.cs
new file mode 100644
index 000000000..b2cd3d984
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssBox.cs
@@ -0,0 +1,1559 @@
+// "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"); }
+ }
+
+ ///
+ /// Gets a value indicating whether this instance or one of its parents has Position = fixed.
+ ///
+ ///
+ /// true if this instance is fixed; otherwise, false.
+ ///
+ public virtual bool IsFixed
+ {
+ get
+ {
+ if (Position == CssConstants.Fixed)
+ return true;
+
+ if (this.ParentBox == null)
+ return false;
+
+ CssBox parent = this;
+
+ while (!(parent.ParentBox == null || parent == parent.ParentBox))
+ {
+ parent = parent.ParentBox;
+
+ if (parent.Position == CssConstants.Fixed)
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// 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);
+ }
+ }
+
+ ///
+ /// Paints the fragment
+ ///
+ /// Device context to use
+ public void Paint(RGraphics g)
+ {
+ try
+ {
+ if (Display != CssConstants.None && Visibility == CssConstants.Visible)
+ {
+ // use initial clip to draw blocks with Position = fixed. I.e. ignrore page margins
+ if (this.Position == CssConstants.Fixed)
+ {
+ g.SuspendClipping();
+ }
+
+ // 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;
+ if (!IsFixed)
+ {
+ //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);
+
+ // Restore clips
+ if (this.Position == CssConstants.Fixed)
+ {
+ g.ResumeClipping();
+ }
+
+ }
+ }
+ 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;
+ double top;
+
+ if (Position == CssConstants.Fixed)
+ {
+ left = 0;
+ top = 0;
+ }
+ else
+ {
+ left = ContainingBlock.Location.X + ContainingBlock.ActualPaddingLeft + ActualMarginLeft + ContainingBlock.ActualBorderLeftWidth;
+ 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);
+
+ if (!IsFixed)
+ {
+ 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;
+ }
+
+ public bool BreakPage()
+ {
+ var container = this.HtmlContainer;
+
+ if (this.Size.Height >= container.PageSize.Height)
+ return false;
+
+ var remTop = (this.Location.Y - container.MarginTop) % container.PageSize.Height;
+ var remBottom = (this.ActualBottom - container.MarginTop) % container.PageSize.Height;
+
+ if (remTop > remBottom)
+ {
+ var diff = container.PageSize.Height - remTop;
+ this.Location = new RPoint(this.Location.X, this.Location.Y + diff + 1);
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 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);
+ var clip = g.GetClip();
+ RRect[] rects = areas.ToArray();
+ RPoint offset = RPoint.Empty;
+ if (!IsFixed)
+ {
+ offset = HtmlContainer.ScrollOffset;
+ }
+
+ for (int i = 0; i < rects.Length; i++)
+ {
+ var actualRect = rects[i];
+ actualRect.Offset(offset);
+
+ if (IsRectVisible(actualRect, clip))
+ {
+ 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);
+
+ if (IsRectVisible(actualRect, clip))
+ {
+ 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.IsFixed)
+ b.Paint(g);
+ }
+ foreach (CssBox b in Boxes)
+ {
+ if (b.Position == CssConstants.Absolute)
+ b.Paint(g);
+ }
+ foreach (CssBox b in Boxes)
+ {
+ if (b.IsFixed)
+ b.Paint(g);
+ }
+
+ if (clipped)
+ g.PopClip();
+
+ if (_listItemBox != null)
+ {
+ _listItemBox.Paint(g);
+ }
+ }
+ }
+
+ private bool IsRectVisible(RRect rect, RRect clip)
+ {
+ rect.X -= 2;
+ rect.Width += 2;
+ clip.Intersect(rect);
+
+ if (clip != RRect.Empty)
+ 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 clip = g.GetClip();
+ var wordRect = word.Rectangle;
+ wordRect.Offset(offset);
+ clip.Intersect(wordRect);
+
+ if (clip != RRect.Empty)
+ {
+ 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);
+ }
+
+ protected override RPoint GetActualLocation(string X, string Y)
+ {
+ var left = CssValueParser.ParseLength(X, this.HtmlContainer.PageSize.Width, this, null);
+ var top = CssValueParser.ParseLength(Y, this.HtmlContainer.PageSize.Height, this, null);
+ return new RPoint(left, top);
+ }
+
+ ///
+ /// 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
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssBoxFrame.cs b/Source/HtmlRenderer.Core/Core/Dom/CssBoxFrame.cs
new file mode 100644
index 000000000..7bb6ed57f
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssBoxFrame.cs
@@ -0,0 +1,609 @@
+// "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.Net;
+using System.Text;
+using System.Threading;
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Handlers;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// CSS box for iframe element.
+ /// If the iframe is of embedded YouTube or Vimeo video it will show image with play.
+ ///
+ internal sealed class CssBoxFrame : CssBox
+ {
+ #region Fields and Consts
+
+ ///
+ /// the image word of this image box
+ ///
+ private readonly CssRectImage _imageWord;
+
+ ///
+ /// is the iframe is of embeded video
+ ///
+ private readonly bool _isVideo;
+
+ ///
+ /// the title of the video
+ ///
+ private string _videoTitle;
+
+ ///
+ /// the url of the video thumbnail image
+ ///
+ private string _videoImageUrl;
+
+ ///
+ /// link to the video on the site
+ ///
+ private string _videoLinkUrl;
+
+ ///
+ /// handler used for image loading by source
+ ///
+ private ImageLoadHandler _imageLoadHandler;
+
+ ///
+ /// is image load is finished, used to know if no image is found
+ ///
+ private bool _imageLoadingComplete;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the parent box of this box
+ /// the html tag data of this box
+ public CssBoxFrame(CssBox parent, HtmlTag tag)
+ : base(parent, tag)
+ {
+ _imageWord = new CssRectImage(this);
+ Words.Add(_imageWord);
+
+ Uri uri;
+ if (Uri.TryCreate(GetAttribute("src"), UriKind.Absolute, out uri))
+ {
+ if (uri.Host.IndexOf("youtube.com", StringComparison.InvariantCultureIgnoreCase) > -1)
+ {
+ _isVideo = true;
+ LoadYoutubeDataAsync(uri);
+ }
+ else if (uri.Host.IndexOf("vimeo.com", StringComparison.InvariantCultureIgnoreCase) > -1)
+ {
+ _isVideo = true;
+ LoadVimeoDataAsync(uri);
+ }
+ }
+
+ if (!_isVideo)
+ {
+ SetErrorBorder();
+ }
+ }
+
+ ///
+ /// Is the css box clickable ("a" element is clickable)
+ ///
+ public override bool IsClickable
+ {
+ get { return true; }
+ }
+
+ ///
+ /// Get the href link of the box (by default get "href" attribute)
+ ///
+ public override string HrefLink
+ {
+ get { return _videoLinkUrl ?? GetAttribute("src"); }
+ }
+
+ ///
+ /// is the iframe is of embeded video
+ ///
+ public bool IsVideo
+ {
+ get { return _isVideo; }
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public override void Dispose()
+ {
+ if (_imageLoadHandler != null)
+ _imageLoadHandler.Dispose();
+ base.Dispose();
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Load YouTube video data (title, image, link) by calling YouTube API.
+ ///
+ private void LoadYoutubeDataAsync(Uri uri)
+ {
+ ThreadPool.QueueUserWorkItem(state =>
+ {
+ try
+ {
+ var apiUri = new Uri(string.Format("http://gdata.youtube.com/feeds/api/videos/{0}?v=2&alt=json", uri.Segments[2]));
+
+ var client = new WebClient();
+ client.Encoding = Encoding.UTF8;
+ client.DownloadStringCompleted += OnDownloadYoutubeApiCompleted;
+ client.DownloadStringAsync(apiUri);
+ }
+ catch (Exception ex)
+ {
+ HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to get youtube video data: " + uri, ex);
+ HtmlContainer.RequestRefresh(false);
+ }
+ });
+ }
+
+ ///
+ /// Parse YouTube API response to get video data (title, image, link).
+ ///
+ private void OnDownloadYoutubeApiCompleted(object sender, DownloadStringCompletedEventArgs e)
+ {
+ try
+ {
+ if (!e.Cancelled)
+ {
+ if (e.Error == null)
+ {
+ var idx = e.Result.IndexOf("\"media$title\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf("\"$t\"", idx);
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf('"', idx + 4);
+ if (idx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', idx + 1);
+ while (e.Result[endIdx - 1] == '\\')
+ endIdx = e.Result.IndexOf('"', endIdx + 1);
+ if (endIdx > -1)
+ {
+ _videoTitle = e.Result.Substring(idx + 1, endIdx - idx - 1).Replace("\\\"", "\"");
+ }
+ }
+ }
+ }
+
+ idx = e.Result.IndexOf("\"media$thumbnail\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ var iidx = e.Result.IndexOf("sddefault", idx);
+ if (iidx > -1)
+ {
+ if (string.IsNullOrEmpty(Width))
+ Width = "640px";
+ if (string.IsNullOrEmpty(Height))
+ Height = "480px";
+ }
+ else
+ {
+ iidx = e.Result.IndexOf("hqdefault", idx);
+ if (iidx > -1)
+ {
+ if (string.IsNullOrEmpty(Width))
+ Width = "480px";
+ if (string.IsNullOrEmpty(Height))
+ Height = "360px";
+ }
+ else
+ {
+ iidx = e.Result.IndexOf("mqdefault", idx);
+ if (iidx > -1)
+ {
+ if (string.IsNullOrEmpty(Width))
+ Width = "320px";
+ if (string.IsNullOrEmpty(Height))
+ Height = "180px";
+ }
+ else
+ {
+ iidx = e.Result.IndexOf("default", idx);
+ if (string.IsNullOrEmpty(Width))
+ Width = "120px";
+ if (string.IsNullOrEmpty(Height))
+ Height = "90px";
+ }
+ }
+ }
+
+ iidx = e.Result.LastIndexOf("http:", iidx, StringComparison.Ordinal);
+ if (iidx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', iidx);
+ if (endIdx > -1)
+ {
+ _videoImageUrl = e.Result.Substring(iidx, endIdx - iidx).Replace("\\\"", "\"").Replace("\\", "");
+ }
+ }
+ }
+
+ idx = e.Result.IndexOf("\"link\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf("http:", idx);
+ if (idx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', idx);
+ if (endIdx > -1)
+ {
+ _videoLinkUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", "");
+ }
+ }
+ }
+ }
+ else
+ {
+ HandleDataLoadFailure(e.Error, "YouTube");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to parse YouTube video response", ex);
+ }
+
+ HandlePostApiCall(sender);
+ }
+
+ ///
+ /// Load Vimeo video data (title, image, link) by calling Vimeo API.
+ ///
+ private void LoadVimeoDataAsync(Uri uri)
+ {
+ ThreadPool.QueueUserWorkItem(state =>
+ {
+ try
+ {
+ var apiUri = new Uri(string.Format("http://vimeo.com/api/v2/video/{0}.json", uri.Segments[2]));
+
+ var client = new WebClient();
+ client.Encoding = Encoding.UTF8;
+ client.DownloadStringCompleted += OnDownloadVimeoApiCompleted;
+ client.DownloadStringAsync(apiUri);
+ }
+ catch (Exception ex)
+ {
+ _imageLoadingComplete = true;
+ SetErrorBorder();
+ HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to get vimeo video data: " + uri, ex);
+ HtmlContainer.RequestRefresh(false);
+ }
+ });
+ }
+
+ ///
+ /// Parse Vimeo API response to get video data (title, image, link).
+ ///
+ private void OnDownloadVimeoApiCompleted(object sender, DownloadStringCompletedEventArgs e)
+ {
+ try
+ {
+ if (!e.Cancelled)
+ {
+ if (e.Error == null)
+ {
+ var idx = e.Result.IndexOf("\"title\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf('"', idx + 7);
+ if (idx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', idx + 1);
+ while (e.Result[endIdx - 1] == '\\')
+ endIdx = e.Result.IndexOf('"', endIdx + 1);
+ if (endIdx > -1)
+ {
+ _videoTitle = e.Result.Substring(idx + 1, endIdx - idx - 1).Replace("\\\"", "\"");
+ }
+ }
+ }
+
+ idx = e.Result.IndexOf("\"thumbnail_large\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ if (string.IsNullOrEmpty(Width))
+ Width = "640";
+ if (string.IsNullOrEmpty(Height))
+ Height = "360";
+ }
+ else
+ {
+ idx = e.Result.IndexOf("thumbnail_medium", idx);
+ if (idx > -1)
+ {
+ if (string.IsNullOrEmpty(Width))
+ Width = "200";
+ if (string.IsNullOrEmpty(Height))
+ Height = "150";
+ }
+ else
+ {
+ idx = e.Result.IndexOf("thumbnail_small", idx);
+ if (string.IsNullOrEmpty(Width))
+ Width = "100";
+ if (string.IsNullOrEmpty(Height))
+ Height = "75";
+ }
+ }
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf("http:", idx);
+ if (idx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', idx);
+ if (endIdx > -1)
+ {
+ _videoImageUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", "");
+ }
+ }
+ }
+
+ idx = e.Result.IndexOf("\"url\"", StringComparison.Ordinal);
+ if (idx > -1)
+ {
+ idx = e.Result.IndexOf("http:", idx);
+ if (idx > -1)
+ {
+ var endIdx = e.Result.IndexOf('"', idx);
+ if (endIdx > -1)
+ {
+ _videoLinkUrl = e.Result.Substring(idx, endIdx - idx).Replace("\\\"", "\"").Replace("\\", "");
+ }
+ }
+ }
+ }
+ else
+ {
+ HandleDataLoadFailure(e.Error, "Vimeo");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to parse Vimeo video response", ex);
+ }
+
+ HandlePostApiCall(sender);
+ }
+
+ ///
+ /// Handle error occurred during video data load to handle if the video was not found.
+ ///
+ /// the exception that occurred during data load web request
+ /// the name of the video source (YouTube/Vimeo/Etc.)
+ private void HandleDataLoadFailure(Exception ex, string source)
+ {
+ var webError = ex as WebException;
+ var webResponse = webError != null ? webError.Response as HttpWebResponse : null;
+ if (webResponse != null && webResponse.StatusCode == HttpStatusCode.NotFound)
+ {
+ _videoTitle = "The video is not found, possibly removed by the user.";
+ }
+ else
+ {
+ HtmlContainer.ReportError(HtmlRenderErrorType.Iframe, "Failed to load " + source + " video data", ex);
+ }
+ }
+
+ ///
+ /// Create image handler for downloading video image if found and release the WebClient instance used for API call.
+ ///
+ private void HandlePostApiCall(object sender)
+ {
+ try
+ {
+ if (_videoImageUrl == null)
+ {
+ _imageLoadingComplete = true;
+ SetErrorBorder();
+ }
+
+ var webClient = (WebClient)sender;
+ webClient.DownloadStringCompleted -= OnDownloadYoutubeApiCompleted;
+ webClient.DownloadStringCompleted -= OnDownloadVimeoApiCompleted;
+ webClient.Dispose();
+
+ HtmlContainer.RequestRefresh(IsLayoutRequired());
+ }
+ catch
+ { }
+ }
+
+ ///
+ /// Paints the fragment
+ ///
+ /// the device to draw to
+ protected override void PaintImp(RGraphics g)
+ {
+ if (_videoImageUrl != null && _imageLoadHandler == null)
+ {
+ _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete);
+ _imageLoadHandler.LoadImage(_videoImageUrl, HtmlTag != null ? HtmlTag.Attributes : null);
+ }
+
+ var rects = CommonUtils.GetFirstValueOrDefault(Rectangles);
+
+ RPoint offset = (HtmlContainer != null && !IsFixed) ? HtmlContainer.ScrollOffset : RPoint.Empty;
+ rects.Offset(offset);
+
+ var clipped = RenderUtils.ClipGraphicsByOverflow(g, this);
+
+ PaintBackground(g, rects, true, true);
+
+ BordersDrawHandler.DrawBoxBorders(g, this, rects, true, true);
+
+ var word = Words[0];
+ var tmpRect = word.Rectangle;
+ tmpRect.Offset(offset);
+ tmpRect.Height -= ActualBorderTopWidth + ActualBorderBottomWidth + ActualPaddingTop + ActualPaddingBottom;
+ tmpRect.Y += ActualBorderTopWidth + ActualPaddingTop;
+ tmpRect.X = Math.Floor(tmpRect.X);
+ tmpRect.Y = Math.Floor(tmpRect.Y);
+ var rect = tmpRect;
+
+ DrawImage(g, offset, rect);
+
+ DrawTitle(g, rect);
+
+ DrawPlay(g, rect);
+
+ if (clipped)
+ g.PopClip();
+ }
+
+ ///
+ /// Draw video image over the iframe if found.
+ ///
+ private void DrawImage(RGraphics g, RPoint offset, RRect rect)
+ {
+ if (_imageWord.Image != null)
+ {
+ if (rect.Width > 0 && rect.Height > 0)
+ {
+ if (_imageWord.ImageRectangle == RRect.Empty)
+ g.DrawImage(_imageWord.Image, rect);
+ else
+ g.DrawImage(_imageWord.Image, rect, _imageWord.ImageRectangle);
+
+ if (_imageWord.Selected)
+ {
+ g.DrawRectangle(GetSelectionBackBrush(g, true), _imageWord.Left + offset.X, _imageWord.Top + offset.Y, _imageWord.Width + 2, DomUtils.GetCssLineBoxByWord(_imageWord).LineHeight);
+ }
+ }
+ }
+ else if (_isVideo && !_imageLoadingComplete)
+ {
+ RenderUtils.DrawImageLoadingIcon(g, HtmlContainer, rect);
+ if (rect.Width > 19 && rect.Height > 19)
+ {
+ g.DrawRectangle(g.GetPen(RColor.LightGray), rect.X, rect.Y, rect.Width, rect.Height);
+ }
+ }
+ }
+
+ ///
+ /// Draw video title on top of the iframe if found.
+ ///
+ private void DrawTitle(RGraphics g, RRect rect)
+ {
+ if (_videoTitle != null && _imageWord.Width > 40 && _imageWord.Height > 40)
+ {
+ var font = HtmlContainer.Adapter.GetFont("Arial", 9f, RFontStyle.Regular);
+ g.DrawRectangle(g.GetSolidBrush(RColor.FromArgb(160, 0, 0, 0)), rect.Left, rect.Top, rect.Width, ActualFont.Height + 7);
+
+ var titleRect = new RRect(rect.Left + 3, rect.Top + 3, rect.Width - 6, rect.Height - 6);
+ g.DrawString(_videoTitle, font, RColor.WhiteSmoke, titleRect.Location, RSize.Empty, false);
+ }
+ }
+
+ ///
+ /// Draw play over the iframe if we found link url.
+ ///
+ private void DrawPlay(RGraphics g, RRect rect)
+ {
+ if (_isVideo && _imageWord.Width > 70 && _imageWord.Height > 50)
+ {
+ var prevMode = g.SetAntiAliasSmoothingMode();
+
+ var size = new RSize(60, 40);
+ var left = rect.Left + (rect.Width - size.Width) / 2;
+ var top = rect.Top + (rect.Height - size.Height) / 2;
+ g.DrawRectangle(g.GetSolidBrush(RColor.FromArgb(160, 0, 0, 0)), left, top, size.Width, size.Height);
+
+ RPoint[] points =
+ {
+ new RPoint(left + size.Width / 3f + 1,top + 3 * size.Height / 4f),
+ new RPoint(left + size.Width / 3f + 1, top + size.Height / 4f),
+ new RPoint(left + 2 * size.Width / 3f + 1, top + size.Height / 2f)
+ };
+ g.DrawPolygon(g.GetSolidBrush(RColor.White), points);
+
+ g.ReturnPreviousSmoothingMode(prevMode);
+ }
+ }
+
+ ///
+ /// Assigns words its width and height
+ ///
+ /// the device to use
+ internal override void MeasureWordsSize(RGraphics g)
+ {
+ if (!_wordsSizeMeasured)
+ {
+ MeasureWordSpacing(g);
+ _wordsSizeMeasured = true;
+ }
+ CssLayoutEngine.MeasureImageSize(_imageWord);
+ }
+
+ ///
+ /// Set error image border on the image box.
+ ///
+ private void SetErrorBorder()
+ {
+ SetAllBorders(CssConstants.Solid, "2px", "#A0A0A0");
+ BorderRightColor = BorderBottomColor = "#E3E3E3";
+ }
+
+ ///
+ /// On image load process is complete with image or without update the image box.
+ ///
+ /// 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 OnLoadImageComplete(RImage image, RRect rectangle, bool async)
+ {
+ _imageWord.Image = image;
+ _imageWord.ImageRectangle = rectangle;
+ _imageLoadingComplete = true;
+ _wordsSizeMeasured = false;
+
+ if (_imageLoadingComplete && image == null)
+ {
+ SetErrorBorder();
+ }
+
+ if (async)
+ {
+ HtmlContainer.RequestRefresh(IsLayoutRequired());
+ }
+ }
+
+ private bool IsLayoutRequired()
+ {
+ var width = new CssLength(Width);
+ var height = new CssLength(Height);
+ return (width.Number <= 0 || width.Unit != CssUnit.Pixels) || (height.Number <= 0 || height.Unit != CssUnit.Pixels);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssBoxHr.cs b/Source/HtmlRenderer.Core/Core/Dom/CssBoxHr.cs
new file mode 100644
index 000000000..8280f47c3
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssBoxHr.cs
@@ -0,0 +1,122 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Handlers;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// CSS box for hr element.
+ ///
+ internal sealed class CssBoxHr : CssBox
+ {
+ ///
+ /// Init.
+ ///
+ /// the parent box of this box
+ /// the html tag data of this box
+ public CssBoxHr(CssBox parent, HtmlTag tag)
+ : base(parent, tag)
+ {
+ Display = CssConstants.Block;
+ }
+
+ ///
+ /// 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 override void PerformLayoutImp(RGraphics g)
+ {
+ if (Display == CssConstants.None)
+ return;
+
+ RectanglesReset();
+
+ 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;
+
+ //width at 100% (or auto)
+ double minwidth = GetMinimumWidth();
+ double width = ContainingBlock.Size.Width
+ - ContainingBlock.ActualPaddingLeft - ContainingBlock.ActualPaddingRight
+ - ContainingBlock.ActualBorderLeftWidth - ContainingBlock.ActualBorderRightWidth
+ - ActualMarginLeft - ActualMarginRight - ActualBorderLeftWidth - ActualBorderRightWidth;
+
+ //Check width if not auto
+ if (Width != CssConstants.Auto && !string.IsNullOrEmpty(Width))
+ {
+ width = CssValueParser.ParseLength(Width, width, this);
+ }
+
+ if (width < minwidth || width >= 9999)
+ width = minwidth;
+
+ double height = ActualHeight;
+ if (height < 1)
+ {
+ height = Size.Height + ActualBorderTopWidth + ActualBorderBottomWidth;
+ }
+ if (height < 1)
+ {
+ height = 2;
+ }
+ if (height <= 2 && ActualBorderTopWidth < 1 && ActualBorderBottomWidth < 1)
+ {
+ BorderTopStyle = BorderBottomStyle = CssConstants.Solid;
+ BorderTopWidth = "1px";
+ BorderBottomWidth = "1px";
+ }
+
+ Size = new RSize(width, height);
+
+ ActualBottom = Location.Y + ActualPaddingTop + ActualPaddingBottom + height;
+ }
+
+ ///
+ /// Paints the fragment
+ ///
+ /// the device to draw to
+ protected override void PaintImp(RGraphics g)
+ {
+ var offset = (HtmlContainer != null && !IsFixed) ? HtmlContainer.ScrollOffset : RPoint.Empty;
+ var rect = new RRect(Bounds.X + offset.X, Bounds.Y + offset.Y, Bounds.Width, Bounds.Height);
+
+ if (rect.Height > 2 && RenderUtils.IsColorVisible(ActualBackgroundColor))
+ {
+ g.DrawRectangle(g.GetSolidBrush(ActualBackgroundColor), rect.X, rect.Y, rect.Width, rect.Height);
+ }
+
+ var b1 = g.GetSolidBrush(ActualBorderTopColor);
+ BordersDrawHandler.DrawBorder(Border.Top, g, this, b1, rect);
+
+ if (rect.Height > 1)
+ {
+ var b2 = g.GetSolidBrush(ActualBorderLeftColor);
+ BordersDrawHandler.DrawBorder(Border.Left, g, this, b2, rect);
+
+ var b3 = g.GetSolidBrush(ActualBorderRightColor);
+ BordersDrawHandler.DrawBorder(Border.Right, g, this, b3, rect);
+
+ var b4 = g.GetSolidBrush(ActualBorderBottomColor);
+ BordersDrawHandler.DrawBorder(Border.Bottom, g, this, b4, rect);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssBoxImage.cs b/Source/HtmlRenderer.Core/Core/Dom/CssBoxImage.cs
new file mode 100644
index 000000000..a33627c83
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssBoxImage.cs
@@ -0,0 +1,210 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Handlers;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// CSS box for image element.
+ ///
+ internal sealed class CssBoxImage : CssBox
+ {
+ #region Fields and Consts
+
+ ///
+ /// the image word of this image box
+ ///
+ private readonly CssRectImage _imageWord;
+
+ ///
+ /// handler used for image loading by source
+ ///
+ private ImageLoadHandler _imageLoadHandler;
+
+ ///
+ /// is image load is finished, used to know if no image is found
+ ///
+ private bool _imageLoadingComplete;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the parent box of this box
+ /// the html tag data of this box
+ public CssBoxImage(CssBox parent, HtmlTag tag)
+ : base(parent, tag)
+ {
+ _imageWord = new CssRectImage(this);
+ Words.Add(_imageWord);
+ }
+
+ ///
+ /// Get the image of this image box.
+ ///
+ public RImage Image
+ {
+ get { return _imageWord.Image; }
+ }
+
+ ///
+ /// Paints the fragment
+ ///
+ /// the device to draw to
+ protected override void PaintImp(RGraphics g)
+ {
+ // load image if it is in visible rectangle
+ if (_imageLoadHandler == null)
+ {
+ _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete);
+ _imageLoadHandler.LoadImage(GetAttribute("src"), HtmlTag != null ? HtmlTag.Attributes : null);
+ }
+
+ var rect = CommonUtils.GetFirstValueOrDefault(Rectangles);
+ RPoint offset = RPoint.Empty;
+
+ if (!IsFixed)
+ offset = HtmlContainer.ScrollOffset;
+
+ rect.Offset(offset);
+
+ var clipped = RenderUtils.ClipGraphicsByOverflow(g, this);
+
+ PaintBackground(g, rect, true, true);
+ BordersDrawHandler.DrawBoxBorders(g, this, rect, true, true);
+
+ RRect r = _imageWord.Rectangle;
+ r.Offset(offset);
+ r.Height -= ActualBorderTopWidth + ActualBorderBottomWidth + ActualPaddingTop + ActualPaddingBottom;
+ r.Y += ActualBorderTopWidth + ActualPaddingTop;
+ r.X = Math.Floor(r.X);
+ r.Y = Math.Floor(r.Y);
+
+ if (_imageWord.Image != null)
+ {
+ if (r.Width > 0 && r.Height > 0)
+ {
+ if (_imageWord.ImageRectangle == RRect.Empty)
+ g.DrawImage(_imageWord.Image, r);
+ else
+ g.DrawImage(_imageWord.Image, r, _imageWord.ImageRectangle);
+
+ if (_imageWord.Selected)
+ {
+ g.DrawRectangle(GetSelectionBackBrush(g, true), _imageWord.Left + offset.X, _imageWord.Top + offset.Y, _imageWord.Width + 2, DomUtils.GetCssLineBoxByWord(_imageWord).LineHeight);
+ }
+ }
+ }
+ else if (_imageLoadingComplete)
+ {
+ if (_imageLoadingComplete && r.Width > 19 && r.Height > 19)
+ {
+ RenderUtils.DrawImageErrorIcon(g, HtmlContainer, r);
+ }
+ }
+ else
+ {
+ RenderUtils.DrawImageLoadingIcon(g, HtmlContainer, r);
+ if (r.Width > 19 && r.Height > 19)
+ {
+ g.DrawRectangle(g.GetPen(RColor.LightGray), r.X, r.Y, r.Width, r.Height);
+ }
+ }
+
+ if (clipped)
+ g.PopClip();
+ }
+
+ ///
+ /// Assigns words its width and height
+ ///
+ /// the device to use
+ internal override void MeasureWordsSize(RGraphics g)
+ {
+ if (!_wordsSizeMeasured)
+ {
+ if (_imageLoadHandler == null && (HtmlContainer.AvoidAsyncImagesLoading || HtmlContainer.AvoidImagesLateLoading))
+ {
+ _imageLoadHandler = new ImageLoadHandler(HtmlContainer, OnLoadImageComplete);
+
+ if (this.Content != null && this.Content != CssConstants.Normal)
+ _imageLoadHandler.LoadImage(this.Content, HtmlTag != null ? HtmlTag.Attributes : null);
+ else
+ _imageLoadHandler.LoadImage(GetAttribute("src"), HtmlTag != null ? HtmlTag.Attributes : null);
+ }
+
+ MeasureWordSpacing(g);
+ _wordsSizeMeasured = true;
+ }
+
+ CssLayoutEngine.MeasureImageSize(_imageWord);
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public override void Dispose()
+ {
+ if (_imageLoadHandler != null)
+ _imageLoadHandler.Dispose();
+ base.Dispose();
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Set error image border on the image box.
+ ///
+ private void SetErrorBorder()
+ {
+ SetAllBorders(CssConstants.Solid, "2px", "#A0A0A0");
+ BorderRightColor = BorderBottomColor = "#E3E3E3";
+ }
+
+ ///
+ /// On image load process is complete with image or without update the image box.
+ ///
+ /// 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 OnLoadImageComplete(RImage image, RRect rectangle, bool async)
+ {
+ _imageWord.Image = image;
+ _imageWord.ImageRectangle = rectangle;
+ _imageLoadingComplete = true;
+ _wordsSizeMeasured = false;
+
+ if (_imageLoadingComplete && image == null)
+ {
+ SetErrorBorder();
+ }
+
+ if (!HtmlContainer.AvoidImagesLateLoading || async)
+ {
+ var width = new CssLength(Width);
+ var height = new CssLength(Height);
+ var layout = (width.Number <= 0 || width.Unit != CssUnit.Pixels) || (height.Number <= 0 || height.Unit != CssUnit.Pixels);
+ HtmlContainer.RequestRefresh(layout);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssBoxProperties.cs b/Source/HtmlRenderer.Core/Core/Dom/CssBoxProperties.cs
new file mode 100644
index 000000000..7abfb53ed
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssBoxProperties.cs
@@ -0,0 +1,1567 @@
+// "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.Globalization;
+using System.Text.RegularExpressions;
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Base class for css box to handle the css properties.
+ /// Has field and property for every css property that can be set, the properties add additional parsing like
+ /// setting the correct border depending what border value was set (single, two , all four).
+ /// Has additional fields to control the location and size of the box and 'actual' css values for some properties
+ /// that require additional calculations and parsing.
+ ///
+ internal abstract class CssBoxProperties
+ {
+ #region CSS Fields
+
+ private string _backgroundColor = "transparent";
+ private string _backgroundGradient = "none";
+ private string _backgroundGradientAngle = "90";
+ private string _backgroundImage = "none";
+ private string _backgroundPosition = "0% 0%";
+ private string _backgroundRepeat = "repeat";
+ private string _borderTopWidth = "medium";
+ private string _borderRightWidth = "medium";
+ private string _borderBottomWidth = "medium";
+ private string _borderLeftWidth = "medium";
+ private string _borderTopColor = "black";
+ private string _borderRightColor = "black";
+ private string _borderBottomColor = "black";
+ private string _borderLeftColor = "black";
+ private string _borderTopStyle = "none";
+ private string _borderRightStyle = "none";
+ private string _borderBottomStyle = "none";
+ private string _borderLeftStyle = "none";
+ private string _borderSpacing = "0";
+ private string _borderCollapse = "separate";
+ private string _bottom;
+ private string _color = "black";
+ private string _content = "normal";
+ private string _cornerNwRadius = "0";
+ private string _cornerNeRadius = "0";
+ private string _cornerSeRadius = "0";
+ private string _cornerSwRadius = "0";
+ private string _cornerRadius = "0";
+ private string _emptyCells = "show";
+ private string _direction = "ltr";
+ private string _display = "inline";
+ private string _fontFamily;
+ private string _fontSize = "medium";
+ private string _fontStyle = "normal";
+ private string _fontVariant = "normal";
+ private string _fontWeight = "normal";
+ private string _float = "none";
+ private string _height = "auto";
+ private string _marginBottom = "0";
+ private string _marginLeft = "0";
+ private string _marginRight = "0";
+ private string _marginTop = "0";
+ private string _left = "auto";
+ private string _lineHeight = "normal";
+ private string _listStyleType = "disc";
+ private string _listStyleImage = string.Empty;
+ private string _listStylePosition = "outside";
+ private string _listStyle = string.Empty;
+ private string _overflow = "visible";
+ private string _paddingLeft = "0";
+ private string _paddingBottom = "0";
+ private string _paddingRight = "0";
+ private string _paddingTop = "0";
+ private string _pageBreakInside = CssConstants.Auto;
+ private string _right;
+ private string _textAlign = string.Empty;
+ private string _textDecoration = string.Empty;
+ private string _textIndent = "0";
+ private string _top = "auto";
+ private string _position = "static";
+ private string _verticalAlign = "baseline";
+ private string _width = "auto";
+ private string _maxWidth = "none";
+ private string _wordSpacing = "normal";
+ private string _wordBreak = "normal";
+ private string _whiteSpace = "normal";
+ private string _visibility = "visible";
+
+ #endregion
+
+
+ #region Fields
+
+ ///
+ /// Gets or sets the location of the box
+ ///
+ private RPoint _location;
+
+ ///
+ /// Gets or sets the size of the box
+ ///
+ private RSize _size;
+
+ private double _actualCornerNw = double.NaN;
+ private double _actualCornerNe = double.NaN;
+ private double _actualCornerSw = double.NaN;
+ private double _actualCornerSe = double.NaN;
+ private RColor _actualColor = RColor.Empty;
+ private double _actualBackgroundGradientAngle = double.NaN;
+ private double _actualHeight = double.NaN;
+ private double _actualWidth = double.NaN;
+ private double _actualPaddingTop = double.NaN;
+ private double _actualPaddingBottom = double.NaN;
+ private double _actualPaddingRight = double.NaN;
+ private double _actualPaddingLeft = double.NaN;
+ private double _actualMarginTop = double.NaN;
+ private double _collapsedMarginTop = double.NaN;
+ private double _actualMarginBottom = double.NaN;
+ private double _actualMarginRight = double.NaN;
+ private double _actualMarginLeft = double.NaN;
+ private double _actualBorderTopWidth = double.NaN;
+ private double _actualBorderLeftWidth = double.NaN;
+ private double _actualBorderBottomWidth = double.NaN;
+ private double _actualBorderRightWidth = double.NaN;
+
+ ///
+ /// the width of whitespace between words
+ ///
+ private double _actualLineHeight = double.NaN;
+
+ private double _actualWordSpacing = double.NaN;
+ private double _actualTextIndent = double.NaN;
+ private double _actualBorderSpacingHorizontal = double.NaN;
+ private double _actualBorderSpacingVertical = double.NaN;
+ private RColor _actualBackgroundGradient = RColor.Empty;
+ private RColor _actualBorderTopColor = RColor.Empty;
+ private RColor _actualBorderLeftColor = RColor.Empty;
+ private RColor _actualBorderBottomColor = RColor.Empty;
+ private RColor _actualBorderRightColor = RColor.Empty;
+ private RColor _actualBackgroundColor = RColor.Empty;
+ private RFont _actualFont;
+
+ #endregion
+
+
+ #region CSS Properties
+
+ public string BorderBottomWidth
+ {
+ get { return _borderBottomWidth; }
+ set
+ {
+ _borderBottomWidth = value;
+ _actualBorderBottomWidth = Single.NaN;
+ }
+ }
+
+ public string BorderLeftWidth
+ {
+ get { return _borderLeftWidth; }
+ set
+ {
+ _borderLeftWidth = value;
+ _actualBorderLeftWidth = Single.NaN;
+ }
+ }
+
+ public string BorderRightWidth
+ {
+ get { return _borderRightWidth; }
+ set
+ {
+ _borderRightWidth = value;
+ _actualBorderRightWidth = Single.NaN;
+ }
+ }
+
+ public string BorderTopWidth
+ {
+ get { return _borderTopWidth; }
+ set
+ {
+ _borderTopWidth = value;
+ _actualBorderTopWidth = Single.NaN;
+ }
+ }
+
+ public string BorderBottomStyle
+ {
+ get { return _borderBottomStyle; }
+ set { _borderBottomStyle = value; }
+ }
+
+ public string BorderLeftStyle
+ {
+ get { return _borderLeftStyle; }
+ set { _borderLeftStyle = value; }
+ }
+
+ public string BorderRightStyle
+ {
+ get { return _borderRightStyle; }
+ set { _borderRightStyle = value; }
+ }
+
+ public string BorderTopStyle
+ {
+ get { return _borderTopStyle; }
+ set { _borderTopStyle = value; }
+ }
+
+ public string BorderBottomColor
+ {
+ get { return _borderBottomColor; }
+ set
+ {
+ _borderBottomColor = value;
+ _actualBorderBottomColor = RColor.Empty;
+ }
+ }
+
+ public string BorderLeftColor
+ {
+ get { return _borderLeftColor; }
+ set
+ {
+ _borderLeftColor = value;
+ _actualBorderLeftColor = RColor.Empty;
+ }
+ }
+
+ public string BorderRightColor
+ {
+ get { return _borderRightColor; }
+ set
+ {
+ _borderRightColor = value;
+ _actualBorderRightColor = RColor.Empty;
+ }
+ }
+
+ public string BorderTopColor
+ {
+ get { return _borderTopColor; }
+ set
+ {
+ _borderTopColor = value;
+ _actualBorderTopColor = RColor.Empty;
+ }
+ }
+
+ public string BorderSpacing
+ {
+ get { return _borderSpacing; }
+ set { _borderSpacing = value; }
+ }
+
+ public string BorderCollapse
+ {
+ get { return _borderCollapse; }
+ set { _borderCollapse = value; }
+ }
+
+ public string CornerRadius
+ {
+ get { return _cornerRadius; }
+ set
+ {
+ MatchCollection r = RegexParserUtils.Match(RegexParserUtils.CssLength, value);
+
+ switch (r.Count)
+ {
+ case 1:
+ CornerNeRadius = r[0].Value;
+ CornerNwRadius = r[0].Value;
+ CornerSeRadius = r[0].Value;
+ CornerSwRadius = r[0].Value;
+ break;
+ case 2:
+ CornerNeRadius = r[0].Value;
+ CornerNwRadius = r[0].Value;
+ CornerSeRadius = r[1].Value;
+ CornerSwRadius = r[1].Value;
+ break;
+ case 3:
+ CornerNeRadius = r[0].Value;
+ CornerNwRadius = r[1].Value;
+ CornerSeRadius = r[2].Value;
+ break;
+ case 4:
+ CornerNeRadius = r[0].Value;
+ CornerNwRadius = r[1].Value;
+ CornerSeRadius = r[2].Value;
+ CornerSwRadius = r[3].Value;
+ break;
+ }
+
+ _cornerRadius = value;
+ }
+ }
+
+ public string CornerNwRadius
+ {
+ get { return _cornerNwRadius; }
+ set { _cornerNwRadius = value; }
+ }
+
+ public string CornerNeRadius
+ {
+ get { return _cornerNeRadius; }
+ set { _cornerNeRadius = value; }
+ }
+
+ public string CornerSeRadius
+ {
+ get { return _cornerSeRadius; }
+ set { _cornerSeRadius = value; }
+ }
+
+ public string CornerSwRadius
+ {
+ get { return _cornerSwRadius; }
+ set { _cornerSwRadius = value; }
+ }
+
+ public string MarginBottom
+ {
+ get { return _marginBottom; }
+ set { _marginBottom = value; }
+ }
+
+ public string MarginLeft
+ {
+ get { return _marginLeft; }
+ set { _marginLeft = value; }
+ }
+
+ public string MarginRight
+ {
+ get { return _marginRight; }
+ set { _marginRight = value; }
+ }
+
+ public string MarginTop
+ {
+ get { return _marginTop; }
+ set { _marginTop = value; }
+ }
+
+ public string PaddingBottom
+ {
+ get { return _paddingBottom; }
+ set
+ {
+ _paddingBottom = value;
+ _actualPaddingBottom = double.NaN;
+ }
+ }
+
+ public string PaddingLeft
+ {
+ get { return _paddingLeft; }
+ set
+ {
+ _paddingLeft = value;
+ _actualPaddingLeft = double.NaN;
+ }
+ }
+
+ public string PaddingRight
+ {
+ get { return _paddingRight; }
+ set
+ {
+ _paddingRight = value;
+ _actualPaddingRight = double.NaN;
+ }
+ }
+
+ public string PaddingTop
+ {
+ get { return _paddingTop; }
+ set
+ {
+ _paddingTop = value;
+ _actualPaddingTop = double.NaN;
+ }
+ }
+
+ public string PageBreakInside
+ {
+ get { return _pageBreakInside; }
+ set
+ {
+ _pageBreakInside = value;
+ }
+ }
+
+ public string Left
+ {
+ get { return _left; }
+ set
+ {
+ _left = value;
+
+ if (Position == CssConstants.Fixed)
+ {
+ _location = GetActualLocation(Left, Top);
+ }
+ }
+ }
+
+ public string Top
+ {
+ get { return _top; }
+ set {
+ _top = value;
+
+ if (Position == CssConstants.Fixed)
+ {
+ _location = GetActualLocation(Left, Top);
+ }
+
+ }
+ }
+
+ public string Width
+ {
+ get { return _width; }
+ set { _width = value; }
+ }
+
+ public string MaxWidth
+ {
+ get { return _maxWidth; }
+ set { _maxWidth = value; }
+ }
+
+ public string Height
+ {
+ get { return _height; }
+ set { _height = value; }
+ }
+
+ public string BackgroundColor
+ {
+ get { return _backgroundColor; }
+ set { _backgroundColor = value; }
+ }
+
+ public string BackgroundImage
+ {
+ get { return _backgroundImage; }
+ set { _backgroundImage = value; }
+ }
+
+ public string BackgroundPosition
+ {
+ get { return _backgroundPosition; }
+ set { _backgroundPosition = value; }
+ }
+
+ public string BackgroundRepeat
+ {
+ get { return _backgroundRepeat; }
+ set { _backgroundRepeat = value; }
+ }
+
+ public string BackgroundGradient
+ {
+ get { return _backgroundGradient; }
+ set { _backgroundGradient = value; }
+ }
+
+ public string BackgroundGradientAngle
+ {
+ get { return _backgroundGradientAngle; }
+ set { _backgroundGradientAngle = value; }
+ }
+
+ public string Color
+ {
+ get { return _color; }
+ set
+ {
+ _color = value;
+ _actualColor = RColor.Empty;
+ }
+ }
+
+ public string Content
+ {
+ get { return _content; }
+ set { _content = value; }
+ }
+
+ public string Display
+ {
+ get { return _display; }
+ set { _display = value; }
+ }
+
+ public string Direction
+ {
+ get { return _direction; }
+ set { _direction = value; }
+ }
+
+ public string EmptyCells
+ {
+ get { return _emptyCells; }
+ set { _emptyCells = value; }
+ }
+
+ public string Float
+ {
+ get { return _float; }
+ set { _float = value; }
+ }
+
+ public string Position
+ {
+ get { return _position; }
+ set { _position = value; }
+ }
+
+ public string LineHeight
+ {
+ get { return _lineHeight; }
+ set { _lineHeight = string.Format(NumberFormatInfo.InvariantInfo, "{0}px", CssValueParser.ParseLength(value, Size.Height, this, CssConstants.Em)); }
+ }
+
+ public string VerticalAlign
+ {
+ get { return _verticalAlign; }
+ set { _verticalAlign = value; }
+ }
+
+ public string TextIndent
+ {
+ get { return _textIndent; }
+ set { _textIndent = NoEms(value); }
+ }
+
+ public string TextAlign
+ {
+ get { return _textAlign; }
+ set { _textAlign = value; }
+ }
+
+ public string TextDecoration
+ {
+ get { return _textDecoration; }
+ set { _textDecoration = value; }
+ }
+
+ public string WhiteSpace
+ {
+ get { return _whiteSpace; }
+ set { _whiteSpace = value; }
+ }
+
+ public string Visibility
+ {
+ get { return _visibility; }
+ set { _visibility = value; }
+ }
+
+ public string WordSpacing
+ {
+ get { return _wordSpacing; }
+ set { _wordSpacing = NoEms(value); }
+ }
+
+ public string WordBreak
+ {
+ get { return _wordBreak; }
+ set { _wordBreak = value; }
+ }
+
+ public string FontFamily
+ {
+ get { return _fontFamily; }
+ set { _fontFamily = value; }
+ }
+
+ public string FontSize
+ {
+ get { return _fontSize; }
+ set
+ {
+ string length = RegexParserUtils.Search(RegexParserUtils.CssLength, value);
+
+ if (length != null)
+ {
+ string computedValue;
+ CssLength len = new CssLength(length);
+
+ if (len.HasError)
+ {
+ computedValue = "medium";
+ }
+ else if (len.Unit == CssUnit.Ems && GetParent() != null)
+ {
+ computedValue = len.ConvertEmToPoints(GetParent().ActualFont.Size).ToString();
+ }
+ else
+ {
+ computedValue = len.ToString();
+ }
+
+ _fontSize = computedValue;
+ }
+ else
+ {
+ _fontSize = value;
+ }
+ }
+ }
+
+ public string FontStyle
+ {
+ get { return _fontStyle; }
+ set { _fontStyle = value; }
+ }
+
+ public string FontVariant
+ {
+ get { return _fontVariant; }
+ set { _fontVariant = value; }
+ }
+
+ public string FontWeight
+ {
+ get { return _fontWeight; }
+ set { _fontWeight = value; }
+ }
+
+ public string ListStyle
+ {
+ get { return _listStyle; }
+ set { _listStyle = value; }
+ }
+
+ public string Overflow
+ {
+ get { return _overflow; }
+ set { _overflow = value; }
+ }
+
+ public string ListStylePosition
+ {
+ get { return _listStylePosition; }
+ set { _listStylePosition = value; }
+ }
+
+ public string ListStyleImage
+ {
+ get { return _listStyleImage; }
+ set { _listStyleImage = value; }
+ }
+
+ public string ListStyleType
+ {
+ get { return _listStyleType; }
+ set { _listStyleType = value; }
+ }
+
+ #endregion CSS Propertier
+
+ ///
+ /// Gets or sets the location of the box
+ ///
+ public RPoint Location
+ {
+ get {
+ if (_location.IsEmpty && Position == CssConstants.Fixed)
+ {
+ var left = Left;
+ var top = Top;
+
+ _location = GetActualLocation(Left, Top);
+ }
+ return _location;
+ }
+ set {
+ _location = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the size of the box
+ ///
+ public RSize Size
+ {
+ get { return _size; }
+ set { _size = value; }
+ }
+
+ ///
+ /// Gets the bounds of the box
+ ///
+ public RRect Bounds
+ {
+ get { return new RRect(Location, Size); }
+ }
+
+ ///
+ /// Gets the width available on the box, counting padding and margin.
+ ///
+ public double AvailableWidth
+ {
+ get { return Size.Width - ActualBorderLeftWidth - ActualPaddingLeft - ActualPaddingRight - ActualBorderRightWidth; }
+ }
+
+ ///
+ /// Gets the right of the box. When setting, it will affect only the width of the box.
+ ///
+ public double ActualRight
+ {
+ get { return Location.X + Size.Width; }
+ set { Size = new RSize(value - Location.X, Size.Height); }
+ }
+
+ ///
+ /// Gets or sets the bottom of the box.
+ /// (When setting, alters only the Size.Height of the box)
+ ///
+ public double ActualBottom
+ {
+ get { return Location.Y + Size.Height; }
+ set { Size = new RSize(Size.Width, value - Location.Y); }
+ }
+
+ ///
+ /// Gets the left of the client rectangle (Where content starts rendering)
+ ///
+ public double ClientLeft
+ {
+ get { return Location.X + ActualBorderLeftWidth + ActualPaddingLeft; }
+ }
+
+ ///
+ /// Gets the top of the client rectangle (Where content starts rendering)
+ ///
+ public double ClientTop
+ {
+ get { return Location.Y + ActualBorderTopWidth + ActualPaddingTop; }
+ }
+
+ ///
+ /// Gets the right of the client rectangle
+ ///
+ public double ClientRight
+ {
+ get { return ActualRight - ActualPaddingRight - ActualBorderRightWidth; }
+ }
+
+ ///
+ /// Gets the bottom of the client rectangle
+ ///
+ public double ClientBottom
+ {
+ get { return ActualBottom - ActualPaddingBottom - ActualBorderBottomWidth; }
+ }
+
+ ///
+ /// Gets the client rectangle
+ ///
+ public RRect ClientRectangle
+ {
+ get { return RRect.FromLTRB(ClientLeft, ClientTop, ClientRight, ClientBottom); }
+ }
+
+ ///
+ /// Gets the actual height
+ ///
+ public double ActualHeight
+ {
+ get
+ {
+ if (double.IsNaN(_actualHeight))
+ {
+ _actualHeight = CssValueParser.ParseLength(Height, Size.Height, this);
+ }
+ return _actualHeight;
+ }
+ }
+
+ ///
+ /// Gets the actual height
+ ///
+ public double ActualWidth
+ {
+ get
+ {
+ if (double.IsNaN(_actualWidth))
+ {
+ _actualWidth = CssValueParser.ParseLength(Width, Size.Width, this);
+ }
+ return _actualWidth;
+ }
+ }
+
+ ///
+ /// Gets the actual top's padding
+ ///
+ public double ActualPaddingTop
+ {
+ get
+ {
+ if (double.IsNaN(_actualPaddingTop))
+ {
+ _actualPaddingTop = CssValueParser.ParseLength(PaddingTop, Size.Width, this);
+ }
+ return _actualPaddingTop;
+ }
+ }
+
+ ///
+ /// Gets the actual padding on the left
+ ///
+ public double ActualPaddingLeft
+ {
+ get
+ {
+ if (double.IsNaN(_actualPaddingLeft))
+ {
+ _actualPaddingLeft = CssValueParser.ParseLength(PaddingLeft, Size.Width, this);
+ }
+ return _actualPaddingLeft;
+ }
+ }
+
+ ///
+ /// Gets the actual Padding of the bottom
+ ///
+ public double ActualPaddingBottom
+ {
+ get
+ {
+ if (double.IsNaN(_actualPaddingBottom))
+ {
+ _actualPaddingBottom = CssValueParser.ParseLength(PaddingBottom, Size.Width, this);
+ }
+ return _actualPaddingBottom;
+ }
+ }
+
+ ///
+ /// Gets the actual padding on the right
+ ///
+ public double ActualPaddingRight
+ {
+ get
+ {
+ if (double.IsNaN(_actualPaddingRight))
+ {
+ _actualPaddingRight = CssValueParser.ParseLength(PaddingRight, Size.Width, this);
+ }
+ return _actualPaddingRight;
+ }
+ }
+
+ ///
+ /// Gets the actual top's Margin
+ ///
+ public double ActualMarginTop
+ {
+ get
+ {
+ if (double.IsNaN(_actualMarginTop))
+ {
+ if (MarginTop == CssConstants.Auto)
+ MarginTop = "0";
+ var actualMarginTop = CssValueParser.ParseLength(MarginTop, Size.Width, this);
+ if (MarginLeft.EndsWith("%"))
+ return actualMarginTop;
+ _actualMarginTop = actualMarginTop;
+ }
+ return _actualMarginTop;
+ }
+ }
+
+ ///
+ /// The margin top value if was effected by margin collapse.
+ ///
+ public double CollapsedMarginTop
+ {
+ get { return double.IsNaN(_collapsedMarginTop) ? 0 : _collapsedMarginTop; }
+ set { _collapsedMarginTop = value; }
+ }
+
+ ///
+ /// Gets the actual Margin on the left
+ ///
+ public double ActualMarginLeft
+ {
+ get
+ {
+ if (double.IsNaN(_actualMarginLeft))
+ {
+ if (MarginLeft == CssConstants.Auto)
+ MarginLeft = "0";
+ var actualMarginLeft = CssValueParser.ParseLength(MarginLeft, Size.Width, this);
+ if (MarginLeft.EndsWith("%"))
+ return actualMarginLeft;
+ _actualMarginLeft = actualMarginLeft;
+ }
+ return _actualMarginLeft;
+ }
+ }
+
+ ///
+ /// Gets the actual Margin of the bottom
+ ///
+ public double ActualMarginBottom
+ {
+ get
+ {
+ if (double.IsNaN(_actualMarginBottom))
+ {
+ if (MarginBottom == CssConstants.Auto)
+ MarginBottom = "0";
+ var actualMarginBottom = CssValueParser.ParseLength(MarginBottom, Size.Width, this);
+ if (MarginLeft.EndsWith("%"))
+ return actualMarginBottom;
+ _actualMarginBottom = actualMarginBottom;
+ }
+ return _actualMarginBottom;
+ }
+ }
+
+ ///
+ /// Gets the actual Margin on the right
+ ///
+ public double ActualMarginRight
+ {
+ get
+ {
+ if (double.IsNaN(_actualMarginRight))
+ {
+ if (MarginRight == CssConstants.Auto)
+ MarginRight = "0";
+ var actualMarginRight = CssValueParser.ParseLength(MarginRight, Size.Width, this);
+ if (MarginLeft.EndsWith("%"))
+ return actualMarginRight;
+ _actualMarginRight = actualMarginRight;
+ }
+ return _actualMarginRight;
+ }
+ }
+
+ ///
+ /// Gets the actual top border width
+ ///
+ public double ActualBorderTopWidth
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderTopWidth))
+ {
+ _actualBorderTopWidth = CssValueParser.GetActualBorderWidth(BorderTopWidth, this);
+ if (string.IsNullOrEmpty(BorderTopStyle) || BorderTopStyle == CssConstants.None)
+ {
+ _actualBorderTopWidth = 0f;
+ }
+ }
+ return _actualBorderTopWidth;
+ }
+ }
+
+ ///
+ /// Gets the actual Left border width
+ ///
+ public double ActualBorderLeftWidth
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderLeftWidth))
+ {
+ _actualBorderLeftWidth = CssValueParser.GetActualBorderWidth(BorderLeftWidth, this);
+ if (string.IsNullOrEmpty(BorderLeftStyle) || BorderLeftStyle == CssConstants.None)
+ {
+ _actualBorderLeftWidth = 0f;
+ }
+ }
+ return _actualBorderLeftWidth;
+ }
+ }
+
+ ///
+ /// Gets the actual Bottom border width
+ ///
+ public double ActualBorderBottomWidth
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderBottomWidth))
+ {
+ _actualBorderBottomWidth = CssValueParser.GetActualBorderWidth(BorderBottomWidth, this);
+ if (string.IsNullOrEmpty(BorderBottomStyle) || BorderBottomStyle == CssConstants.None)
+ {
+ _actualBorderBottomWidth = 0f;
+ }
+ }
+ return _actualBorderBottomWidth;
+ }
+ }
+
+ ///
+ /// Gets the actual Right border width
+ ///
+ public double ActualBorderRightWidth
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderRightWidth))
+ {
+ _actualBorderRightWidth = CssValueParser.GetActualBorderWidth(BorderRightWidth, this);
+ if (string.IsNullOrEmpty(BorderRightStyle) || BorderRightStyle == CssConstants.None)
+ {
+ _actualBorderRightWidth = 0f;
+ }
+ }
+ return _actualBorderRightWidth;
+ }
+ }
+
+ ///
+ /// Gets the actual top border Color
+ ///
+ public RColor ActualBorderTopColor
+ {
+ get
+ {
+ if (_actualBorderTopColor.IsEmpty)
+ {
+ _actualBorderTopColor = GetActualColor(BorderTopColor);
+ }
+ return _actualBorderTopColor;
+ }
+ }
+
+ protected abstract RPoint GetActualLocation(string X, string Y);
+
+ protected abstract RColor GetActualColor(string colorStr);
+
+ ///
+ /// Gets the actual Left border Color
+ ///
+ public RColor ActualBorderLeftColor
+ {
+ get
+ {
+ if ((_actualBorderLeftColor.IsEmpty))
+ {
+ _actualBorderLeftColor = GetActualColor(BorderLeftColor);
+ }
+ return _actualBorderLeftColor;
+ }
+ }
+
+ ///
+ /// Gets the actual Bottom border Color
+ ///
+ public RColor ActualBorderBottomColor
+ {
+ get
+ {
+ if ((_actualBorderBottomColor.IsEmpty))
+ {
+ _actualBorderBottomColor = GetActualColor(BorderBottomColor);
+ }
+ return _actualBorderBottomColor;
+ }
+ }
+
+ ///
+ /// Gets the actual Right border Color
+ ///
+ public RColor ActualBorderRightColor
+ {
+ get
+ {
+ if ((_actualBorderRightColor.IsEmpty))
+ {
+ _actualBorderRightColor = GetActualColor(BorderRightColor);
+ }
+ return _actualBorderRightColor;
+ }
+ }
+
+ ///
+ /// Gets the actual length of the north west corner
+ ///
+ public double ActualCornerNw
+ {
+ get
+ {
+ if (double.IsNaN(_actualCornerNw))
+ {
+ _actualCornerNw = CssValueParser.ParseLength(CornerNwRadius, 0, this);
+ }
+ return _actualCornerNw;
+ }
+ }
+
+ ///
+ /// Gets the actual length of the north east corner
+ ///
+ public double ActualCornerNe
+ {
+ get
+ {
+ if (double.IsNaN(_actualCornerNe))
+ {
+ _actualCornerNe = CssValueParser.ParseLength(CornerNeRadius, 0, this);
+ }
+ return _actualCornerNe;
+ }
+ }
+
+ ///
+ /// Gets the actual length of the south east corner
+ ///
+ public double ActualCornerSe
+ {
+ get
+ {
+ if (double.IsNaN(_actualCornerSe))
+ {
+ _actualCornerSe = CssValueParser.ParseLength(CornerSeRadius, 0, this);
+ }
+ return _actualCornerSe;
+ }
+ }
+
+ ///
+ /// Gets the actual length of the south west corner
+ ///
+ public double ActualCornerSw
+ {
+ get
+ {
+ if (double.IsNaN(_actualCornerSw))
+ {
+ _actualCornerSw = CssValueParser.ParseLength(CornerSwRadius, 0, this);
+ }
+ return _actualCornerSw;
+ }
+ }
+
+ ///
+ /// Gets a value indicating if at least one of the corners of the box is rounded
+ ///
+ public bool IsRounded
+ {
+ get { return ActualCornerNe > 0f || ActualCornerNw > 0f || ActualCornerSe > 0f || ActualCornerSw > 0f; }
+ }
+
+ ///
+ /// Gets the actual width of whitespace between words.
+ ///
+ public double ActualWordSpacing
+ {
+ get { return _actualWordSpacing; }
+ }
+
+ ///
+ ///
+ /// Gets the actual color for the text.
+ ///
+ public RColor ActualColor
+ {
+ get
+ {
+ if (_actualColor.IsEmpty)
+ {
+ _actualColor = GetActualColor(Color);
+ }
+
+ return _actualColor;
+ }
+ }
+
+ ///
+ /// Gets the actual background color of the box
+ ///
+ public RColor ActualBackgroundColor
+ {
+ get
+ {
+ if (_actualBackgroundColor.IsEmpty)
+ {
+ _actualBackgroundColor = GetActualColor(BackgroundColor);
+ }
+
+ return _actualBackgroundColor;
+ }
+ }
+
+ ///
+ /// Gets the second color that creates a gradient for the background
+ ///
+ public RColor ActualBackgroundGradient
+ {
+ get
+ {
+ if (_actualBackgroundGradient.IsEmpty)
+ {
+ _actualBackgroundGradient = GetActualColor(BackgroundGradient);
+ }
+ return _actualBackgroundGradient;
+ }
+ }
+
+ ///
+ /// Gets the actual angle specified for the background gradient
+ ///
+ public double ActualBackgroundGradientAngle
+ {
+ get
+ {
+ if (double.IsNaN(_actualBackgroundGradientAngle))
+ {
+ _actualBackgroundGradientAngle = CssValueParser.ParseNumber(BackgroundGradientAngle, 360f);
+ }
+
+ return _actualBackgroundGradientAngle;
+ }
+ }
+
+ ///
+ /// Gets the actual font of the parent
+ ///
+ public RFont ActualParentFont
+ {
+ get { return GetParent() == null ? ActualFont : GetParent().ActualFont; }
+ }
+
+ ///
+ /// Gets the font that should be actually used to paint the text of the box
+ ///
+ public RFont ActualFont
+ {
+ get
+ {
+ if (_actualFont == null)
+ {
+ if (string.IsNullOrEmpty(FontFamily))
+ {
+ FontFamily = CssConstants.DefaultFont;
+ }
+ if (string.IsNullOrEmpty(FontSize))
+ {
+ FontSize = CssConstants.FontSize.ToString(CultureInfo.InvariantCulture) + "pt";
+ }
+
+ RFontStyle st = RFontStyle.Regular;
+
+ if (FontStyle == CssConstants.Italic || FontStyle == CssConstants.Oblique)
+ {
+ st |= RFontStyle.Italic;
+ }
+
+ if (FontWeight != CssConstants.Normal && FontWeight != CssConstants.Lighter && !string.IsNullOrEmpty(FontWeight) && FontWeight != CssConstants.Inherit)
+ {
+ st |= RFontStyle.Bold;
+ }
+
+ double fsize;
+ double parentSize = CssConstants.FontSize;
+
+ if (GetParent() != null)
+ parentSize = GetParent().ActualFont.Size;
+
+ switch (FontSize)
+ {
+ case CssConstants.Medium:
+ fsize = CssConstants.FontSize;
+ break;
+ case CssConstants.XXSmall:
+ fsize = CssConstants.FontSize - 4;
+ break;
+ case CssConstants.XSmall:
+ fsize = CssConstants.FontSize - 3;
+ break;
+ case CssConstants.Small:
+ fsize = CssConstants.FontSize - 2;
+ break;
+ case CssConstants.Large:
+ fsize = CssConstants.FontSize + 2;
+ break;
+ case CssConstants.XLarge:
+ fsize = CssConstants.FontSize + 3;
+ break;
+ case CssConstants.XXLarge:
+ fsize = CssConstants.FontSize + 4;
+ break;
+ case CssConstants.Smaller:
+ fsize = parentSize - 2;
+ break;
+ case CssConstants.Larger:
+ fsize = parentSize + 2;
+ break;
+ default:
+ fsize = CssValueParser.ParseLength(FontSize, parentSize, parentSize, null, true, true);
+ break;
+ }
+
+ if (fsize <= 1f)
+ {
+ fsize = CssConstants.FontSize;
+ }
+
+ _actualFont = GetCachedFont(FontFamily, fsize, st);
+ }
+ return _actualFont;
+ }
+ }
+
+ protected abstract RFont GetCachedFont(string fontFamily, double fsize, RFontStyle st);
+
+ ///
+ /// Gets the line height
+ ///
+ public double ActualLineHeight
+ {
+ get
+ {
+ if (double.IsNaN(_actualLineHeight))
+ {
+ _actualLineHeight = .9f * CssValueParser.ParseLength(LineHeight, Size.Height, this);
+ }
+ return _actualLineHeight;
+ }
+ }
+
+ ///
+ /// Gets the text indentation (on first line only)
+ ///
+ public double ActualTextIndent
+ {
+ get
+ {
+ if (double.IsNaN(_actualTextIndent))
+ {
+ _actualTextIndent = CssValueParser.ParseLength(TextIndent, Size.Width, this);
+ }
+
+ return _actualTextIndent;
+ }
+ }
+
+ ///
+ /// Gets the actual horizontal border spacing for tables
+ ///
+ public double ActualBorderSpacingHorizontal
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderSpacingHorizontal))
+ {
+ MatchCollection matches = RegexParserUtils.Match(RegexParserUtils.CssLength, BorderSpacing);
+
+ if (matches.Count == 0)
+ {
+ _actualBorderSpacingHorizontal = 0;
+ }
+ else if (matches.Count > 0)
+ {
+ _actualBorderSpacingHorizontal = CssValueParser.ParseLength(matches[0].Value, 1, this);
+ }
+ }
+
+
+ return _actualBorderSpacingHorizontal;
+ }
+ }
+
+ ///
+ /// Gets the actual vertical border spacing for tables
+ ///
+ public double ActualBorderSpacingVertical
+ {
+ get
+ {
+ if (double.IsNaN(_actualBorderSpacingVertical))
+ {
+ MatchCollection matches = RegexParserUtils.Match(RegexParserUtils.CssLength, BorderSpacing);
+
+ if (matches.Count == 0)
+ {
+ _actualBorderSpacingVertical = 0;
+ }
+ else if (matches.Count == 1)
+ {
+ _actualBorderSpacingVertical = CssValueParser.ParseLength(matches[0].Value, 1, this);
+ }
+ else
+ {
+ _actualBorderSpacingVertical = CssValueParser.ParseLength(matches[1].Value, 1, this);
+ }
+ }
+ return _actualBorderSpacingVertical;
+ }
+ }
+
+ ///
+ /// Get the parent of this css properties instance.
+ ///
+ ///
+ protected abstract CssBoxProperties GetParent();
+
+ ///
+ /// Gets the height of the font in the specified units
+ ///
+ ///
+ public double GetEmHeight()
+ {
+ return ActualFont.Height;
+ }
+
+ ///
+ /// Ensures that the specified length is converted to pixels if necessary
+ ///
+ ///
+ protected string NoEms(string length)
+ {
+ var len = new CssLength(length);
+ if (len.Unit == CssUnit.Ems)
+ {
+ length = len.ConvertEmToPixels(GetEmHeight()).ToString();
+ }
+ return length;
+ }
+
+ ///
+ /// Set the style/width/color for all 4 borders on the box.
+ /// if null is given for a value it will not be set.
+ ///
+ /// optional: the style to set
+ /// optional: the width to set
+ /// optional: the color to set
+ protected void SetAllBorders(string style = null, string width = null, string color = null)
+ {
+ if (style != null)
+ BorderLeftStyle = BorderTopStyle = BorderRightStyle = BorderBottomStyle = style;
+ if (width != null)
+ BorderLeftWidth = BorderTopWidth = BorderRightWidth = BorderBottomWidth = width;
+ if (color != null)
+ BorderLeftColor = BorderTopColor = BorderRightColor = BorderBottomColor = color;
+ }
+
+ ///
+ /// Measures the width of whitespace between words (set ).
+ ///
+ protected void MeasureWordSpacing(RGraphics g)
+ {
+ if (double.IsNaN(ActualWordSpacing))
+ {
+ _actualWordSpacing = CssUtils.WhiteSpace(g, this);
+ if (WordSpacing != CssConstants.Normal)
+ {
+ string len = RegexParserUtils.Search(RegexParserUtils.CssLength, WordSpacing);
+ _actualWordSpacing += CssValueParser.ParseLength(len, 1, this);
+ }
+ }
+ }
+
+ ///
+ /// Inherits inheritable values from specified box.
+ ///
+ /// Set to true to inherit all CSS properties instead of only the ineritables
+ /// Box to inherit the properties
+ protected void InheritStyle(CssBox p, bool everything)
+ {
+ if (p != null)
+ {
+ _borderSpacing = p._borderSpacing;
+ _borderCollapse = p._borderCollapse;
+ _color = p._color;
+ _emptyCells = p._emptyCells;
+ _whiteSpace = p._whiteSpace;
+ _visibility = p._visibility;
+ _textIndent = p._textIndent;
+ _textAlign = p._textAlign;
+ _verticalAlign = p._verticalAlign;
+ _fontFamily = p._fontFamily;
+ _fontSize = p._fontSize;
+ _fontStyle = p._fontStyle;
+ _fontVariant = p._fontVariant;
+ _fontWeight = p._fontWeight;
+ _listStyleImage = p._listStyleImage;
+ _listStylePosition = p._listStylePosition;
+ _listStyleType = p._listStyleType;
+ _listStyle = p._listStyle;
+ _lineHeight = p._lineHeight;
+ _wordBreak = p.WordBreak;
+ _direction = p._direction;
+
+ if (everything)
+ {
+ _backgroundColor = p._backgroundColor;
+ _backgroundGradient = p._backgroundGradient;
+ _backgroundGradientAngle = p._backgroundGradientAngle;
+ _backgroundImage = p._backgroundImage;
+ _backgroundPosition = p._backgroundPosition;
+ _backgroundRepeat = p._backgroundRepeat;
+ _borderTopWidth = p._borderTopWidth;
+ _borderRightWidth = p._borderRightWidth;
+ _borderBottomWidth = p._borderBottomWidth;
+ _borderLeftWidth = p._borderLeftWidth;
+ _borderTopColor = p._borderTopColor;
+ _borderRightColor = p._borderRightColor;
+ _borderBottomColor = p._borderBottomColor;
+ _borderLeftColor = p._borderLeftColor;
+ _borderTopStyle = p._borderTopStyle;
+ _borderRightStyle = p._borderRightStyle;
+ _borderBottomStyle = p._borderBottomStyle;
+ _borderLeftStyle = p._borderLeftStyle;
+ _bottom = p._bottom;
+ _cornerNwRadius = p._cornerNwRadius;
+ _cornerNeRadius = p._cornerNeRadius;
+ _cornerSeRadius = p._cornerSeRadius;
+ _cornerSwRadius = p._cornerSwRadius;
+ _cornerRadius = p._cornerRadius;
+ _display = p._display;
+ _float = p._float;
+ _height = p._height;
+ _marginBottom = p._marginBottom;
+ _marginLeft = p._marginLeft;
+ _marginRight = p._marginRight;
+ _marginTop = p._marginTop;
+ _left = p._left;
+ _lineHeight = p._lineHeight;
+ _overflow = p._overflow;
+ _paddingLeft = p._paddingLeft;
+ _paddingBottom = p._paddingBottom;
+ _paddingRight = p._paddingRight;
+ _paddingTop = p._paddingTop;
+ _right = p._right;
+ _textDecoration = p._textDecoration;
+ _top = p._top;
+ _position = p._position;
+ _width = p._width;
+ _maxWidth = p._maxWidth;
+ _wordSpacing = p._wordSpacing;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngine.cs b/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngine.cs
new file mode 100644
index 000000000..a24f9e708
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngine.cs
@@ -0,0 +1,718 @@
+// "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)
+ {
+ ApplyHorizontalAlignment(g, linebox);
+ ApplyRightToLeft(blockBox, linebox);
+ BubbleRectangles(blockBox, linebox);
+ ApplyVerticalAlignment(g, 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.Position != CssConstants.Fixed) ? b.ActualMarginLeft + b.ActualBorderLeftWidth + b.ActualPaddingLeft : 0;
+ double rightspacing = (b.Position != CssConstants.Absolute && b.Position != CssConstants.Fixed) ? 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;
+
+ if (!box.IsFixed)
+ {
+ word.BreakPage();
+ }
+
+ 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 ApplyHorizontalAlignment(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;
+ }
+ }
+
+ ///
+ /// 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 * .5f);
+ 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;
+ }
+
+ if (line.Rectangles.Count > 0)
+ {
+ foreach (CssBox b in ToList(line.Rectangles.Keys))
+ {
+ RRect r = line.Rectangles[b];
+ line.Rectangles[b] = 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;
+ }
+
+ if (line.Rectangles.Count > 0)
+ {
+ foreach (CssBox b in ToList(line.Rectangles.Keys))
+ {
+ RRect r = line.Rectangles[b];
+ line.Rectangles[b] = 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;
+ // }
+ //}
+ }
+
+ ///
+ /// todo: optimizate, not creating a list each time
+ ///
+ private static List ToList(IEnumerable collection)
+ {
+ List result = new List();
+ foreach (T item in collection)
+ {
+ result.Add(item);
+ }
+ return result;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngineTable.cs b/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngineTable.cs
new file mode 100644
index 000000000..79627161a
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssLayoutEngineTable.cs
@@ -0,0 +1,1039 @@
+// "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.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Layout engine for tables executing the complex layout of tables with rows/columns/headers/etc.
+ ///
+ internal sealed class CssLayoutEngineTable
+ {
+ #region Fields and Consts
+
+ ///
+ /// the main box of the table
+ ///
+ private readonly CssBox _tableBox;
+
+ ///
+ ///
+ ///
+ private CssBox _caption;
+
+ private CssBox _headerBox;
+
+ private CssBox _footerBox;
+
+ ///
+ /// collection of all rows boxes
+ ///
+ private readonly List _bodyrows = new List();
+
+ ///
+ /// collection of all columns boxes
+ ///
+ private readonly List _columns = new List();
+
+ ///
+ ///
+ ///
+ private readonly List _allRows = new List();
+
+ private int _columnCount;
+
+ private bool _widthSpecified;
+
+ private double[] _columnWidths;
+
+ private double[] _columnMinWidths;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ ///
+ private CssLayoutEngineTable(CssBox tableBox)
+ {
+ _tableBox = tableBox;
+ }
+
+ ///
+ /// Get the table cells spacing for all the cells in the table.
+ /// Used to calculate the spacing the table has in addition to regular padding and borders.
+ ///
+ /// the table box to calculate the spacing for
+ /// the calculated spacing
+ public static double GetTableSpacing(CssBox tableBox)
+ {
+ int count = 0;
+ int columns = 0;
+ foreach (var box in tableBox.Boxes)
+ {
+ if (box.Display == CssConstants.TableColumn)
+ {
+ columns += GetSpan(box);
+ }
+ else if (box.Display == CssConstants.TableRowGroup)
+ {
+ foreach (CssBox cr in tableBox.Boxes)
+ {
+ count++;
+ if (cr.Display == CssConstants.TableRow)
+ columns = Math.Max(columns, cr.Boxes.Count);
+ }
+ }
+ else if (box.Display == CssConstants.TableRow)
+ {
+ count++;
+ columns = Math.Max(columns, box.Boxes.Count);
+ }
+
+ // limit the amount of rows to process for performance
+ if (count > 30)
+ break;
+ }
+
+ // +1 columns because padding is between the cell and table borders
+ return (columns + 1) * GetHorizontalSpacing(tableBox);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void PerformLayout(RGraphics g, CssBox tableBox)
+ {
+ ArgChecker.AssertArgNotNull(g, "g");
+ ArgChecker.AssertArgNotNull(tableBox, "tableBox");
+
+ try
+ {
+ var table = new CssLayoutEngineTable(tableBox);
+ table.Layout(g);
+ }
+ catch (Exception ex)
+ {
+ tableBox.HtmlContainer.ReportError(HtmlRenderErrorType.Layout, "Failed table layout", ex);
+ }
+ }
+
+
+ #region Private Methods
+
+ ///
+ /// Analyzes the Table and assigns values to this CssTable object.
+ /// To be called from the constructor
+ ///
+ private void Layout(RGraphics g)
+ {
+ MeasureWords(_tableBox, g);
+
+ // get the table boxes into the proper fields
+ AssignBoxKinds();
+
+ // Insert EmptyBoxes for vertical cell spanning.
+ InsertEmptyBoxes();
+
+ // Determine Row and Column Count, and ColumnWidths
+ var availCellSpace = CalculateCountAndWidth();
+
+ DetermineMissingColumnWidths(availCellSpace);
+
+ // Check for minimum sizes (increment widths if necessary)
+ EnforceMinimumSize();
+
+ // While table width is larger than it should, and width is reducible
+ EnforceMaximumSize();
+
+ // Ensure there's no padding
+ _tableBox.PaddingLeft = _tableBox.PaddingTop = _tableBox.PaddingRight = _tableBox.PaddingBottom = "0";
+
+ //Actually layout cells!
+ LayoutCells(g);
+ }
+
+ ///
+ /// Get the table boxes into the proper fields.
+ ///
+ private void AssignBoxKinds()
+ {
+ foreach (var box in _tableBox.Boxes)
+ {
+ switch (box.Display)
+ {
+ case CssConstants.TableCaption:
+ _caption = box;
+ break;
+ case CssConstants.TableRow:
+ _bodyrows.Add(box);
+ break;
+ case CssConstants.TableRowGroup:
+ foreach (CssBox childBox in box.Boxes)
+ if (childBox.Display == CssConstants.TableRow)
+ _bodyrows.Add(childBox);
+ break;
+ case CssConstants.TableHeaderGroup:
+ if (_headerBox != null)
+ _bodyrows.Add(box);
+ else
+ _headerBox = box;
+ break;
+ case CssConstants.TableFooterGroup:
+ if (_footerBox != null)
+ _bodyrows.Add(box);
+ else
+ _footerBox = box;
+ break;
+ case CssConstants.TableColumn:
+ for (int i = 0; i < GetSpan(box); i++)
+ _columns.Add(box);
+ break;
+ case CssConstants.TableColumnGroup:
+ if (box.Boxes.Count == 0)
+ {
+ int gspan = GetSpan(box);
+ for (int i = 0; i < gspan; i++)
+ {
+ _columns.Add(box);
+ }
+ }
+ else
+ {
+ foreach (CssBox bb in box.Boxes)
+ {
+ int bbspan = GetSpan(bb);
+ for (int i = 0; i < bbspan; i++)
+ {
+ _columns.Add(bb);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (_headerBox != null)
+ _allRows.AddRange(_headerBox.Boxes);
+
+ _allRows.AddRange(_bodyrows);
+
+ if (_footerBox != null)
+ _allRows.AddRange(_footerBox.Boxes);
+ }
+
+ ///
+ /// Insert EmptyBoxes for vertical cell spanning.
+ ///
+ private void InsertEmptyBoxes()
+ {
+ if (!_tableBox._tableFixed)
+ {
+ int currow = 0;
+ List rows = _bodyrows;
+
+ foreach (CssBox row in rows)
+ {
+ for (int k = 0; k < row.Boxes.Count; k++)
+ {
+ CssBox cell = row.Boxes[k];
+ int rowspan = GetRowSpan(cell);
+ int realcol = GetCellRealColumnIndex(row, cell); //Real column of the cell
+
+ for (int i = currow + 1; i < currow + rowspan; i++)
+ {
+ if (rows.Count > i)
+ {
+ int colcount = 0;
+ for (int j = 0; j < rows[i].Boxes.Count; j++)
+ {
+ if (colcount == realcol)
+ {
+ rows[i].Boxes.Insert(colcount, new CssSpacingBox(_tableBox, ref cell, currow));
+ break;
+ }
+ colcount++;
+ realcol -= GetColSpan(rows[i].Boxes[j]) - 1;
+ }
+ }
+ }
+ }
+ currow++;
+ }
+
+ _tableBox._tableFixed = true;
+ }
+ }
+
+ ///
+ /// Determine Row and Column Count, and ColumnWidths
+ ///
+ ///
+ private double CalculateCountAndWidth()
+ {
+ //Columns
+ if (_columns.Count > 0)
+ {
+ _columnCount = _columns.Count;
+ }
+ else
+ {
+ foreach (CssBox b in _allRows)
+ _columnCount = Math.Max(_columnCount, b.Boxes.Count);
+ }
+
+ //Initialize column widths array with NaNs
+ _columnWidths = new double[_columnCount];
+ for (int i = 0; i < _columnWidths.Length; i++)
+ _columnWidths[i] = double.NaN;
+
+ double availCellSpace = GetAvailableCellWidth();
+
+ if (_columns.Count > 0)
+ {
+ // Fill ColumnWidths array by scanning column widths
+ for (int i = 0; i < _columns.Count; i++)
+ {
+ CssLength len = new CssLength(_columns[i].Width); //Get specified width
+
+ if (len.Number > 0) //If some width specified
+ {
+ if (len.IsPercentage) //Get width as a percentage
+ {
+ _columnWidths[i] = CssValueParser.ParseNumber(_columns[i].Width, availCellSpace);
+ }
+ else if (len.Unit == CssUnit.Pixels || len.Unit == CssUnit.None)
+ {
+ _columnWidths[i] = len.Number; //Get width as an absolute-pixel value
+ }
+ }
+ }
+ }
+ else
+ {
+ // Fill ColumnWidths array by scanning width in table-cell definitions
+ foreach (CssBox row in _allRows)
+ {
+ //Check for column width in table-cell definitions
+ for (int i = 0; i < _columnCount; i++)
+ {
+ if (i < 20 || double.IsNaN(_columnWidths[i])) // limit column width check
+ {
+ if (i < row.Boxes.Count && row.Boxes[i].Display == CssConstants.TableCell)
+ {
+ double len = CssValueParser.ParseLength(row.Boxes[i].Width, availCellSpace, row.Boxes[i]);
+ if (len > 0) //If some width specified
+ {
+ int colspan = GetColSpan(row.Boxes[i]);
+ len /= Convert.ToSingle(colspan);
+ for (int j = i; j < i + colspan; j++)
+ {
+ _columnWidths[j] = double.IsNaN(_columnWidths[j]) ? len : Math.Max(_columnWidths[j], len);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return availCellSpace;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ private void DetermineMissingColumnWidths(double availCellSpace)
+ {
+ double occupedSpace = 0f;
+ if (_widthSpecified) //If a width was specified,
+ {
+ //Assign NaNs equally with space left after gathering not-NaNs
+ int numOfNans = 0;
+
+ //Calculate number of NaNs and occupied space
+ foreach (double colWidth in _columnWidths)
+ {
+ if (double.IsNaN(colWidth))
+ numOfNans++;
+ else
+ occupedSpace += colWidth;
+ }
+ var orgNumOfNans = numOfNans;
+
+ double[] orgColWidths = null;
+ if (numOfNans < _columnWidths.Length)
+ {
+ orgColWidths = new double[_columnWidths.Length];
+ for (int i = 0; i < _columnWidths.Length; i++)
+ orgColWidths[i] = _columnWidths[i];
+ }
+
+ if (numOfNans > 0)
+ {
+ // Determine the max width for each column
+ double[] minFullWidths, maxFullWidths;
+ GetColumnsMinMaxWidthByContent(true, out minFullWidths, out maxFullWidths);
+
+ // set the columns that can fulfill by the max width in a loop because it changes the nanWidth
+ int oldNumOfNans;
+ do
+ {
+ oldNumOfNans = numOfNans;
+
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ var nanWidth = (availCellSpace - occupedSpace) / numOfNans;
+ if (double.IsNaN(_columnWidths[i]) && nanWidth > maxFullWidths[i])
+ {
+ _columnWidths[i] = maxFullWidths[i];
+ numOfNans--;
+ occupedSpace += maxFullWidths[i];
+ }
+ }
+ } while (oldNumOfNans != numOfNans);
+
+ if (numOfNans > 0)
+ {
+ // Determine width that will be assigned to un assigned widths
+ double nanWidth = (availCellSpace - occupedSpace) / numOfNans;
+
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (double.IsNaN(_columnWidths[i]))
+ _columnWidths[i] = nanWidth;
+ }
+ }
+ }
+
+ if (numOfNans == 0 && occupedSpace < availCellSpace)
+ {
+ if (orgNumOfNans > 0)
+ {
+ // spread extra width between all non width specified columns
+ double extWidth = (availCellSpace - occupedSpace) / orgNumOfNans;
+ for (int i = 0; i < _columnWidths.Length; i++)
+ if (orgColWidths == null || double.IsNaN(orgColWidths[i]))
+ _columnWidths[i] += extWidth;
+ }
+ else
+ {
+ // spread extra width between all columns with respect to relative sizes
+ for (int i = 0; i < _columnWidths.Length; i++)
+ _columnWidths[i] += (availCellSpace - occupedSpace) * (_columnWidths[i] / occupedSpace);
+ }
+ }
+ }
+ else
+ {
+ //Get the minimum and maximum full length of NaN boxes
+ double[] minFullWidths, maxFullWidths;
+ GetColumnsMinMaxWidthByContent(true, out minFullWidths, out maxFullWidths);
+
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (double.IsNaN(_columnWidths[i]))
+ _columnWidths[i] = minFullWidths[i];
+ occupedSpace += _columnWidths[i];
+ }
+
+ // spread extra width between all columns
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (maxFullWidths[i] > _columnWidths[i])
+ {
+ var temp = _columnWidths[i];
+ _columnWidths[i] = Math.Min(_columnWidths[i] + (availCellSpace - occupedSpace) / Convert.ToSingle(_columnWidths.Length - i), maxFullWidths[i]);
+ occupedSpace = occupedSpace + _columnWidths[i] - temp;
+ }
+ }
+ }
+ }
+
+ ///
+ /// While table width is larger than it should, and width is reductable.
+ /// If table max width is limited by we need to lower the columns width even if it will result in clipping
+ ///
+ private void EnforceMaximumSize()
+ {
+ int curCol = 0;
+ var widthSum = GetWidthSum();
+ while (widthSum > GetAvailableTableWidth() && CanReduceWidth())
+ {
+ while (!CanReduceWidth(curCol))
+ curCol++;
+
+ _columnWidths[curCol] -= 1f;
+
+ curCol++;
+
+ if (curCol >= _columnWidths.Length)
+ curCol = 0;
+ }
+
+ // if table max width is limited by we need to lower the columns width even if it will result in clipping
+ var maxWidth = GetMaxTableWidth();
+ if (maxWidth < 90999)
+ {
+ widthSum = GetWidthSum();
+ if (maxWidth < widthSum)
+ {
+ //Get the minimum and maximum full length of NaN boxes
+ double[] minFullWidths, maxFullWidths;
+ GetColumnsMinMaxWidthByContent(false, out minFullWidths, out maxFullWidths);
+
+ // lower all the columns to the minimum
+ for (int i = 0; i < _columnWidths.Length; i++)
+ _columnWidths[i] = minFullWidths[i];
+
+ // either min for all column is not enought and we need to lower it more resulting in clipping
+ // or we now have extra space so we can give it to columns than need it
+ widthSum = GetWidthSum();
+ if (maxWidth < widthSum)
+ {
+ // lower the width of columns starting from the largest one until the max width is satisfied
+ for (int a = 0; a < 15 && maxWidth < widthSum - 0.1; a++) // limit iteration so bug won't create infinite loop
+ {
+ int nonMaxedColumns = 0;
+ double largeWidth = 0f, secLargeWidth = 0f;
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (_columnWidths[i] > largeWidth + 0.1)
+ {
+ secLargeWidth = largeWidth;
+ largeWidth = _columnWidths[i];
+ nonMaxedColumns = 1;
+ }
+ else if (_columnWidths[i] > largeWidth - 0.1)
+ {
+ nonMaxedColumns++;
+ }
+ }
+
+ double decrease = secLargeWidth > 0 ? largeWidth - secLargeWidth : (widthSum - maxWidth) / _columnWidths.Length;
+ if (decrease * nonMaxedColumns > widthSum - maxWidth)
+ decrease = (widthSum - maxWidth) / nonMaxedColumns;
+ for (int i = 0; i < _columnWidths.Length; i++)
+ if (_columnWidths[i] > largeWidth - 0.1)
+ _columnWidths[i] -= decrease;
+
+ widthSum = GetWidthSum();
+ }
+ }
+ else
+ {
+ // spread extra width to columns that didn't reached max width where trying to spread it between all columns
+ for (int a = 0; a < 15 && maxWidth > widthSum + 0.1; a++) // limit iteration so bug won't create infinite loop
+ {
+ int nonMaxedColumns = 0;
+ for (int i = 0; i < _columnWidths.Length; i++)
+ if (_columnWidths[i] + 1 < maxFullWidths[i])
+ nonMaxedColumns++;
+ if (nonMaxedColumns == 0)
+ nonMaxedColumns = _columnWidths.Length;
+
+ bool hit = false;
+ double minIncrement = (maxWidth - widthSum) / nonMaxedColumns;
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (_columnWidths[i] + 0.1 < maxFullWidths[i])
+ {
+ minIncrement = Math.Min(minIncrement, maxFullWidths[i] - _columnWidths[i]);
+ hit = true;
+ }
+ }
+
+ for (int i = 0; i < _columnWidths.Length; i++)
+ if (!hit || _columnWidths[i] + 1 < maxFullWidths[i])
+ _columnWidths[i] += minIncrement;
+
+ widthSum = GetWidthSum();
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Check for minimum sizes (increment widths if necessary)
+ ///
+ private void EnforceMinimumSize()
+ {
+ foreach (CssBox row in _allRows)
+ {
+ foreach (CssBox cell in row.Boxes)
+ {
+ int colspan = GetColSpan(cell);
+ int col = GetCellRealColumnIndex(row, cell);
+ int affectcol = col + colspan - 1;
+
+ if (_columnWidths.Length > col && _columnWidths[col] < GetColumnMinWidths()[col])
+ {
+ double diff = GetColumnMinWidths()[col] - _columnWidths[col];
+ _columnWidths[affectcol] = GetColumnMinWidths()[affectcol];
+
+ if (col < _columnWidths.Length - 1)
+ {
+ _columnWidths[col + 1] -= diff;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Layout the cells by the calculated table layout
+ ///
+ ///
+ private void LayoutCells(RGraphics g)
+ {
+ double startx = Math.Max(_tableBox.ClientLeft + GetHorizontalSpacing(), 0);
+ double starty = Math.Max(_tableBox.ClientTop + GetVerticalSpacing(), 0);
+ double cury = starty;
+ double maxRight = startx;
+ double maxBottom = 0f;
+ int currentrow = 0;
+
+ // change start X by if the table should align to center or right
+ if (_tableBox.TextAlign == CssConstants.Center || _tableBox.TextAlign == CssConstants.Right)
+ {
+ double maxRightCalc = GetWidthSum();
+ startx = _tableBox.TextAlign == CssConstants.Right
+ ? GetAvailableTableWidth() - maxRightCalc
+ : startx + (GetAvailableTableWidth() - maxRightCalc) / 2;
+
+ _tableBox.Location = new RPoint(startx - _tableBox.ActualBorderLeftWidth - _tableBox.ActualPaddingLeft - GetHorizontalSpacing(), _tableBox.Location.Y);
+ }
+
+ for (int i = 0; i < _allRows.Count; i++)
+ {
+ var row = _allRows[i];
+ double curx = startx;
+ int curCol = 0;
+ bool breakPage = false;
+
+ for (int j = 0; j < row.Boxes.Count; j++)
+ {
+ CssBox cell = row.Boxes[j];
+ if (curCol >= _columnWidths.Length)
+ break;
+
+ int rowspan = GetRowSpan(cell);
+ var columnIndex = GetCellRealColumnIndex(row, cell);
+ double width = GetCellWidth(columnIndex, cell);
+ cell.Location = new RPoint(curx, cury);
+ cell.Size = new RSize(width, 0f);
+ cell.PerformLayout(g); //That will automatically set the bottom of the cell
+
+ //Alter max bottom only if row is cell's row + cell's rowspan - 1
+ CssSpacingBox sb = cell as CssSpacingBox;
+ if (sb != null)
+ {
+ if (sb.EndRow == currentrow)
+ {
+ maxBottom = Math.Max(maxBottom, sb.ExtendedBox.ActualBottom);
+ }
+ }
+ else if (rowspan == 1)
+ {
+ maxBottom = Math.Max(maxBottom, cell.ActualBottom);
+ }
+ maxRight = Math.Max(maxRight, cell.ActualRight);
+ curCol++;
+ curx = cell.ActualRight + GetHorizontalSpacing();
+ }
+
+ foreach (CssBox cell in row.Boxes)
+ {
+ CssSpacingBox spacer = cell as CssSpacingBox;
+
+ if (spacer == null && GetRowSpan(cell) == 1)
+ {
+ cell.ActualBottom = maxBottom;
+ CssLayoutEngine.ApplyCellVerticalAlignment(g, cell);
+ }
+ else if (spacer != null && spacer.EndRow == currentrow)
+ {
+ spacer.ExtendedBox.ActualBottom = maxBottom;
+ CssLayoutEngine.ApplyCellVerticalAlignment(g, spacer.ExtendedBox);
+ }
+
+ // If one cell crosses page borders then don't need to check other cells in the row
+ if (_tableBox.PageBreakInside == CssConstants.Avoid)
+ {
+ breakPage = cell.BreakPage();
+ if (breakPage)
+ {
+ cury = cell.Location.Y;
+ break;
+ }
+ }
+ }
+
+ if (breakPage) // go back to move the whole row to the next page
+ {
+ if (i == 1) // do not leave single row in previous page
+ i = -1; // Start layout from the first row on new page
+ else
+ i--;
+
+ maxBottom = 0;
+ continue;
+ }
+
+ cury = maxBottom + GetVerticalSpacing();
+
+ currentrow++;
+ }
+
+ maxRight = Math.Max(maxRight, _tableBox.Location.X + _tableBox.ActualWidth);
+ _tableBox.ActualRight = maxRight + GetHorizontalSpacing() + _tableBox.ActualBorderRightWidth;
+ _tableBox.ActualBottom = Math.Max(maxBottom, starty) + GetVerticalSpacing() + _tableBox.ActualBorderBottomWidth;
+ }
+
+ ///
+ /// Gets the spanned width of a cell (With of all columns it spans minus one).
+ ///
+ private double GetSpannedMinWidth(CssBox row, CssBox cell, int realcolindex, int colspan)
+ {
+ double w = 0f;
+ for (int i = realcolindex; i < row.Boxes.Count || i < realcolindex + colspan - 1; i++)
+ {
+ if (i < GetColumnMinWidths().Length)
+ w += GetColumnMinWidths()[i];
+ }
+ return w;
+ }
+
+ ///
+ /// Gets the cell column index checking its position and other cells colspans
+ ///
+ ///
+ ///
+ ///
+ private static int GetCellRealColumnIndex(CssBox row, CssBox cell)
+ {
+ int i = 0;
+
+ foreach (CssBox b in row.Boxes)
+ {
+ if (b.Equals(cell))
+ break;
+ i += GetColSpan(b);
+ }
+
+ return i;
+ }
+
+ ///
+ /// Gets the cells width, taking colspan and being in the specified column
+ ///
+ ///
+ ///
+ ///
+ private double GetCellWidth(int column, CssBox b)
+ {
+ double colspan = Convert.ToSingle(GetColSpan(b));
+ double sum = 0f;
+
+ for (int i = column; i < column + colspan; i++)
+ {
+ if (column >= _columnWidths.Length)
+ break;
+ if (_columnWidths.Length <= i)
+ break;
+ sum += _columnWidths[i];
+ }
+
+ sum += (colspan - 1) * GetHorizontalSpacing();
+
+ return sum; // -b.ActualBorderLeftWidth - b.ActualBorderRightWidth - b.ActualPaddingRight - b.ActualPaddingLeft;
+ }
+
+ ///
+ /// Gets the colspan of the specified box
+ ///
+ ///
+ private static int GetColSpan(CssBox b)
+ {
+ string att = b.GetAttribute("colspan", "1");
+ int colspan;
+
+ if (!int.TryParse(att, out colspan))
+ {
+ return 1;
+ }
+
+ return colspan;
+ }
+
+ ///
+ /// Gets the rowspan of the specified box
+ ///
+ ///
+ private static int GetRowSpan(CssBox b)
+ {
+ string att = b.GetAttribute("rowspan", "1");
+ int rowspan;
+
+ if (!int.TryParse(att, out rowspan))
+ {
+ return 1;
+ }
+
+ return rowspan;
+ }
+
+ ///
+ /// Recursively measures words inside the box
+ ///
+ /// the box to measure
+ /// Device to use
+ private static void MeasureWords(CssBox box, RGraphics g)
+ {
+ if (box != null)
+ {
+ foreach (var childBox in box.Boxes)
+ {
+ childBox.MeasureWordsSize(g);
+ MeasureWords(childBox, g);
+ }
+ }
+ }
+
+ ///
+ /// Tells if the columns widths can be reduced,
+ /// by checking the minimum widths of all cells
+ ///
+ ///
+ private bool CanReduceWidth()
+ {
+ for (int i = 0; i < _columnWidths.Length; i++)
+ {
+ if (CanReduceWidth(i))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Tells if the specified column can be reduced,
+ /// by checking its minimum width
+ ///
+ ///
+ ///
+ private bool CanReduceWidth(int columnIndex)
+ {
+ if (_columnWidths.Length >= columnIndex || GetColumnMinWidths().Length >= columnIndex)
+ return false;
+ return _columnWidths[columnIndex] > GetColumnMinWidths()[columnIndex];
+ }
+
+ ///
+ /// Gets the available width for the whole table.
+ /// It also sets the value of WidthSpecified
+ ///
+ ///
+ ///
+ /// The table's width can be larger than the result of this method, because of the minimum
+ /// size that individual boxes.
+ ///
+ private double GetAvailableTableWidth()
+ {
+ CssLength tblen = new CssLength(_tableBox.Width);
+
+ if (tblen.Number > 0)
+ {
+ _widthSpecified = true;
+ return CssValueParser.ParseLength(_tableBox.Width, _tableBox.ParentBox.AvailableWidth, _tableBox);
+ }
+ else
+ {
+ return _tableBox.ParentBox.AvailableWidth;
+ }
+ }
+
+ ///
+ /// Gets the available width for the whole table.
+ /// It also sets the value of WidthSpecified
+ ///
+ ///
+ ///
+ /// The table's width can be larger than the result of this method, because of the minimum
+ /// size that individual boxes.
+ ///
+ private double GetMaxTableWidth()
+ {
+ var tblen = new CssLength(_tableBox.MaxWidth);
+ if (tblen.Number > 0)
+ {
+ _widthSpecified = true;
+ return CssValueParser.ParseLength(_tableBox.MaxWidth, _tableBox.ParentBox.AvailableWidth, _tableBox);
+ }
+ else
+ {
+ return 9999f;
+ }
+ }
+
+ ///
+ /// Calculate the min and max width for each column of the table by the content in all rows.
+ /// the min width possible without clipping content
+ /// the max width the cell content can take without wrapping
+ ///
+ /// if to measure only columns that have no calculated width
+ /// return the min width for each column - the min width possible without clipping content
+ /// return the max width for each column - the max width the cell content can take without wrapping
+ private void GetColumnsMinMaxWidthByContent(bool onlyNans, out double[] minFullWidths, out double[] maxFullWidths)
+ {
+ maxFullWidths = new double[_columnWidths.Length];
+ minFullWidths = new double[_columnWidths.Length];
+
+ foreach (CssBox row in _allRows)
+ {
+ for (int i = 0; i < row.Boxes.Count; i++)
+ {
+ int col = GetCellRealColumnIndex(row, row.Boxes[i]);
+ col = _columnWidths.Length > col ? col : _columnWidths.Length - 1;
+
+ if ((!onlyNans || double.IsNaN(_columnWidths[col])) && i < row.Boxes.Count)
+ {
+ double minWidth, maxWidth;
+ row.Boxes[i].GetMinMaxWidth(out minWidth, out maxWidth);
+
+ var colSpan = GetColSpan(row.Boxes[i]);
+ minWidth = minWidth / colSpan;
+ maxWidth = maxWidth / colSpan;
+ for (int j = 0; j < colSpan; j++)
+ {
+ minFullWidths[col + j] = Math.Max(minFullWidths[col + j], minWidth);
+ maxFullWidths[col + j] = Math.Max(maxFullWidths[col + j], maxWidth);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the width available for cells
+ ///
+ ///
+ ///
+ /// It takes away the cell-spacing from
+ ///
+ private double GetAvailableCellWidth()
+ {
+ return GetAvailableTableWidth() - GetHorizontalSpacing() * (_columnCount + 1) - _tableBox.ActualBorderLeftWidth - _tableBox.ActualBorderRightWidth;
+ }
+
+ ///
+ /// Gets the current sum of column widths
+ ///
+ ///
+ private double GetWidthSum()
+ {
+ double f = 0f;
+
+ foreach (double t in _columnWidths)
+ {
+ if (double.IsNaN(t))
+ throw new Exception("CssTable Algorithm error: There's a NaN in column widths");
+ else
+ f += t;
+ }
+
+ //Take cell-spacing
+ f += GetHorizontalSpacing() * (_columnWidths.Length + 1);
+
+ //Take table borders
+ f += _tableBox.ActualBorderLeftWidth + _tableBox.ActualBorderRightWidth;
+
+ return f;
+ }
+
+ ///
+ /// Gets the span attribute of the tag of the specified box
+ ///
+ ///
+ private static int GetSpan(CssBox b)
+ {
+ double f = CssValueParser.ParseNumber(b.GetAttribute("span"), 1);
+
+ return Math.Max(1, Convert.ToInt32(f));
+ }
+
+ ///
+ /// Gets the minimum width of each column
+ ///
+ private double[] GetColumnMinWidths()
+ {
+ if (_columnMinWidths == null)
+ {
+ _columnMinWidths = new double[_columnWidths.Length];
+
+ foreach (CssBox row in _allRows)
+ {
+ foreach (CssBox cell in row.Boxes)
+ {
+ int colspan = GetColSpan(cell);
+ int col = GetCellRealColumnIndex(row, cell);
+ int affectcol = Math.Min(col + colspan, _columnMinWidths.Length) - 1;
+ double spannedwidth = GetSpannedMinWidth(row, cell, col, colspan) + (colspan - 1) * GetHorizontalSpacing();
+
+ _columnMinWidths[affectcol] = Math.Max(_columnMinWidths[affectcol], cell.GetMinimumWidth() - spannedwidth);
+ }
+ }
+ }
+
+ return _columnMinWidths;
+ }
+
+ ///
+ /// Gets the actual horizontal spacing of the table
+ ///
+ private double GetHorizontalSpacing()
+ {
+ return _tableBox.BorderCollapse == CssConstants.Collapse ? -1f : _tableBox.ActualBorderSpacingHorizontal;
+ }
+
+ ///
+ /// Gets the actual horizontal spacing of the table
+ ///
+ private static double GetHorizontalSpacing(CssBox box)
+ {
+ return box.BorderCollapse == CssConstants.Collapse ? -1f : box.ActualBorderSpacingHorizontal;
+ }
+
+ ///
+ /// Gets the actual vertical spacing of the table
+ ///
+ private double GetVerticalSpacing()
+ {
+ return _tableBox.BorderCollapse == CssConstants.Collapse ? -1f : _tableBox.ActualBorderSpacingVertical;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssLength.cs b/Source/HtmlRenderer.Core/Core/Dom/CssLength.cs
new file mode 100644
index 000000000..6b7eebf20
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssLength.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Globalization;
+using TheArtOfDev.HtmlRenderer.Core.Parse;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents and gets info about a CSS Length
+ ///
+ ///
+ /// http://www.w3.org/TR/CSS21/syndata.html#length-units
+ ///
+ internal sealed class CssLength
+ {
+ #region Fields
+
+ private readonly double _number;
+ private readonly bool _isRelative;
+ private readonly CssUnit _unit;
+ private readonly string _length;
+ private readonly bool _isPercentage;
+ private readonly bool _hasError;
+
+ #endregion
+
+
+ ///
+ /// Creates a new CssLength from a length specified on a CSS style sheet or fragment
+ ///
+ /// Length as specified in the Style Sheet or style fragment
+ public CssLength(string length)
+ {
+ _length = length;
+ _number = 0f;
+ _unit = CssUnit.None;
+ _isPercentage = false;
+
+ //Return zero if no length specified, zero specified
+ if (string.IsNullOrEmpty(length) || length == "0")
+ return;
+
+ //If percentage, use ParseNumber
+ if (length.EndsWith("%"))
+ {
+ _number = CssValueParser.ParseNumber(length, 1);
+ _isPercentage = true;
+ return;
+ }
+
+ //If no units, has error
+ if (length.Length < 3)
+ {
+ double.TryParse(length, out _number);
+ _hasError = true;
+ return;
+ }
+
+ //Get units of the length
+ string u = length.Substring(length.Length - 2, 2);
+
+ //Number of the length
+ string number = length.Substring(0, length.Length - 2);
+
+ //TODO: Units behave different in paper and in screen!
+ switch (u)
+ {
+ case CssConstants.Em:
+ _unit = CssUnit.Ems;
+ _isRelative = true;
+ break;
+ case CssConstants.Ex:
+ _unit = CssUnit.Ex;
+ _isRelative = true;
+ break;
+ case CssConstants.Px:
+ _unit = CssUnit.Pixels;
+ _isRelative = true;
+ break;
+ case CssConstants.Mm:
+ _unit = CssUnit.Milimeters;
+ break;
+ case CssConstants.Cm:
+ _unit = CssUnit.Centimeters;
+ break;
+ case CssConstants.In:
+ _unit = CssUnit.Inches;
+ break;
+ case CssConstants.Pt:
+ _unit = CssUnit.Points;
+ break;
+ case CssConstants.Pc:
+ _unit = CssUnit.Picas;
+ break;
+ default:
+ _hasError = true;
+ return;
+ }
+
+ if (!double.TryParse(number, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out _number))
+ {
+ _hasError = true;
+ }
+ }
+
+
+ #region Props
+
+ ///
+ /// Gets the number in the length
+ ///
+ public double Number
+ {
+ get { return _number; }
+ }
+
+ ///
+ /// Gets if the length has some parsing error
+ ///
+ public bool HasError
+ {
+ get { return _hasError; }
+ }
+
+
+ ///
+ /// Gets if the length represents a precentage (not actually a length)
+ ///
+ public bool IsPercentage
+ {
+ get { return _isPercentage; }
+ }
+
+
+ ///
+ /// Gets if the length is specified in relative units
+ ///
+ public bool IsRelative
+ {
+ get { return _isRelative; }
+ }
+
+ ///
+ /// Gets the unit of the length
+ ///
+ public CssUnit Unit
+ {
+ get { return _unit; }
+ }
+
+ ///
+ /// Gets the length as specified in the string
+ ///
+ public string Length
+ {
+ get { return _length; }
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// If length is in Ems, returns its value in points
+ ///
+ /// Em size factor to multiply
+ /// Points size of this em
+ /// If length has an error or isn't in ems
+ public CssLength ConvertEmToPoints(double emSize)
+ {
+ if (HasError)
+ throw new InvalidOperationException("Invalid length");
+ if (Unit != CssUnit.Ems)
+ throw new InvalidOperationException("Length is not in ems");
+
+ return new CssLength(string.Format("{0}pt", Convert.ToSingle(Number * emSize).ToString("0.0", NumberFormatInfo.InvariantInfo)));
+ }
+
+ ///
+ /// If length is in Ems, returns its value in pixels
+ ///
+ /// Pixel size factor to multiply
+ /// Pixels size of this em
+ /// If length has an error or isn't in ems
+ public CssLength ConvertEmToPixels(double pixelFactor)
+ {
+ if (HasError)
+ throw new InvalidOperationException("Invalid length");
+ if (Unit != CssUnit.Ems)
+ throw new InvalidOperationException("Length is not in ems");
+
+ return new CssLength(string.Format("{0}px", Convert.ToSingle(Number * pixelFactor).ToString("0.0", NumberFormatInfo.InvariantInfo)));
+ }
+
+ ///
+ /// Returns the length formatted ready for CSS interpreting.
+ ///
+ ///
+ public override string ToString()
+ {
+ if (HasError)
+ {
+ return string.Empty;
+ }
+ else if (IsPercentage)
+ {
+ return string.Format(NumberFormatInfo.InvariantInfo, "{0}%", Number);
+ }
+ else
+ {
+ string u = string.Empty;
+
+ switch (Unit)
+ {
+ case CssUnit.None:
+ break;
+ case CssUnit.Ems:
+ u = "em";
+ break;
+ case CssUnit.Pixels:
+ u = "px";
+ break;
+ case CssUnit.Ex:
+ u = "ex";
+ break;
+ case CssUnit.Inches:
+ u = "in";
+ break;
+ case CssUnit.Centimeters:
+ u = "cm";
+ break;
+ case CssUnit.Milimeters:
+ u = "mm";
+ break;
+ case CssUnit.Points:
+ u = "pt";
+ break;
+ case CssUnit.Picas:
+ u = "pc";
+ break;
+ }
+
+ return string.Format(NumberFormatInfo.InvariantInfo, "{0}{1}", Number, u);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssLineBox.cs b/Source/HtmlRenderer.Core/Core/Dom/CssLineBox.cs
new file mode 100644
index 000000000..dd2db8925
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssLineBox.cs
@@ -0,0 +1,293 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents a line of text.
+ ///
+ ///
+ /// To learn more about line-boxes see CSS spec:
+ /// http://www.w3.org/TR/CSS21/visuren.html
+ ///
+ internal sealed class CssLineBox
+ {
+ #region Fields and Consts
+
+ private readonly List _words;
+ private readonly CssBox _ownerBox;
+ private readonly Dictionary _rects;
+ private readonly List _relatedBoxes;
+
+ #endregion
+
+
+ ///
+ /// Creates a new LineBox
+ ///
+ public CssLineBox(CssBox ownerBox)
+ {
+ _rects = new Dictionary();
+ _relatedBoxes = new List();
+ _words = new List();
+ _ownerBox = ownerBox;
+ _ownerBox.LineBoxes.Add(this);
+ }
+
+ ///
+ /// Gets a list of boxes related with the linebox.
+ /// To know the words of the box inside this linebox, use the method.
+ ///
+ public List RelatedBoxes
+ {
+ get { return _relatedBoxes; }
+ }
+
+ ///
+ /// Gets the words inside the linebox
+ ///
+ public List Words
+ {
+ get { return _words; }
+ }
+
+ ///
+ /// Gets the owner box
+ ///
+ public CssBox OwnerBox
+ {
+ get { return _ownerBox; }
+ }
+
+ ///
+ /// Gets a List of rectangles that are to be painted on this linebox
+ ///
+ public Dictionary Rectangles
+ {
+ get { return _rects; }
+ }
+
+ ///
+ /// Get the height of this box line (the max height of all the words)
+ ///
+ public double LineHeight
+ {
+ get
+ {
+ double height = 0;
+ foreach (var rect in _rects)
+ {
+ height = Math.Max(height, rect.Value.Height);
+ }
+ return height;
+ }
+ }
+
+ ///
+ /// Get the bottom of this box line (the max bottom of all the words)
+ ///
+ public double LineBottom
+ {
+ get
+ {
+ double bottom = 0;
+ foreach (var rect in _rects)
+ {
+ bottom = Math.Max(bottom, rect.Value.Bottom);
+ }
+ return bottom;
+ }
+ }
+
+ ///
+ /// Lets the linebox add the word an its box to their lists if necessary.
+ ///
+ ///
+ internal void ReportExistanceOf(CssRect word)
+ {
+ if (!Words.Contains(word))
+ {
+ Words.Add(word);
+ }
+
+ if (!RelatedBoxes.Contains(word.OwnerBox))
+ {
+ RelatedBoxes.Add(word.OwnerBox);
+ }
+ }
+
+ ///
+ /// Return the words of the specified box that live in this linebox
+ ///
+ ///
+ ///
+ internal List WordsOf(CssBox box)
+ {
+ List r = new List();
+
+ foreach (CssRect word in Words)
+ if (word.OwnerBox.Equals(box))
+ r.Add(word);
+
+ return r;
+ }
+
+ ///
+ /// Updates the specified rectangle of the specified box.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal void UpdateRectangle(CssBox box, double x, double y, double r, double b)
+ {
+ double leftspacing = box.ActualBorderLeftWidth + box.ActualPaddingLeft;
+ double rightspacing = box.ActualBorderRightWidth + box.ActualPaddingRight;
+ double topspacing = box.ActualBorderTopWidth + box.ActualPaddingTop;
+ double bottomspacing = box.ActualBorderBottomWidth + box.ActualPaddingTop;
+
+ if ((box.FirstHostingLineBox != null && box.FirstHostingLineBox.Equals(this)) || box.IsImage)
+ x -= leftspacing;
+ if ((box.LastHostingLineBox != null && box.LastHostingLineBox.Equals(this)) || box.IsImage)
+ r += rightspacing;
+
+ if (!box.IsImage)
+ {
+ y -= topspacing;
+ b += bottomspacing;
+ }
+
+
+ if (!Rectangles.ContainsKey(box))
+ {
+ Rectangles.Add(box, RRect.FromLTRB(x, y, r, b));
+ }
+ else
+ {
+ RRect f = Rectangles[box];
+ Rectangles[box] = RRect.FromLTRB(
+ Math.Min(f.X, x), Math.Min(f.Y, y),
+ Math.Max(f.Right, r), Math.Max(f.Bottom, b));
+ }
+
+ if (box.ParentBox != null && box.ParentBox.IsInline)
+ {
+ UpdateRectangle(box.ParentBox, x, y, r, b);
+ }
+ }
+
+ ///
+ /// Copies the rectangles to their specified box
+ ///
+ internal void AssignRectanglesToBoxes()
+ {
+ foreach (CssBox b in Rectangles.Keys)
+ {
+ b.Rectangles.Add(this, Rectangles[b]);
+ }
+ }
+
+ ///
+ /// Sets the baseline of the words of the specified box to certain height
+ ///
+ /// Device info
+ /// box to check words
+ /// baseline
+ internal void SetBaseLine(RGraphics g, CssBox b, double baseline)
+ {
+ //TODO: Aqui me quede, checar poniendo "by the" con un font-size de 3em
+ List ws = WordsOf(b);
+
+ if (!Rectangles.ContainsKey(b))
+ return;
+
+ RRect r = Rectangles[b];
+
+ //Save top of words related to the top of rectangle
+ double gap = 0f;
+
+ if (ws.Count > 0)
+ {
+ gap = ws[0].Top - r.Top;
+ }
+ else
+ {
+ CssRect firstw = b.FirstWordOccourence(b, this);
+
+ if (firstw != null)
+ {
+ gap = firstw.Top - r.Top;
+ }
+ }
+
+ //New top that words will have
+ //float newtop = baseline - (Height - OwnerBox.FontDescent - 3); //OLD
+ double newtop = baseline; // -GetBaseLineHeight(b, g); //OLD
+
+ if (b.ParentBox != null &&
+ b.ParentBox.Rectangles.ContainsKey(this) &&
+ r.Height < b.ParentBox.Rectangles[this].Height)
+ {
+ //Do this only if rectangle is shorter than parent's
+ double recttop = newtop - gap;
+ RRect newr = new RRect(r.X, recttop, r.Width, r.Height);
+ Rectangles[b] = newr;
+ b.OffsetRectangle(this, gap);
+ }
+
+ foreach (var word in ws)
+ {
+ if (!word.IsImage)
+ word.Top = newtop;
+ }
+ }
+
+ ///
+ /// Check if the given word is the last selected word in the line.
+ /// It can either be the last word in the line or the next word has no selection.
+ ///
+ /// the word to check
+ ///
+ public bool IsLastSelectedWord(CssRect word)
+ {
+ for (int i = 0; i < _words.Count - 1; i++)
+ {
+ if (_words[i] == word)
+ {
+ return !_words[i + 1].Selected;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Returns the words of the linebox
+ ///
+ ///
+ public override string ToString()
+ {
+ string[] ws = new string[Words.Count];
+ for (int i = 0; i < ws.Length; i++)
+ {
+ ws[i] = Words[i].Text;
+ }
+ return string.Join(" ", ws);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssRect.cs b/Source/HtmlRenderer.Core/Core/Dom/CssRect.cs
new file mode 100644
index 000000000..d7ff14ac1
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssRect.cs
@@ -0,0 +1,291 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Handlers;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents a word inside an inline box
+ ///
+ ///
+ /// Because of performance, words of text are the most atomic
+ /// element in the project. It should be characters, but come on,
+ /// imagine the performance when drawing char by char on the device.
+ /// It may change for future versions of the library.
+ ///
+ internal abstract class CssRect
+ {
+ #region Fields and Consts
+
+ ///
+ /// the CSS box owner of the word
+ ///
+ private readonly CssBox _ownerBox;
+
+ ///
+ /// Rectangle
+ ///
+ private RRect _rect;
+
+ ///
+ /// If the word is selected this points to the selection handler for more data
+ ///
+ private SelectionHandler _selection;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the CSS box owner of the word
+ protected CssRect(CssBox owner)
+ {
+ _ownerBox = owner;
+ }
+
+ ///
+ /// Gets the Box where this word belongs.
+ ///
+ public CssBox OwnerBox
+ {
+ get { return _ownerBox; }
+ }
+
+ ///
+ /// Gets or sets the bounds of the rectangle
+ ///
+ public RRect Rectangle
+ {
+ get { return _rect; }
+ set { _rect = value; }
+ }
+
+ ///
+ /// Left of the rectangle
+ ///
+ public double Left
+ {
+ get { return _rect.X; }
+ set { _rect.X = value; }
+ }
+
+ ///
+ /// Top of the rectangle
+ ///
+ public double Top
+ {
+ get { return _rect.Y; }
+ set { _rect.Y = value; }
+ }
+
+ ///
+ /// Width of the rectangle
+ ///
+ public double Width
+ {
+ get { return _rect.Width; }
+ set { _rect.Width = value; }
+ }
+
+ ///
+ /// Get the full width of the word including the spacing.
+ ///
+ public double FullWidth
+ {
+ get { return _rect.Width + ActualWordSpacing; }
+ }
+
+ ///
+ /// Gets the actual width of whitespace between words.
+ ///
+ public double ActualWordSpacing
+ {
+ get { return (OwnerBox != null ? (HasSpaceAfter ? OwnerBox.ActualWordSpacing : 0) + (IsImage ? OwnerBox.ActualWordSpacing : 0) : 0); }
+ }
+
+ ///
+ /// Height of the rectangle
+ ///
+ public double Height
+ {
+ get { return _rect.Height; }
+ set { _rect.Height = value; }
+ }
+
+ ///
+ /// Gets or sets the right of the rectangle. When setting, it only affects the Width of the rectangle.
+ ///
+ public double Right
+ {
+ get { return Rectangle.Right; }
+ set { Width = value - Left; }
+ }
+
+ ///
+ /// Gets or sets the bottom of the rectangle. When setting, it only affects the Height of the rectangle.
+ ///
+ public double Bottom
+ {
+ get { return Rectangle.Bottom; }
+ set { Height = value - Top; }
+ }
+
+ ///
+ /// If the word is selected this points to the selection handler for more data
+ ///
+ public SelectionHandler Selection
+ {
+ get { return _selection; }
+ set { _selection = value; }
+ }
+
+ ///
+ /// was there a whitespace before the word chars (before trim)
+ ///
+ public virtual bool HasSpaceBefore
+ {
+ get { return false; }
+ }
+
+ ///
+ /// was there a whitespace after the word chars (before trim)
+ ///
+ public virtual bool HasSpaceAfter
+ {
+ get { return false; }
+ }
+
+ ///
+ /// Gets the image this words represents (if one exists)
+ ///
+ public virtual RImage Image
+ {
+ get { return null; }
+ // ReSharper disable ValueParameterNotUsed
+ set { }
+ // ReSharper restore ValueParameterNotUsed
+ }
+
+ ///
+ /// Gets if the word represents an image.
+ ///
+ public virtual bool IsImage
+ {
+ get { return false; }
+ }
+
+ ///
+ /// Gets a bool indicating if this word is composed only by spaces.
+ /// Spaces include tabs and line breaks
+ ///
+ public virtual bool IsSpaces
+ {
+ get { return true; }
+ }
+
+ ///
+ /// Gets if the word is composed by only a line break
+ ///
+ public virtual bool IsLineBreak
+ {
+ get { return false; }
+ }
+
+ ///
+ /// Gets the text of the word
+ ///
+ public virtual string Text
+ {
+ get { return null; }
+ }
+
+ ///
+ /// is the word is currently selected
+ ///
+ public bool Selected
+ {
+ get { return _selection != null; }
+ }
+
+ ///
+ /// the selection start index if the word is partially selected (-1 if not selected or fully selected)
+ ///
+ public int SelectedStartIndex
+ {
+ get { return _selection != null ? _selection.GetSelectingStartIndex(this) : -1; }
+ }
+
+ ///
+ /// the selection end index if the word is partially selected (-1 if not selected or fully selected)
+ ///
+ public int SelectedEndIndexOffset
+ {
+ get { return _selection != null ? _selection.GetSelectedEndIndexOffset(this) : -1; }
+ }
+
+ ///
+ /// the selection start offset if the word is partially selected (-1 if not selected or fully selected)
+ ///
+ public double SelectedStartOffset
+ {
+ get { return _selection != null ? _selection.GetSelectedStartOffset(this) : -1; }
+ }
+
+ ///
+ /// the selection end offset if the word is partially selected (-1 if not selected or fully selected)
+ ///
+ public double SelectedEndOffset
+ {
+ get { return _selection != null ? _selection.GetSelectedEndOffset(this) : -1; }
+ }
+
+ ///
+ /// Gets or sets an offset to be considered in measurements
+ ///
+ internal double LeftGlyphPadding
+ {
+ get { return OwnerBox != null ? OwnerBox.ActualFont.LeftPadding : 0; }
+ }
+
+ ///
+ /// Represents this word for debugging purposes
+ ///
+ ///
+ public override string ToString()
+ {
+ return string.Format("{0} ({1} char{2})", Text.Replace(' ', '-').Replace("\n", "\\n"), Text.Length, Text.Length != 1 ? "s" : string.Empty);
+ }
+
+ public bool BreakPage()
+ {
+ var container = this.OwnerBox.HtmlContainer;
+
+ if (this.Height >= container.PageSize.Height)
+ return false;
+
+ var remTop = (this.Top - container.MarginTop) % container.PageSize.Height;
+ var remBottom = (this.Bottom - container.MarginTop) % container.PageSize.Height;
+
+ if (remTop > remBottom)
+ {
+ this.Top += container.PageSize.Height - remTop + 1;
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssRectImage.cs b/Source/HtmlRenderer.Core/Core/Dom/CssRectImage.cs
new file mode 100644
index 000000000..c2f53800f
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssRectImage.cs
@@ -0,0 +1,81 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents a word inside an inline box
+ ///
+ internal sealed class CssRectImage : CssRect
+ {
+ #region Fields and Consts
+
+ ///
+ /// the image object if it is image word (can be null if not loaded)
+ ///
+ private RImage _image;
+
+ ///
+ /// the image rectangle restriction as returned from image load event
+ ///
+ private RRect _imageRectangle;
+
+ #endregion
+
+
+ ///
+ /// Creates a new BoxWord which represents an image
+ ///
+ /// the CSS box owner of the word
+ public CssRectImage(CssBox owner)
+ : base(owner)
+ { }
+
+ ///
+ /// Gets the image this words represents (if one exists)
+ ///
+ public override RImage Image
+ {
+ get { return _image; }
+ set { _image = value; }
+ }
+
+ ///
+ /// Gets if the word represents an image.
+ ///
+ public override bool IsImage
+ {
+ get { return true; }
+ }
+
+ ///
+ /// the image rectange restriction as returned from image load event
+ ///
+ public RRect ImageRectangle
+ {
+ get { return _imageRectangle; }
+ set { _imageRectangle = value; }
+ }
+
+ ///
+ /// Represents this word for debugging purposes
+ ///
+ ///
+ public override string ToString()
+ {
+ return "Image";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssRectWord.cs b/Source/HtmlRenderer.Core/Core/Dom/CssRectWord.cs
new file mode 100644
index 000000000..cc5499fad
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssRectWord.cs
@@ -0,0 +1,113 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents a word inside an inline box
+ ///
+ internal sealed class CssRectWord : CssRect
+ {
+ #region Fields and Consts
+
+ ///
+ /// The word text
+ ///
+ private readonly string _text;
+
+ ///
+ /// was there a whitespace before the word chars (before trim)
+ ///
+ private readonly bool _hasSpaceBefore;
+
+ ///
+ /// was there a whitespace after the word chars (before trim)
+ ///
+ private readonly bool _hasSpaceAfter;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the CSS box owner of the word
+ /// the word chars
+ /// was there a whitespace before the word chars (before trim)
+ /// was there a whitespace after the word chars (before trim)
+ public CssRectWord(CssBox owner, string text, bool hasSpaceBefore, bool hasSpaceAfter)
+ : base(owner)
+ {
+ _text = text;
+ _hasSpaceBefore = hasSpaceBefore;
+ _hasSpaceAfter = hasSpaceAfter;
+ }
+
+ ///
+ /// was there a whitespace before the word chars (before trim)
+ ///
+ public override bool HasSpaceBefore
+ {
+ get { return _hasSpaceBefore; }
+ }
+
+ ///
+ /// was there a whitespace after the word chars (before trim)
+ ///
+ public override bool HasSpaceAfter
+ {
+ get { return _hasSpaceAfter; }
+ }
+
+ ///
+ /// Gets a bool indicating if this word is composed only by spaces.
+ /// Spaces include tabs and line breaks
+ ///
+ public override bool IsSpaces
+ {
+ get
+ {
+ foreach (var c in Text)
+ {
+ if (!char.IsWhiteSpace(c))
+ return false;
+ }
+ return true;
+ }
+ }
+
+ ///
+ /// Gets if the word is composed by only a line break
+ ///
+ public override bool IsLineBreak
+ {
+ get { return Text == "\n"; }
+ }
+
+ ///
+ /// Gets the text of the word
+ ///
+ public override string Text
+ {
+ get { return _text; }
+ }
+
+ ///
+ /// Represents this word for debugging purposes
+ ///
+ ///
+ public override string ToString()
+ {
+ return string.Format("{0} ({1} char{2})", Text.Replace(' ', '-').Replace("\n", "\\n"), Text.Length, Text.Length != 1 ? "s" : string.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssSpacingBox.cs b/Source/HtmlRenderer.Core/Core/Dom/CssSpacingBox.cs
new file mode 100644
index 000000000..0ea65d99b
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssSpacingBox.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Used to make space on vertical cell combination
+ ///
+ internal sealed class CssSpacingBox : CssBox
+ {
+ #region Fields and Consts
+
+ private readonly CssBox _extendedBox;
+
+ ///
+ /// the index of the row where box starts
+ ///
+ private readonly int _startRow;
+
+ ///
+ /// the index of the row where box ends
+ ///
+ private readonly int _endRow;
+
+ #endregion
+
+
+ public CssSpacingBox(CssBox tableBox, ref CssBox extendedBox, int startRow)
+ : base(tableBox, new HtmlTag("none", false, new Dictionary { { "colspan", "1" } }))
+ {
+ _extendedBox = extendedBox;
+ Display = CssConstants.None;
+
+ _startRow = startRow;
+ _endRow = startRow + Int32.Parse(extendedBox.GetAttribute("rowspan", "1")) - 1;
+ }
+
+ public CssBox ExtendedBox
+ {
+ get { return _extendedBox; }
+ }
+
+ ///
+ /// Gets the index of the row where box starts
+ ///
+ public int StartRow
+ {
+ get { return _startRow; }
+ }
+
+ ///
+ /// Gets the index of the row where box ends
+ ///
+ public int EndRow
+ {
+ get { return _endRow; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/CssUnit.cs b/Source/HtmlRenderer.Core/Core/Dom/CssUnit.cs
new file mode 100644
index 000000000..269893e8d
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/CssUnit.cs
@@ -0,0 +1,33 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// Represents the possible units of the CSS lengths
+ ///
+ ///
+ /// http://www.w3.org/TR/CSS21/syndata.html#length-units
+ ///
+ internal enum CssUnit
+ {
+ None,
+ Ems,
+ Pixels,
+ Ex,
+ Inches,
+ Centimeters,
+ Milimeters,
+ Points,
+ Picas
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/HoverBoxBlock.cs b/Source/HtmlRenderer.Core/Core/Dom/HoverBoxBlock.cs
new file mode 100644
index 000000000..1f919ec66
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/HoverBoxBlock.cs
@@ -0,0 +1,57 @@
+// "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 TheArtOfDev.HtmlRenderer.Core.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ ///
+ /// CSS boxes that have ":hover" selector on them.
+ ///
+ internal sealed class HoverBoxBlock
+ {
+ ///
+ /// the box that has :hover css on
+ ///
+ private readonly CssBox _cssBox;
+
+ ///
+ /// the :hover style block data
+ ///
+ private readonly CssBlock _cssBlock;
+
+ ///
+ /// Init.
+ ///
+ public HoverBoxBlock(CssBox cssBox, CssBlock cssBlock)
+ {
+ _cssBox = cssBox;
+ _cssBlock = cssBlock;
+ }
+
+ ///
+ /// the box that has :hover css on
+ ///
+ public CssBox CssBox
+ {
+ get { return _cssBox; }
+ }
+
+ ///
+ /// the :hover style block data
+ ///
+ public CssBlock CssBlock
+ {
+ get { return _cssBlock; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Dom/HtmlTag.cs b/Source/HtmlRenderer.Core/Core/Dom/HtmlTag.cs
new file mode 100644
index 000000000..ca282fc29
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Dom/HtmlTag.cs
@@ -0,0 +1,115 @@
+// "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.Collections.Generic;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Dom
+{
+ internal sealed class HtmlTag
+ {
+ #region Fields and Consts
+
+ ///
+ /// the name of the html tag
+ ///
+ private readonly string _name;
+
+ ///
+ /// if the tag is single placed; in other words it doesn't have a separate closing tag;
+ ///
+ private readonly bool _isSingle;
+
+ ///
+ /// collection of attributes and their value the html tag has
+ ///
+ private readonly Dictionary _attributes;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the name of the html tag
+ /// if the tag is single placed; in other words it doesn't have a separate closing tag;
+ /// collection of attributes and their value the html tag has
+ public HtmlTag(string name, bool isSingle, Dictionary attributes = null)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(name, "name");
+
+ _name = name;
+ _isSingle = isSingle;
+ _attributes = attributes;
+ }
+
+ ///
+ /// Gets the name of this tag
+ ///
+ public string Name
+ {
+ get { return _name; }
+ }
+
+ ///
+ /// Gets collection of attributes and their value the html tag has
+ ///
+ public Dictionary Attributes
+ {
+ get { return _attributes; }
+ }
+
+ ///
+ /// Gets if the tag is single placed; in other words it doesn't have a separate closing tag;
+ /// e.g. <br>
+ ///
+ public bool IsSingle
+ {
+ get { return _isSingle; }
+ }
+
+ ///
+ /// is the html tag has attributes.
+ ///
+ /// true - has attributes, false - otherwise
+ public bool HasAttributes()
+ {
+ return _attributes != null && _attributes.Count > 0;
+ }
+
+ ///
+ /// Gets a boolean indicating if the attribute list has the specified attribute
+ ///
+ /// attribute name to check if exists
+ /// true - attribute exists, false - otherwise
+ public bool HasAttribute(string attribute)
+ {
+ return _attributes != null && _attributes.ContainsKey(attribute);
+ }
+
+ ///
+ /// Get attribute value for given attribute name or null if not exists.
+ ///
+ /// attribute name to get by
+ /// optional: value to return if attribute is not specified
+ /// attribute value or null if not found
+ public string TryGetAttribute(string attribute, string defaultValue = null)
+ {
+ return _attributes != null && _attributes.ContainsKey(attribute) ? _attributes[attribute] : defaultValue;
+ }
+
+ public override string ToString()
+ {
+ return string.Format("<{0}>", _name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/CssBlock.cs b/Source/HtmlRenderer.Core/Core/Entities/CssBlock.cs
new file mode 100644
index 000000000..2982b3159
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/CssBlock.cs
@@ -0,0 +1,235 @@
+// "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.Collections.Generic;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Represents a block of CSS property values.
+ /// Contains collection of key-value pairs that are CSS properties for specific css class.
+ /// Css class can be either custom or html tag name.
+ ///
+ ///
+ /// To learn more about CSS blocks visit CSS spec: http://www.w3.org/TR/CSS21/syndata.html#block
+ ///
+ public sealed class CssBlock
+ {
+ #region Fields and Consts
+
+ ///
+ /// the name of the css class of the block
+ ///
+ private readonly string _class;
+
+ ///
+ /// the CSS block properties and values
+ ///
+ private readonly Dictionary _properties;
+
+ ///
+ /// additional selectors to used in hierarchy (p className1 > className2)
+ ///
+ private readonly List _selectors;
+
+ ///
+ /// is the css block has :hover pseudo-class
+ ///
+ private readonly bool _hover;
+
+ #endregion
+
+
+ ///
+ /// Creates a new block from the block's source
+ ///
+ /// the name of the css class of the block
+ /// the CSS block properties and values
+ /// optional: additional selectors to used in hierarchy
+ /// optional: is the css block has :hover pseudo-class
+ public CssBlock(string @class, Dictionary properties, List selectors = null, bool hover = false)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(@class, "@class");
+ ArgChecker.AssertArgNotNull(properties, "properties");
+
+ _class = @class;
+ _selectors = selectors;
+ _properties = properties;
+ _hover = hover;
+ }
+
+ ///
+ /// the name of the css class of the block
+ ///
+ public string Class
+ {
+ get { return _class; }
+ }
+
+ ///
+ /// additional selectors to used in hierarchy (p className1 > className2)
+ ///
+ public List Selectors
+ {
+ get { return _selectors; }
+ }
+
+ ///
+ /// Gets the CSS block properties and its values
+ ///
+ public IDictionary Properties
+ {
+ get { return _properties; }
+ }
+
+ ///
+ /// is the css block has :hover pseudo-class
+ ///
+ public bool Hover
+ {
+ get { return _hover; }
+ }
+
+ ///
+ /// Merge the other block properties into this css block.
+ /// Other block properties can overwrite this block properties.
+ ///
+ /// the css block to merge with
+ public void Merge(CssBlock other)
+ {
+ ArgChecker.AssertArgNotNull(other, "other");
+
+ foreach (var prop in other._properties.Keys)
+ {
+ _properties[prop] = other._properties[prop];
+ }
+ }
+
+ ///
+ /// Create deep copy of the CssBlock.
+ ///
+ /// new CssBlock with same data
+ public CssBlock Clone()
+ {
+ return new CssBlock(_class, new Dictionary(_properties), _selectors != null ? new List(_selectors) : null);
+ }
+
+ ///
+ /// Check if the two css blocks are the same (same class, selectors and properties).
+ ///
+ /// the other block to compare to
+ /// true - the two blocks are the same, false - otherwise
+ public bool Equals(CssBlock other)
+ {
+ if (ReferenceEquals(null, other))
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+ if (!Equals(other._class, _class))
+ return false;
+
+ if (!Equals(other._properties.Count, _properties.Count))
+ return false;
+
+ foreach (var property in _properties)
+ {
+ if (!other._properties.ContainsKey(property.Key))
+ return false;
+ if (!Equals(other._properties[property.Key], property.Value))
+ return false;
+ }
+
+ if (!EqualsSelector(other))
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Check if the selectors of the css blocks is the same.
+ ///
+ /// the other block to compare to
+ /// true - the selectors on blocks are the same, false - otherwise
+ public bool EqualsSelector(CssBlock other)
+ {
+ if (ReferenceEquals(null, other))
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+
+ if (other.Hover != Hover)
+ return false;
+ if (other._selectors == null && _selectors != null)
+ return false;
+ if (other._selectors != null && _selectors == null)
+ return false;
+
+ if (other._selectors != null && _selectors != null)
+ {
+ if (!Equals(other._selectors.Count, _selectors.Count))
+ return false;
+
+ for (int i = 0; i < _selectors.Count; i++)
+ {
+ if (!Equals(other._selectors[i].Class, _selectors[i].Class))
+ return false;
+ if (!Equals(other._selectors[i].DirectParent, _selectors[i].DirectParent))
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Check if the two css blocks are the same (same class, selectors and properties).
+ ///
+ /// the other block to compare to
+ /// true - the two blocks are the same, false - otherwise
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+ if (obj.GetType() != typeof(CssBlock))
+ return false;
+ return Equals((CssBlock)obj);
+ }
+
+ ///
+ /// Serves as a hash function for a particular type.
+ ///
+ /// A hash code for the current .
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((_class != null ? _class.GetHashCode() : 0) * 397) ^ (_properties != null ? _properties.GetHashCode() : 0);
+ }
+ }
+
+ ///
+ /// Returns a that represents the current .
+ ///
+ public override string ToString()
+ {
+ var str = _class + " { ";
+ foreach (var property in _properties)
+ {
+ str += string.Format("{0}={1}; ", property.Key, property.Value);
+ }
+ return str + " }";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/CssBlockSelectorItem.cs b/Source/HtmlRenderer.Core/Core/Entities/CssBlockSelectorItem.cs
new file mode 100644
index 000000000..76ad35919
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/CssBlockSelectorItem.cs
@@ -0,0 +1,74 @@
+// "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 TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Holds single class selector in css block hierarchical selection (p class1 > div.class2)
+ ///
+ public struct CssBlockSelectorItem
+ {
+ #region Fields and Consts
+
+ ///
+ /// the name of the css class of the block
+ ///
+ private readonly string _class;
+
+ ///
+ /// is the selector item has to be direct parent
+ ///
+ private readonly bool _directParent;
+
+ #endregion
+
+
+ ///
+ /// Creates a new block from the block's source
+ ///
+ /// the name of the css class of the block
+ ///
+ public CssBlockSelectorItem(string @class, bool directParent)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(@class, "@class");
+
+ _class = @class;
+ _directParent = directParent;
+ }
+
+ ///
+ /// the name of the css class of the block
+ ///
+ public string Class
+ {
+ get { return _class; }
+ }
+
+ ///
+ /// is the selector item has to be direct parent
+ ///
+ public bool DirectParent
+ {
+ get { return _directParent; }
+ }
+
+ ///
+ /// Returns a that represents the current .
+ ///
+ public override string ToString()
+ {
+ return _class + (_directParent ? " > " : string.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlGenerationStyle.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlGenerationStyle.cs
new file mode 100644
index 000000000..1d9225a69
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlGenerationStyle.cs
@@ -0,0 +1,35 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Controls the way styles are generated when html is generated.
+ ///
+ public enum HtmlGenerationStyle
+ {
+ ///
+ /// styles are not generated at all
+ ///
+ None = 0,
+
+ ///
+ /// style are inserted in style attribute for each html tag
+ ///
+ Inline = 1,
+
+ ///
+ /// style section is generated in the head of the html
+ ///
+ InHeader = 2
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlImageLoadEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlImageLoadEventArgs.cs
new file mode 100644
index 000000000..5d5a4c5af
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlImageLoadEventArgs.cs
@@ -0,0 +1,176 @@
+// "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.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Callback used in to allow setting image externally and async.
+ /// The callback can provide path to image file path, URL or the actual image to use.
+ /// If is given (not ) then only the specified rectangle will
+ /// be used from the loaded image and not all of it, also the rectangle will be used for size and not the actual image size.
+ ///
+ /// the path to the image to load (file path or URL)
+ /// the image to use
+ /// optional: limit to specific rectangle in the loaded image
+ public delegate void HtmlImageLoadCallback(string path, Object image, RRect imageRectangle);
+
+ ///
+ /// Invoked 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. Can also use the asynchronous image overwrite not to block HTML rendering is applicable.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ public sealed class HtmlImageLoadEventArgs : EventArgs
+ {
+ #region Fields and Consts
+
+ ///
+ /// use to cancel the image loading by html renderer, the provided image will be used.
+ ///
+ private bool _handled;
+
+ ///
+ /// the source of the image (file path or uri)
+ ///
+ private readonly string _src;
+
+ ///
+ /// collection of all the attributes that are defined on the image element
+ ///
+ private readonly Dictionary _attributes;
+
+ ///
+ /// Callback used to allow setting image externally and async.
+ ///
+ private readonly HtmlImageLoadCallback _callback;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the source of the image (file path or Uri)
+ /// collection of all the attributes that are defined on the image element
+ /// Callback used to allow setting image externally and async.
+ internal HtmlImageLoadEventArgs(string src, Dictionary attributes, HtmlImageLoadCallback callback)
+ {
+ _src = src;
+ _attributes = attributes;
+ _callback = callback;
+ }
+
+ ///
+ /// the source of the image (file path, URL or inline data)
+ ///
+ public string Src
+ {
+ get { return _src; }
+ }
+
+ ///
+ /// collection of all the attributes that are defined on the image element or CSS style
+ ///
+ public Dictionary Attributes
+ {
+ get { return _attributes; }
+ }
+
+ ///
+ /// Indicate the image load is handled asynchronously.
+ /// Cancel this image loading and overwrite the image asynchronously using callback method.
+ ///
+ public bool Handled
+ {
+ get { return _handled; }
+ set { _handled = value; }
+ }
+
+ ///
+ /// Callback to overwrite the loaded image with error image.
+ /// Can be called directly from delegate handler or asynchronously after setting to True.
+ ///
+ public void Callback()
+ {
+ _handled = true;
+ _callback(null, null, new RRect());
+ }
+
+ ///
+ /// Callback to overwrite the loaded image with image to load from given URI.
+ /// Can be called directly from delegate handler or asynchronously after setting to True.
+ ///
+ /// the path to the image to load (file path or URL)
+ public void Callback(string path)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(path, "path");
+
+ _handled = true;
+ _callback(path, null, RRect.Empty);
+ }
+
+ ///
+ /// Callback to overwrite the loaded image with image to load from given URI.
+ /// Can be called directly from delegate handler or asynchronously after setting to True.
+ /// Only the specified rectangle (x,y,width,height) will be used from the loaded image and not all of it, also
+ /// the rectangle will be used for size and not the actual image size.
+ ///
+ /// the path to the image to load (file path or URL)
+ /// optional: limit to specific rectangle of the image and not all of it
+ public void Callback(string path, double x, double y, double width, double height)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(path, "path");
+
+ _handled = true;
+ _callback(path, null, new RRect(x, y, width, height));
+ }
+
+ ///
+ /// Callback to overwrite the loaded image with given image object.
+ /// Can be called directly from delegate handler or asynchronously after setting to True.
+ /// If is given (not ) then only the specified rectangle will
+ /// be used from the loaded image and not all of it, also the rectangle will be used for size and not the actual image size.
+ ///
+ /// the image to load
+ public void Callback(Object image)
+ {
+ ArgChecker.AssertArgNotNull(image, "image");
+
+ _handled = true;
+ _callback(null, image, RRect.Empty);
+ }
+
+ ///
+ /// Callback to overwrite the loaded image with given image object.
+ /// Can be called directly from delegate handler or asynchronously after setting to True.
+ /// Only the specified rectangle (x,y,width,height) will be used from the loaded image and not all of it, also
+ /// the rectangle will be used for size and not the actual image size.
+ ///
+ /// the image to load
+ /// optional: limit to specific rectangle of the image and not all of it
+ public void Callback(Object image, double x, double y, double width, double height)
+ {
+ ArgChecker.AssertArgNotNull(image, "image");
+
+ _handled = true;
+ _callback(null, image, new RRect(x, y, width, height));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedEventArgs.cs
new file mode 100644
index 000000000..f39c70a44
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedEventArgs.cs
@@ -0,0 +1,78 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Raised when the user clicks on a link in the html.
+ ///
+ public sealed class HtmlLinkClickedEventArgs : EventArgs
+ {
+ ///
+ /// the link href that was clicked
+ ///
+ private readonly string _link;
+
+ ///
+ /// collection of all the attributes that are defined on the link element
+ ///
+ private readonly Dictionary _attributes;
+
+ ///
+ /// use to cancel the execution of the link
+ ///
+ private bool _handled;
+
+ ///
+ /// Init.
+ ///
+ /// the link href that was clicked
+ public HtmlLinkClickedEventArgs(string link, Dictionary attributes)
+ {
+ _link = link;
+ _attributes = attributes;
+ }
+
+ ///
+ /// the link href that was clicked
+ ///
+ public string Link
+ {
+ get { return _link; }
+ }
+
+ ///
+ /// collection of all the attributes that are defined on the link element
+ ///
+ public Dictionary Attributes
+ {
+ get { return _attributes; }
+ }
+
+ ///
+ /// use to cancel the execution of the link
+ ///
+ public bool Handled
+ {
+ get { return _handled; }
+ set { _handled = value; }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("Link: {0}, Handled: {1}", _link, _handled);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedException.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedException.cs
new file mode 100644
index 000000000..eb04c6eb1
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlLinkClickedException.cs
@@ -0,0 +1,44 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Exception thrown when client code subscribed to LinkClicked event thrown exception.
+ ///
+ public sealed class HtmlLinkClickedException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HtmlLinkClickedException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
+ public HtmlLinkClickedException(string message)
+ : base(message)
+ { }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception. The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified.
+ public HtmlLinkClickedException(string message, Exception innerException)
+ : base(message, innerException)
+ { }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlRefreshEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlRefreshEventArgs.cs
new file mode 100644
index 000000000..7ab630ccc
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlRefreshEventArgs.cs
@@ -0,0 +1,51 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Raised when html renderer requires refresh of the control hosting (invalidation and re-layout).
+ /// It can happen if some async event has occurred that requires re-paint and re-layout of the html.
+ /// Example: async download of image is complete.
+ ///
+ public sealed class HtmlRefreshEventArgs : EventArgs
+ {
+ ///
+ /// is re-layout is required for the refresh
+ ///
+ private readonly bool _layout;
+
+ ///
+ /// Init.
+ ///
+ /// is re-layout is required for the refresh
+ public HtmlRefreshEventArgs(bool layout)
+ {
+ _layout = layout;
+ }
+
+ ///
+ /// is re-layout is required for the refresh
+ ///
+ public bool Layout
+ {
+ get { return _layout; }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("Layout: {0}", _layout);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorEventArgs.cs
new file mode 100644
index 000000000..4696ed3b9
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorEventArgs.cs
@@ -0,0 +1,79 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Raised when an error occurred during html rendering.
+ ///
+ public sealed class HtmlRenderErrorEventArgs : EventArgs
+ {
+ ///
+ /// error type that is reported
+ ///
+ private readonly HtmlRenderErrorType _type;
+
+ ///
+ /// the error message
+ ///
+ private readonly string _message;
+
+ ///
+ /// the exception that occurred (can be null)
+ ///
+ private readonly Exception _exception;
+
+ ///
+ /// Init.
+ ///
+ /// the type of error to report
+ /// the error message
+ /// optional: the exception that occurred
+ public HtmlRenderErrorEventArgs(HtmlRenderErrorType type, string message, Exception exception = null)
+ {
+ _type = type;
+ _message = message;
+ _exception = exception;
+ }
+
+ ///
+ /// error type that is reported
+ ///
+ public HtmlRenderErrorType Type
+ {
+ get { return _type; }
+ }
+
+ ///
+ /// the error message
+ ///
+ public string Message
+ {
+ get { return _message; }
+ }
+
+ ///
+ /// the exception that occurred (can be null)
+ ///
+ public Exception Exception
+ {
+ get { return _exception; }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("Type: {0}", _type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorType.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorType.cs
new file mode 100644
index 000000000..681d90be1
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlRenderErrorType.cs
@@ -0,0 +1,30 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Enum of possible error types that can be reported.
+ ///
+ public enum HtmlRenderErrorType
+ {
+ General = 0,
+ CssParsing = 1,
+ HtmlParsing = 2,
+ Image = 3,
+ Paint = 4,
+ Layout = 5,
+ KeyboardMouse = 6,
+ Iframe = 7,
+ ContextMenu = 8,
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlScrollEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlScrollEventArgs.cs
new file mode 100644
index 000000000..c43f96c92
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlScrollEventArgs.cs
@@ -0,0 +1,59 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters.Entities;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Raised when Html Renderer request scroll to specific location.
+ /// This can occur on document anchor click.
+ ///
+ public sealed class HtmlScrollEventArgs : EventArgs
+ {
+ ///
+ /// the location to scroll to
+ ///
+ private readonly RPoint _location;
+
+ ///
+ /// Init.
+ ///
+ /// the location to scroll to
+ public HtmlScrollEventArgs(RPoint location)
+ {
+ _location = location;
+ }
+
+ ///
+ /// the x location to scroll to
+ ///
+ public double X
+ {
+ get { return _location.X; }
+ }
+
+ ///
+ /// the x location to scroll to
+ ///
+ public double Y
+ {
+ get { return _location.Y; }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("Location: {0}", _location);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/HtmlStylesheetLoadEventArgs.cs b/Source/HtmlRenderer.Core/Core/Entities/HtmlStylesheetLoadEventArgs.cs
new file mode 100644
index 000000000..66d520583
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/HtmlStylesheetLoadEventArgs.cs
@@ -0,0 +1,110 @@
+// "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;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Invoked 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.
+ ///
+ public sealed class HtmlStylesheetLoadEventArgs : EventArgs
+ {
+ #region Fields and Consts
+
+ ///
+ /// the source of the stylesheet as found in the HTML (file path or URL)
+ ///
+ private readonly string _src;
+
+ ///
+ /// collection of all the attributes that are defined on the link element
+ ///
+ private readonly Dictionary _attributes;
+
+ ///
+ /// provide the new source (file path or URL) to load stylesheet from
+ ///
+ private string _setSrc;
+
+ ///
+ /// provide the stylesheet to load
+ ///
+ private string _setStyleSheet;
+
+ ///
+ /// provide the stylesheet data to load
+ ///
+ private CssData _setStyleSheetData;
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ /// the source of the image (file path or URL)
+ /// collection of all the attributes that are defined on the image element
+ internal HtmlStylesheetLoadEventArgs(string src, Dictionary attributes)
+ {
+ _src = src;
+ _attributes = attributes;
+ }
+
+ ///
+ /// the source of the stylesheet as found in the HTML (file path or URL)
+ ///
+ public string Src
+ {
+ get { return _src; }
+ }
+
+ ///
+ /// collection of all the attributes that are defined on the link element
+ ///
+ public Dictionary Attributes
+ {
+ get { return _attributes; }
+ }
+
+ ///
+ /// provide the new source (file path or URL) to load stylesheet from
+ ///
+ public string SetSrc
+ {
+ get { return _setSrc; }
+ set { _setSrc = value; }
+ }
+
+ ///
+ /// provide the stylesheet to load
+ ///
+ public string SetStyleSheet
+ {
+ get { return _setStyleSheet; }
+ set { _setStyleSheet = value; }
+ }
+
+ ///
+ /// provide the stylesheet data to load
+ ///
+ public CssData SetStyleSheetData
+ {
+ get { return _setStyleSheetData; }
+ set { _setStyleSheetData = value; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Entities/LinkElementData.cs b/Source/HtmlRenderer.Core/Core/Entities/LinkElementData.cs
new file mode 100644
index 000000000..5f10399ed
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Entities/LinkElementData.cs
@@ -0,0 +1,91 @@
+// "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"
+
+namespace TheArtOfDev.HtmlRenderer.Core.Entities
+{
+ ///
+ /// Holds data on link element in HTML.
+ /// Used to expose data outside of HTML Renderer internal structure.
+ ///
+ public sealed class LinkElementData
+ {
+ ///
+ /// the id of the link element if present
+ ///
+ private readonly string _id;
+
+ ///
+ /// the href data of the link
+ ///
+ private readonly string _href;
+
+ ///
+ /// the rectangle of element as calculated by html layout
+ ///
+ private readonly T _rectangle;
+
+ ///
+ /// Init.
+ ///
+ public LinkElementData(string id, string href, T rectangle)
+ {
+ _id = id;
+ _href = href;
+ _rectangle = rectangle;
+ }
+
+ ///
+ /// the id of the link element if present
+ ///
+ public string Id
+ {
+ get { return _id; }
+ }
+
+ ///
+ /// the href data of the link
+ ///
+ public string Href
+ {
+ get { return _href; }
+ }
+
+ ///
+ /// the rectangle of element as calculated by html layout
+ ///
+ public T Rectangle
+ {
+ get { return _rectangle; }
+ }
+
+ ///
+ /// Is the link is directed to another element in the html
+ ///
+ public bool IsAnchor
+ {
+ get { return _href.Length > 0 && _href[0] == '#'; }
+ }
+
+ ///
+ /// Return the id of the element this anchor link is referencing.
+ ///
+ public string AnchorId
+ {
+ get { return IsAnchor && _href.Length > 1 ? _href.Substring(1) : string.Empty; }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("Id: {0}, Href: {1}, Rectangle: {2}", _id, _href, _rectangle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/BackgroundImageDrawHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/BackgroundImageDrawHandler.cs
new file mode 100644
index 000000000..275f23320
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/BackgroundImageDrawHandler.cs
@@ -0,0 +1,165 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Dom;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// Contains all the paint code to paint different background images.
+ ///
+ internal static class BackgroundImageDrawHandler
+ {
+ ///
+ /// Draw the background image of the given box in the given rectangle.
+ /// Handle background-repeat and background-position values.
+ ///
+ /// the device to draw into
+ /// the box to draw its background image
+ /// the handler that loads image to draw
+ /// the rectangle to draw image in
+ public static void DrawBackgroundImage(RGraphics g, CssBox box, ImageLoadHandler imageLoadHandler, RRect rectangle)
+ {
+ // image size depends if specific rectangle given in image loader
+ var imgSize = new RSize(imageLoadHandler.Rectangle == RRect.Empty ? imageLoadHandler.Image.Width : imageLoadHandler.Rectangle.Width,
+ imageLoadHandler.Rectangle == RRect.Empty ? imageLoadHandler.Image.Height : imageLoadHandler.Rectangle.Height);
+
+ // get the location by BackgroundPosition value
+ var location = GetLocation(box.BackgroundPosition, rectangle, imgSize);
+
+ var srcRect = imageLoadHandler.Rectangle == RRect.Empty
+ ? new RRect(0, 0, imgSize.Width, imgSize.Height)
+ : new RRect(imageLoadHandler.Rectangle.Left, imageLoadHandler.Rectangle.Top, imgSize.Width, imgSize.Height);
+
+ // initial image destination rectangle
+ var destRect = new RRect(location, imgSize);
+
+ // need to clip so repeated image will be cut on rectangle
+ var lRectangle = rectangle;
+ lRectangle.Intersect(g.GetClip());
+ g.PushClip(lRectangle);
+
+ switch (box.BackgroundRepeat)
+ {
+ case "no-repeat":
+ g.DrawImage(imageLoadHandler.Image, destRect, srcRect);
+ break;
+ case "repeat-x":
+ DrawRepeatX(g, imageLoadHandler, rectangle, srcRect, destRect, imgSize);
+ break;
+ case "repeat-y":
+ DrawRepeatY(g, imageLoadHandler, rectangle, srcRect, destRect, imgSize);
+ break;
+ default:
+ DrawRepeat(g, imageLoadHandler, rectangle, srcRect, destRect, imgSize);
+ break;
+ }
+
+ g.PopClip();
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Get top-left location to start drawing the image at depending on background-position value.
+ ///
+ /// the background-position value
+ /// the rectangle to position image in
+ /// the size of the image
+ /// the top-left location
+ private static RPoint GetLocation(string backgroundPosition, RRect rectangle, RSize imgSize)
+ {
+ double left = rectangle.Left;
+ if (backgroundPosition.IndexOf("left", StringComparison.OrdinalIgnoreCase) > -1)
+ {
+ left = (rectangle.Left + .5f);
+ }
+ else if (backgroundPosition.IndexOf("right", StringComparison.OrdinalIgnoreCase) > -1)
+ {
+ left = rectangle.Right - imgSize.Width;
+ }
+ else if (backgroundPosition.IndexOf("0", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ left = (rectangle.Left + (rectangle.Width - imgSize.Width) / 2 + .5f);
+ }
+
+ double top = rectangle.Top;
+ if (backgroundPosition.IndexOf("top", StringComparison.OrdinalIgnoreCase) > -1)
+ {
+ top = rectangle.Top;
+ }
+ else if (backgroundPosition.IndexOf("bottom", StringComparison.OrdinalIgnoreCase) > -1)
+ {
+ top = rectangle.Bottom - imgSize.Height;
+ }
+ else if (backgroundPosition.IndexOf("0", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ top = (rectangle.Top + (rectangle.Height - imgSize.Height) / 2 + .5f);
+ }
+
+ return new RPoint(left, top);
+ }
+
+ ///
+ /// Draw the background image at the required location repeating it over the X axis.
+ /// Adjust location to left if starting location doesn't include all the range (adjusted to center or right).
+ ///
+ private static void DrawRepeatX(RGraphics g, ImageLoadHandler imageLoadHandler, RRect rectangle, RRect srcRect, RRect destRect, RSize imgSize)
+ {
+ while (destRect.X > rectangle.X)
+ destRect.X -= imgSize.Width;
+
+ using (var brush = g.GetTextureBrush(imageLoadHandler.Image, srcRect, destRect.Location))
+ {
+ g.DrawRectangle(brush, rectangle.X, destRect.Y, rectangle.Width, srcRect.Height);
+ }
+ }
+
+ ///
+ /// Draw the background image at the required location repeating it over the Y axis.
+ /// Adjust location to top if starting location doesn't include all the range (adjusted to center or bottom).
+ ///
+ private static void DrawRepeatY(RGraphics g, ImageLoadHandler imageLoadHandler, RRect rectangle, RRect srcRect, RRect destRect, RSize imgSize)
+ {
+ while (destRect.Y > rectangle.Y)
+ destRect.Y -= imgSize.Height;
+
+ using (var brush = g.GetTextureBrush(imageLoadHandler.Image, srcRect, destRect.Location))
+ {
+ g.DrawRectangle(brush, destRect.X, rectangle.Y, srcRect.Width, rectangle.Height);
+ }
+ }
+
+ ///
+ /// Draw the background image at the required location repeating it over the X and Y axis.
+ /// Adjust location to left-top if starting location doesn't include all the range (adjusted to center or bottom/right).
+ ///
+ private static void DrawRepeat(RGraphics g, ImageLoadHandler imageLoadHandler, RRect rectangle, RRect srcRect, RRect destRect, RSize imgSize)
+ {
+ while (destRect.X > rectangle.X)
+ destRect.X -= imgSize.Width;
+ while (destRect.Y > rectangle.Y)
+ destRect.Y -= imgSize.Height;
+
+ using (var brush = g.GetTextureBrush(imageLoadHandler.Image, srcRect, destRect.Location))
+ {
+ g.DrawRectangle(brush, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/BordersDrawHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/BordersDrawHandler.cs
new file mode 100644
index 000000000..b20c331cb
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/BordersDrawHandler.cs
@@ -0,0 +1,371 @@
+// "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 TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Dom;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// Contains all the complex paint code to paint different style borders.
+ ///
+ internal static class BordersDrawHandler
+ {
+ #region Fields and Consts
+
+ ///
+ /// used for all border paint to use the same points and not create new array each time.
+ ///
+ private static readonly RPoint[] _borderPts = new RPoint[4];
+
+ #endregion
+
+
+ ///
+ /// Draws all the border of the box with respect to style, width, etc.
+ ///
+ /// the device to draw into
+ /// the box to draw borders for
+ /// the bounding rectangle to draw in
+ /// is it the first rectangle of the element
+ /// is it the last rectangle of the element
+ public static void DrawBoxBorders(RGraphics g, CssBox box, RRect rect, bool isFirst, bool isLast)
+ {
+ if (rect.Width > 0 && rect.Height > 0)
+ {
+ if (!(string.IsNullOrEmpty(box.BorderTopStyle) || box.BorderTopStyle == CssConstants.None || box.BorderTopStyle == CssConstants.Hidden) && box.ActualBorderTopWidth > 0)
+ {
+ DrawBorder(Border.Top, box, g, rect, isFirst, isLast);
+ }
+ if (isFirst && !(string.IsNullOrEmpty(box.BorderLeftStyle) || box.BorderLeftStyle == CssConstants.None || box.BorderLeftStyle == CssConstants.Hidden) && box.ActualBorderLeftWidth > 0)
+ {
+ DrawBorder(Border.Left, box, g, rect, true, isLast);
+ }
+ if (!(string.IsNullOrEmpty(box.BorderBottomStyle) || box.BorderBottomStyle == CssConstants.None || box.BorderBottomStyle == CssConstants.Hidden) && box.ActualBorderBottomWidth > 0)
+ {
+ DrawBorder(Border.Bottom, box, g, rect, isFirst, isLast);
+ }
+ if (isLast && !(string.IsNullOrEmpty(box.BorderRightStyle) || box.BorderRightStyle == CssConstants.None || box.BorderRightStyle == CssConstants.Hidden) && box.ActualBorderRightWidth > 0)
+ {
+ DrawBorder(Border.Right, box, g, rect, isFirst, true);
+ }
+ }
+ }
+
+ ///
+ /// Draw simple border.
+ ///
+ /// Desired border
+ /// the device to draw to
+ /// Box which the border corresponds
+ /// the brush to use
+ /// the bounding rectangle to draw in
+ /// Beveled border path, null if there is no rounded corners
+ public static void DrawBorder(Border border, RGraphics g, CssBox box, RBrush brush, RRect rectangle)
+ {
+ SetInOutsetRectanglePoints(border, box, rectangle, true, true);
+ g.DrawPolygon(brush, _borderPts);
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Draw specific border (top/bottom/left/right) with the box data (style/width/rounded).
+ ///
+ /// desired border to draw
+ /// the box to draw its borders, contain the borders data
+ /// the device to draw into
+ /// the rectangle the border is enclosing
+ /// Specifies if the border is for a starting line (no bevel on left)
+ /// Specifies if the border is for an ending line (no bevel on right)
+ private static void DrawBorder(Border border, CssBox box, RGraphics g, RRect rect, bool isLineStart, bool isLineEnd)
+ {
+ var style = GetStyle(border, box);
+ var color = GetColor(border, box, style);
+
+ var borderPath = GetRoundedBorderPath(g, border, box, rect);
+ if (borderPath != null)
+ {
+ // rounded border need special path
+ Object prevMode = null;
+ if (box.HtmlContainer != null && !box.HtmlContainer.AvoidGeometryAntialias && box.IsRounded)
+ prevMode = g.SetAntiAliasSmoothingMode();
+
+ var pen = GetPen(g, style, color, GetWidth(border, box));
+ using (borderPath)
+ g.DrawPath(pen, borderPath);
+
+ g.ReturnPreviousSmoothingMode(prevMode);
+ }
+ else
+ {
+ // non rounded border
+ if (style == CssConstants.Inset || style == CssConstants.Outset)
+ {
+ // inset/outset border needs special rectangle
+ SetInOutsetRectanglePoints(border, box, rect, isLineStart, isLineEnd);
+ g.DrawPolygon(g.GetSolidBrush(color), _borderPts);
+ }
+ else
+ {
+ // solid/dotted/dashed border draw as simple line
+ var pen = GetPen(g, style, color, GetWidth(border, box));
+ switch (border)
+ {
+ case Border.Top:
+ g.DrawLine(pen, Math.Ceiling(rect.Left), rect.Top + box.ActualBorderTopWidth / 2, rect.Right - 1, rect.Top + box.ActualBorderTopWidth / 2);
+ break;
+ case Border.Left:
+ g.DrawLine(pen, rect.Left + box.ActualBorderLeftWidth / 2, Math.Ceiling(rect.Top), rect.Left + box.ActualBorderLeftWidth / 2, Math.Floor(rect.Bottom));
+ break;
+ case Border.Bottom:
+ g.DrawLine(pen, Math.Ceiling(rect.Left), rect.Bottom - box.ActualBorderBottomWidth / 2, rect.Right - 1, rect.Bottom - box.ActualBorderBottomWidth / 2);
+ break;
+ case Border.Right:
+ g.DrawLine(pen, rect.Right - box.ActualBorderRightWidth / 2, Math.Ceiling(rect.Top), rect.Right - box.ActualBorderRightWidth / 2, Math.Floor(rect.Bottom));
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Set rectangle for inset/outset border as it need diagonal connection to other borders.
+ ///
+ /// Desired border
+ /// Box which the border corresponds
+ /// the rectangle the border is enclosing
+ /// Specifies if the border is for a starting line (no bevel on left)
+ /// Specifies if the border is for an ending line (no bevel on right)
+ /// Beveled border path, null if there is no rounded corners
+ private static void SetInOutsetRectanglePoints(Border border, CssBox b, RRect r, bool isLineStart, bool isLineEnd)
+ {
+ switch (border)
+ {
+ case Border.Top:
+ _borderPts[0] = new RPoint(r.Left, r.Top);
+ _borderPts[1] = new RPoint(r.Right, r.Top);
+ _borderPts[2] = new RPoint(r.Right, r.Top + b.ActualBorderTopWidth);
+ _borderPts[3] = new RPoint(r.Left, r.Top + b.ActualBorderTopWidth);
+ if (isLineEnd)
+ _borderPts[2].X -= b.ActualBorderRightWidth;
+ if (isLineStart)
+ _borderPts[3].X += b.ActualBorderLeftWidth;
+ break;
+ case Border.Right:
+ _borderPts[0] = new RPoint(r.Right - b.ActualBorderRightWidth, r.Top + b.ActualBorderTopWidth);
+ _borderPts[1] = new RPoint(r.Right, r.Top);
+ _borderPts[2] = new RPoint(r.Right, r.Bottom);
+ _borderPts[3] = new RPoint(r.Right - b.ActualBorderRightWidth, r.Bottom - b.ActualBorderBottomWidth);
+ break;
+ case Border.Bottom:
+ _borderPts[0] = new RPoint(r.Left, r.Bottom - b.ActualBorderBottomWidth);
+ _borderPts[1] = new RPoint(r.Right, r.Bottom - b.ActualBorderBottomWidth);
+ _borderPts[2] = new RPoint(r.Right, r.Bottom);
+ _borderPts[3] = new RPoint(r.Left, r.Bottom);
+ if (isLineStart)
+ _borderPts[0].X += b.ActualBorderLeftWidth;
+ if (isLineEnd)
+ _borderPts[1].X -= b.ActualBorderRightWidth;
+ break;
+ case Border.Left:
+ _borderPts[0] = new RPoint(r.Left, r.Top);
+ _borderPts[1] = new RPoint(r.Left + b.ActualBorderLeftWidth, r.Top + b.ActualBorderTopWidth);
+ _borderPts[2] = new RPoint(r.Left + b.ActualBorderLeftWidth, r.Bottom - b.ActualBorderBottomWidth);
+ _borderPts[3] = new RPoint(r.Left, r.Bottom);
+ break;
+ }
+ }
+
+ ///
+ /// Makes a border path for rounded borders.
+ /// To support rounded dotted/dashed borders we need to use arc in the border path.
+ /// Return null if the border is not rounded.
+ ///
+ /// the device to draw into
+ /// Desired border
+ /// Box which the border corresponds
+ /// the rectangle the border is enclosing
+ /// Beveled border path, null if there is no rounded corners
+ private static RGraphicsPath GetRoundedBorderPath(RGraphics g, Border border, CssBox b, RRect r)
+ {
+ RGraphicsPath path = null;
+ switch (border)
+ {
+ case Border.Top:
+ if (b.ActualCornerNw > 0 || b.ActualCornerNe > 0)
+ {
+ path = g.GetGraphicsPath();
+ path.Start(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNw);
+
+ if (b.ActualCornerNw > 0)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerNw, r.Top + b.ActualBorderTopWidth / 2, b.ActualCornerNw, RGraphicsPath.Corner.TopLeft);
+
+ path.LineTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerNe, r.Top + b.ActualBorderTopWidth / 2);
+
+ if (b.ActualCornerNe > 0)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNe, b.ActualCornerNe, RGraphicsPath.Corner.TopRight);
+ }
+ break;
+ case Border.Bottom:
+ if (b.ActualCornerSw > 0 || b.ActualCornerSe > 0)
+ {
+ path = g.GetGraphicsPath();
+ path.Start(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSe);
+
+ if (b.ActualCornerSe > 0)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerSe, r.Bottom - b.ActualBorderBottomWidth / 2, b.ActualCornerSe, RGraphicsPath.Corner.BottomRight);
+
+ path.LineTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerSw, r.Bottom - b.ActualBorderBottomWidth / 2);
+
+ if (b.ActualCornerSw > 0)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSw, b.ActualCornerSw, RGraphicsPath.Corner.BottomLeft);
+ }
+ break;
+ case Border.Right:
+ if (b.ActualCornerNe > 0 || b.ActualCornerSe > 0)
+ {
+ path = g.GetGraphicsPath();
+
+ bool noTop = b.BorderTopStyle == CssConstants.None || b.BorderTopStyle == CssConstants.Hidden;
+ bool noBottom = b.BorderBottomStyle == CssConstants.None || b.BorderBottomStyle == CssConstants.Hidden;
+ path.Start(r.Right - b.ActualBorderRightWidth / 2 - (noTop ? b.ActualCornerNe : 0), r.Top + b.ActualBorderTopWidth / 2 + (noTop ? 0 : b.ActualCornerNe));
+
+ if (b.ActualCornerNe > 0 && noTop)
+ path.ArcTo(r.Right - b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNe, b.ActualCornerNe, RGraphicsPath.Corner.TopRight);
+
+ path.LineTo(r.Right - b.ActualBorderRightWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSe);
+
+ if (b.ActualCornerSe > 0 && noBottom)
+ path.ArcTo(r.Right - b.ActualBorderRightWidth / 2 - b.ActualCornerSe, r.Bottom - b.ActualBorderBottomWidth / 2, b.ActualCornerSe, RGraphicsPath.Corner.BottomRight);
+ }
+ break;
+ case Border.Left:
+ if (b.ActualCornerNw > 0 || b.ActualCornerSw > 0)
+ {
+ path = g.GetGraphicsPath();
+
+ bool noTop = b.BorderTopStyle == CssConstants.None || b.BorderTopStyle == CssConstants.Hidden;
+ bool noBottom = b.BorderBottomStyle == CssConstants.None || b.BorderBottomStyle == CssConstants.Hidden;
+ path.Start(r.Left + b.ActualBorderLeftWidth / 2 + (noBottom ? b.ActualCornerSw : 0), r.Bottom - b.ActualBorderBottomWidth / 2 - (noBottom ? 0 : b.ActualCornerSw));
+
+ if (b.ActualCornerSw > 0 && noBottom)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2, r.Bottom - b.ActualBorderBottomWidth / 2 - b.ActualCornerSw, b.ActualCornerSw, RGraphicsPath.Corner.BottomLeft);
+
+ path.LineTo(r.Left + b.ActualBorderLeftWidth / 2, r.Top + b.ActualBorderTopWidth / 2 + b.ActualCornerNw);
+
+ if (b.ActualCornerNw > 0 && noTop)
+ path.ArcTo(r.Left + b.ActualBorderLeftWidth / 2 + b.ActualCornerNw, r.Top + b.ActualBorderTopWidth / 2, b.ActualCornerNw, RGraphicsPath.Corner.TopLeft);
+ }
+ break;
+ }
+
+ return path;
+ }
+
+ ///
+ /// Get pen to be used for border draw respecting its style.
+ ///
+ private static RPen GetPen(RGraphics g, string style, RColor color, double width)
+ {
+ var p = g.GetPen(color);
+ p.Width = width;
+ switch (style)
+ {
+ case "solid":
+ p.DashStyle = RDashStyle.Solid;
+ break;
+ case "dotted":
+ p.DashStyle = RDashStyle.Dot;
+ break;
+ case "dashed":
+ p.DashStyle = RDashStyle.Dash;
+ break;
+ }
+ return p;
+ }
+
+ ///
+ /// Get the border color for the given box border.
+ ///
+ private static RColor GetColor(Border border, CssBoxProperties box, string style)
+ {
+ switch (border)
+ {
+ case Border.Top:
+ return style == CssConstants.Inset ? Darken(box.ActualBorderTopColor) : box.ActualBorderTopColor;
+ case Border.Right:
+ return style == CssConstants.Outset ? Darken(box.ActualBorderRightColor) : box.ActualBorderRightColor;
+ case Border.Bottom:
+ return style == CssConstants.Outset ? Darken(box.ActualBorderBottomColor) : box.ActualBorderBottomColor;
+ case Border.Left:
+ return style == CssConstants.Inset ? Darken(box.ActualBorderLeftColor) : box.ActualBorderLeftColor;
+ default:
+ throw new ArgumentOutOfRangeException("border");
+ }
+ }
+
+ ///
+ /// Get the border width for the given box border.
+ ///
+ private static double GetWidth(Border border, CssBoxProperties box)
+ {
+ switch (border)
+ {
+ case Border.Top:
+ return box.ActualBorderTopWidth;
+ case Border.Right:
+ return box.ActualBorderRightWidth;
+ case Border.Bottom:
+ return box.ActualBorderBottomWidth;
+ case Border.Left:
+ return box.ActualBorderLeftWidth;
+ default:
+ throw new ArgumentOutOfRangeException("border");
+ }
+ }
+
+ ///
+ /// Get the border style for the given box border.
+ ///
+ private static string GetStyle(Border border, CssBoxProperties box)
+ {
+ switch (border)
+ {
+ case Border.Top:
+ return box.BorderTopStyle;
+ case Border.Right:
+ return box.BorderRightStyle;
+ case Border.Bottom:
+ return box.BorderBottomStyle;
+ case Border.Left:
+ return box.BorderLeftStyle;
+ default:
+ throw new ArgumentOutOfRangeException("border");
+ }
+ }
+
+ ///
+ /// Makes the specified color darker for inset/outset borders.
+ ///
+ private static RColor Darken(RColor c)
+ {
+ return RColor.FromArgb(c.R / 2, c.G / 2, c.B / 2);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/ContextMenuHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/ContextMenuHandler.cs
new file mode 100644
index 000000000..051884ed6
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/ContextMenuHandler.cs
@@ -0,0 +1,509 @@
+// "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.Globalization;
+using System.IO;
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Core.Dom;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// Handle context menu.
+ ///
+ internal sealed class ContextMenuHandler : IDisposable
+ {
+ #region Fields and Consts
+
+ ///
+ /// select all text
+ ///
+ private static readonly string _selectAll;
+
+ ///
+ /// copy selected text
+ ///
+ private static readonly string _copy;
+
+ ///
+ /// copy the link source
+ ///
+ private static readonly string _copyLink;
+
+ ///
+ /// open link (as left mouse click)
+ ///
+ private static readonly string _openLink;
+
+ ///
+ /// copy the source of the image
+ ///
+ private static readonly string _copyImageLink;
+
+ ///
+ /// copy image to clipboard
+ ///
+ private static readonly string _copyImage;
+
+ ///
+ /// save image to disk
+ ///
+ private static readonly string _saveImage;
+
+ ///
+ /// open video in browser
+ ///
+ private static readonly string _openVideo;
+
+ ///
+ /// copy video url to browser
+ ///
+ private static readonly string _copyVideoUrl;
+
+ ///
+ /// the selection handler linked to the context menu handler
+ ///
+ private readonly SelectionHandler _selectionHandler;
+
+ ///
+ /// the html container the handler is on
+ ///
+ private readonly HtmlContainerInt _htmlContainer;
+
+ ///
+ /// the last context menu shown
+ ///
+ private RContextMenu _contextMenu;
+
+ ///
+ /// the control that the context menu was shown on
+ ///
+ private RControl _parentControl;
+
+ ///
+ /// the css rectangle that context menu shown on
+ ///
+ private CssRect _currentRect;
+
+ ///
+ /// the css link box that context menu shown on
+ ///
+ private CssBox _currentLink;
+
+ #endregion
+
+
+ ///
+ /// Init context menu items strings.
+ ///
+ static ContextMenuHandler()
+ {
+ if (CultureInfo.CurrentUICulture.Name.StartsWith("fr", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Tout sélectionner";
+ _copy = "Copier";
+ _copyLink = "Copier l'adresse du lien";
+ _openLink = "Ouvrir le lien";
+ _copyImageLink = "Copier l'URL de l'image";
+ _copyImage = "Copier l'image";
+ _saveImage = "Enregistrer l'image sous...";
+ _openVideo = "Ouvrir la vidéo";
+ _copyVideoUrl = "Copier l'URL de l'vidéo";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("de", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Alle auswählen";
+ _copy = "Kopieren";
+ _copyLink = "Link-Adresse kopieren";
+ _openLink = "Link öffnen";
+ _copyImageLink = "Bild-URL kopieren";
+ _copyImage = "Bild kopieren";
+ _saveImage = "Bild speichern unter...";
+ _openVideo = "Video öffnen";
+ _copyVideoUrl = "Video-URL kopieren";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("it", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Seleziona tutto";
+ _copy = "Copia";
+ _copyLink = "Copia indirizzo del link";
+ _openLink = "Apri link";
+ _copyImageLink = "Copia URL immagine";
+ _copyImage = "Copia immagine";
+ _saveImage = "Salva immagine con nome...";
+ _openVideo = "Apri il video";
+ _copyVideoUrl = "Copia URL video";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("es", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Seleccionar todo";
+ _copy = "Copiar";
+ _copyLink = "Copiar dirección de enlace";
+ _openLink = "Abrir enlace";
+ _copyImageLink = "Copiar URL de la imagen";
+ _copyImage = "Copiar imagen";
+ _saveImage = "Guardar imagen como...";
+ _openVideo = "Abrir video";
+ _copyVideoUrl = "Copiar URL de la video";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("ru", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Выбрать все";
+ _copy = "Копировать";
+ _copyLink = "Копировать адрес ссылки";
+ _openLink = "Перейти по ссылке";
+ _copyImageLink = "Копировать адрес изображения";
+ _copyImage = "Копировать изображение";
+ _saveImage = "Сохранить изображение как...";
+ _openVideo = "Открыть видео";
+ _copyVideoUrl = "Копировать адрес видео";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("sv", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Välj allt";
+ _copy = "Kopiera";
+ _copyLink = "Kopiera länkadress";
+ _openLink = "Öppna länk";
+ _copyImageLink = "Kopiera bildens URL";
+ _copyImage = "Kopiera bild";
+ _saveImage = "Spara bild som...";
+ _openVideo = "Öppna video";
+ _copyVideoUrl = "Kopiera video URL";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("hu", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Összes kiválasztása";
+ _copy = "Másolás";
+ _copyLink = "Hivatkozás címének másolása";
+ _openLink = "Hivatkozás megnyitása";
+ _copyImageLink = "Kép URL másolása";
+ _copyImage = "Kép másolása";
+ _saveImage = "Kép mentése másként...";
+ _openVideo = "Videó megnyitása";
+ _copyVideoUrl = "Videó URL másolása";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("cs", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Vybrat vše";
+ _copy = "Kopírovat";
+ _copyLink = "Kopírovat adresu odkazu";
+ _openLink = "Otevřít odkaz";
+ _copyImageLink = "Kopírovat URL snímku";
+ _copyImage = "Kopírovat snímek";
+ _saveImage = "Uložit snímek jako...";
+ _openVideo = "Otevřít video";
+ _copyVideoUrl = "Kopírovat URL video";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("da", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Vælg alt";
+ _copy = "Kopiér";
+ _copyLink = "Kopier link-adresse";
+ _openLink = "Åbn link";
+ _copyImageLink = "Kopier billede-URL";
+ _copyImage = "Kopier billede";
+ _saveImage = "Gem billede som...";
+ _openVideo = "Åbn video";
+ _copyVideoUrl = "Kopier video-URL";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("nl", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Alles selecteren";
+ _copy = "Kopiëren";
+ _copyLink = "Link adres kopiëren";
+ _openLink = "Link openen";
+ _copyImageLink = "URL Afbeelding kopiëren";
+ _copyImage = "Afbeelding kopiëren";
+ _saveImage = "Bewaar afbeelding als...";
+ _openVideo = "Video openen";
+ _copyVideoUrl = "URL video kopiëren";
+ }
+ else if (CultureInfo.CurrentUICulture.Name.StartsWith("fi", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _selectAll = "Valitse kaikki";
+ _copy = "Kopioi";
+ _copyLink = "Kopioi linkin osoite";
+ _openLink = "Avaa linkki";
+ _copyImageLink = "Kopioi kuvan URL";
+ _copyImage = "Kopioi kuva";
+ _saveImage = "Tallena kuva nimellä...";
+ _openVideo = "Avaa video";
+ _copyVideoUrl = "Kopioi video URL";
+ }
+ else
+ {
+ _selectAll = "Select all";
+ _copy = "Copy";
+ _copyLink = "Copy link address";
+ _openLink = "Open link";
+ _copyImageLink = "Copy image URL";
+ _copyImage = "Copy image";
+ _saveImage = "Save image as...";
+ _openVideo = "Open video";
+ _copyVideoUrl = "Copy video URL";
+ }
+ }
+
+ ///
+ /// Init.
+ ///
+ /// the selection handler linked to the context menu handler
+ /// the html container the handler is on
+ public ContextMenuHandler(SelectionHandler selectionHandler, HtmlContainerInt htmlContainer)
+ {
+ ArgChecker.AssertArgNotNull(selectionHandler, "selectionHandler");
+ ArgChecker.AssertArgNotNull(htmlContainer, "htmlContainer");
+
+ _selectionHandler = selectionHandler;
+ _htmlContainer = htmlContainer;
+ }
+
+ ///
+ /// Show context menu clicked on given rectangle.
+ ///
+ /// the parent control to show the context menu on
+ /// the rectangle that was clicked to show context menu
+ /// the link that was clicked to show context menu on
+ public void ShowContextMenu(RControl parent, CssRect rect, CssBox link)
+ {
+ try
+ {
+ DisposeContextMenu();
+
+ _parentControl = parent;
+ _currentRect = rect;
+ _currentLink = link;
+ _contextMenu = _htmlContainer.Adapter.GetContextMenu();
+
+ if (rect != null)
+ {
+ bool isVideo = false;
+ if (link != null)
+ {
+ isVideo = link is CssBoxFrame && ((CssBoxFrame)link).IsVideo;
+ var linkExist = !string.IsNullOrEmpty(link.HrefLink);
+ _contextMenu.AddItem(isVideo ? _openVideo : _openLink, linkExist, OnOpenLinkClick);
+ if (_htmlContainer.IsSelectionEnabled)
+ {
+ _contextMenu.AddItem(isVideo ? _copyVideoUrl : _copyLink, linkExist, OnCopyLinkClick);
+ }
+ _contextMenu.AddDivider();
+ }
+
+ if (rect.IsImage && !isVideo)
+ {
+ _contextMenu.AddItem(_saveImage, rect.Image != null, OnSaveImageClick);
+ if (_htmlContainer.IsSelectionEnabled)
+ {
+ _contextMenu.AddItem(_copyImageLink, !string.IsNullOrEmpty(_currentRect.OwnerBox.GetAttribute("src")), OnCopyImageLinkClick);
+ _contextMenu.AddItem(_copyImage, rect.Image != null, OnCopyImageClick);
+ }
+ _contextMenu.AddDivider();
+ }
+
+ if (_htmlContainer.IsSelectionEnabled)
+ {
+ _contextMenu.AddItem(_copy, rect.Selected, OnCopyClick);
+ }
+ }
+
+ if (_htmlContainer.IsSelectionEnabled)
+ {
+ _contextMenu.AddItem(_selectAll, true, OnSelectAllClick);
+ }
+
+ if (_contextMenu.ItemsCount > 0)
+ {
+ _contextMenu.RemoveLastDivider();
+ _contextMenu.Show(parent, parent.MouseLocation);
+ }
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to show context menu", ex);
+ }
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// 2
+ public void Dispose()
+ {
+ DisposeContextMenu();
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Dispose of the last used context menu.
+ ///
+ private void DisposeContextMenu()
+ {
+ try
+ {
+ if (_contextMenu != null)
+ _contextMenu.Dispose();
+ _contextMenu = null;
+ _parentControl = null;
+ _currentRect = null;
+ _currentLink = null;
+ }
+ catch
+ { }
+ }
+
+ ///
+ /// Handle link click.
+ ///
+ private void OnOpenLinkClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _currentLink.HtmlContainer.HandleLinkClicked(_parentControl, _parentControl.MouseLocation, _currentLink);
+ }
+ catch (HtmlLinkClickedException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to open link", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Copy the href of a link to clipboard.
+ ///
+ private void OnCopyLinkClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _htmlContainer.Adapter.SetToClipboard(_currentLink.HrefLink);
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to copy link url to clipboard", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Open save as dialog to save the image
+ ///
+ private void OnSaveImageClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ var imageSrc = _currentRect.OwnerBox.GetAttribute("src");
+ _htmlContainer.Adapter.SaveToFile(_currentRect.Image, Path.GetFileName(imageSrc) ?? "image", Path.GetExtension(imageSrc) ?? "png");
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to save image", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Copy the image source to clipboard.
+ ///
+ private void OnCopyImageLinkClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _htmlContainer.Adapter.SetToClipboard(_currentRect.OwnerBox.GetAttribute("src"));
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to copy image url to clipboard", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Copy image object to clipboard.
+ ///
+ private void OnCopyImageClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _htmlContainer.Adapter.SetToClipboard(_currentRect.Image);
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to copy image to clipboard", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Copy selected text.
+ ///
+ private void OnCopyClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _selectionHandler.CopySelectedHtml();
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to copy text to clipboard", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ ///
+ /// Select all text.
+ ///
+ private void OnSelectAllClick(object sender, EventArgs eventArgs)
+ {
+ try
+ {
+ _selectionHandler.SelectAll(_parentControl);
+ }
+ catch (Exception ex)
+ {
+ _htmlContainer.ReportError(HtmlRenderErrorType.ContextMenu, "Failed to select all text", ex);
+ }
+ finally
+ {
+ DisposeContextMenu();
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/FontsHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/FontsHandler.cs
new file mode 100644
index 000000000..83c037eac
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/FontsHandler.cs
@@ -0,0 +1,196 @@
+// "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.Handlers
+{
+ ///
+ /// Utilities for fonts and fonts families handling.
+ ///
+ internal sealed class FontsHandler
+ {
+ #region Fields and Consts
+
+ ///
+ ///
+ ///
+ private readonly RAdapter _adapter;
+
+ ///
+ /// Allow to map not installed fonts to different
+ ///
+ private readonly Dictionary _fontsMapping = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// collection of all installed and added font families to check if font exists
+ ///
+ private readonly Dictionary _existingFontFamilies = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// cache of all the font used not to create same font again and again
+ ///
+ private readonly Dictionary>> _fontsCache = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase);
+
+ #endregion
+
+
+ ///
+ /// Init.
+ ///
+ public FontsHandler(RAdapter adapter)
+ {
+ ArgChecker.AssertArgNotNull(adapter, "global");
+
+ _adapter = adapter;
+ }
+
+ ///
+ /// Check if the given font family exists by name
+ ///
+ /// the font to check
+ /// true - font exists by given family name, false - otherwise
+ public bool IsFontExists(string family)
+ {
+ bool exists = _existingFontFamilies.ContainsKey(family);
+ if (!exists)
+ {
+ string mappedFamily;
+ if (_fontsMapping.TryGetValue(family, out mappedFamily))
+ {
+ exists = _existingFontFamilies.ContainsKey(mappedFamily);
+ }
+ }
+ return exists;
+ }
+
+ ///
+ /// Adds a font family to be used.
+ ///
+ /// The font family to add.
+ public void AddFontFamily(RFontFamily fontFamily)
+ {
+ ArgChecker.AssertArgNotNull(fontFamily, "family");
+
+ _existingFontFamilies[fontFamily.Name] = fontFamily;
+ }
+
+ ///
+ /// Adds a font mapping from to iff the is not found.
+ /// When the font is used in rendered html and is not found in existing
+ /// fonts (installed or added) it will be replaced by .
+ ///
+ /// the font family to replace
+ /// the font family to replace with
+ public void AddFontFamilyMapping(string fromFamily, string toFamily)
+ {
+ ArgChecker.AssertArgNotNullOrEmpty(fromFamily, "fromFamily");
+ ArgChecker.AssertArgNotNullOrEmpty(toFamily, "toFamily");
+
+ _fontsMapping[fromFamily] = toFamily;
+ }
+
+ ///
+ /// Get cached font instance for the given font properties.
+ /// Improve performance not to create same font multiple times.
+ ///
+ /// cached font instance
+ public RFont GetCachedFont(string family, double size, RFontStyle style)
+ {
+ var font = TryGetFont(family, size, style);
+ if (font == null)
+ {
+ if (!_existingFontFamilies.ContainsKey(family))
+ {
+ string mappedFamily;
+ if (_fontsMapping.TryGetValue(family, out mappedFamily))
+ {
+ font = TryGetFont(mappedFamily, size, style);
+ if (font == null)
+ {
+ font = CreateFont(mappedFamily, size, style);
+ _fontsCache[mappedFamily][size][style] = font;
+ }
+ }
+ }
+
+ if (font == null)
+ {
+ font = CreateFont(family, size, style);
+ }
+
+ _fontsCache[family][size][style] = font;
+ }
+ return font;
+ }
+
+
+ #region Private methods
+
+ ///
+ /// Get cached font if it exists in cache or null if it is not.
+ ///
+ private RFont TryGetFont(string family, double size, RFontStyle style)
+ {
+ RFont font = null;
+ if (_fontsCache.ContainsKey(family))
+ {
+ var a = _fontsCache[family];
+ if (a.ContainsKey(size))
+ {
+ var b = a[size];
+ if (b.ContainsKey(style))
+ {
+ font = b[style];
+ }
+ }
+ else
+ {
+ _fontsCache[family][size] = new Dictionary();
+ }
+ }
+ else
+ {
+ _fontsCache[family] = new Dictionary>();
+ _fontsCache[family][size] = new Dictionary();
+ }
+ return font;
+ }
+
+ ///
+ // create font (try using existing font family to support custom fonts)
+ ///
+ private RFont CreateFont(string family, double size, RFontStyle style)
+ {
+ RFontFamily fontFamily;
+ try
+ {
+ return _existingFontFamilies.TryGetValue(family, out fontFamily)
+ ? _adapter.CreateFont(fontFamily, size, style)
+ : _adapter.CreateFont(family, size, style);
+ }
+ catch
+ {
+ // handle possibility of no requested style exists for the font, use regular then
+ return _existingFontFamilies.TryGetValue(family, out fontFamily)
+ ? _adapter.CreateFont(fontFamily, size, RFontStyle.Regular)
+ : _adapter.CreateFont(family, size, RFontStyle.Regular);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/ImageDownloader.cs b/Source/HtmlRenderer.Core/Core/Handlers/ImageDownloader.cs
new file mode 100644
index 000000000..2f8b0f5e6
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/ImageDownloader.cs
@@ -0,0 +1,264 @@
+// "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.ComponentModel;
+using System.IO;
+using System.Net;
+using System.Threading;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// On download file async complete, success or fail.
+ ///
+ /// The online image uri
+ /// the path to the downloaded file
+ /// the error if download failed
+ /// is the file download request was canceled
+ public delegate void DownloadFileAsyncCallback(Uri imageUri, string filePath, Exception error, bool canceled);
+
+ ///
+ /// Handler for downloading images from the web.
+ /// Single instance of the handler used for all images downloaded in a single html, this way if the html contains more
+ /// than one reference to the same image it will be downloaded only once.
+ /// Also handles corrupt, partial and canceled downloads by first downloading to temp file and only if successful moving to cached
+ /// file location.
+ ///
+ internal sealed class ImageDownloader : IDisposable
+ {
+ ///
+ /// the web client used to download image from URL (to cancel on dispose)
+ ///
+ private readonly List _clients = new List();
+
+ ///
+ /// dictionary of image cache path to callbacks of download to handle multiple requests to download the same image
+ ///
+ private readonly Dictionary> _imageDownloadCallbacks = new Dictionary>();
+
+ public ImageDownloader()
+ {
+ ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
+ }
+
+ ///
+ /// Makes a request to download the image from the server and raises the when it's down.
+ ///
+ /// The online image uri
+ /// the path on disk to download the file to
+ /// is to download the file sync or async (true-async)
+ /// This callback will be called with local file path. If something went wrong in the download it will return null.
+ public void DownloadImage(Uri imageUri, string filePath, bool async, DownloadFileAsyncCallback cachedFileCallback)
+ {
+ ArgChecker.AssertArgNotNull(imageUri, "imageUri");
+ ArgChecker.AssertArgNotNull(cachedFileCallback, "cachedFileCallback");
+
+ // to handle if the file is already been downloaded
+ bool download = true;
+ lock (_imageDownloadCallbacks)
+ {
+ if (_imageDownloadCallbacks.ContainsKey(filePath))
+ {
+ download = false;
+ _imageDownloadCallbacks[filePath].Add(cachedFileCallback);
+ }
+ else
+ {
+ _imageDownloadCallbacks[filePath] = new List { cachedFileCallback };
+ }
+ }
+
+ if (download)
+ {
+ var tempPath = Path.GetTempFileName();
+ if (async)
+ ThreadPool.QueueUserWorkItem(DownloadImageFromUrlAsync, new DownloadData(imageUri, tempPath, filePath));
+ else
+ DownloadImageFromUrl(imageUri, tempPath, filePath);
+ }
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ ReleaseObjects();
+ }
+
+
+ #region Private/Protected methods
+
+ ///
+ /// Download the requested file in the URI to the given file path.
+ /// Use async sockets API to download from web, .
+ ///
+ private void DownloadImageFromUrl(Uri source, string tempPath, string filePath)
+ {
+ try
+ {
+ using (var client = new WebClient())
+ {
+ _clients.Add(client);
+ client.DownloadFile(source, tempPath);
+ OnDownloadImageCompleted(client, source, tempPath, filePath, null, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ OnDownloadImageCompleted(null, source, tempPath, filePath, ex, false);
+ }
+ }
+
+ ///
+ /// Download the requested file in the URI to the given file path.
+ /// Use async sockets API to download from web, .
+ ///
+ /// key value pair of URL and file info to download the file to
+ private void DownloadImageFromUrlAsync(object data)
+ {
+ var downloadData = (DownloadData)data;
+ try
+ {
+ var client = new WebClient();
+ _clients.Add(client);
+ client.DownloadFileCompleted += OnDownloadImageAsyncCompleted;
+ client.DownloadFileAsync(downloadData._uri, downloadData._tempPath, downloadData);
+ }
+ catch (Exception ex)
+ {
+ OnDownloadImageCompleted(null, downloadData._uri, downloadData._tempPath, downloadData._filePath, ex, false);
+ }
+ }
+
+ ///
+ /// On download image complete to local file.
+ /// If the download canceled do nothing, if failed report error.
+ ///
+ private void OnDownloadImageAsyncCompleted(object sender, AsyncCompletedEventArgs e)
+ {
+ var downloadData = (DownloadData)e.UserState;
+ try
+ {
+ using (var client = (WebClient)sender)
+ {
+ client.DownloadFileCompleted -= OnDownloadImageAsyncCompleted;
+ OnDownloadImageCompleted(client, downloadData._uri, downloadData._tempPath, downloadData._filePath, e.Error, e.Cancelled);
+ }
+ }
+ catch (Exception ex)
+ {
+ OnDownloadImageCompleted(null, downloadData._uri, downloadData._tempPath, downloadData._filePath, ex, false);
+ }
+ }
+
+ ///
+ /// Checks if the file was downloaded and raises the cachedFileCallback from
+ ///
+ private void OnDownloadImageCompleted(WebClient client, Uri source, string tempPath, string filePath, Exception error, bool cancelled)
+ {
+ if (!cancelled)
+ {
+ if (error == null)
+ {
+ var contentType = CommonUtils.GetResponseContentType(client);
+ if (contentType == null || !contentType.StartsWith("image", StringComparison.OrdinalIgnoreCase))
+ {
+ error = new Exception("Failed to load image, not image content type: " + contentType);
+ }
+
+ }
+
+ if (error == null)
+ {
+ if (File.Exists(tempPath))
+ {
+ try
+ {
+ File.Move(tempPath, filePath);
+ }
+ catch (Exception ex)
+ {
+ error = new Exception("Failed to move downloaded image from temp to cache location", ex);
+ }
+ }
+
+ error = File.Exists(filePath) ? null : (error ?? new Exception("Failed to download image, unknown error"));
+ }
+ }
+
+ List callbacksList;
+ lock (_imageDownloadCallbacks)
+ {
+ if (_imageDownloadCallbacks.TryGetValue(filePath, out callbacksList))
+ _imageDownloadCallbacks.Remove(filePath);
+ }
+
+ if (callbacksList != null)
+ {
+ foreach (var cachedFileCallback in callbacksList)
+ {
+ try
+ {
+ cachedFileCallback(source, filePath, error, cancelled);
+ }
+ catch
+ { }
+ }
+ }
+ }
+
+ ///
+ /// Release the image and client objects.
+ ///
+ private void ReleaseObjects()
+ {
+ _imageDownloadCallbacks.Clear();
+ while (_clients.Count > 0)
+ {
+ try
+ {
+ var client = _clients[0];
+ client.CancelAsync();
+ client.Dispose();
+ _clients.RemoveAt(0);
+ }
+ catch
+ { }
+ }
+ }
+
+ #endregion
+
+
+ #region Inner class: DownloadData
+
+ private sealed class DownloadData
+ {
+ public readonly Uri _uri;
+ public readonly string _tempPath;
+ public readonly string _filePath;
+
+ public DownloadData(Uri uri, string tempPath, string filePath)
+ {
+ _uri = uri;
+ _tempPath = tempPath;
+ _filePath = filePath;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/HtmlRenderer.Core/Core/Handlers/ImageLoadHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/ImageLoadHandler.cs
new file mode 100644
index 000000000..d91c63646
--- /dev/null
+++ b/Source/HtmlRenderer.Core/Core/Handlers/ImageLoadHandler.cs
@@ -0,0 +1,396 @@
+// "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.IO;
+using System.Text;
+using System.Threading;
+using TheArtOfDev.HtmlRenderer.Adapters;
+using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Utils;
+
+namespace TheArtOfDev.HtmlRenderer.Core.Handlers
+{
+ ///
+ /// Handler for all loading image logic.
+ ///