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 [![Build status](https://ci.appveyor.com/api/projects/status/cm8xpf8ebt3hyi3e)](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. - -![Renderer.png](https://raw.githubusercontent.com/ArthurHub/HTML-Renderer/master/Art/demo_winforms.png) - -### 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.
+ ///

+ /// Loading by .
+ /// Loading by file path.
+ /// Loading by URI.
+ ///

+ ///
+ /// + /// + /// Supports sync and async image loading. + /// + /// + /// If the image object is created by the handler on calling dispose of the handler the image will be released, this + /// makes release of unused images faster as they can be large.
+ /// Disposing image load handler will also cancel download of image from the web. + ///
+ ///
+ internal sealed class ImageLoadHandler : IDisposable + { + #region Fields and Consts + + /// + /// the container of the html to handle load image for + /// + private readonly HtmlContainerInt _htmlContainer; + + /// + /// callback raised when image load process is complete with image or without + /// + private readonly ActionInt _loadCompleteCallback; + + /// + /// Must be open as long as the image is in use + /// + private FileStream _imageFileStream; + + /// + /// the image instance of the loaded image + /// + private RImage _image; + + /// + /// the image rectangle restriction as returned from image load event + /// + private RRect _imageRectangle; + + /// + /// to know if image load event callback was sync or async raised + /// + private bool _asyncCallback; + + /// + /// flag to indicate if to release the image object on box dispose (only if image was loaded by the box) + /// + private bool _releaseImageObject; + + /// + /// is the handler has been disposed + /// + private bool _disposed; + + #endregion + + + /// + /// Init. + /// + /// the container of the html to handle load image for + /// callback raised when image load process is complete with image or without + public ImageLoadHandler(HtmlContainerInt htmlContainer, ActionInt loadCompleteCallback) + { + ArgChecker.AssertArgNotNull(htmlContainer, "htmlContainer"); + ArgChecker.AssertArgNotNull(loadCompleteCallback, "loadCompleteCallback"); + + _htmlContainer = htmlContainer; + _loadCompleteCallback = loadCompleteCallback; + } + + /// + /// the image instance of the loaded image + /// + public RImage Image + { + get { return _image; } + } + + /// + /// the image rectangle restriction as returned from image load event + /// + public RRect Rectangle + { + get { return _imageRectangle; } + } + + /// + /// Set image of this image box by analyzing the src attribute.
+ /// Load the image from inline base64 encoded string.
+ /// Or from calling property/method on the bridge object that returns image or URL to image.
+ /// Or from file path
+ /// Or from URI. + ///
+ /// + /// File path and URI image loading is executed async and after finishing calling + /// on the main thread and not thread-pool. + /// + /// the source of the image to load + /// the collection of attributes on the element to use in event + /// the image object (null if failed) + public void LoadImage(string src, Dictionary attributes) + { + try + { + var args = new HtmlImageLoadEventArgs(src, attributes, OnHtmlImageLoadEventCallback); + _htmlContainer.RaiseHtmlImageLoadEvent(args); + _asyncCallback = !_htmlContainer.AvoidAsyncImagesLoading; + + if (!args.Handled) + { + if (!string.IsNullOrEmpty(src)) + { + if (src.StartsWith("data:image", StringComparison.CurrentCultureIgnoreCase)) + { + SetFromInlineData(src); + } + else + { + SetImageFromPath(src); + } + } + else + { + ImageLoadComplete(false); + } + } + } + catch (Exception ex) + { + _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Exception in handling image source", ex); + ImageLoadComplete(false); + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _disposed = true; + ReleaseObjects(); + } + + + #region Private methods + + /// + /// Set the image using callback from load image event, use the given data. + /// + /// the path to the image to load (file path or uri) + /// the image to load + /// optional: limit to specific rectangle of the image and not all of it + private void OnHtmlImageLoadEventCallback(string path, object image, RRect imageRectangle) + { + if (!_disposed) + { + _imageRectangle = imageRectangle; + + if (image != null) + { + _image = _htmlContainer.Adapter.ConvertImage(image); + ImageLoadComplete(_asyncCallback); + } + else if (!string.IsNullOrEmpty(path)) + { + SetImageFromPath(path); + } + else + { + ImageLoadComplete(_asyncCallback); + } + } + } + + /// + /// Load the image from inline base64 encoded string data. + /// + /// the source that has the base64 encoded image + private void SetFromInlineData(string src) + { + _image = GetImageFromData(src); + if (_image == null) + _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed extract image from inline data"); + _releaseImageObject = true; + ImageLoadComplete(false); + } + + /// + /// Extract image object from inline base64 encoded data in the src of the html img element. + /// + /// the source that has the base64 encoded image + /// image from base64 data string or null if failed + private RImage GetImageFromData(string src) + { + var s = src.Substring(src.IndexOf(':') + 1).Split(new[] { ',' }, 2); + if (s.Length == 2) + { + int imagePartsCount = 0, base64PartsCount = 0; + foreach (var part in s[0].Split(new[] { ';' })) + { + var pPart = part.Trim(); + if (pPart.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) + imagePartsCount++; + if (pPart.Equals("base64", StringComparison.InvariantCultureIgnoreCase)) + base64PartsCount++; + } + + if (imagePartsCount > 0) + { + byte[] imageData = base64PartsCount > 0 ? Convert.FromBase64String(s[1].Trim()) : new UTF8Encoding().GetBytes(Uri.UnescapeDataString(s[1].Trim())); + return _htmlContainer.Adapter.ImageFromStream(new MemoryStream(imageData)); + } + } + return null; + } + + /// + /// Load image from path of image file or URL. + /// + /// the file path or uri to load image from + private void SetImageFromPath(string path) + { + var uri = CommonUtils.TryGetUri(path); + if (uri != null && uri.Scheme != "file") + { + SetImageFromUrl(uri); + } + else + { + Console.WriteLine($"SetImageFromPath:{path}"); + var fileInfo = CommonUtils.TryGetFileInfo(uri != null ? uri.AbsolutePath : path); + if (fileInfo != null) + { + SetImageFromFile(fileInfo); + } + else + { + _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed load image, invalid source: " + path); + ImageLoadComplete(false); + } + } + } + + /// + /// Load the image file on thread-pool thread and calling after. + /// + /// the file path to get the image from + private void SetImageFromFile(FileInfo source) + { + Console.WriteLine($"SetImageFromFile:{source.FullName}"); + if (source.Exists) + { + if (_htmlContainer.AvoidAsyncImagesLoading) + LoadImageFromFile(source.FullName); + else + ThreadPool.QueueUserWorkItem(state => LoadImageFromFile(source.FullName)); + } + else + { + ImageLoadComplete(); + } + } + + /// + /// Load the image file on thread-pool thread and calling after.
+ /// Calling on the main thread and not thread-pool. + ///
+ /// the file path to get the image from + private void LoadImageFromFile(string source) + { + try + { + var imageFileStream = File.Open(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + lock (_loadCompleteCallback) + { + _imageFileStream = imageFileStream; + if (!_disposed) + _image = _htmlContainer.Adapter.ImageFromStream(_imageFileStream); + _releaseImageObject = true; + } + ImageLoadComplete(); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from disk: " + source, ex); + ImageLoadComplete(); + } + } + + /// + /// Load image from the given URI by downloading it.
+ /// Create local file name in temp folder from the URI, if the file already exists use it as it has already been downloaded. + /// If not download the file. + ///
+ private void SetImageFromUrl(Uri source) + { + var filePath = CommonUtils.GetLocalfileName(source); + if (filePath.Exists && filePath.Length > 0) + { + SetImageFromFile(filePath); + } + else + { + _htmlContainer.GetImageDownloader().DownloadImage(source, filePath.FullName, !_htmlContainer.AvoidAsyncImagesLoading, OnDownloadImageCompleted); + } + } + + /// + /// On download image complete to local file use to load the image file.
+ /// If the download canceled do nothing, if failed report error. + ///
+ private void OnDownloadImageCompleted(Uri imageUri, string filePath, Exception error, bool canceled) + { + if (!canceled && !_disposed) + { + if (error == null) + { + LoadImageFromFile(filePath); + } + else + { + _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from URL: " + imageUri, error); + ImageLoadComplete(); + } + } + } + + /// + /// Flag image load complete and request refresh for re-layout and invalidate. + /// + private void ImageLoadComplete(bool async = true) + { + // can happen if some operation return after the handler was disposed + if (_disposed) + ReleaseObjects(); + else + _loadCompleteCallback(_image, _imageRectangle, async); + } + + /// + /// Release the image and client objects. + /// + private void ReleaseObjects() + { + lock (_loadCompleteCallback) + { + if (_releaseImageObject && _image != null) + { + _image.Dispose(); + _image = null; + } + if (_imageFileStream != null) + { + _imageFileStream.Dispose(); + _imageFileStream = null; + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Handlers/SelectionHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/SelectionHandler.cs new file mode 100644 index 000000000..9913a9c5d --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Handlers/SelectionHandler.cs @@ -0,0 +1,693 @@ +// "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.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core.Handlers +{ + /// + /// Handler for text selection in the html. + /// + internal sealed class SelectionHandler : IDisposable + { + #region Fields and Consts + + /// + /// the root of the handled html tree + /// + private readonly CssBox _root; + + /// + /// handler for showing context menu on right click + /// + private readonly ContextMenuHandler _contextMenuHandler; + + /// + /// the mouse location when selection started used to ignore small selections + /// + private RPoint _selectionStartPoint; + + /// + /// the starting word of html selection
+ /// where the user started the selection, if the selection is backwards then it will be the last selected word. + ///
+ private CssRect _selectionStart; + + /// + /// the ending word of html selection
+ /// where the user ended the selection, if the selection is backwards then it will be the first selected word. + ///
+ private CssRect _selectionEnd; + + /// + /// the selection start index if the first selected word is partially selected (-1 if not selected or fully selected) + /// + private int _selectionStartIndex = -1; + + /// + /// the selection end index if the last selected word is partially selected (-1 if not selected or fully selected) + /// + private int _selectionEndIndex = -1; + + /// + /// the selection start offset if the first selected word is partially selected (-1 if not selected or fully selected) + /// + private double _selectionStartOffset = -1; + + /// + /// the selection end offset if the last selected word is partially selected (-1 if not selected or fully selected) + /// + private double _selectionEndOffset = -1; + + /// + /// is the selection goes backward in the html, the starting word comes after the ending word in DFS traversing.
+ ///
+ private bool _backwardSelection; + + /// + /// used to ignore mouse up after selection + /// + private bool _inSelection; + + /// + /// current selection process is after double click (full word selection) + /// + private bool _isDoubleClickSelect; + + /// + /// used to know if selection is in the control or started outside so it needs to be ignored + /// + private bool _mouseDownInControl; + + /// + /// used to handle drag & drop + /// + private bool _mouseDownOnSelectedWord; + + /// + /// is the cursor on the control has been changed by the selection handler + /// + private bool _cursorChanged; + + /// + /// used to know if double click selection is requested + /// + private DateTime _lastMouseDown; + + /// + /// used to know if drag & drop was already started not to execute the same operation over + /// + private object _dragDropData; + + #endregion + + + /// + /// Init. + /// + /// the root of the handled html tree + public SelectionHandler(CssBox root) + { + ArgChecker.AssertArgNotNull(root, "root"); + + _root = root; + _contextMenuHandler = new ContextMenuHandler(this, root.HtmlContainer); + } + + /// + /// Select all the words in the html. + /// + /// the control hosting the html to invalidate + public void SelectAll(RControl control) + { + if (_root.HtmlContainer.IsSelectionEnabled) + { + ClearSelection(); + SelectAllWords(_root); + control.Invalidate(); + } + } + + /// + /// Select the word at the given location if found. + /// + /// the control hosting the html to invalidate + /// the location to select word at + public void SelectWord(RControl control, RPoint loc) + { + if (_root.HtmlContainer.IsSelectionEnabled) + { + var word = DomUtils.GetCssBoxWord(_root, loc); + if (word != null) + { + word.Selection = this; + _selectionStartPoint = loc; + _selectionStart = _selectionEnd = word; + control.Invalidate(); + } + } + } + + /// + /// Handle mouse down to handle selection. + /// + /// the control hosting the html to invalidate + /// the location of the mouse on the html + /// + public void HandleMouseDown(RControl parent, RPoint loc, bool isMouseInContainer) + { + bool clear = !isMouseInContainer; + if (isMouseInContainer) + { + _mouseDownInControl = true; + _isDoubleClickSelect = (DateTime.Now - _lastMouseDown).TotalMilliseconds < 400; + _lastMouseDown = DateTime.Now; + _mouseDownOnSelectedWord = false; + + if (_root.HtmlContainer.IsSelectionEnabled && parent.LeftMouseButton) + { + var word = DomUtils.GetCssBoxWord(_root, loc); + if (word != null && word.Selected) + { + _mouseDownOnSelectedWord = true; + } + else + { + clear = true; + } + } + else if (parent.RightMouseButton) + { + var rect = DomUtils.GetCssBoxWord(_root, loc); + var link = DomUtils.GetLinkBox(_root, loc); + if (_root.HtmlContainer.IsContextMenuEnabled) + { + _contextMenuHandler.ShowContextMenu(parent, rect, link); + } + clear = rect == null || !rect.Selected; + } + } + + if (clear) + { + ClearSelection(); + parent.Invalidate(); + } + } + + /// + /// Handle mouse up to handle selection and link click. + /// + /// the control hosting the html to invalidate + /// is the left mouse button has been released + /// is the mouse up should be ignored + public bool HandleMouseUp(RControl parent, bool leftMouseButton) + { + bool ignore = false; + _mouseDownInControl = false; + if (_root.HtmlContainer.IsSelectionEnabled) + { + ignore = _inSelection; + if (!_inSelection && leftMouseButton && _mouseDownOnSelectedWord) + { + ClearSelection(); + parent.Invalidate(); + } + + _mouseDownOnSelectedWord = false; + _inSelection = false; + } + ignore = ignore || (DateTime.Now - _lastMouseDown > TimeSpan.FromSeconds(1)); + return ignore; + } + + /// + /// Handle mouse move to handle hover cursor and text selection. + /// + /// the control hosting the html to set cursor and invalidate + /// the location of the mouse on the html + public void HandleMouseMove(RControl parent, RPoint loc) + { + if (_root.HtmlContainer.IsSelectionEnabled && _mouseDownInControl && parent.LeftMouseButton) + { + if (_mouseDownOnSelectedWord) + { + // make sure not to start drag-drop on click but when it actually moves as it fucks mouse-up + if ((DateTime.Now - _lastMouseDown).TotalMilliseconds > 200) + StartDragDrop(parent); + } + else + { + HandleSelection(parent, loc, !_isDoubleClickSelect); + _inSelection = _selectionStart != null && _selectionEnd != null && (_selectionStart != _selectionEnd || _selectionStartIndex != _selectionEndIndex); + } + } + else + { + // Handle mouse hover over the html to change the cursor depending if hovering word, link of other. + var link = DomUtils.GetLinkBox(_root, loc); + if (link != null) + { + _cursorChanged = true; + parent.SetCursorHand(); + } + else if (_root.HtmlContainer.IsSelectionEnabled) + { + var word = DomUtils.GetCssBoxWord(_root, loc); + _cursorChanged = word != null && !word.IsImage && !(word.Selected && (word.SelectedStartIndex < 0 || word.Left + word.SelectedStartOffset <= loc.X) && (word.SelectedEndOffset < 0 || word.Left + word.SelectedEndOffset >= loc.X)); + if (_cursorChanged) + parent.SetCursorIBeam(); + else + parent.SetCursorDefault(); + } + else if (_cursorChanged) + { + parent.SetCursorDefault(); + } + } + } + + /// + /// On mouse leave change the cursor back to default. + /// + /// the control hosting the html to set cursor and invalidate + public void HandleMouseLeave(RControl parent) + { + if (_cursorChanged) + { + _cursorChanged = false; + parent.SetCursorDefault(); + } + } + + /// + /// Copy the currently selected html segment to clipboard.
+ /// Copy rich html text and plain text. + ///
+ public void CopySelectedHtml() + { + if (_root.HtmlContainer.IsSelectionEnabled) + { + var html = DomUtils.GenerateHtml(_root, HtmlGenerationStyle.Inline, true); + var plainText = DomUtils.GetSelectedPlainText(_root); + if (!string.IsNullOrEmpty(plainText)) + _root.HtmlContainer.Adapter.SetToClipboard(html, plainText); + } + } + + /// + /// Get the currently selected text segment in the html.
+ ///
+ public string GetSelectedText() + { + return _root.HtmlContainer.IsSelectionEnabled ? DomUtils.GetSelectedPlainText(_root) : null; + } + + /// + /// Copy the currently selected html segment with style.
+ ///
+ public string GetSelectedHtml() + { + return _root.HtmlContainer.IsSelectionEnabled ? DomUtils.GenerateHtml(_root, HtmlGenerationStyle.Inline, true) : null; + } + + /// + /// The selection start index if the first selected word is partially selected (-1 if not selected or fully selected)
+ /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. + ///
+ /// + /// Handles backward selecting by returning the selection end data instead of start. + /// + /// the word to return the selection start index for + /// data value or -1 if not applicable + public int GetSelectingStartIndex(CssRect word) + { + return word == (_backwardSelection ? _selectionEnd : _selectionStart) ? (_backwardSelection ? _selectionEndIndex : _selectionStartIndex) : -1; + } + + /// + /// The selection end index if the last selected word is partially selected (-1 if not selected or fully selected)
+ /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. + ///
+ /// + /// Handles backward selecting by returning the selection end data instead of start. + /// + /// the word to return the selection end index for + public int GetSelectedEndIndexOffset(CssRect word) + { + return word == (_backwardSelection ? _selectionStart : _selectionEnd) ? (_backwardSelection ? _selectionStartIndex : _selectionEndIndex) : -1; + } + + /// + /// The selection start offset if the first selected word is partially selected (-1 if not selected or fully selected)
+ /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. + ///
+ /// + /// Handles backward selecting by returning the selection end data instead of start. + /// + /// the word to return the selection start offset for + public double GetSelectedStartOffset(CssRect word) + { + return word == (_backwardSelection ? _selectionEnd : _selectionStart) ? (_backwardSelection ? _selectionEndOffset : _selectionStartOffset) : -1; + } + + /// + /// The selection end offset if the last selected word is partially selected (-1 if not selected or fully selected)
+ /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. + ///
+ /// + /// Handles backward selecting by returning the selection end data instead of start. + /// + /// the word to return the selection end offset for + public double GetSelectedEndOffset(CssRect word) + { + return word == (_backwardSelection ? _selectionStart : _selectionEnd) ? (_backwardSelection ? _selectionStartOffset : _selectionEndOffset) : -1; + } + + /// + /// Clear the current selection. + /// + public void ClearSelection() + { + // clear drag and drop + _dragDropData = null; + + ClearSelection(_root); + + _selectionStartOffset = -1; + _selectionStartIndex = -1; + _selectionEndOffset = -1; + _selectionEndIndex = -1; + + _selectionStartPoint = RPoint.Empty; + _selectionStart = null; + _selectionEnd = null; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// 2 + public void Dispose() + { + _contextMenuHandler.Dispose(); + } + + + #region Private methods + + /// + /// Handle html text selection by mouse move over the html with left mouse button pressed.
+ /// Calculate the words in the selected range and set their selected property. + ///
+ /// the control hosting the html to invalidate + /// the mouse location + /// true - partial word selection allowed, false - only full words selection + private void HandleSelection(RControl control, RPoint loc, bool allowPartialSelect) + { + // get the line under the mouse or nearest from the top + var lineBox = DomUtils.GetCssLineBox(_root, loc); + if (lineBox != null) + { + // get the word under the mouse + var word = DomUtils.GetCssBoxWord(lineBox, loc); + + // if no word found under the mouse use the last or the first word in the line + if (word == null && lineBox.Words.Count > 0) + { + if (loc.Y > lineBox.LineBottom) + { + // under the line + word = lineBox.Words[lineBox.Words.Count - 1]; + } + else if (loc.X < lineBox.Words[0].Left) + { + // before the line + word = lineBox.Words[0]; + } + else if (loc.X > lineBox.Words[lineBox.Words.Count - 1].Right) + { + // at the end of the line + word = lineBox.Words[lineBox.Words.Count - 1]; + } + } + + // if there is matching word + if (word != null) + { + if (_selectionStart == null) + { + // on start set the selection start word + _selectionStartPoint = loc; + _selectionStart = word; + if (allowPartialSelect) + CalculateWordCharIndexAndOffset(control, word, loc, true); + } + + // always set selection end word + _selectionEnd = word; + if (allowPartialSelect) + CalculateWordCharIndexAndOffset(control, word, loc, false); + + ClearSelection(_root); + if (CheckNonEmptySelection(loc, allowPartialSelect)) + { + CheckSelectionDirection(); + SelectWordsInRange(_root, _backwardSelection ? _selectionEnd : _selectionStart, _backwardSelection ? _selectionStart : _selectionEnd); + } + else + { + _selectionEnd = null; + } + + _cursorChanged = true; + control.SetCursorIBeam(); + control.Invalidate(); + } + } + } + + /// + /// Clear the selection from all the words in the css box recursively. + /// + /// the css box to selectionStart clear at + private static void ClearSelection(CssBox box) + { + foreach (var word in box.Words) + { + word.Selection = null; + } + foreach (var childBox in box.Boxes) + { + ClearSelection(childBox); + } + } + + /// + /// Start drag & drop operation on the currently selected html segment. + /// + /// the control to start the drag & drop on + private void StartDragDrop(RControl control) + { + if (_dragDropData == null) + { + var html = DomUtils.GenerateHtml(_root, HtmlGenerationStyle.Inline, true); + var plainText = DomUtils.GetSelectedPlainText(_root); + _dragDropData = control.Adapter.GetClipboardDataObject(html, plainText); + } + control.DoDragDropCopy(_dragDropData); + } + + /// + /// Select all the words that are under DOM hierarchy.
+ ///
+ /// the box to start select all at + public void SelectAllWords(CssBox box) + { + foreach (var word in box.Words) + { + word.Selection = this; + } + + foreach (var childBox in box.Boxes) + { + SelectAllWords(childBox); + } + } + + /// + /// Check if the current selection is non empty, has some selection data. + /// + /// + /// true - partial word selection allowed, false - only full words selection + /// true - is non empty selection, false - empty selection + private bool CheckNonEmptySelection(RPoint loc, bool allowPartialSelect) + { + // full word selection is never empty + if (!allowPartialSelect) + return true; + + // if end selection location is near starting location then the selection is empty + if (Math.Abs(_selectionStartPoint.X - loc.X) <= 1 && Math.Abs(_selectionStartPoint.Y - loc.Y) < 5) + return false; + + // selection is empty if on same word and same index + return _selectionStart != _selectionEnd || _selectionStartIndex != _selectionEndIndex; + } + + /// + /// Select all the words that are between word and word in the DOM hierarchy.
+ ///
+ /// the root of the DOM sub-tree the selection is in + /// selection start word limit + /// selection end word limit + private void SelectWordsInRange(CssBox root, CssRect selectionStart, CssRect selectionEnd) + { + bool inSelection = false; + SelectWordsInRange(root, selectionStart, selectionEnd, ref inSelection); + } + + /// + /// Select all the words that are between word and word in the DOM hierarchy. + /// + /// the current traversal node + /// selection start word limit + /// selection end word limit + /// used to know the traversal is currently in selected range + /// + private bool SelectWordsInRange(CssBox box, CssRect selectionStart, CssRect selectionEnd, ref bool inSelection) + { + foreach (var boxWord in box.Words) + { + if (!inSelection && boxWord == selectionStart) + { + inSelection = true; + } + if (inSelection) + { + boxWord.Selection = this; + + if (selectionStart == selectionEnd || boxWord == selectionEnd) + { + return true; + } + } + } + + foreach (var childBox in box.Boxes) + { + if (SelectWordsInRange(childBox, selectionStart, selectionEnd, ref inSelection)) + { + return true; + } + } + + return false; + } + + /// + /// Calculate the character index and offset by characters for the given word and given offset.
+ /// . + ///
+ /// used to create graphics to measure string + /// the word to calculate its index and offset + /// the location to calculate for + /// to set the starting or ending char and offset data + private void CalculateWordCharIndexAndOffset(RControl control, CssRect word, RPoint loc, bool selectionStart) + { + int selectionIndex; + double selectionOffset; + CalculateWordCharIndexAndOffset(control, word, loc, selectionStart, out selectionIndex, out selectionOffset); + + if (selectionStart) + { + _selectionStartIndex = selectionIndex; + _selectionStartOffset = selectionOffset; + } + else + { + _selectionEndIndex = selectionIndex; + _selectionEndOffset = selectionOffset; + } + } + + /// + /// Calculate the character index and offset by characters for the given word and given offset.
+ /// If the location is below the word line then set the selection to the end.
+ /// If the location is to the right of the word then set the selection to the end.
+ /// If the offset is to the left of the word set the selection to the beginning.
+ /// Otherwise calculate the width of each substring to find the char the location is on. + ///
+ /// used to create graphics to measure string + /// the word to calculate its index and offset + /// the location to calculate for + /// is to include the first character in the calculation + /// return the index of the char under the location + /// return the offset of the char under the location + private static void CalculateWordCharIndexAndOffset(RControl control, CssRect word, RPoint loc, bool inclusive, out int selectionIndex, out double selectionOffset) + { + selectionIndex = 0; + selectionOffset = 0f; + var offset = loc.X - word.Left; + if (word.Text == null) + { + // not a text word - set full selection + selectionIndex = -1; + selectionOffset = -1; + } + else if (offset > word.Width - word.OwnerBox.ActualWordSpacing || loc.Y > DomUtils.GetCssLineBoxByWord(word).LineBottom) + { + // mouse under the line, to the right of the word - set to the end of the word + selectionIndex = word.Text.Length; + selectionOffset = word.Width; + } + else if (offset > 0) + { + // calculate partial word selection + int charFit; + double charFitWidth; + var maxWidth = offset + (inclusive ? 0 : 1.5f * word.LeftGlyphPadding); + control.MeasureString(word.Text, word.OwnerBox.ActualFont, maxWidth, out charFit, out charFitWidth); + + selectionIndex = charFit; + selectionOffset = charFitWidth; + } + } + + /// + /// Check if the selection direction is forward or backward.
+ /// Is the selection start word is before the selection end word in DFS traversal. + ///
+ private void CheckSelectionDirection() + { + if (_selectionStart == _selectionEnd) + { + _backwardSelection = _selectionStartIndex > _selectionEndIndex; + } + else if (DomUtils.GetCssLineBoxByWord(_selectionStart) == DomUtils.GetCssLineBoxByWord(_selectionEnd)) + { + _backwardSelection = _selectionStart.Left > _selectionEnd.Left; + } + else + { + _backwardSelection = _selectionStart.Top >= _selectionEnd.Bottom; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Handlers/StylesheetLoadHandler.cs b/Source/HtmlRenderer.Core/Core/Handlers/StylesheetLoadHandler.cs new file mode 100644 index 000000000..719f3cebb --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Handlers/StylesheetLoadHandler.cs @@ -0,0 +1,192 @@ +// "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.Net; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core.Handlers +{ + /// + /// Handler for loading a stylesheet data. + /// + internal static class StylesheetLoadHandler + { + /// + /// Load stylesheet data from the given source.
+ /// The source can be local file or web URI.
+ /// First raise event to allow the client to overwrite the stylesheet loading.
+ /// If the stylesheet is downloaded from URI we will try to correct local URIs to absolute.
+ ///
+ /// the container of the html to handle load stylesheet for + /// the source of the element to load the stylesheet by + /// the attributes of the link element + /// return the stylesheet string that has been loaded (null if failed or is given) + /// return stylesheet data object that was provided by overwrite (null if failed or is given) + public static void LoadStylesheet(HtmlContainerInt htmlContainer, string src, Dictionary attributes, out string stylesheet, out CssData stylesheetData) + { + ArgChecker.AssertArgNotNull(htmlContainer, "htmlContainer"); + + stylesheet = null; + stylesheetData = null; + try + { + var args = new HtmlStylesheetLoadEventArgs(src, attributes); + htmlContainer.RaiseHtmlStylesheetLoadEvent(args); + + if (!string.IsNullOrEmpty(args.SetStyleSheet)) + { + stylesheet = args.SetStyleSheet; + } + else if (args.SetStyleSheetData != null) + { + stylesheetData = args.SetStyleSheetData; + } + else if (args.SetSrc != null) + { + stylesheet = LoadStylesheet(htmlContainer, args.SetSrc); + } + else + { + stylesheet = LoadStylesheet(htmlContainer, src); + } + } + catch (Exception ex) + { + htmlContainer.ReportError(HtmlRenderErrorType.CssParsing, "Exception in handling stylesheet source", ex); + } + } + + + #region Private methods + + /// + /// Load stylesheet string from given source (file path or uri). + /// + /// the container of the html to handle load stylesheet for + /// the file path or uri to load the stylesheet from + /// the stylesheet string + private static string LoadStylesheet(HtmlContainerInt htmlContainer, string src) + { + var uri = CommonUtils.TryGetUri(src); + if (uri == null || uri.Scheme == "file") + { + return LoadStylesheetFromFile(htmlContainer, uri != null ? uri.AbsolutePath : src); + } + else + { + return LoadStylesheetFromUri(htmlContainer, uri); + } + } + + /// + /// Load the stylesheet from local file by given path. + /// + /// the container of the html to handle load stylesheet for + /// the stylesheet file to load + /// the loaded stylesheet string + private static string LoadStylesheetFromFile(HtmlContainerInt htmlContainer, string path) + { + var fileInfo = CommonUtils.TryGetFileInfo(path); + if (fileInfo != null) + { + if (fileInfo.Exists) + { + using (var sr = new StreamReader(fileInfo.FullName)) + { + return sr.ReadToEnd(); + } + } + else + { + htmlContainer.ReportError(HtmlRenderErrorType.CssParsing, "No stylesheet found by path: " + path); + } + } + else + { + htmlContainer.ReportError(HtmlRenderErrorType.CssParsing, "Failed load image, invalid source: " + path); + } + return string.Empty; + } + + /// + /// Load the stylesheet from uri by downloading the string. + /// + /// the container of the html to handle load stylesheet for + /// the uri to download from + /// the loaded stylesheet string + private static string LoadStylesheetFromUri(HtmlContainerInt htmlContainer, Uri uri) + { + using (var client = new WebClient()) + { + var stylesheet = client.DownloadString(uri); + try + { + stylesheet = CorrectRelativeUrls(stylesheet, uri); + } + catch (Exception ex) + { + htmlContainer.ReportError(HtmlRenderErrorType.CssParsing, "Error in correcting relative URL in loaded stylesheet", ex); + } + return stylesheet; + } + } + + /// + /// Make relative URLs absolute in the stylesheet using the URI of the stylesheet. + /// + /// the stylesheet to correct + /// the stylesheet uri to use to create absolute URLs + /// Corrected stylesheet + private static string CorrectRelativeUrls(string stylesheet, Uri baseUri) + { + int idx = 0; + while (idx >= 0 && idx < stylesheet.Length) + { + idx = stylesheet.IndexOf("url(", idx, StringComparison.OrdinalIgnoreCase); + if (idx >= 0) + { + int endIdx = stylesheet.IndexOf(')', idx); + if (endIdx > idx + 4) + { + var offset1 = 4 + (stylesheet[idx + 4] == '\'' ? 1 : 0); + var offset2 = (stylesheet[endIdx - 1] == '\'' ? 1 : 0); + var urlStr = stylesheet.Substring(idx + offset1, endIdx - idx - offset1 - offset2); + Uri url; + if (Uri.TryCreate(urlStr, UriKind.Relative, out url)) + { + url = new Uri(baseUri, url); + stylesheet = stylesheet.Remove(idx + 4, endIdx - idx - 4); + stylesheet = stylesheet.Insert(idx + 4, url.AbsoluteUri); + idx += url.AbsoluteUri.Length + 4; + } + else + { + idx = endIdx + 1; + } + } + else + { + idx += 4; + } + } + } + + return stylesheet; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/HtmlContainerInt.cs b/Source/HtmlRenderer.Core/Core/HtmlContainerInt.cs new file mode 100644 index 000000000..1d71de9fb --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/HtmlContainerInt.cs @@ -0,0 +1,1062 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Dom; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Handlers; +using TheArtOfDev.HtmlRenderer.Core.Parse; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core +{ + /// + /// Low level handling of Html Renderer logic.
+ /// Allows html layout and rendering without association to actual control, those allowing to handle html rendering on any graphics object.
+ /// Using this class will require the client to handle all propagation's of mouse/keyboard events, layout/paint calls, scrolling offset, + /// location/size/rectangle handling and UI refresh requests.
+ ///
+ /// + /// + /// MaxSize and ActualSize:
+ /// The max width and height of the rendered html.
+ /// The max width will effect the html layout wrapping lines, resize images and tables where possible.
+ /// The max height does NOT effect layout, but will not render outside it (clip).
+ /// can exceed the max size by layout restrictions (unwrap-able line, set image size, etc.).
+ /// Set zero for unlimited (width/height separately).
+ ///
+ /// + /// ScrollOffset:
+ /// This will adjust the rendered html by the given offset so the content will be "scrolled".
+ /// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered + /// at -100, therefore outside the client rectangle. + ///
+ /// + /// LinkClicked event
+ /// Raised when the user clicks on a link in the html.
+ /// Allows canceling the execution of the link to overwrite by custom logic.
+ /// If error occurred in event handler it will propagate up the stack. + ///
+ /// + /// StylesheetLoad event:
+ /// Raised when a stylesheet is about to be loaded by file path or URL in 'link' element.
+ /// Allows to overwrite the loaded stylesheet by providing the stylesheet data manually, or different source (file or URL) to load from.
+ /// Example: The stylesheet 'href' can be non-valid URI string that is interpreted in the overwrite delegate by custom logic to pre-loaded stylesheet object
+ /// If no alternative data is provided the original source will be used.
+ ///
+ /// + /// ImageLoad event:
+ /// Raised when an image is about to be loaded by file path, URL or inline data in 'img' element or background-image CSS style.
+ /// Allows to overwrite the loaded image by providing the image object manually, or different source (file or URL) to load from.
+ /// Example: image 'src' can be non-valid string that is interpreted in the overwrite delegate by custom logic to resource image object
+ /// Example: image 'src' in the html is relative - the overwrite intercepts the load and provide full source URL to load the image from
+ /// Example: image download requires authentication - the overwrite intercepts the load, downloads the image to disk using custom code and provide + /// file path to load the image from.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ /// + /// Refresh event:
+ /// Raised when html renderer requires refresh of the control hosting (invalidation and re-layout).
+ /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. + ///
+ /// + /// RenderError event:
+ /// Raised when an error occurred during html rendering.
+ ///
+ ///
+ public sealed class HtmlContainerInt : IDisposable + { + #region Fields and Consts + + /// + /// Main adapter to framework specific logic. + /// + private readonly RAdapter _adapter; + + /// + /// parser for CSS data + /// + private readonly CssParser _cssParser; + + /// + /// the root css box of the parsed html + /// + private CssBox _root; + + /// + /// list of all css boxes that have ":hover" selector on them + /// + private List _hoverBoxes; + + /// + /// Handler for text selection in the html. + /// + private SelectionHandler _selectionHandler; + + /// + /// Handler for downloading of images in the html + /// + private ImageDownloader _imageDownloader; + + /// + /// the text fore color use for selected text + /// + private RColor _selectionForeColor; + + /// + /// the back-color to use for selected text + /// + private RColor _selectionBackColor; + + /// + /// the parsed stylesheet data used for handling the html + /// + private CssData _cssData; + + /// + /// Is content selection is enabled for the rendered html (default - true).
+ /// If set to 'false' the rendered html will be static only with ability to click on links. + ///
+ private bool _isSelectionEnabled = true; + + /// + /// Is the build-in context menu enabled (default - true) + /// + private bool _isContextMenuEnabled = true; + + /// + /// Gets or sets a value indicating if anti-aliasing should be avoided + /// for geometry like backgrounds and borders + /// + private bool _avoidGeometryAntialias; + + /// + /// Gets or sets a value indicating if image asynchronous loading should be avoided (default - false).
+ ///
+ private bool _avoidAsyncImagesLoading; + + /// + /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
+ ///
+ private bool _avoidImagesLateLoading; + + /// + /// is the load of the html document is complete + /// + private bool _loadComplete; + + /// + /// the top-left most location of the rendered html + /// + private RPoint _location; + + /// + /// the max width and height of the rendered html, effects layout, actual size cannot exceed this values.
+ /// Set zero for unlimited.
+ ///
+ private RSize _maxSize; + + /// + /// Gets or sets the scroll offset of the document for scroll controls + /// + private RPoint _scrollOffset; + + /// + /// The actual size of the rendered html (after layout) + /// + private RSize _actualSize; + + /// + /// the top margin between the page start and the text + /// + private int _marginTop; + + /// + /// the bottom margin between the page end and the text + /// + private int _marginBottom; + + /// + /// the left margin between the page start and the text + /// + private int _marginLeft; + + /// + /// the right margin between the page end and the text + /// + private int _marginRight; + + #endregion + + + /// + /// Init. + /// + public HtmlContainerInt(RAdapter adapter) + { + ArgChecker.AssertArgNotNull(adapter, "global"); + + _adapter = adapter; + _cssParser = new CssParser(adapter); + } + + /// + /// + /// + internal RAdapter Adapter + { + get { return _adapter; } + } + + /// + /// parser for CSS data + /// + internal CssParser CssParser + { + get { return _cssParser; } + } + + /// + /// Raised when the set html document has been fully loaded.
+ /// Allows manipulation of the html dom, scroll position, etc. + ///
+ public event EventHandler LoadComplete; + + /// + /// Raised when the user clicks on a link in the html.
+ /// Allows canceling the execution of the link. + ///
+ public event EventHandler LinkClicked; + + /// + /// Raised when html renderer requires refresh of the control hosting (invalidation and re-layout). + /// + /// + /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. + /// + public event EventHandler Refresh; + + /// + /// Raised when Html Renderer request scroll to specific location.
+ /// This can occur on document anchor click. + ///
+ public event EventHandler ScrollChange; + + /// + /// Raised when an error occurred during html rendering.
+ ///
+ /// + /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. + /// + public event EventHandler RenderError; + + /// + /// Raised when a stylesheet is about to be loaded by file path or URI by link element.
+ /// This event allows to provide the stylesheet manually or provide new source (file or Uri) to load from.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ public event EventHandler StylesheetLoad; + + /// + /// Raised when an image is about to be loaded by file path or URI.
+ /// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI. + ///
+ public event EventHandler ImageLoad; + + /// + /// the parsed stylesheet data used for handling the html + /// + public CssData CssData + { + get { return _cssData; } + } + + /// + /// Gets or sets a value indicating if anti-aliasing should be avoided for geometry like backgrounds and borders (default - false). + /// + public bool AvoidGeometryAntialias + { + get { return _avoidGeometryAntialias; } + set { _avoidGeometryAntialias = value; } + } + + /// + /// Gets or sets a value indicating if image asynchronous loading should be avoided (default - false).
+ /// True - images are loaded synchronously during html parsing.
+ /// False - images are loaded asynchronously to html parsing when downloaded from URL or loaded from disk.
+ ///
+ /// + /// Asynchronously image loading allows to unblock html rendering while image is downloaded or loaded from disk using IO + /// ports to achieve better performance.
+ /// Asynchronously image loading should be avoided when the full html content must be available during render, like render to image. + ///
+ public bool AvoidAsyncImagesLoading + { + get { return _avoidAsyncImagesLoading; } + set { _avoidAsyncImagesLoading = value; } + } + + /// + /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
+ /// True - images are loaded as soon as the html is parsed.
+ /// False - images that are not visible because of scroll location are not loaded until they are scrolled to. + ///
+ /// + /// Images late loading improve performance if the page contains image outside the visible scroll area, especially if there is large + /// amount of images, as all image loading is delayed (downloading and loading into memory).
+ /// Late image loading may effect the layout and actual size as image without set size will not have actual size until they are loaded + /// resulting in layout change during user scroll.
+ /// Early image loading may also effect the layout if image without known size above the current scroll location are loaded as they + /// will push the html elements down. + ///
+ public bool AvoidImagesLateLoading + { + get { return _avoidImagesLateLoading; } + set { _avoidImagesLateLoading = value; } + } + + /// + /// Is content selection is enabled for the rendered html (default - true).
+ /// If set to 'false' the rendered html will be static only with ability to click on links. + ///
+ public bool IsSelectionEnabled + { + get { return _isSelectionEnabled; } + set { _isSelectionEnabled = value; } + } + + /// + /// Is the build-in context menu enabled and will be shown on mouse right click (default - true) + /// + public bool IsContextMenuEnabled + { + get { return _isContextMenuEnabled; } + set { _isContextMenuEnabled = value; } + } + + /// + /// The scroll offset of the html.
+ /// This will adjust the rendered html by the given offset so the content will be "scrolled".
+ ///
+ /// + /// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered as it + /// will be at -100 therefore outside the client rectangle. + /// + public RPoint ScrollOffset + { + get { return _scrollOffset; } + set { _scrollOffset = value; } + } + + /// + /// The top-left most location of the rendered html.
+ /// This will offset the top-left corner of the rendered html. + ///
+ public RPoint Location + { + get { return _location; } + set { _location = value; } + } + + /// + /// The max width and height of the rendered html.
+ /// The max width will effect the html layout wrapping lines, resize images and tables where possible.
+ /// The max height does NOT effect layout, but will not render outside it (clip).
+ /// can be exceed the max size by layout restrictions (unwrapable line, set image size, etc.).
+ /// Set zero for unlimited (width\height separately).
+ ///
+ public RSize MaxSize + { + get { return _maxSize; } + set { _maxSize = value; } + } + + /// + /// The actual size of the rendered html (after layout) + /// + public RSize ActualSize + { + get { return _actualSize; } + set { _actualSize = value; } + } + + public RSize PageSize { get; set; } + + /// + /// the top margin between the page start and the text + /// + public int MarginTop + { + get { return _marginTop; } + set + { + if (value > -1) + _marginTop = value; + } + } + + /// + /// the bottom margin between the page end and the text + /// + public int MarginBottom + { + get { return _marginBottom; } + set + { + if (value > -1) + _marginBottom = value; + } + } + + /// + /// the left margin between the page start and the text + /// + public int MarginLeft + { + get { return _marginLeft; } + set + { + if (value > -1) + _marginLeft = value; + } + } + + /// + /// the right margin between the page end and the text + /// + public int MarginRight + { + get { return _marginRight; } + set + { + if (value > -1) + _marginRight = value; + } + } + + /// + /// Set all 4 margins to the given value. + /// + /// + public void SetMargins(int value) + { + if (value > -1) + _marginBottom = _marginLeft = _marginTop = _marginRight = value; + } + + /// + /// Get the currently selected text segment in the html. + /// + public string SelectedText + { + get { return _selectionHandler.GetSelectedText(); } + } + + /// + /// Copy the currently selected html segment with style. + /// + public string SelectedHtml + { + get { return _selectionHandler.GetSelectedHtml(); } + } + + /// + /// the root css box of the parsed html + /// + internal CssBox Root + { + get { return _root; } + } + + /// + /// the text fore color use for selected text + /// + internal RColor SelectionForeColor + { + get { return _selectionForeColor; } + set { _selectionForeColor = value; } + } + + /// + /// the back-color to use for selected text + /// + internal RColor SelectionBackColor + { + get { return _selectionBackColor; } + set { _selectionBackColor = value; } + } + + /// + /// Init with optional document and stylesheet. + /// + /// the html to init with, init empty if not given + /// optional: the stylesheet to init with, init default if not given + public void SetHtml(string htmlSource, CssData baseCssData = null) + { + Clear(); + if (!string.IsNullOrEmpty(htmlSource)) + { + _loadComplete = false; + _cssData = baseCssData ?? _adapter.DefaultCssData; + + DomParser parser = new DomParser(_cssParser); + _root = parser.GenerateCssTree(htmlSource, this, ref _cssData); + if (_root != null) + { + _selectionHandler = new SelectionHandler(_root); + _imageDownloader = new ImageDownloader(); + } + } + } + + /// + /// Clear the content of the HTML container releasing any resources used to render previously existing content. + /// + public void Clear() + { + if (_root != null) + { + _root.Dispose(); + _root = null; + + if (_selectionHandler != null) + _selectionHandler.Dispose(); + _selectionHandler = null; + + if (_imageDownloader != null) + _imageDownloader.Dispose(); + _imageDownloader = null; + + _hoverBoxes = null; + } + } + + /// + /// Clear the current selection. + /// + public void ClearSelection() + { + if (_selectionHandler != null) + { + _selectionHandler.ClearSelection(); + RequestRefresh(false); + } + } + + /// + /// Get html from the current DOM tree with style if requested. + /// + /// Optional: controls the way styles are generated when html is generated (default: ) + /// generated html + public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline) + { + return DomUtils.GenerateHtml(_root, styleGen); + } + + /// + /// Get attribute value of element at the given x,y location by given key.
+ /// If more than one element exist with the attribute at the location the inner most is returned. + ///
+ /// the location to find the attribute at + /// the attribute key to get value by + /// found attribute value or null if not found + public string GetAttributeAt(RPoint location, string attribute) + { + ArgChecker.AssertArgNotNullOrEmpty(attribute, "attribute"); + + var cssBox = DomUtils.GetCssBox(_root, OffsetByScroll(location)); + return cssBox != null ? DomUtils.GetAttribute(cssBox, attribute) : null; + } + + /// + /// Get all the links in the HTML with the element rectangle and href data. + /// + /// collection of all the links in the HTML + public List> GetLinks() + { + var linkBoxes = new List(); + DomUtils.GetAllLinkBoxes(_root, linkBoxes); + + var linkElements = new List>(); + foreach (var box in linkBoxes) + { + linkElements.Add(new LinkElementData(box.GetAttribute("id"), box.GetAttribute("href"), CommonUtils.GetFirstValueOrDefault(box.Rectangles, box.Bounds))); + } + return linkElements; + } + + /// + /// Get css link href at the given x,y location. + /// + /// the location to find the link at + /// css link href if exists or null + public string GetLinkAt(RPoint location) + { + var link = DomUtils.GetLinkBox(_root, OffsetByScroll(location)); + return link != null ? link.HrefLink : null; + } + + /// + /// Get the rectangle of html element as calculated by html layout.
+ /// Element if found by id (id attribute on the html element).
+ /// Note: to get the screen rectangle you need to adjust by the hosting control.
+ ///
+ /// the id of the element to get its rectangle + /// the rectangle of the element or null if not found + public RRect? GetElementRectangle(string elementId) + { + ArgChecker.AssertArgNotNullOrEmpty(elementId, "elementId"); + + var box = DomUtils.GetBoxById(_root, elementId.ToLower()); + return box != null ? CommonUtils.GetFirstValueOrDefault(box.Rectangles, box.Bounds) : (RRect?)null; + } + + /// + /// Measures the bounds of box and children, recursively. + /// + /// Device context to draw + public void PerformLayout(RGraphics g) + { + ArgChecker.AssertArgNotNull(g, "g"); + + _actualSize = RSize.Empty; + if (_root != null) + { + // if width is not restricted we set it to large value to get the actual later + _root.Size = new RSize(_maxSize.Width > 0 ? _maxSize.Width : 99999, 0); + _root.Location = _location; + _root.PerformLayout(g); + + if (_maxSize.Width <= 0.1) + { + // in case the width is not restricted we need to double layout, first will find the width so second can layout by it (center alignment) + _root.Size = new RSize((int)Math.Ceiling(_actualSize.Width), 0); + _actualSize = RSize.Empty; + _root.PerformLayout(g); + } + + if (!_loadComplete) + { + _loadComplete = true; + EventHandler handler = LoadComplete; + if (handler != null) + handler(this, EventArgs.Empty); + } + } + } + + /// + /// Render the html using the given device. + /// + /// the device to use to render + public void PerformPaint(RGraphics g) + { + ArgChecker.AssertArgNotNull(g, "g"); + + if (MaxSize.Height > 0) + { + g.PushClip(new RRect(_location.X, _location.Y, Math.Min(_maxSize.Width, PageSize.Width), Math.Min(_maxSize.Height, PageSize.Height))); + } + else + { + g.PushClip(new RRect(MarginLeft, MarginTop, PageSize.Width, PageSize.Height)); + } + + if (_root != null) + { + _root.Paint(g); + } + + g.PopClip(); + } + + /// + /// Handle mouse down to handle selection. + /// + /// the control hosting the html to invalidate + /// the location of the mouse + public void HandleMouseDown(RControl parent, RPoint location) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + try + { + if (_selectionHandler != null) + _selectionHandler.HandleMouseDown(parent, OffsetByScroll(location), IsMouseInContainer(location)); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse down handle", ex); + } + } + + /// + /// Handle mouse up to handle selection and link click. + /// + /// the control hosting the html to invalidate + /// the location of the mouse + /// the mouse event data + public void HandleMouseUp(RControl parent, RPoint location, RMouseEvent e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + try + { + if (_selectionHandler != null && IsMouseInContainer(location)) + { + var ignore = _selectionHandler.HandleMouseUp(parent, e.LeftButton); + if (!ignore && e.LeftButton) + { + var loc = OffsetByScroll(location); + var link = DomUtils.GetLinkBox(_root, loc); + if (link != null) + { + HandleLinkClicked(parent, location, link); + } + } + } + } + catch (HtmlLinkClickedException) + { + throw; + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse up handle", ex); + } + } + + /// + /// Handle mouse double click to select word under the mouse. + /// + /// the control hosting the html to set cursor and invalidate + /// the location of the mouse + public void HandleMouseDoubleClick(RControl parent, RPoint location) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + try + { + if (_selectionHandler != null && IsMouseInContainer(location)) + _selectionHandler.SelectWord(parent, OffsetByScroll(location)); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse double click handle", ex); + } + } + + /// + /// Handle mouse move to handle hover cursor and text selection. + /// + /// the control hosting the html to set cursor and invalidate + /// the location of the mouse + public void HandleMouseMove(RControl parent, RPoint location) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + try + { + var loc = OffsetByScroll(location); + if (_selectionHandler != null && IsMouseInContainer(location)) + _selectionHandler.HandleMouseMove(parent, loc); + + /* + if( _hoverBoxes != null ) + { + bool refresh = false; + foreach(var hoverBox in _hoverBoxes) + { + foreach(var rect in hoverBox.Item1.Rectangles.Values) + { + if( rect.Contains(loc) ) + { + //hoverBox.Item1.Color = "gold"; + refresh = true; + } + } + } + + if(refresh) + RequestRefresh(true); + } + */ + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse move handle", ex); + } + } + + /// + /// Handle mouse leave to handle hover cursor. + /// + /// the control hosting the html to set cursor and invalidate + public void HandleMouseLeave(RControl parent) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + try + { + if (_selectionHandler != null) + _selectionHandler.HandleMouseLeave(parent); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed mouse leave handle", ex); + } + } + + /// + /// Handle key down event for selection and copy. + /// + /// the control hosting the html to invalidate + /// the pressed key + public void HandleKeyDown(RControl parent, RKeyEvent e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + ArgChecker.AssertArgNotNull(e, "e"); + + try + { + if (e.Control && _selectionHandler != null) + { + // select all + if (e.AKeyCode) + { + _selectionHandler.SelectAll(parent); + } + + // copy currently selected text + if (e.CKeyCode) + { + _selectionHandler.CopySelectedHtml(); + } + } + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.KeyboardMouse, "Failed key down handle", ex); + } + } + + /// + /// Raise the stylesheet load event with the given event args. + /// + /// the event args + internal void RaiseHtmlStylesheetLoadEvent(HtmlStylesheetLoadEventArgs args) + { + try + { + EventHandler handler = StylesheetLoad; + if (handler != null) + handler(this, args); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.CssParsing, "Failed stylesheet load event", ex); + } + } + + /// + /// Raise the image load event with the given event args. + /// + /// the event args + internal void RaiseHtmlImageLoadEvent(HtmlImageLoadEventArgs args) + { + try + { + EventHandler handler = ImageLoad; + if (handler != null) + handler(this, args); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.Image, "Failed image load event", ex); + } + } + + /// + /// Request invalidation and re-layout of the control hosting the renderer. + /// + /// is re-layout is required for the refresh + public void RequestRefresh(bool layout) + { + try + { + EventHandler handler = Refresh; + if (handler != null) + handler(this, new HtmlRefreshEventArgs(layout)); + } + catch (Exception ex) + { + ReportError(HtmlRenderErrorType.General, "Failed refresh request", ex); + } + } + + /// + /// Report error in html render process. + /// + /// the type of error to report + /// the error message + /// optional: the exception that occured + internal void ReportError(HtmlRenderErrorType type, string message, Exception exception = null) + { + try + { + EventHandler handler = RenderError; + if (handler != null) + handler(this, new HtmlRenderErrorEventArgs(type, message, exception)); + } + catch + { } + } + + /// + /// Handle link clicked going over event and using if not canceled. + /// + /// the control hosting the html to invalidate + /// the location of the mouse + /// the link that was clicked + internal void HandleLinkClicked(RControl parent, RPoint location, CssBox link) + { + EventHandler clickHandler = LinkClicked; + if (clickHandler != null) + { + var args = new HtmlLinkClickedEventArgs(link.HrefLink, link.HtmlTag.Attributes); + try + { + clickHandler(this, args); + } + catch (Exception ex) + { + throw new HtmlLinkClickedException("Error in link clicked intercept", ex); + } + if (args.Handled) + return; + } + + if (!string.IsNullOrEmpty(link.HrefLink)) + { + if (link.HrefLink.StartsWith("#") && link.HrefLink.Length > 1) + { + EventHandler scrollHandler = ScrollChange; + if (scrollHandler != null) + { + var rect = GetElementRectangle(link.HrefLink.Substring(1)); + if (rect.HasValue) + { + scrollHandler(this, new HtmlScrollEventArgs(rect.Value.Location)); + HandleMouseMove(parent, location); + } + } + } + else + { + var nfo = new ProcessStartInfo(link.HrefLink); + nfo.UseShellExecute = true; + Process.Start(nfo); + } + } + } + + /// + /// Add css box that has ":hover" selector to be handled on mouse hover. + /// + /// the box that has the hover selector + /// the css block with the css data with the selector + internal void AddHoverBox(CssBox box, CssBlock block) + { + ArgChecker.AssertArgNotNull(box, "box"); + ArgChecker.AssertArgNotNull(block, "block"); + + if (_hoverBoxes == null) + _hoverBoxes = new List(); + + _hoverBoxes.Add(new HoverBoxBlock(box, block)); + } + + /// + /// Get image downloader to be used to download images for the current html rendering.
+ /// Lazy create single downloader to be used for all images in the current html. + ///
+ internal ImageDownloader GetImageDownloader() + { + return _imageDownloader; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// 2 + public void Dispose() + { + Dispose(true); + } + + + #region Private methods + + /// + /// Adjust the offset of the given location by the current scroll offset. + /// + /// the location to adjust + /// the adjusted location + private RPoint OffsetByScroll(RPoint location) + { + return new RPoint(location.X - ScrollOffset.X, location.Y - ScrollOffset.Y); + } + + /// + /// Check if the mouse is currently on the html container.
+ /// Relevant if the html container is not filled in the hosted control (location is not zero and the size is not the full size of the control). + ///
+ private bool IsMouseInContainer(RPoint location) + { + return location.X >= _location.X && location.X <= _location.X + _actualSize.Width && location.Y >= _location.Y + ScrollOffset.Y && location.Y <= _location.Y + ScrollOffset.Y + _actualSize.Height; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + private void Dispose(bool all) + { + try + { + if (all) + { + LinkClicked = null; + Refresh = null; + RenderError = null; + StylesheetLoad = null; + ImageLoad = null; + } + + _cssData = null; + if (_root != null) + _root.Dispose(); + _root = null; + if (_selectionHandler != null) + _selectionHandler.Dispose(); + _selectionHandler = null; + } + catch + { } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/HtmlRendererUtils.cs b/Source/HtmlRenderer.Core/Core/HtmlRendererUtils.cs new file mode 100644 index 000000000..9a414a7fe --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/HtmlRendererUtils.cs @@ -0,0 +1,123 @@ +// "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; + +namespace TheArtOfDev.HtmlRenderer.Core +{ + /// + /// General utilities. + /// + public static class HtmlRendererUtils + { + /// + /// Measure the size of the html by performing layout under the given restrictions. + /// + /// the graphics to use + /// the html to calculate the layout for + /// the minimal size of the rendered html (zero - not limit the width/height) + /// the maximum size of the rendered html, if not zero and html cannot be layout within the limit it will be clipped (zero - not limit the width/height) + /// return: the size of the html to be rendered within the min/max limits + public static RSize MeasureHtmlByRestrictions(RGraphics g, HtmlContainerInt htmlContainer, RSize minSize, RSize maxSize) + { + // first layout without size restriction to know html actual size + htmlContainer.PerformLayout(g); + + if (maxSize.Width > 0 && maxSize.Width < htmlContainer.ActualSize.Width) + { + // to allow the actual size be smaller than max we need to set max size only if it is really larger + htmlContainer.MaxSize = new RSize(maxSize.Width, 0); + htmlContainer.PerformLayout(g); + } + + // restrict the final size by min/max + var finalWidth = Math.Max(maxSize.Width > 0 ? Math.Min(maxSize.Width, (int)htmlContainer.ActualSize.Width) : (int)htmlContainer.ActualSize.Width, minSize.Width); + + // if the final width is larger than the actual we need to re-layout so the html can take the full given width. + if (finalWidth > htmlContainer.ActualSize.Width) + { + htmlContainer.MaxSize = new RSize(finalWidth, 0); + htmlContainer.PerformLayout(g); + } + + var finalHeight = Math.Max(maxSize.Height > 0 ? Math.Min(maxSize.Height, (int)htmlContainer.ActualSize.Height) : (int)htmlContainer.ActualSize.Height, minSize.Height); + + return new RSize(finalWidth, finalHeight); + } + + + /// + /// Perform the layout of the html container by given size restrictions returning the final size.
+ /// The layout can be effected by the HTML content in the if or + /// is set to true.
+ /// Handle minimum and maximum size restrictions.
+ /// Handle auto size and auto size for height only. if is true + /// is ignored.
+ ///
+ /// the graphics used for layout + /// the html container to layout + /// the current size + /// the min size restriction - can be empty for no restriction + /// the max size restriction - can be empty for no restriction + /// if to modify the size (width and height) by html content layout + /// if to modify the height by html content layout + public static RSize Layout(RGraphics g, HtmlContainerInt htmlContainer, RSize size, RSize minSize, RSize maxSize, bool autoSize, bool autoSizeHeightOnly) + { + if (autoSize) + htmlContainer.MaxSize = new RSize(0, 0); + else if (autoSizeHeightOnly) + htmlContainer.MaxSize = new RSize(size.Width, 0); + else + htmlContainer.MaxSize = size; + + htmlContainer.PerformLayout(g); + + RSize newSize = size; + if (autoSize || autoSizeHeightOnly) + { + if (autoSize) + { + if (maxSize.Width > 0 && maxSize.Width < htmlContainer.ActualSize.Width) + { + // to allow the actual size be smaller than max we need to set max size only if it is really larger + htmlContainer.MaxSize = maxSize; + htmlContainer.PerformLayout(g); + } + else if (minSize.Width > 0 && minSize.Width > htmlContainer.ActualSize.Width) + { + // if min size is larger than the actual we need to re-layout so all 100% layouts will be correct + htmlContainer.MaxSize = new RSize(minSize.Width, 0); + htmlContainer.PerformLayout(g); + } + newSize = htmlContainer.ActualSize; + } + else if (Math.Abs(size.Height - htmlContainer.ActualSize.Height) > 0.01) + { + var prevWidth = size.Width; + + // make sure the height is not lower than min if given + newSize.Height = minSize.Height > 0 && minSize.Height > htmlContainer.ActualSize.Height + ? minSize.Height + : htmlContainer.ActualSize.Height; + + // handle if changing the height of the label affects the desired width and those require re-layout + if (Math.Abs(prevWidth - size.Width) > 0.01) + return Layout(g, htmlContainer, size, minSize, maxSize, false, true); + } + } + + return newSize; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Parse/CssParser.cs b/Source/HtmlRenderer.Core/Core/Parse/CssParser.cs new file mode 100644 index 000000000..045cbb687 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Parse/CssParser.cs @@ -0,0 +1,964 @@ +// "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.Text; +using System.Text.RegularExpressions; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core.Parse +{ + /// + /// Parser to parse CSS stylesheet source string into CSS objects. + /// + internal sealed class CssParser + { + #region Fields and Consts + + /// + /// split CSS rule + /// + private static readonly char[] _cssBlockSplitters = new[] { '}', ';' }; + + /// + /// + /// + private readonly RAdapter _adapter; + + /// + /// Utility for value parsing. + /// + private readonly CssValueParser _valueParser; + + /// + /// The chars to trim the css class name by + /// + private static readonly char[] _cssClassTrimChars = new[] { '\r', '\n', '\t', ' ', '-', '!', '<', '>' }; + + #endregion + + + /// + /// Init. + /// + public CssParser(RAdapter adapter) + { + ArgChecker.AssertArgNotNull(adapter, "global"); + + _valueParser = new CssValueParser(adapter); + _adapter = adapter; + } + + /// + /// Parse the given stylesheet source to CSS blocks dictionary.
+ /// The CSS blocks are organized into two level buckets of media type and class name.
+ /// Root media type are found under 'all' bucket.
+ /// 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. + ///
+ /// + /// raw css stylesheet to parse + /// true - combine the parsed css data with default css data, false - return only the parsed css data + /// the CSS data with parsed CSS objects (never null) + public CssData ParseStyleSheet(string stylesheet, bool combineWithDefault) + { + var cssData = combineWithDefault ? _adapter.DefaultCssData.Clone() : new CssData(); + if (!string.IsNullOrEmpty(stylesheet)) + { + ParseStyleSheet(cssData, stylesheet); + } + return cssData; + } + + /// + /// Parse the given stylesheet source to CSS blocks dictionary.
+ /// The CSS blocks are organized into two level buckets of media type and class name.
+ /// Root media type are found under 'all' bucket.
+ /// The parsed css blocks are added to the given css data, merged if class name already exists. + ///
+ /// the CSS data to fill with parsed CSS objects + /// raw css stylesheet to parse + public void ParseStyleSheet(CssData cssData, string stylesheet) + { + if (!String.IsNullOrEmpty(stylesheet)) + { + stylesheet = RemoveStylesheetComments(stylesheet); + + ParseStyleBlocks(cssData, stylesheet); + + ParseMediaStyleBlocks(cssData, stylesheet); + } + } + + /// + /// Parse single CSS block source into CSS block instance. + /// + /// the name of the css class of the block + /// the CSS block to parse + /// the created CSS block instance + public CssBlock ParseCssBlock(string className, string blockSource) + { + return ParseCssBlockImp(className, blockSource); + } + + /// + /// Parse a complex font family css property to check if it contains multiple fonts and if the font exists.
+ /// returns the font family name to use or 'inherit' if failed. + ///
+ /// the font-family value to parse + /// parsed font-family value + public string ParseFontFamily(string value) + { + return ParseFontFamilyProperty(value); + } + + /// + /// Parses a color value in CSS style; e.g. #ff0000, red, rgb(255,0,0), rgb(100%, 0, 0) + /// + /// color string value to parse + /// color value + public RColor ParseColor(string colorStr) + { + return _valueParser.GetActualColor(colorStr); + } + + + #region Private methods + + /// + /// Remove comments from the given stylesheet. + /// + /// the stylesheet to remove comments from + /// stylesheet without comments + private static string RemoveStylesheetComments(string stylesheet) + { + StringBuilder sb = null; + + int prevIdx = 0, startIdx = 0; + while (startIdx > -1 && startIdx < stylesheet.Length) + { + startIdx = stylesheet.IndexOf("/*", startIdx); + if (startIdx > -1) + { + if (sb == null) + sb = new StringBuilder(stylesheet.Length); + sb.Append(stylesheet.Substring(prevIdx, startIdx - prevIdx)); + + var endIdx = stylesheet.IndexOf("*/", startIdx + 2); + if (endIdx < 0) + endIdx = stylesheet.Length; + + prevIdx = startIdx = endIdx + 2; + } + else if (sb != null) + { + sb.Append(stylesheet.Substring(prevIdx)); + } + } + + return sb != null ? sb.ToString() : stylesheet; + } + + /// + /// Parse given stylesheet for CSS blocks
+ /// This blocks are added under the "all" keyword. + ///
+ /// the CSS data to fill with parsed CSS objects + /// the stylesheet to parse + private void ParseStyleBlocks(CssData cssData, string stylesheet) + { + var startIdx = 0; + int endIdx = 0; + while (startIdx < stylesheet.Length && endIdx > -1) + { + endIdx = startIdx; + while (endIdx + 1 < stylesheet.Length) + { + endIdx++; + if (stylesheet[endIdx] == '}') + startIdx = endIdx + 1; + if (stylesheet[endIdx] == '{') + break; + } + + int midIdx = endIdx + 1; + if (endIdx > -1) + { + endIdx++; + while (endIdx < stylesheet.Length) + { + if (stylesheet[endIdx] == '{') + startIdx = midIdx + 1; + if (stylesheet[endIdx] == '}') + break; + endIdx++; + } + + if (endIdx < stylesheet.Length) + { + while (Char.IsWhiteSpace(stylesheet[startIdx])) + startIdx++; + var substring = stylesheet.Substring(startIdx, endIdx - startIdx + 1); + FeedStyleBlock(cssData, substring); + } + startIdx = endIdx + 1; + } + } + } + + /// + /// Parse given stylesheet for media CSS blocks
+ /// This blocks are added under the specific media block they are found. + ///
+ /// the CSS data to fill with parsed CSS objects + /// the stylesheet to parse + private void ParseMediaStyleBlocks(CssData cssData, string stylesheet) + { + int startIdx = 0; + string atrule; + while ((atrule = RegexParserUtils.GetCssAtRules(stylesheet, ref startIdx)) != null) + { + //Just process @media rules + if (!atrule.StartsWith("@media", StringComparison.InvariantCultureIgnoreCase)) + continue; + + //Extract specified media types + MatchCollection types = RegexParserUtils.Match(RegexParserUtils.CssMediaTypes, atrule); + + if (types.Count == 1) + { + string line = types[0].Value; + + if (line.StartsWith("@media", StringComparison.InvariantCultureIgnoreCase) && line.EndsWith("{")) + { + //Get specified media types in the at-rule + string[] media = line.Substring(6, line.Length - 7).Split(' '); + + //Scan media types + foreach (string t in media) + { + if (!String.IsNullOrEmpty(t.Trim())) + { + //Get blocks inside the at-rule + var insideBlocks = RegexParserUtils.Match(RegexParserUtils.CssBlocks, atrule); + + //Scan blocks and feed them to the style sheet + foreach (Match insideBlock in insideBlocks) + { + FeedStyleBlock(cssData, insideBlock.Value, t.Trim()); + } + } + } + } + } + } + } + + /// + /// Feeds the style with a block about the specific media.
+ /// When no media is specified, "all" will be used. + ///
+ /// + /// the CSS block to handle + /// optional: the media (default - all) + private void FeedStyleBlock(CssData cssData, string block, string media = "all") + { + int startIdx = block.IndexOf("{", StringComparison.Ordinal); + int endIdx = startIdx > -1 ? block.IndexOf("}", startIdx) : -1; + if (startIdx > -1 && endIdx > -1) + { + string blockSource = block.Substring(startIdx + 1, endIdx - startIdx - 1); + var classes = block.Substring(0, startIdx).Split(','); + + foreach (string cls in classes) + { + string className = cls.Trim(_cssClassTrimChars); + if (!String.IsNullOrEmpty(className)) + { + var newblock = ParseCssBlockImp(className, blockSource); + if (newblock != null) + { + cssData.AddCssBlock(media, newblock); + } + } + } + } + } + + /// + /// Parse single CSS block source into CSS block instance. + /// + /// the name of the css class of the block + /// the CSS block to parse + /// the created CSS block instance + private CssBlock ParseCssBlockImp(string className, string blockSource) + { + className = className.ToLower(); + string psedoClass = null; + var colonIdx = className.IndexOf(":", StringComparison.Ordinal); + if (colonIdx > -1 && !className.StartsWith("::")) + { + psedoClass = colonIdx < className.Length - 1 ? className.Substring(colonIdx + 1).Trim() : null; + className = className.Substring(0, colonIdx).Trim(); + } + + if (!string.IsNullOrEmpty(className) && (psedoClass == null || psedoClass == "link" || psedoClass == "hover")) + { + string firstClass; + var selectors = ParseCssBlockSelector(className, out firstClass); + + var properties = ParseCssBlockProperties(blockSource); + + return new CssBlock(firstClass, properties, selectors, psedoClass == "hover"); + } + + return null; + } + + /// + /// Parse css block selector to support hierarchical selector (p class1 > class2). + /// + /// the class selector to parse + /// return the main class the css block is on + /// returns the hierarchy of classes or null if single class selector + private static List ParseCssBlockSelector(string className, out string firstClass) + { + List selectors = null; + + firstClass = null; + int endIdx = className.Length - 1; + while (endIdx > -1) + { + bool directParent = false; + while (char.IsWhiteSpace(className[endIdx]) || className[endIdx] == '>') + { + directParent = directParent || className[endIdx] == '>'; + endIdx--; + } + + var startIdx = endIdx; + while (startIdx > -1 && !char.IsWhiteSpace(className[startIdx]) && className[startIdx] != '>') + startIdx--; + + if (startIdx > -1) + { + if (selectors == null) + selectors = new List(); + + var subclass = className.Substring(startIdx + 1, endIdx - startIdx); + + if (firstClass == null) + { + firstClass = subclass; + } + else + { + while (char.IsWhiteSpace(className[startIdx]) || className[startIdx] == '>') + startIdx--; + selectors.Add(new CssBlockSelectorItem(subclass, directParent)); + } + } + else if (firstClass != null) + { + selectors.Add(new CssBlockSelectorItem(className.Substring(0, endIdx + 1), directParent)); + } + + endIdx = startIdx; + } + + firstClass = firstClass ?? className; + return selectors; + } + + /// + /// Parse the properties of the given css block into a key-value dictionary. + /// + /// the raw css block to parse + /// dictionary with parsed css block properties + private Dictionary ParseCssBlockProperties(string blockSource) + { + var properties = new Dictionary(); + int startIdx = 0; + while (startIdx < blockSource.Length) + { + int endIdx = blockSource.IndexOfAny(_cssBlockSplitters, startIdx); + + // If blockSource contains "data:image" then skip first semicolon since it is a part of image definition + // example: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA......" + if (startIdx >= 0 && endIdx - startIdx >= 10 && blockSource.Length - startIdx >= 10 && blockSource.IndexOf("data:image", startIdx, endIdx - startIdx) >= 0) + endIdx = blockSource.IndexOfAny(_cssBlockSplitters, endIdx + 1); + + if (endIdx < 0) + endIdx = blockSource.Length - 1; + + var splitIdx = blockSource.IndexOf(':', startIdx, endIdx - startIdx); + if (splitIdx > -1) + { + //Extract property name and value + startIdx = startIdx + (blockSource[startIdx] == ' ' ? 1 : 0); + var adjEndIdx = endIdx - (blockSource[endIdx] == ' ' || blockSource[endIdx] == ';' ? 1 : 0); + string propName = blockSource.Substring(startIdx, splitIdx - startIdx).Trim().ToLower(); + splitIdx = splitIdx + (blockSource[splitIdx + 1] == ' ' ? 2 : 1); + if (adjEndIdx >= splitIdx) + { + string propValue = blockSource.Substring(splitIdx, adjEndIdx - splitIdx + 1).Trim(); + if (!propValue.StartsWith("url", StringComparison.InvariantCultureIgnoreCase)) + propValue = propValue.ToLower(); + AddProperty(propName, propValue, properties); + } + } + startIdx = endIdx + 1; + } + return properties; + } + + /// + /// Add the given property to the given properties collection, if the property is complex containing + /// multiple css properties then parse them and add the inner properties. + /// + /// the name of the css property to add + /// the value of the css property to add + /// the properties collection to add to + private void AddProperty(string propName, string propValue, Dictionary properties) + { + // remove !important css crap + propValue = propValue.Replace("!important", string.Empty).Trim(); + + if (propName == "width" || propName == "height" || propName == "lineheight") + { + ParseLengthProperty(propName, propValue, properties); + } + else if (propName == "color" || propName == "backgroundcolor" || propName == "bordertopcolor" || propName == "borderbottomcolor" || propName == "borderleftcolor" || propName == "borderrightcolor") + { + ParseColorProperty(propName, propValue, properties); + } + else if (propName == "font") + { + ParseFontProperty(propValue, properties); + } + else if (propName == "border") + { + ParseBorderProperty(propValue, null, properties); + } + else if (propName == "border-left") + { + ParseBorderProperty(propValue, "-left", properties); + } + else if (propName == "border-top") + { + ParseBorderProperty(propValue, "-top", properties); + } + else if (propName == "border-right") + { + ParseBorderProperty(propValue, "-right", properties); + } + else if (propName == "border-bottom") + { + ParseBorderProperty(propValue, "-bottom", properties); + } + else if (propName == "margin") + { + ParseMarginProperty(propValue, properties); + } + else if (propName == "border-style") + { + ParseBorderStyleProperty(propValue, properties); + } + else if (propName == "border-width") + { + ParseBorderWidthProperty(propValue, properties); + } + else if (propName == "border-color") + { + ParseBorderColorProperty(propValue, properties); + } + else if (propName == "padding") + { + ParsePaddingProperty(propValue, properties); + } + else if (propName == "background-image") + { + properties["background-image"] = ParseImageProperty(propValue); + } + else if (propName == "content") + { + properties["content"] = ParseImageProperty(propValue); + } + else if (propName == "font-family") + { + properties["font-family"] = ParseFontFamilyProperty(propValue); + } + else + { + properties[propName] = propValue; + } + } + + /// + /// Parse length property to add only valid lengths. + /// + /// the name of the css property to add + /// the value of the css property to add + /// the properties collection to add to + private static void ParseLengthProperty(string propName, string propValue, Dictionary properties) + { + if (CssValueParser.IsValidLength(propValue) || propValue.Equals(CssConstants.Auto, StringComparison.OrdinalIgnoreCase)) + { + properties[propName] = propValue; + } + } + + /// + /// Parse color property to add only valid color. + /// + /// the name of the css property to add + /// the value of the css property to add + /// the properties collection to add to + private void ParseColorProperty(string propName, string propValue, Dictionary properties) + { + if (_valueParser.IsColorValid(propValue)) + { + properties[propName] = propValue; + } + } + + /// + /// Parse a complex font property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private void ParseFontProperty(string propValue, Dictionary properties) + { + int mustBePos; + string mustBe = RegexParserUtils.Search(RegexParserUtils.CssFontSizeAndLineHeight, propValue, out mustBePos); + + if (!string.IsNullOrEmpty(mustBe)) + { + mustBe = mustBe.Trim(); + //Check for style||variant||weight on the left + string leftSide = propValue.Substring(0, mustBePos); + string fontStyle = RegexParserUtils.Search(RegexParserUtils.CssFontStyle, leftSide); + string fontVariant = RegexParserUtils.Search(RegexParserUtils.CssFontVariant, leftSide); + string fontWeight = RegexParserUtils.Search(RegexParserUtils.CssFontWeight, leftSide); + + //Check for family on the right + string rightSide = propValue.Substring(mustBePos + mustBe.Length); + string fontFamily = rightSide.Trim(); //Parser.Search(Parser.CssFontFamily, rightSide); //TODO: Would this be right? + + //Check for font-size and line-height + string fontSize = mustBe; + string lineHeight = string.Empty; + + if (mustBe.Contains("/") && mustBe.Length > mustBe.IndexOf("/", StringComparison.Ordinal) + 1) + { + int slashPos = mustBe.IndexOf("/", StringComparison.Ordinal); + fontSize = mustBe.Substring(0, slashPos); + lineHeight = mustBe.Substring(slashPos + 1); + } + + if (!string.IsNullOrEmpty(fontFamily)) + properties["font-family"] = ParseFontFamilyProperty(fontFamily); + if (!string.IsNullOrEmpty(fontStyle)) + properties["font-style"] = fontStyle; + if (!string.IsNullOrEmpty(fontVariant)) + properties["font-variant"] = fontVariant; + if (!string.IsNullOrEmpty(fontWeight)) + properties["font-weight"] = fontWeight; + if (!string.IsNullOrEmpty(fontSize)) + properties["font-size"] = fontSize; + if (!string.IsNullOrEmpty(lineHeight)) + properties["line-height"] = lineHeight; + } + else + { + // Check for: caption | icon | menu | message-box | small-caption | status-bar + //TODO: Interpret font values of: caption | icon | menu | message-box | small-caption | status-bar + } + } + + /// + /// + /// + /// the value of the property to parse + /// parsed value + private static string ParseImageProperty(string propValue) + { + int startIdx = propValue.IndexOf("url(", StringComparison.InvariantCultureIgnoreCase); + if (startIdx > -1) + { + startIdx += 4; + var endIdx = propValue.IndexOf(')', startIdx); + if (endIdx > -1) + { + endIdx -= 1; + while (startIdx < endIdx && (char.IsWhiteSpace(propValue[startIdx]) || propValue[startIdx] == '\'' || propValue[startIdx] == '"')) + startIdx++; + while (startIdx < endIdx && (char.IsWhiteSpace(propValue[endIdx]) || propValue[endIdx] == '\'' || propValue[endIdx] == '"')) + endIdx--; + + if (startIdx <= endIdx) + return propValue.Substring(startIdx, endIdx - startIdx + 1); + } + } + return propValue; + } + + /// + /// Parse a complex font family css property to check if it contains multiple fonts and if the font exists.
+ /// returns the font family name to use or 'inherit' if failed. + ///
+ /// the value of the property to parse + /// parsed font-family value + private string ParseFontFamilyProperty(string propValue) + { + int start = 0; + while (start > -1 && start < propValue.Length) + { + while (char.IsWhiteSpace(propValue[start]) || propValue[start] == ',' || propValue[start] == '\'' || propValue[start] == '"') + start++; + var end = propValue.IndexOf(',', start); + if (end < 0) + end = propValue.Length; + var adjEnd = end - 1; + while (char.IsWhiteSpace(propValue[adjEnd]) || propValue[adjEnd] == '\'' || propValue[adjEnd] == '"') + adjEnd--; + + var font = propValue.Substring(start, adjEnd - start + 1); + + if (_adapter.IsFontExists(font)) + { + return font; + } + + start = end; + } + + return CssConstants.Inherit; + } + + /// + /// Parse a complex border property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the left, top, right or bottom direction of the border to parse + /// the properties collection to add the specific properties to + private void ParseBorderProperty(string propValue, string direction, Dictionary properties) + { + string borderWidth; + string borderStyle; + string borderColor; + ParseBorder(propValue, out borderWidth, out borderStyle, out borderColor); + + if (direction != null) + { + if (borderWidth != null) + properties["border" + direction + "-width"] = borderWidth; + if (borderStyle != null) + properties["border" + direction + "-style"] = borderStyle; + if (borderColor != null) + properties["border" + direction + "-color"] = borderColor; + } + else + { + if (borderWidth != null) + ParseBorderWidthProperty(borderWidth, properties); + if (borderStyle != null) + ParseBorderStyleProperty(borderStyle, properties); + if (borderColor != null) + ParseBorderColorProperty(borderColor, properties); + } + } + + /// + /// Parse a complex margin property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private static void ParseMarginProperty(string propValue, Dictionary properties) + { + string bottom, top, left, right; + SplitMultiDirectionValues(propValue, out left, out top, out right, out bottom); + + if (left != null) + properties["margin-left"] = left; + if (top != null) + properties["margin-top"] = top; + if (right != null) + properties["margin-right"] = right; + if (bottom != null) + properties["margin-bottom"] = bottom; + } + + /// + /// Parse a complex border style property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private static void ParseBorderStyleProperty(string propValue, Dictionary properties) + { + string bottom, top, left, right; + SplitMultiDirectionValues(propValue, out left, out top, out right, out bottom); + + if (left != null) + properties["border-left-style"] = left; + if (top != null) + properties["border-top-style"] = top; + if (right != null) + properties["border-right-style"] = right; + if (bottom != null) + properties["border-bottom-style"] = bottom; + } + + /// + /// Parse a complex border width property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private static void ParseBorderWidthProperty(string propValue, Dictionary properties) + { + string bottom, top, left, right; + SplitMultiDirectionValues(propValue, out left, out top, out right, out bottom); + + if (left != null) + properties["border-left-width"] = left; + if (top != null) + properties["border-top-width"] = top; + if (right != null) + properties["border-right-width"] = right; + if (bottom != null) + properties["border-bottom-width"] = bottom; + } + + /// + /// Parse a complex border color property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private static void ParseBorderColorProperty(string propValue, Dictionary properties) + { + string bottom, top, left, right; + SplitMultiDirectionValues(propValue, out left, out top, out right, out bottom); + + if (left != null) + properties["border-left-color"] = left; + if (top != null) + properties["border-top-color"] = top; + if (right != null) + properties["border-right-color"] = right; + if (bottom != null) + properties["border-bottom-color"] = bottom; + } + + /// + /// Parse a complex padding property value that contains multiple css properties into specific css properties. + /// + /// the value of the property to parse to specific values + /// the properties collection to add the specific properties to + private static void ParsePaddingProperty(string propValue, Dictionary properties) + { + string bottom, top, left, right; + SplitMultiDirectionValues(propValue, out left, out top, out right, out bottom); + + if (left != null) + properties["padding-left"] = left; + if (top != null) + properties["padding-top"] = top; + if (right != null) + properties["padding-right"] = right; + if (bottom != null) + properties["padding-bottom"] = bottom; + } + + /// + /// Split multi direction value into the proper direction values (left, top, right, bottom). + /// + private static void SplitMultiDirectionValues(string propValue, out string left, out string top, out string right, out string bottom) + { + top = null; + left = null; + right = null; + bottom = null; + string[] values = SplitValues(propValue); + switch (values.Length) + { + case 1: + top = left = right = bottom = values[0]; + break; + case 2: + top = bottom = values[0]; + left = right = values[1]; + break; + case 3: + top = values[0]; + left = right = values[1]; + bottom = values[2]; + break; + case 4: + top = values[0]; + right = values[1]; + bottom = values[2]; + left = values[3]; + break; + } + } + + /// + /// Split the value by the specified separator; e.g. Useful in values like 'padding:5 4 3 inherit' + /// + /// Value to be splitted + /// + /// Splitted and trimmed values + private static string[] SplitValues(string value, char separator = ' ') + { + //TODO: CRITICAL! Don't split values on parenthesis (like rgb(0, 0, 0)) or quotes ("strings") + + if (!string.IsNullOrEmpty(value)) + { + string[] values = value.Split(separator); + List result = new List(); + + foreach (string t in values) + { + string val = t.Trim(); + + if (!string.IsNullOrEmpty(val)) + { + result.Add(val); + } + } + + return result.ToArray(); + } + + return new string[0]; + } + + /// + /// + /// + /// + /// + /// + /// + public void ParseBorder(string value, out string width, out string style, out string color) + { + width = style = color = null; + if (!string.IsNullOrEmpty(value)) + { + int idx = 0; + int length; + while ((idx = CommonUtils.GetNextSubString(value, idx, out length)) > -1) + { + if (width == null) + width = ParseBorderWidth(value, idx, length); + if (style == null) + style = ParseBorderStyle(value, idx, length); + if (color == null) + color = ParseBorderColor(value, idx, length); + idx = idx + length + 1; + } + } + } + + /// + /// Parse the given substring to extract border width substring. + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// found border width value or null + private static string ParseBorderWidth(string str, int idx, int length) + { + if ((length > 2 && char.IsDigit(str[idx])) || (length > 3 && str[idx] == '.')) + { + string unit = null; + if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Px)) + unit = CssConstants.Px; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Pt)) + unit = CssConstants.Pt; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Em)) + unit = CssConstants.Em; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Ex)) + unit = CssConstants.Ex; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.In)) + unit = CssConstants.In; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Cm)) + unit = CssConstants.Cm; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Mm)) + unit = CssConstants.Mm; + else if (CommonUtils.SubStringEquals(str, idx + length - 2, 2, CssConstants.Pc)) + unit = CssConstants.Pc; + + if (unit != null) + { + if (CssValueParser.IsFloat(str, idx, length - 2)) + return str.Substring(idx, length); + } + } + else + { + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Thin)) + return CssConstants.Thin; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Medium)) + return CssConstants.Medium; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Thick)) + return CssConstants.Thick; + } + return null; + } + + /// + /// Parse the given substring to extract border style substring.
+ /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// found border width value or null + private static string ParseBorderStyle(string str, int idx, int length) + { + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.None)) + return CssConstants.None; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Solid)) + return CssConstants.Solid; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Hidden)) + return CssConstants.Hidden; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Dotted)) + return CssConstants.Dotted; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Dashed)) + return CssConstants.Dashed; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Double)) + return CssConstants.Double; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Groove)) + return CssConstants.Groove; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Ridge)) + return CssConstants.Ridge; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Inset)) + return CssConstants.Inset; + if (CommonUtils.SubStringEquals(str, idx, length, CssConstants.Outset)) + return CssConstants.Outset; + return null; + } + + /// + /// Parse the given substring to extract border style substring.
+ /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// found border width value or null + private string ParseBorderColor(string str, int idx, int length) + { + RColor color; + return _valueParser.TryGetColor(str, idx, length, out color) ? str.Substring(idx, length) : null; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Parse/CssValueParser.cs b/Source/HtmlRenderer.Core/Core/Parse/CssValueParser.cs new file mode 100644 index 000000000..b7c721b28 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Parse/CssValueParser.cs @@ -0,0 +1,543 @@ +// "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 TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Dom; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core.Parse +{ + /// + /// Parse CSS properties values like numbers, Urls, etc. + /// + internal sealed class CssValueParser + { + #region Fields and Consts + + /// + /// + /// + private readonly RAdapter _adapter; + + #endregion + + + /// + /// Init. + /// + public CssValueParser(RAdapter adapter) + { + ArgChecker.AssertArgNotNull(adapter, "global"); + + _adapter = adapter; + } + + /// + /// Check if the given substring is a valid double number. + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// true - valid double number, false - otherwise + public static bool IsFloat(string str, int idx, int length) + { + if (length < 1) + return false; + + bool sawDot = false; + for (int i = 0; i < length; i++) + { + if (str[idx + i] == '.') + { + if (sawDot) + return false; + sawDot = true; + } + else if (!char.IsDigit(str[idx + i])) + { + return false; + } + } + return true; + } + + /// + /// Check if the given substring is a valid double number. + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// true - valid int number, false - otherwise + public static bool IsInt(string str, int idx, int length) + { + if (length < 1) + return false; + + for (int i = 0; i < length; i++) + { + if (!char.IsDigit(str[idx + i])) + return false; + } + return true; + } + + /// + /// Check if the given string is a valid length value. + /// + /// the string value to check + /// true - valid, false - invalid + public static bool IsValidLength(string value) + { + if (value.Length > 1) + { + string number = string.Empty; + if (value.EndsWith("%")) + { + number = value.Substring(0, value.Length - 1); + } + else if (value.Length > 2) + { + number = value.Substring(0, value.Length - 2); + } + double stub; + return double.TryParse(number, out stub); + } + return false; + } + + /// + /// Evals a number and returns it. If number is a percentage, it will be multiplied by + /// + /// Number to be parsed + /// Number that represents the 100% if parsed number is a percentage + /// Parsed number. Zero if error while parsing. + public static double ParseNumber(string number, double hundredPercent) + { + if (string.IsNullOrEmpty(number)) + { + return 0f; + } + + string toParse = number; + bool isPercent = number.EndsWith("%"); + double result; + + if (isPercent) + toParse = number.Substring(0, number.Length - 1); + + if (!double.TryParse(toParse, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out result)) + { + return 0f; + } + + if (isPercent) + { + result = (result / 100f) * hundredPercent; + } + + return result; + } + + /// + /// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em) + /// + /// Specified length + /// Equivalent to 100 percent when length is percentage + /// if the length is in pixels and the length is font related it needs to use 72/96 factor + /// + /// the parsed length value with adjustments + public static double ParseLength(string length, double hundredPercent, CssBoxProperties box, bool fontAdjust = false) + { + return ParseLength(length, hundredPercent, box.GetEmHeight(), null, fontAdjust, false); + } + + /// + /// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em) + /// + /// Specified length + /// Equivalent to 100 percent when length is percentage + /// + /// + /// the parsed length value with adjustments + public static double ParseLength(string length, double hundredPercent, CssBoxProperties box, string defaultUnit) + { + return ParseLength(length, hundredPercent, box.GetEmHeight(), defaultUnit, false, false); + } + + /// + /// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em) + /// + /// Specified length + /// Equivalent to 100 percent when length is percentage + /// + /// + /// if the length is in pixels and the length is font related it needs to use 72/96 factor + /// Allows the return double to be in points. If false, result will be pixels + /// the parsed length value with adjustments + public static double ParseLength(string length, double hundredPercent, double emFactor, string defaultUnit, bool fontAdjust, bool returnPoints) + { + //Return zero if no length specified, zero specified + if (string.IsNullOrEmpty(length) || length == "0") + return 0f; + + //If percentage, use ParseNumber + if (length.EndsWith("%")) + return ParseNumber(length, hundredPercent); + + //Get units of the length + bool hasUnit; + string unit = GetUnit(length, defaultUnit, out hasUnit); + + //Factor will depend on the unit + double factor; + + //Number of the length + string number = hasUnit ? length.Substring(0, length.Length - 2) : length; + + //TODO: Units behave different in paper and in screen! + switch (unit) + { + case CssConstants.Em: + factor = emFactor; + break; + case CssConstants.Ex: + factor = emFactor / 2; + break; + case CssConstants.Px: + factor = fontAdjust ? 72f / 96f : 1f; //TODO:a check support for hi dpi + break; + case CssConstants.Mm: + factor = 3.779527559f; //3 pixels per millimeter + break; + case CssConstants.Cm: + factor = 37.795275591f; //37 pixels per centimeter + break; + case CssConstants.In: + factor = 96f; //96 pixels per inch + break; + case CssConstants.Pt: + factor = 96f / 72f; // 1 point = 1/72 of inch + + if (returnPoints) + { + return ParseNumber(number, hundredPercent); + } + + break; + case CssConstants.Pc: + factor = 16f; // 1 pica = 12 points + break; + default: + factor = 0f; + break; + } + + return factor * ParseNumber(number, hundredPercent); + } + + /// + /// Get the unit to use for the length, use default if no unit found in length string. + /// + private static string GetUnit(string length, string defaultUnit, out bool hasUnit) + { + var unit = length.Length >= 3 ? length.Substring(length.Length - 2, 2) : string.Empty; + switch (unit) + { + case CssConstants.Em: + case CssConstants.Ex: + case CssConstants.Px: + case CssConstants.Mm: + case CssConstants.Cm: + case CssConstants.In: + case CssConstants.Pt: + case CssConstants.Pc: + hasUnit = true; + break; + default: + hasUnit = false; + unit = defaultUnit ?? String.Empty; + break; + } + return unit; + } + + /// + /// Check if the given color string value is valid. + /// + /// color string value to parse + /// true - valid, false - invalid + public bool IsColorValid(string colorValue) + { + RColor color; + return TryGetColor(colorValue, 0, colorValue.Length, out color); + } + + /// + /// Parses a color value in CSS style; e.g. #ff0000, red, rgb(255,0,0), rgb(100%, 0, 0) + /// + /// color string value to parse + /// Color value + public RColor GetActualColor(string colorValue) + { + RColor color; + TryGetColor(colorValue, 0, colorValue.Length, out color); + return color; + } + + /// + /// Parses a color value in CSS style; e.g. #ff0000, RED, RGB(255,0,0), RGB(100%, 0, 0) + /// + /// color substring value to parse + /// substring start idx + /// substring length + /// return the parsed color + /// true - valid color, false - otherwise + public bool TryGetColor(string str, int idx, int length, out RColor color) + { + try + { + if (!string.IsNullOrEmpty(str)) + { + if (length > 1 && str[idx] == '#') + { + return GetColorByHex(str, idx, length, out color); + } + else if (length > 10 && CommonUtils.SubStringEquals(str, idx, 4, "rgb(") && str[length - 1] == ')') + { + return GetColorByRgb(str, idx, length, out color); + } + else if (length > 13 && CommonUtils.SubStringEquals(str, idx, 5, "rgba(") && str[length - 1] == ')') + { + return GetColorByRgba(str, idx, length, out color); + } + else + { + return GetColorByName(str, idx, length, out color); + } + } + } + catch + { } + color = RColor.Black; + return false; + } + + /// + /// Parses a border value in CSS style; e.g. 1px, 1, thin, thick, medium + /// + /// + /// + /// + public static double GetActualBorderWidth(string borderValue, CssBoxProperties b) + { + if (string.IsNullOrEmpty(borderValue)) + { + return GetActualBorderWidth(CssConstants.Medium, b); + } + + switch (borderValue) + { + case CssConstants.Thin: + return 1f; + case CssConstants.Medium: + return 2f; + case CssConstants.Thick: + return 4f; + default: + return Math.Abs(ParseLength(borderValue, 1, b)); + } + } + + + #region Private methods + + /// + /// Get color by parsing given hex value color string (#A28B34). + /// + /// true - valid color, false - otherwise + private static bool GetColorByHex(string str, int idx, int length, out RColor color) + { + int r = -1; + int g = -1; + int b = -1; + if (length == 7) + { + r = ParseHexInt(str, idx + 1, 2); + g = ParseHexInt(str, idx + 3, 2); + b = ParseHexInt(str, idx + 5, 2); + } + else if (length == 4) + { + r = ParseHexInt(str, idx + 1, 1); + r = r * 16 + r; + g = ParseHexInt(str, idx + 2, 1); + g = g * 16 + g; + b = ParseHexInt(str, idx + 3, 1); + b = b * 16 + b; + } + if (r > -1 && g > -1 && b > -1) + { + color = RColor.FromArgb(r, g, b); + return true; + } + color = RColor.Empty; + return false; + } + + /// + /// Get color by parsing given RGB value color string (RGB(255,180,90)) + /// + /// true - valid color, false - otherwise + private static bool GetColorByRgb(string str, int idx, int length, out RColor color) + { + int r = -1; + int g = -1; + int b = -1; + + if (length > 10) + { + int s = idx + 4; + r = ParseIntAtIndex(str, ref s); + if (s < idx + length) + { + g = ParseIntAtIndex(str, ref s); + } + if (s < idx + length) + { + b = ParseIntAtIndex(str, ref s); + } + } + + if (r > -1 && g > -1 && b > -1) + { + color = RColor.FromArgb(r, g, b); + return true; + } + color = RColor.Empty; + return false; + } + + /// + /// Get color by parsing given RGBA value color string (RGBA(255,180,90,180)) + /// + /// true - valid color, false - otherwise + private static bool GetColorByRgba(string str, int idx, int length, out RColor color) + { + int r = -1; + int g = -1; + int b = -1; + int a = -1; + + if (length > 13) + { + int s = idx + 5; + r = ParseIntAtIndex(str, ref s); + + if (s < idx + length) + { + g = ParseIntAtIndex(str, ref s); + } + if (s < idx + length) + { + b = ParseIntAtIndex(str, ref s); + } + if (s < idx + length) + { + a = ParseIntAtIndex(str, ref s); + } + } + + if (r > -1 && g > -1 && b > -1 && a > -1) + { + color = RColor.FromArgb(a, r, g, b); + return true; + } + color = RColor.Empty; + return false; + } + + /// + /// Get color by given name, including .NET name. + /// + /// true - valid color, false - otherwise + private bool GetColorByName(string str, int idx, int length, out RColor color) + { + color = _adapter.GetColor(str.Substring(idx, length)); + return color.A > 0; + } + + /// + /// Parse the given decimal number string to positive int value.
+ /// Start at given , ignore whitespaces and take + /// as many digits as possible to parse to int. + ///
+ /// the string to parse + /// the index to start parsing at + /// parsed int or 0 + private static int ParseIntAtIndex(string str, ref int startIdx) + { + int len = 0; + while (char.IsWhiteSpace(str, startIdx)) + startIdx++; + while (char.IsDigit(str, startIdx + len)) + len++; + var val = ParseInt(str, startIdx, len); + startIdx = startIdx + len + 1; + return val; + } + + /// + /// Parse the given decimal number string to positive int value. + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// int value, -1 if not valid + private static int ParseInt(string str, int idx, int length) + { + if (length < 1) + return -1; + + int num = 0; + for (int i = 0; i < length; i++) + { + int c = str[idx + i]; + if (!(c >= 48 && c <= 57)) + return -1; + + num = num * 10 + c - 48; + } + return num; + } + + /// + /// Parse the given hex number string to positive int value. + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// int value, -1 if not valid + private static int ParseHexInt(string str, int idx, int length) + { + if (length < 1) + return -1; + + int num = 0; + for (int i = 0; i < length; i++) + { + int c = str[idx + i]; + if (!(c >= 48 && c <= 57) && !(c >= 65 && c <= 70) && !(c >= 97 && c <= 102)) + return -1; + + num = num * 16 + (c <= 57 ? c - 48 : (10 + c - (c <= 70 ? 65 : 97))); + } + return num; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Parse/DomParser.cs b/Source/HtmlRenderer.Core/Core/Parse/DomParser.cs new file mode 100644 index 000000000..d841485f0 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Parse/DomParser.cs @@ -0,0 +1,900 @@ +// "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 TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Dom; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Handlers; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.Core.Parse +{ + /// + /// Handle css DOM tree generation from raw html and stylesheet. + /// + internal sealed class DomParser + { + #region Fields and Consts + + /// + /// Parser for CSS + /// + private readonly CssParser _cssParser; + + #endregion + + + /// + /// Init. + /// + public DomParser(CssParser cssParser) + { + ArgChecker.AssertArgNotNull(cssParser, "cssParser"); + + _cssParser = cssParser; + } + + /// + /// Generate css tree by parsing the given html and applying the given css style data on it. + /// + /// the html to parse + /// the html container to use for reference resolve + /// the css data to use + /// the root of the generated tree + public CssBox GenerateCssTree(string html, HtmlContainerInt htmlContainer, ref CssData cssData) + { + var root = HtmlParser.ParseDocument(html); + if (root != null) + { + root.HtmlContainer = htmlContainer; + + bool cssDataChanged = false; + CascadeParseStyles(root, htmlContainer, ref cssData, ref cssDataChanged); + + CascadeApplyStyles(root, cssData); + + SetTextSelectionStyle(htmlContainer, cssData); + + CorrectTextBoxes(root); + + CorrectImgBoxes(root); + + bool followingBlock = true; + CorrectLineBreaksBlocks(root, ref followingBlock); + + CorrectInlineBoxesParent(root); + + CorrectBlockInsideInline(root); + + CorrectInlineBoxesParent(root); + } + return root; + } + + + #region Private methods + + /// + /// Read styles defined inside the dom structure in links and style elements.
+ /// If the html tag is "style" tag parse it content and add to the css data for all future tags parsing.
+ /// If the html tag is "link" that point to style data parse it content and add to the css data for all future tags parsing.
+ ///
+ /// the box to parse style data in + /// the html container to use for reference resolve + /// the style data to fill with found styles + /// check if the css data has been modified by the handled html not to change the base css data + private void CascadeParseStyles(CssBox box, HtmlContainerInt htmlContainer, ref CssData cssData, ref bool cssDataChanged) + { + if (box.HtmlTag != null) + { + // Check for the tag + if (box.HtmlTag.Name.Equals("link", StringComparison.CurrentCultureIgnoreCase) && + box.GetAttribute("rel", string.Empty).Equals("stylesheet", StringComparison.CurrentCultureIgnoreCase)) + { + CloneCssData(ref cssData, ref cssDataChanged); + string stylesheet; + CssData stylesheetData; + StylesheetLoadHandler.LoadStylesheet(htmlContainer, box.GetAttribute("href", string.Empty), box.HtmlTag.Attributes, out stylesheet, out stylesheetData); + if (stylesheet != null) + _cssParser.ParseStyleSheet(cssData, stylesheet); + else if (stylesheetData != null) + cssData.Combine(stylesheetData); + } + + // Check for the ", endIdx, StringComparison.OrdinalIgnoreCase); + if (endIdx > -1) + AddTextBox(source, endIdxS, endIdx, ref curBox); + } + } + } + startIdx = tagIdx > -1 && endIdx > 0 ? endIdx : -1; + } + + // handle pieces of html without proper structure + if (endIdx > -1 && endIdx < source.Length) + { + // there is text after the end of last element + var endText = new SubString(source, endIdx, source.Length - endIdx); + if (!endText.IsEmptyOrWhitespace()) + { + var abox = CssBox.CreateBox(root); + abox.Text = endText; + } + } + + return root; + } + + + #region Private methods + + /// + /// Add html text anon box to the current box, this box will have the rendered text
+ /// Adding box also for text that contains only whitespaces because we don't know yet if + /// the box is preformatted. At later stage they will be removed if not relevant. + ///
+ /// the html source to parse + /// the start of the html part + /// the index of the next html tag + /// the current box in html tree parsing + private static void AddTextBox(string source, int startIdx, int tagIdx, ref CssBox curBox) + { + var text = tagIdx > startIdx ? new SubString(source, startIdx, tagIdx - startIdx) : null; + if (text != null) + { + var abox = CssBox.CreateBox(curBox); + abox.Text = text; + } + } + + /// + /// Parse the html part, the part from prev parsing index to the beginning of the next html tag.
+ ///
+ /// the html source to parse + /// the index of the next html tag + /// the current box in html tree parsing + /// the end of the parsed part, the new start index + private static int ParseHtmlTag(string source, int tagIdx, ref CssBox curBox) + { + var endIdx = source.IndexOf('>', tagIdx + 1); + if (endIdx > 0) + { + string tagName; + Dictionary tagAttributes; + var length = endIdx - tagIdx + 1 - (source[endIdx - 1] == '/' ? 1 : 0); + if (ParseHtmlTag(source, tagIdx, length, out tagName, out tagAttributes)) + { + if (!HtmlUtils.IsSingleTag(tagName) && curBox.ParentBox != null) + { + // need to find the parent tag to go one level up + curBox = DomUtils.FindParent(curBox.ParentBox, tagName, curBox); + } + } + else if (!string.IsNullOrEmpty(tagName)) + { + //new SubString(source, lastEnd + 1, tagmatch.Index - lastEnd - 1) + var isSingle = HtmlUtils.IsSingleTag(tagName) || source[endIdx - 1] == '/'; + var tag = new HtmlTag(tagName, isSingle, tagAttributes); + + if (isSingle) + { + // the current box is not changed + CssBox.CreateBox(tag, curBox); + } + else + { + // go one level down, make the new box the current box + curBox = CssBox.CreateBox(tag, curBox); + } + } + else + { + endIdx = tagIdx + 1; + } + } + return endIdx; + } + + /// + /// Parse raw html tag source to object.
+ /// Extract attributes found on the tag. + ///
+ /// the html source to parse + /// the start index of the tag in the source + /// the length of the tag from the start index in the source + /// return the name of the html tag + /// return the dictionary of tag attributes + /// true - the tag is closing tag, false - otherwise + private static bool ParseHtmlTag(string source, int idx, int length, out string name, out Dictionary attributes) + { + idx++; + length = length - (source[idx + length - 3] == '/' ? 3 : 2); + + // Check if is end tag + var isClosing = false; + if (source[idx] == '/') + { + idx++; + length--; + isClosing = true; + } + + int spaceIdx = idx; + while (spaceIdx < idx + length && !char.IsWhiteSpace(source, spaceIdx)) + spaceIdx++; + + // Get the name of the tag + name = source.Substring(idx, spaceIdx - idx).ToLower(); + + attributes = null; + if (!isClosing && idx + length > spaceIdx) + { + ExtractAttributes(source, spaceIdx, length - (spaceIdx - idx), out attributes); + } + + return isClosing; + } + + /// + /// Extract html tag attributes from the given sub-string. + /// + /// the html source to parse + /// the start index of the tag attributes in the source + /// the length of the tag attributes from the start index in the source + /// return the dictionary of tag attributes + private static void ExtractAttributes(string source, int idx, int length, out Dictionary attributes) + { + attributes = null; + + int startIdx = idx; + while (startIdx < idx + length) + { + while (startIdx < idx + length && char.IsWhiteSpace(source, startIdx)) + startIdx++; + + var endIdx = startIdx + 1; + while (endIdx < idx + length && !char.IsWhiteSpace(source, endIdx) && source[endIdx] != '=') + endIdx++; + + if (startIdx < idx + length) + { + var key = source.Substring(startIdx, endIdx - startIdx); + var value = ""; + + startIdx = endIdx + 1; + while (startIdx < idx + length && (char.IsWhiteSpace(source, startIdx) || source[startIdx] == '=')) + startIdx++; + + bool hasPChar = false; + if (startIdx < idx + length) + { + char pChar = source[startIdx]; + if (pChar == '"' || pChar == '\'') + { + hasPChar = true; + startIdx++; + } + + endIdx = startIdx + (hasPChar ? 0 : 1); + while (endIdx < idx + length && (hasPChar ? source[endIdx] != pChar : !char.IsWhiteSpace(source, endIdx))) + endIdx++; + + value = source.Substring(startIdx, endIdx - startIdx); + value = HtmlUtils.DecodeHtml(value); + } + + if (key.Length != 0) + { + if (attributes == null) + attributes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + attributes[key.ToLower()] = value; + } + + startIdx = endIdx + (hasPChar ? 2 : 1); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Parse/RegexParserHelper.cs b/Source/HtmlRenderer.Core/Core/Parse/RegexParserHelper.cs new file mode 100644 index 000000000..86a690b50 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Parse/RegexParserHelper.cs @@ -0,0 +1,227 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they bagin 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 System.Text.RegularExpressions; + +namespace HtmlRenderer.Parse +{ + /// + /// Collection of regular expressions used when parsing + /// + internal static class RegexParserHelper + { + #region Fields and Consts + + /// + /// Extracts CSS style comments; e.g. /* comment */ + /// + public const string CssComments = @"/\*[^*/]*\*/"; + + /// + /// Extracts the media types from a media at-rule; e.g. @media print, 3d, screen { + /// + public const string CssMediaTypes = @"@media[^\{\}]*\{"; + + /// + /// Extracts defined blocks in CSS. + /// WARNING: Blocks will include blocks inside at-rules. + /// + public const string CssBlocks = @"[^\{\}]*\{[^\{\}]*\}"; + + /// + /// Extracts a number; e.g. 5, 6, 7.5, 0.9 + /// + public const string CssNumber = @"{[0-9]+|[0-9]*\.[0-9]+}"; + + /// + /// Extracts css percentages from the string; e.g. 100% .5% 5.4% + /// + public const string CssPercentage = @"([0-9]+|[0-9]*\.[0-9]+)\%"; //TODO: Check if works fine + + /// + /// Extracts CSS lengths; e.g. 9px 3pt .89em + /// + public const string CssLength = @"([0-9]+|[0-9]*\.[0-9]+)(em|ex|px|in|cm|mm|pt|pc)"; + + /// + /// Extracts CSS colors; e.g. black white #fff #fe98cd rgb(5,5,5) rgb(45%, 0, 0) + /// + public const string CssColors = @"(#\S{6}|#\S{3}|rgb\(\s*[0-9]{1,3}\%?\s*\,\s*[0-9]{1,3}\%?\s*\,\s*[0-9]{1,3}\%?\s*\)|maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray)"; + + /// + /// Extracts line-height values (normal, numbers, lengths, percentages) + /// + public const string CssLineHeight = "(normal|" + CssNumber + "|" + CssLength + "|" + CssPercentage + ")"; + + /// + /// Extracts CSS border styles; e.g. solid none dotted + /// + public const string CssBorderStyle = @"(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)"; + + /// + /// Extracts CSS border widthe; e.g. 1px thin 3em + /// + public const string CssBorderWidth = "(" + CssLength + "|thin|medium|thick)"; + + /// + /// Extracts font-family values + /// + public const string CssFontFamily = "(\"[^\"]*\"|'[^']*'|\\S+\\s*)(\\s*\\,\\s*(\"[^\"]*\"|'[^']*'|\\S+))*"; + + /// + /// Extracts CSS font-styles; e.g. normal italic oblique + /// + public const string CssFontStyle = "(normal|italic|oblique)"; + + /// + /// Extracts CSS font-variant values; e.g. normal, small-caps + /// + public const string CssFontVariant = "(normal|small-caps)"; + + /// + /// Extracts font-weight values; e.g. normal, bold, bolder... + /// + public const string CssFontWeight = "(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)"; + + /// + /// Exracts font sizes: xx-small, larger, small, 34pt, 30%, 2em + /// + public const string CssFontSize = "(" + CssLength + "|" + CssPercentage + "|xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller)"; + + /// + /// Gets the font-size[/line-height]? on the font shorthand property. + /// Check http://www.w3.org/TR/CSS21/fonts.html#font-shorthand + /// + public const string CssFontSizeAndLineHeight = CssFontSize + @"(\/" + CssLineHeight + @")?(\s|$)"; + + /// + /// Extracts HTML tags + /// + public const string HtmlTag = @"<[^<>]*>"; + + /// + /// Extracts attributes from a HTML tag; e.g. att=value, att="value" + /// + public const string HmlTagAttributes = "(?\\b\\w+\\b)\\s*=\\s*(?\"[^\"]*\"|'[^']*'|[^\"'<>\\s]+)"; + + /// + /// the regexes cache that is used by the parser so not to create regex each time + /// + private static readonly Dictionary _regexes = new Dictionary(); + + #endregion + + + /// + /// Get CSS at rule from the given stylesheet. + /// + /// the stylesheet data to retrieve the rule from + /// the index to start the search for the rule, on return will be the value of the end of the found rule + /// the found at rule or null if not exists + public static string GetCssAtRules(string stylesheet, ref int startIdx) + { + startIdx = stylesheet.IndexOf('@', startIdx); + if (startIdx > -1) + { + int count = 1; + int endIdx = stylesheet.IndexOf('{', startIdx); + if (endIdx > -1) + { + while (count > 0 && endIdx < stylesheet.Length) + { + endIdx++; + if (stylesheet[endIdx] == '{') + { + count++; + } + else if (stylesheet[endIdx] == '}') + { + count--; + } + } + if (endIdx < stylesheet.Length) + { + var atrule = stylesheet.Substring(startIdx, endIdx - startIdx + 1); + startIdx = endIdx; + return atrule; + } + } + } + return null; + } + + /// + /// Extracts matches from the specified source + /// + /// Regular expression to extract matches + /// Source to extract matches + /// Collection of matches + public static MatchCollection Match(string regex, string source) + { + var r = GetRegex(regex); + return r.Matches(source); + } + + /// + /// Searches the specified regex on the source + /// + /// + /// + /// + public static string Search(string regex, string source) + { + int position; + return Search(regex, source, out position); + } + + /// + /// Searches the specified regex on the source + /// + /// + /// + /// + /// + public static string Search(string regex, string source, out int position) + { + MatchCollection matches = Match(regex, source); + + if (matches.Count > 0) + { + position = matches[0].Index; + return matches[0].Value; + } + else + { + position = -1; + } + + return null; + } + + /// + /// Get regex instance for the given regex string. + /// + /// the regex string to use + /// the regex instance + private static Regex GetRegex(string regex) + { + Regex r; + if (!_regexes.TryGetValue(regex, out r)) + { + r = new Regex(regex, RegexOptions.IgnoreCase | RegexOptions.Singleline); + _regexes[regex] = r; + } + return r; + } + } +} diff --git a/Source/HtmlRenderer.Core/Core/Parse/RegexParserUtils.cs b/Source/HtmlRenderer.Core/Core/Parse/RegexParserUtils.cs new file mode 100644 index 000000000..806fca4cf --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Parse/RegexParserUtils.cs @@ -0,0 +1,197 @@ +// "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 System.Text.RegularExpressions; + +namespace TheArtOfDev.HtmlRenderer.Core.Parse +{ + /// + /// Collection of regular expressions used when parsing + /// + internal static class RegexParserUtils + { + #region Fields and Consts + + /// + /// Extracts the media types from a media at-rule; e.g. @media print, 3d, screen { + /// + public const string CssMediaTypes = @"@media[^\{\}]*\{"; + + /// + /// Extracts defined blocks in CSS. + /// WARNING: Blocks will include blocks inside at-rules. + /// + public const string CssBlocks = @"[^\{\}]*\{[^\{\}]*\}"; + + /// + /// Extracts a number; e.g. 5, 6, 7.5, 0.9 + /// + public const string CssNumber = @"{[0-9]+|[0-9]*\.[0-9]+}"; + + /// + /// Extracts css percentages from the string; e.g. 100% .5% 5.4% + /// + public const string CssPercentage = @"([0-9]+|[0-9]*\.[0-9]+)\%"; + + /// + /// Extracts CSS lengths; e.g. 9px 3pt .89em + /// + public const string CssLength = @"([0-9]+|[0-9]*\.[0-9]+)(em|ex|px|in|cm|mm|pt|pc)"; + + /// + /// Extracts line-height values (normal, numbers, lengths, percentages) + /// + public const string CssLineHeight = "(normal|" + CssNumber + "|" + CssLength + "|" + CssPercentage + ")"; + + /// + /// Extracts font-family values + /// + public const string CssFontFamily = "(\"[^\"]*\"|'[^']*'|\\S+\\s*)(\\s*\\,\\s*(\"[^\"]*\"|'[^']*'|\\S+))*"; + + /// + /// Extracts CSS font-styles; e.g. normal italic oblique + /// + public const string CssFontStyle = "(normal|italic|oblique)"; + + /// + /// Extracts CSS font-variant values; e.g. normal, small-caps + /// + public const string CssFontVariant = "(normal|small-caps)"; + + /// + /// Extracts font-weight values; e.g. normal, bold, bolder... + /// + public const string CssFontWeight = "(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)"; + + /// + /// Exracts font sizes: xx-small, larger, small, 34pt, 30%, 2em + /// + public const string CssFontSize = "(" + CssLength + "|" + CssPercentage + "|xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller)"; + + /// + /// Gets the font-size[/line-height]? on the font shorthand property. + /// Check http://www.w3.org/TR/CSS21/fonts.html#font-shorthand + /// + public const string CssFontSizeAndLineHeight = CssFontSize + @"(\/" + CssLineHeight + @")?(\s|$)"; + + /// + /// the regexes cache that is used by the parser so not to create regex each time + /// + private static readonly Dictionary _regexes = new Dictionary(); + + #endregion + + + /// + /// Get CSS at rule from the given stylesheet. + /// + /// the stylesheet data to retrieve the rule from + /// the index to start the search for the rule, on return will be the value of the end of the found rule + /// the found at rule or null if not exists + public static string GetCssAtRules(string stylesheet, ref int startIdx) + { + startIdx = stylesheet.IndexOf('@', startIdx); + if (startIdx > -1) + { + int count = 1; + int endIdx = stylesheet.IndexOf('{', startIdx); + if (endIdx > -1) + { + while (count > 0 && endIdx < stylesheet.Length) + { + endIdx++; + if (stylesheet[endIdx] == '{') + { + count++; + } + else if (stylesheet[endIdx] == '}') + { + count--; + } + } + if (endIdx < stylesheet.Length) + { + var atrule = stylesheet.Substring(startIdx, endIdx - startIdx + 1); + startIdx = endIdx; + return atrule; + } + } + } + return null; + } + + /// + /// Extracts matches from the specified source + /// + /// Regular expression to extract matches + /// Source to extract matches + /// Collection of matches + public static MatchCollection Match(string regex, string source) + { + var r = GetRegex(regex); + return r.Matches(source); + } + + /// + /// Searches the specified regex on the source + /// + /// + /// + /// + public static string Search(string regex, string source) + { + int position; + return Search(regex, source, out position); + } + + /// + /// Searches the specified regex on the source + /// + /// + /// + /// + /// + public static string Search(string regex, string source, out int position) + { + MatchCollection matches = Match(regex, source); + + if (matches.Count > 0) + { + position = matches[0].Index; + return matches[0].Value; + } + else + { + position = -1; + } + + return null; + } + + /// + /// Get regex instance for the given regex string. + /// + /// the regex string to use + /// the regex instance + private static Regex GetRegex(string regex) + { + Regex r; + if (!_regexes.TryGetValue(regex, out r)) + { + r = new Regex(regex, RegexOptions.IgnoreCase | RegexOptions.Singleline); + _regexes[regex] = r; + } + return r; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/ArgChecker.cs b/Source/HtmlRenderer.Core/Core/Utils/ArgChecker.cs new file mode 100644 index 000000000..1b8b3e72d --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/ArgChecker.cs @@ -0,0 +1,117 @@ +// "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.IO; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Static class that contains argument-checking methods + /// + public static class ArgChecker + { + /// + /// Validate given is true, otherwise throw exception. + /// + /// Exception type to throw. + /// Condition to assert. + /// Exception message in-case of assert failure. + public static void AssertIsTrue(bool condition, string message) where TException : Exception, new() + { + // Checks whether the condition is false + if (!condition) + { + // Throwing exception + throw (TException)Activator.CreateInstance(typeof(TException), message); + } + } + + /// + /// Validate given argument isn't Null. + /// + /// argument to validate + /// Name of the argument checked + /// if is Null + public static void AssertArgNotNull(object arg, string argName) + { + if (arg == null) + { + throw new ArgumentNullException(argName); + } + } + + /// + /// Validate given argument isn't . + /// + /// argument to validate + /// Name of the argument checked + /// if is + public static void AssertArgNotNull(IntPtr arg, string argName) + { + if (arg == IntPtr.Zero) + { + throw new ArgumentException("IntPtr argument cannot be Zero", argName); + } + } + + /// + /// Validate given argument isn't Null or empty. + /// + /// argument to validate + /// Name of the argument checked + /// if is Null or empty + public static void AssertArgNotNullOrEmpty(string arg, string argName) + { + if (string.IsNullOrEmpty(arg)) + { + throw new ArgumentNullException(argName); + } + } + + /// + /// Validate given argument isn't Null. + /// + /// Type expected of + /// argument to validate + /// Name of the argument checked + /// if is Null + /// cast as + public static T AssertArgOfType(object arg, string argName) + { + AssertArgNotNull(arg, argName); + + if (arg is T) + { + return (T)arg; + } + throw new ArgumentException(string.Format("Given argument isn't of type '{0}'.", typeof(T).Name), argName); + } + + /// + /// Validate given argument isn't Null or empty AND argument value is the path of existing file. + /// + /// argument to validate + /// Name of the argument checked + /// if is Null or empty + /// if file-path not exist + public static void AssertFileExist(string arg, string argName) + { + AssertArgNotNullOrEmpty(arg, argName); + + if (false == File.Exists(arg)) + { + throw new FileNotFoundException(string.Format("Given file in argument '{0}' not exist.", argName), arg); + } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/CommonUtils.cs b/Source/HtmlRenderer.Core/Core/Utils/CommonUtils.cs new file mode 100644 index 000000000..12c03ad32 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/CommonUtils.cs @@ -0,0 +1,505 @@ +// "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 System.IO; +using System.Net; +using System.Text; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + internal delegate void ActionInt(T obj); + + internal delegate void ActionInt(T1 arg1, T2 arg2); + + internal delegate void ActionInt(T1 arg1, T2 arg2, T3 arg3); + + /// + /// Utility methods for general stuff. + /// + internal static class CommonUtils + { + #region Fields and Consts + + /// + /// Table to convert numbers into roman digits + /// + private static readonly string[,] _romanDigitsTable = + { + { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" }, + { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" }, + { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" }, + { + "", "M", "MM", "MMM", "M(V)", "(V)", "(V)M", + "(V)MM", "(V)MMM", "M(X)" + } + }; + + private static readonly string[,] _hebrewDigitsTable = + { + { "א", "ב", "ג", "ד", "ה", "ו", "ז", "ח", "ט" }, + { "י", "כ", "ל", "מ", "נ", "ס", "ע", "פ", "צ" }, + { "ק", "ר", "ש", "ת", "תק", "תר", "תש", "תת", "תתק", } + }; + + private static readonly string[,] _georgianDigitsTable = + { + { "ა", "ბ", "გ", "დ", "ე", "ვ", "ზ", "ჱ", "თ" }, + { "ი", "პ", "ლ", "მ", "ნ", "ჲ", "ო", "პ", "ჟ" }, + { "რ", "ს", "ტ", "ჳ", "ფ", "ქ", "ღ", "ყ", "შ" } + }; + + private static readonly string[,] _armenianDigitsTable = + { + { "Ա", "Բ", "Գ", "Դ", "Ե", "Զ", "Է", "Ը", "Թ" }, + { "Ժ", "Ի", "Լ", "Խ", "Ծ", "Կ", "Հ", "Ձ", "Ղ" }, + { "Ճ", "Մ", "Յ", "Ն", "Շ", "Ո", "Չ", "Պ", "Ջ" } + }; + + private static readonly string[] _hiraganaDigitsTable = new[] + { + "あ", "ぃ", "ぅ", "ぇ", "ぉ", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ", "ま", "み", "む", "め", "も", "ゃ", "ゅ", "ょ", "ら", "り", "る", "れ", "ろ", "ゎ", "ゐ", "ゑ", "を", "ん" + }; + + private static readonly string[] _satakanaDigitsTable = new[] + { + "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ヰ", "ヱ", "ヲ", "ン" + }; + + /// + /// the temp path to use for local files + /// + public static String _tempPath; + + #endregion + + + /// + /// Check if the given char is of Asian range. + /// + /// the character to check + /// true - Asian char, false - otherwise + public static bool IsAsianCharecter(char ch) + { + return ch >= 0x4e00 && ch <= 0xFA2D; + } + + /// + /// Check if the given char is a digit character (0-9) and (0-9, a-f for HEX) + /// + /// the character to check + /// optional: is hex digit check + /// true - is digit, false - not a digit + public static bool IsDigit(char ch, bool hex = false) + { + return (ch >= '0' && ch <= '9') || (hex && ((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))); + } + + /// + /// Convert the given char to digit. + /// + /// the character to check + /// optional: is hex digit check + /// true - is digit, false - not a digit + public static int ToDigit(char ch, bool hex = false) + { + if (ch >= '0' && ch <= '9') + return ch - '0'; + else if (hex) + { + if (ch >= 'a' && ch <= 'f') + return ch - 'a' + 10; + else if (ch >= 'A' && ch <= 'F') + return ch - 'A' + 10; + } + + return 0; + } + + /// + /// Get size that is max of and for width and height separately. + /// + public static RSize Max(RSize size, RSize other) + { + return new RSize(Math.Max(size.Width, other.Width), Math.Max(size.Height, other.Height)); + } + + /// + /// Get Uri object for the given path if it is valid uri path. + /// + /// the path to get uri for + /// uri or null if not valid + public static Uri TryGetUri(string path) + { + try + { + if (Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute)) + { + return new Uri(path); + } + } + catch + { } + + return null; + } + + /// + /// Get the first value in the given dictionary. + /// + /// the type of dictionary key + /// the type of dictionary value + /// the dictionary + /// optional: the default value to return of no elements found in dictionary + /// first element or default value + public static TValue GetFirstValueOrDefault(IDictionary dic, TValue defaultValue = default(TValue)) + { + if (dic != null) + { + foreach (var value in dic) + return value.Value; + } + return defaultValue; + } + + /// + /// Get file info object for the given path if it is valid file path. + /// + /// the path to get file info for + /// file info or null if not valid + public static FileInfo TryGetFileInfo(string path) + { + try + { + return new FileInfo(path); + } + catch + { } + + return null; + } + + /// + /// Get web client response content type. + /// + /// the web client to get the response content type from + /// response content type or null + public static string GetResponseContentType(WebClient client) + { + foreach (string header in client.ResponseHeaders) + { + if (header.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) + return client.ResponseHeaders[header]; + } + return null; + } + + /// + /// Gets the representation of the online uri on the local disk. + /// + /// The online image uri. + /// The path of the file on the disk. + public static FileInfo GetLocalfileName(Uri imageUri) + { + StringBuilder fileNameBuilder = new StringBuilder(); + string absoluteUri = imageUri.AbsoluteUri; + int lastSlash = absoluteUri.LastIndexOf('/'); + if (lastSlash == -1) + { + return null; + } + + string uriUntilSlash = absoluteUri.Substring(0, lastSlash); + fileNameBuilder.Append(uriUntilSlash.GetHashCode().ToString()); + fileNameBuilder.Append('_'); + + string restOfUri = absoluteUri.Substring(lastSlash + 1); + int indexOfParams = restOfUri.IndexOf('?'); + if (indexOfParams == -1) + { + string ext = ".png"; + int indexOfDot = restOfUri.IndexOf('.'); + if (indexOfDot > -1) + { + ext = restOfUri.Substring(indexOfDot); + restOfUri = restOfUri.Substring(0, indexOfDot); + } + + fileNameBuilder.Append(restOfUri); + fileNameBuilder.Append(ext); + } + else + { + int indexOfDot = restOfUri.IndexOf('.'); + if (indexOfDot == -1 || indexOfDot > indexOfParams) + { + //The uri is not for a filename + fileNameBuilder.Append(restOfUri); + fileNameBuilder.Append(".png"); + } + else if (indexOfParams > indexOfDot) + { + //Adds the filename without extension. + fileNameBuilder.Append(restOfUri, 0, indexOfDot); + //Adds the parameters + fileNameBuilder.Append(restOfUri, indexOfParams, restOfUri.Length - indexOfParams); + //Adds the filename extension. + fileNameBuilder.Append(restOfUri, indexOfDot, indexOfParams - indexOfDot); + } + } + + var validFileName = GetValidFileName(fileNameBuilder.ToString()); + if (validFileName.Length > 25) + { + validFileName = validFileName.Substring(0, 24) + validFileName.Substring(24).GetHashCode() + Path.GetExtension(validFileName); + } + + if (_tempPath == null) + { + _tempPath = Path.Combine(Path.GetTempPath(), "HtmlRenderer"); + if (!Directory.Exists(_tempPath)) + Directory.CreateDirectory(_tempPath); + } + + return new FileInfo(Path.Combine(_tempPath, validFileName)); + } + + /// + /// Get substring separated by whitespace starting from the given idex. + /// + /// the string to get substring in + /// the index to start substring search from + /// return the length of the found string + /// the index of the substring, -1 if no valid sub-string found + public static int GetNextSubString(string str, int idx, out int length) + { + while (idx < str.Length && Char.IsWhiteSpace(str[idx])) + idx++; + if (idx < str.Length) + { + var endIdx = idx + 1; + while (endIdx < str.Length && !Char.IsWhiteSpace(str[endIdx])) + endIdx++; + length = endIdx - idx; + return idx; + } + length = 0; + return -1; + } + + /// + /// Compare that the substring of is equal to + /// Assume given substring is not empty and all indexes are valid!
+ ///
+ /// true - equals, false - not equals + public static bool SubStringEquals(string str, int idx, int length, string str2) + { + if (length == str2.Length && idx + length <= str.Length) + { + for (int i = 0; i < length; i++) + { + if (Char.ToLowerInvariant(str[idx + i]) != Char.ToLowerInvariant(str2[i])) + return false; + } + return true; + } + return false; + } + + /// + /// Replaces invalid filename chars to '_' + /// + /// The possibly-not-valid filename + /// A valid filename. + private static string GetValidFileName(string source) + { + string retVal = source; + char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); + foreach (var invalidFileNameChar in invalidFileNameChars) + { + retVal = retVal.Replace(invalidFileNameChar, '_'); + } + return retVal; + } + + /// + /// Convert number to alpha numeric system by the requested style (UpperAlpha, LowerRoman, Hebrew, etc.). + /// + /// the number to convert + /// the css style to convert by + /// converted string + public static string ConvertToAlphaNumber(int number, string style = CssConstants.UpperAlpha) + { + if (number == 0) + return string.Empty; + + if (style.Equals(CssConstants.LowerGreek, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToGreekNumber(number); + } + else if (style.Equals(CssConstants.LowerRoman, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToRomanNumbers(number, true); + } + else if (style.Equals(CssConstants.UpperRoman, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToRomanNumbers(number, false); + } + else if (style.Equals(CssConstants.Armenian, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToSpecificNumbers(number, _armenianDigitsTable); + } + else if (style.Equals(CssConstants.Georgian, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToSpecificNumbers(number, _georgianDigitsTable); + } + else if (style.Equals(CssConstants.Hebrew, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToSpecificNumbers(number, _hebrewDigitsTable); + } + else if (style.Equals(CssConstants.Hiragana, StringComparison.InvariantCultureIgnoreCase) || style.Equals(CssConstants.HiraganaIroha, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToSpecificNumbers2(number, _hiraganaDigitsTable); + } + else if (style.Equals(CssConstants.Katakana, StringComparison.InvariantCultureIgnoreCase) || style.Equals(CssConstants.KatakanaIroha, StringComparison.InvariantCultureIgnoreCase)) + { + return ConvertToSpecificNumbers2(number, _satakanaDigitsTable); + } + else + { + var lowercase = style.Equals(CssConstants.LowerAlpha, StringComparison.InvariantCultureIgnoreCase) || style.Equals(CssConstants.LowerLatin, StringComparison.InvariantCultureIgnoreCase); + return ConvertToEnglishNumber(number, lowercase); + } + } + + /// + /// Convert the given integer into alphabetic numeric format (D, AU, etc.) + /// + /// the number to convert + /// is to use lowercase + /// the roman number string + private static string ConvertToEnglishNumber(int number, bool lowercase) + { + var sb = string.Empty; + int alphStart = lowercase ? 97 : 65; + while (number > 0) + { + var n = number % 26 - 1; + if (n >= 0) + { + sb = (Char)(alphStart + n) + sb; + number = number / 26; + } + else + { + sb = (Char)(alphStart + 25) + sb; + number = (number - 1) / 26; + } + } + + return sb; + } + + /// + /// Convert the given integer into alphabetic numeric format (alpha, AU, etc.) + /// + /// the number to convert + /// the roman number string + private static string ConvertToGreekNumber(int number) + { + var sb = string.Empty; + while (number > 0) + { + var n = number % 24 - 1; + if (n > 16) + n++; + if (n >= 0) + { + sb = (Char)(945 + n) + sb; + number = number / 24; + } + else + { + sb = (Char)(945 + 24) + sb; + number = (number - 1) / 25; + } + } + + return sb; + } + + /// + /// Convert the given integer into roman numeric format (II, VI, IX, etc.) + /// + /// the number to convert + /// if to use lowercase letters for roman digits + /// the roman number string + private static string ConvertToRomanNumbers(int number, bool lowercase) + { + var sb = string.Empty; + for (int i = 1000, j = 3; i > 0; i /= 10, j--) + { + int digit = number / i; + sb += string.Format(_romanDigitsTable[j, digit]); + number -= digit * i; + } + return lowercase ? sb.ToLower() : sb; + } + + /// + /// Convert the given integer into given alphabet numeric system. + /// + /// the number to convert + /// the alphabet system to use + /// the number string + private static string ConvertToSpecificNumbers(int number, string[,] alphabet) + { + int level = 0; + var sb = string.Empty; + while (number > 0 && level < alphabet.GetLength(0)) + { + var n = number % 10; + if (n > 0) + sb = alphabet[level, number % 10 - 1].ToString(CultureInfo.InvariantCulture) + sb; + number /= 10; + level++; + } + return sb; + } + + /// + /// Convert the given integer into given alphabet numeric system. + /// + /// the number to convert + /// the alphabet system to use + /// the number string + private static string ConvertToSpecificNumbers2(int number, string[] alphabet) + { + for (int i = 20; i > 0; i--) + { + if (number > 49 * i - i + 1) + number++; + } + + var sb = string.Empty; + while (number > 0) + { + sb = alphabet[Math.Max(0, number % 49 - 1)].ToString(CultureInfo.InvariantCulture) + sb; + number /= 49; + } + return sb; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/CssConstants.cs b/Source/HtmlRenderer.Core/Core/Utils/CssConstants.cs new file mode 100644 index 000000000..655849256 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/CssConstants.cs @@ -0,0 +1,169 @@ +// "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.Utils +{ + /// + /// String constants to avoid typing errors. + /// + internal static class CssConstants + { + public const string Absolute = "absolute"; + public const string Auto = "auto"; + public const string Avoid = "avoid"; + public const string Baseline = "baseline"; + public const string Blink = "blink"; + public const string Block = "block"; + public const string InlineBlock = "inline-block"; + public const string Bold = "bold"; + public const string Bolder = "bolder"; + public const string Bottom = "bottom"; + public const string BreakAll = "break-all"; + public const string KeepAll = "keep-all"; + public const string Center = "center"; + public const string Collapse = "collapse"; + public const string Cursive = "cursive"; + public const string Circle = "circle"; + public const string Decimal = "decimal"; + public const string DecimalLeadingZero = "decimal-leading-zero"; + public const string Disc = "disc"; + public const string Fantasy = "fantasy"; + public const string Fixed = "fixed"; + public const string Hide = "hide"; + public const string Inherit = "inherit"; + public const string Inline = "inline"; + public const string InlineTable = "inline-table"; + public const string Inset = "inset"; + public const string Italic = "italic"; + public const string Justify = "justify"; + public const string Large = "large"; + public const string Larger = "larger"; + public const string Left = "left"; + public const string Lighter = "lighter"; + public const string LineThrough = "line-through"; + public const string ListItem = "list-item"; + public const string Ltr = "ltr"; + public const string LowerAlpha = "lower-alpha"; + public const string LowerLatin = "lower-latin"; + public const string LowerRoman = "lower-roman"; + public const string LowerGreek = "lower-greek"; + public const string Armenian = "armenian"; + public const string Georgian = "georgian"; + public const string Hebrew = "hebrew"; + public const string Hiragana = "hiragana"; + public const string HiraganaIroha = "hiragana-iroha"; + public const string Katakana = "katakana"; + public const string KatakanaIroha = "katakana-iroha"; + public const string Medium = "medium"; + public const string Middle = "middle"; + public const string Monospace = "monospace"; + public const string None = "none"; + public const string Normal = "normal"; + public const string NoWrap = "nowrap"; + public const string Oblique = "oblique"; + public const string Outset = "outset"; + public const string Overline = "overline"; + public const string Pre = "pre"; + public const string PreWrap = "pre-wrap"; + public const string PreLine = "pre-line"; + public const string Right = "right"; + public const string Rtl = "rtl"; + public const string SansSerif = "sans-serif"; + public const string Serif = "serif"; + public const string Show = "show"; + public const string Small = "small"; + public const string Smaller = "smaller"; + public const string Solid = "solid"; + public const string Sub = "sub"; + public const string Super = "super"; + public const string Square = "square"; + public const string Table = "table"; + public const string TableRow = "table-row"; + public const string TableRowGroup = "table-row-group"; + public const string TableHeaderGroup = "table-header-group"; + public const string TableFooterGroup = "table-footer-group"; + public const string TableColumn = "table-column"; + public const string TableColumnGroup = "table-column-group"; + public const string TableCell = "table-cell"; + public const string TableCaption = "table-caption"; + public const string TextBottom = "text-bottom"; + public const string TextTop = "text-top"; + public const string Thin = "thin"; + public const string Thick = "thick"; + public const string Top = "top"; + public const string Underline = "underline"; + public const string UpperAlpha = "upper-alpha"; + public const string UpperLatin = "upper-latin"; + public const string UpperRoman = "upper-roman"; + public const string XLarge = "x-large"; + public const string XSmall = "x-small"; + public const string XXLarge = "xx-large"; + public const string XXSmall = "xx-small"; + public const string Visible = "visible"; + public const string Hidden = "hidden"; + public const string Dotted = "dotted"; + public const string Dashed = "dashed"; + public const string Double = "double"; + public const string Groove = "groove"; + public const string Ridge = "ridge"; + + /// + /// Centimeters + /// + public const string Cm = "cm"; + + /// + /// Millimeters + /// + public const string Mm = "mm"; + + /// + /// Pixels + /// + public const string Px = "px"; + + /// + /// Inches + /// + public const string In = "in"; + + /// + /// Em - The font size of the relevant font + /// + public const string Em = "em"; + + /// + /// The 'x-height' of the relevan font + /// + public const string Ex = "ex"; + + /// + /// Points + /// + public const string Pt = "pt"; + + /// + /// Picas + /// + public const string Pc = "pc"; + + /// + /// Default font size in points. Change this value to modify the default font size. + /// + public const double FontSize = 11f; + + /// + /// Default font used for the generic 'serif' family + /// + public const string DefaultFont = "Segoe UI"; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/CssUtils.cs b/Source/HtmlRenderer.Core/Core/Utils/CssUtils.cs new file mode 100644 index 000000000..69f949390 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/CssUtils.cs @@ -0,0 +1,414 @@ +// "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.Parse; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Utility method for handling CSS stuff. + /// + internal static class CssUtils + { + #region Fields and Consts + + /// + /// Brush for selection background + /// + private static readonly RColor _defaultSelectionBackcolor = RColor.FromArgb(0xa9, 0x33, 0x99, 0xFF); + + #endregion + + + /// + /// Brush for selection background + /// + public static RColor DefaultSelectionBackcolor + { + get { return _defaultSelectionBackcolor; } + } + + /// + /// Gets the white space width of the specified box + /// + /// + /// + /// + public static double WhiteSpace(RGraphics g, CssBoxProperties box) + { + double w = box.ActualFont.GetWhitespaceWidth(g); + if (!(String.IsNullOrEmpty(box.WordSpacing) || box.WordSpacing == CssConstants.Normal)) + { + w += CssValueParser.ParseLength(box.WordSpacing, 0, box, true); + } + return w; + } + + /// + /// Get CSS box property value by the CSS name.
+ /// Used as a mapping between CSS property and the class property. + ///
+ /// the CSS box to get it's property value + /// the name of the CSS property + /// the value of the property, null if no such property exists + public static string GetPropertyValue(CssBox cssBox, string propName) + { + switch (propName) + { + case "border-bottom-width": + return cssBox.BorderBottomWidth; + case "border-left-width": + return cssBox.BorderLeftWidth; + case "border-right-width": + return cssBox.BorderRightWidth; + case "border-top-width": + return cssBox.BorderTopWidth; + case "border-bottom-style": + return cssBox.BorderBottomStyle; + case "border-left-style": + return cssBox.BorderLeftStyle; + case "border-right-style": + return cssBox.BorderRightStyle; + case "border-top-style": + return cssBox.BorderTopStyle; + case "border-bottom-color": + return cssBox.BorderBottomColor; + case "border-left-color": + return cssBox.BorderLeftColor; + case "border-right-color": + return cssBox.BorderRightColor; + case "border-top-color": + return cssBox.BorderTopColor; + case "border-spacing": + return cssBox.BorderSpacing; + case "border-collapse": + return cssBox.BorderCollapse; + case "corner-radius": + return cssBox.CornerRadius; + case "corner-nw-radius": + return cssBox.CornerNwRadius; + case "corner-ne-radius": + return cssBox.CornerNeRadius; + case "corner-se-radius": + return cssBox.CornerSeRadius; + case "corner-sw-radius": + return cssBox.CornerSwRadius; + case "margin-bottom": + return cssBox.MarginBottom; + case "margin-left": + return cssBox.MarginLeft; + case "margin-right": + return cssBox.MarginRight; + case "margin-top": + return cssBox.MarginTop; + case "padding-bottom": + return cssBox.PaddingBottom; + case "padding-left": + return cssBox.PaddingLeft; + case "padding-right": + return cssBox.PaddingRight; + case "padding-top": + return cssBox.PaddingTop; + case "page-break-inside": + return cssBox.PageBreakInside; + case "left": + return cssBox.Left; + case "top": + return cssBox.Top; + case "width": + return cssBox.Width; + case "max-width": + return cssBox.MaxWidth; + case "height": + return cssBox.Height; + case "background-color": + return cssBox.BackgroundColor; + case "background-image": + return cssBox.BackgroundImage; + case "background-position": + return cssBox.BackgroundPosition; + case "background-repeat": + return cssBox.BackgroundRepeat; + case "background-gradient": + return cssBox.BackgroundGradient; + case "background-gradient-angle": + return cssBox.BackgroundGradientAngle; + case "content": + return cssBox.Content; + case "color": + return cssBox.Color; + case "display": + return cssBox.Display; + case "direction": + return cssBox.Direction; + case "empty-cells": + return cssBox.EmptyCells; + case "float": + return cssBox.Float; + case "position": + return cssBox.Position; + case "line-height": + return cssBox.LineHeight; + case "vertical-align": + return cssBox.VerticalAlign; + case "text-indent": + return cssBox.TextIndent; + case "text-align": + return cssBox.TextAlign; + case "text-decoration": + return cssBox.TextDecoration; + case "white-space": + return cssBox.WhiteSpace; + case "word-break": + return cssBox.WordBreak; + case "visibility": + return cssBox.Visibility; + case "word-spacing": + return cssBox.WordSpacing; + case "font-family": + return cssBox.FontFamily; + case "font-size": + return cssBox.FontSize; + case "font-style": + return cssBox.FontStyle; + case "font-variant": + return cssBox.FontVariant; + case "font-weight": + return cssBox.FontWeight; + case "list-style": + return cssBox.ListStyle; + case "list-style-position": + return cssBox.ListStylePosition; + case "list-style-image": + return cssBox.ListStyleImage; + case "list-style-type": + return cssBox.ListStyleType; + case "overflow": + return cssBox.Overflow; + } + return null; + } + + /// + /// Set CSS box property value by the CSS name.
+ /// Used as a mapping between CSS property and the class property. + ///
+ /// the CSS box to set it's property value + /// the name of the CSS property + /// the value to set + public static void SetPropertyValue(CssBox cssBox, string propName, string value) + { + switch (propName) + { + case "border-bottom-width": + cssBox.BorderBottomWidth = value; + break; + case "border-left-width": + cssBox.BorderLeftWidth = value; + break; + case "border-right-width": + cssBox.BorderRightWidth = value; + break; + case "border-top-width": + cssBox.BorderTopWidth = value; + break; + case "border-bottom-style": + cssBox.BorderBottomStyle = value; + break; + case "border-left-style": + cssBox.BorderLeftStyle = value; + break; + case "border-right-style": + cssBox.BorderRightStyle = value; + break; + case "border-top-style": + cssBox.BorderTopStyle = value; + break; + case "border-bottom-color": + cssBox.BorderBottomColor = value; + break; + case "border-left-color": + cssBox.BorderLeftColor = value; + break; + case "border-right-color": + cssBox.BorderRightColor = value; + break; + case "border-top-color": + cssBox.BorderTopColor = value; + break; + case "border-spacing": + cssBox.BorderSpacing = value; + break; + case "border-collapse": + cssBox.BorderCollapse = value; + break; + case "corner-radius": + cssBox.CornerRadius = value; + break; + case "corner-nw-radius": + cssBox.CornerNwRadius = value; + break; + case "corner-ne-radius": + cssBox.CornerNeRadius = value; + break; + case "corner-se-radius": + cssBox.CornerSeRadius = value; + break; + case "corner-sw-radius": + cssBox.CornerSwRadius = value; + break; + case "margin-bottom": + cssBox.MarginBottom = value; + break; + case "margin-left": + cssBox.MarginLeft = value; + break; + case "margin-right": + cssBox.MarginRight = value; + break; + case "margin-top": + cssBox.MarginTop = value; + break; + case "padding-bottom": + cssBox.PaddingBottom = value; + break; + case "padding-left": + cssBox.PaddingLeft = value; + break; + case "padding-right": + cssBox.PaddingRight = value; + break; + case "padding-top": + cssBox.PaddingTop = value; + break; + case "page-break-inside": + cssBox.PageBreakInside = value; + break; + case "left": + cssBox.Left = value; + break; + case "top": + cssBox.Top = value; + break; + case "width": + cssBox.Width = value; + break; + case "max-width": + cssBox.MaxWidth = value; + break; + case "height": + cssBox.Height = value; + break; + case "background-color": + cssBox.BackgroundColor = value; + break; + case "background-image": + cssBox.BackgroundImage = value; + break; + case "background-position": + cssBox.BackgroundPosition = value; + break; + case "background-repeat": + cssBox.BackgroundRepeat = value; + break; + case "background-gradient": + cssBox.BackgroundGradient = value; + break; + case "background-gradient-angle": + cssBox.BackgroundGradientAngle = value; + break; + case "color": + cssBox.Color = value; + break; + case "content": + cssBox.Content = value; + break; + case "display": + cssBox.Display = value; + break; + case "direction": + cssBox.Direction = value; + break; + case "empty-cells": + cssBox.EmptyCells = value; + break; + case "float": + cssBox.Float = value; + break; + case "position": + cssBox.Position = value; + break; + case "line-height": + cssBox.LineHeight = value; + break; + case "vertical-align": + cssBox.VerticalAlign = value; + break; + case "text-indent": + cssBox.TextIndent = value; + break; + case "text-align": + cssBox.TextAlign = value; + break; + case "text-decoration": + cssBox.TextDecoration = value; + break; + case "white-space": + cssBox.WhiteSpace = value; + break; + case "word-break": + cssBox.WordBreak = value; + break; + case "visibility": + cssBox.Visibility = value; + break; + case "word-spacing": + cssBox.WordSpacing = value; + break; + case "font-family": + cssBox.FontFamily = value; + break; + case "font-size": + cssBox.FontSize = value; + break; + case "font-style": + cssBox.FontStyle = value; + break; + case "font-variant": + cssBox.FontVariant = value; + break; + case "font-weight": + cssBox.FontWeight = value; + break; + case "list-style": + cssBox.ListStyle = value; + break; + case "list-style-position": + cssBox.ListStylePosition = value; + break; + case "list-style-image": + cssBox.ListStyleImage = value; + break; + case "list-style-type": + cssBox.ListStyleType = value; + break; + case "overflow": + cssBox.Overflow = value; + break; + } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/DomUtils.cs b/Source/HtmlRenderer.Core/Core/Utils/DomUtils.cs new file mode 100644 index 000000000..6d00a4abc --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/DomUtils.cs @@ -0,0 +1,919 @@ +// "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.Text; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Dom; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Parse; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Utility class for traversing DOM structure and execution stuff on it. + /// + internal sealed class DomUtils + { + /// + /// Check if the given location is inside the given box deep.
+ /// Check inner boxes and all lines that the given box spans to. + ///
+ /// the box to check + /// the location to check + /// true - location inside the box, false - otherwise + public static bool IsInBox(CssBox box, RPoint location) + { + foreach (var line in box.Rectangles) + { + if (line.Value.Contains(location)) + return true; + } + foreach (var childBox in box.Boxes) + { + if (IsInBox(childBox, location)) + return true; + } + return false; + } + + /// + /// Check if the given box contains only inline child boxes. + /// + /// the box to check + /// true - only inline child boxes, false - otherwise + public static bool ContainsInlinesOnly(CssBox box) + { + foreach (CssBox b in box.Boxes) + { + if (!b.IsInline) + { + return false; + } + } + + return true; + } + + /// + /// Recursively searches for the parent with the specified HTML Tag name + /// + /// + /// + /// + public static CssBox FindParent(CssBox root, string tagName, CssBox box) + { + if (box == null) + { + return root; + } + else if (box.HtmlTag != null && box.HtmlTag.Name.Equals(tagName, StringComparison.CurrentCultureIgnoreCase)) + { + return box.ParentBox ?? root; + } + else + { + return FindParent(root, tagName, box.ParentBox); + } + } + + /// + /// Gets the previous sibling of this box. + /// + /// Box before this one on the tree. Null if its the first + public static CssBox GetPreviousSibling(CssBox b) + { + if (b.ParentBox != null) + { + int index = b.ParentBox.Boxes.IndexOf(b); + if (index > 0) + { + int diff = 1; + CssBox sib = b.ParentBox.Boxes[index - diff]; + + while ((sib.Display == CssConstants.None || sib.Position == CssConstants.Absolute || sib.Position == CssConstants.Fixed) && index - diff - 1 >= 0) + { + sib = b.ParentBox.Boxes[index - ++diff]; + } + + return (sib.Display == CssConstants.None || sib.Position == CssConstants.Fixed) ? null : sib; + } + } + return null; + } + + /// + /// Gets the previous sibling of this box. + /// + /// Box before this one on the tree. Null if its the first + public static CssBox GetPreviousContainingBlockSibling(CssBox b) + { + var conBlock = b; + int index = conBlock.ParentBox.Boxes.IndexOf(conBlock); + while (conBlock.ParentBox != null && index < 1 && conBlock.Display != CssConstants.Block && conBlock.Display != CssConstants.Table && conBlock.Display != CssConstants.TableCell && conBlock.Display != CssConstants.ListItem) + { + conBlock = conBlock.ParentBox; + index = conBlock.ParentBox != null ? conBlock.ParentBox.Boxes.IndexOf(conBlock) : -1; + } + conBlock = conBlock.ParentBox; + if (conBlock != null && index > 0) + { + int diff = 1; + CssBox sib = conBlock.Boxes[index - diff]; + + while ((sib.Display == CssConstants.None || sib.Position == CssConstants.Absolute || sib.Position == CssConstants.Fixed) && index - diff - 1 >= 0) + { + sib = conBlock.Boxes[index - ++diff]; + } + + return sib.Display == CssConstants.None ? null : sib; + } + return null; + } + + /// + /// fix word space for first word in inline tag. + /// + /// the box to check + public static bool IsBoxHasWhitespace(CssBox box) + { + if (!box.Words[0].IsImage && box.Words[0].HasSpaceBefore && box.IsInline) + { + var sib = GetPreviousContainingBlockSibling(box); + if (sib != null && sib.IsInline) + return true; + } + return false; + } + + /// + /// Gets the next sibling of this box. + /// + /// Box before this one on the tree. Null if its the first + public static CssBox GetNextSibling(CssBox b) + { + CssBox sib = null; + if (b.ParentBox != null) + { + var index = b.ParentBox.Boxes.IndexOf(b) + 1; + while (index <= b.ParentBox.Boxes.Count - 1) + { + var pSib = b.ParentBox.Boxes[index]; + if (pSib.Display != CssConstants.None && pSib.Position != CssConstants.Absolute && pSib.Position != CssConstants.Fixed) + { + sib = pSib; + break; + } + index++; + } + } + return sib; + } + + /// + /// Get attribute value by given key starting search from given box, search up the tree until + /// attribute found or root. + /// + /// the box to start lookup at + /// the attribute to get + /// the value of the attribute or null if not found + public static string GetAttribute(CssBox box, string attribute) + { + string value = null; + while (box != null && value == null) + { + value = box.GetAttribute(attribute, null); + box = box.ParentBox; + } + return value; + } + + /// + /// Get css box under the given sub-tree at the given x,y location, get the inner most.
+ /// the location must be in correct scroll offset. + ///
+ /// the box to start search from + /// the location to find the box by + /// Optional: if to get only visible boxes (default - true) + /// css link box if exists or null + public static CssBox GetCssBox(CssBox box, RPoint location, bool visible = true) + { + if (box != null) + { + if ((!visible || box.Visibility == CssConstants.Visible) && (box.Bounds.IsEmpty || box.Bounds.Contains(location))) + { + foreach (var childBox in box.Boxes) + { + if (CommonUtils.GetFirstValueOrDefault(box.Rectangles, box.Bounds).Contains(location)) + { + return GetCssBox(childBox, location) ?? childBox; + } + } + } + } + + return null; + } + + /// + /// Collect all link boxes found in the HTML tree. + /// + /// the box to start search from + /// collection to add all link boxes to + public static void GetAllLinkBoxes(CssBox box, List linkBoxes) + { + if (box != null) + { + if (box.IsClickable && box.Visibility == CssConstants.Visible) + { + linkBoxes.Add(box); + } + + foreach (var childBox in box.Boxes) + { + GetAllLinkBoxes(childBox, linkBoxes); + } + } + } + + /// + /// Get css link box under the given sub-tree at the given x,y location.
+ /// the location must be in correct scroll offset. + ///
+ /// the box to start search from + /// the location to find the box by + /// css link box if exists or null + public static CssBox GetLinkBox(CssBox box, RPoint location) + { + if (box != null) + { + if (box.IsClickable && box.Visibility == CssConstants.Visible) + { + if (IsInBox(box, location)) + return box; + } + + if (box.ClientRectangle.IsEmpty || box.ClientRectangle.Contains(location)) + { + foreach (var childBox in box.Boxes) + { + var foundBox = GetLinkBox(childBox, location); + if (foundBox != null) + return foundBox; + } + } + } + + return null; + } + + /// + /// Get css box under the given sub-tree with the given id.
+ ///
+ /// the box to start search from + /// the id to find the box by + /// css box if exists or null + public static CssBox GetBoxById(CssBox box, string id) + { + if (box != null && !string.IsNullOrEmpty(id)) + { + if (box.HtmlTag != null && id.Equals(box.HtmlTag.TryGetAttribute("id"), StringComparison.OrdinalIgnoreCase)) + { + return box; + } + + foreach (var childBox in box.Boxes) + { + var foundBox = GetBoxById(childBox, id); + if (foundBox != null) + return foundBox; + } + } + + return null; + } + + /// + /// Get css line box under the given sub-tree at the given y location or the nearest line from the top.
+ /// the location must be in correct scroll offset. + ///
+ /// the box to start search from + /// the location to find the box at + /// css word box if exists or null + public static CssLineBox GetCssLineBox(CssBox box, RPoint location) + { + CssLineBox line = null; + if (box != null) + { + if (box.LineBoxes.Count > 0) + { + if (box.HtmlTag == null || box.HtmlTag.Name != "td" || box.Bounds.Contains(location)) + { + foreach (var lineBox in box.LineBoxes) + { + foreach (var rect in lineBox.Rectangles) + { + if (rect.Value.Top <= location.Y) + { + line = lineBox; + } + + if (rect.Value.Top > location.Y) + { + return line; + } + } + } + } + } + + foreach (var childBox in box.Boxes) + { + line = GetCssLineBox(childBox, location) ?? line; + } + } + + return line; + } + + /// + /// Get css word box under the given sub-tree at the given x,y location.
+ /// the location must be in correct scroll offset. + ///
+ /// the box to start search from + /// the location to find the box at + /// css word box if exists or null + public static CssRect GetCssBoxWord(CssBox box, RPoint location) + { + if (box != null && box.Visibility == CssConstants.Visible) + { + if (box.LineBoxes.Count > 0) + { + foreach (var lineBox in box.LineBoxes) + { + var wordBox = GetCssBoxWord(lineBox, location); + if (wordBox != null) + return wordBox; + } + } + + if (box.ClientRectangle.IsEmpty || box.ClientRectangle.Contains(location)) + { + foreach (var childBox in box.Boxes) + { + var foundWord = GetCssBoxWord(childBox, location); + if (foundWord != null) + { + return foundWord; + } + } + } + } + + return null; + } + + /// + /// Get css word box under the given sub-tree at the given x,y location.
+ /// the location must be in correct scroll offset. + ///
+ /// the line box to search in + /// the location to find the box at + /// css word box if exists or null + public static CssRect GetCssBoxWord(CssLineBox lineBox, RPoint location) + { + foreach (var rects in lineBox.Rectangles) + { + foreach (var word in rects.Key.Words) + { + // add word spacing to word width so sentence won't have hols in it when moving the mouse + var rect = word.Rectangle; + rect.Width += word.OwnerBox.ActualWordSpacing; + if (rect.Contains(location)) + { + return word; + } + } + } + return null; + } + + /// + /// Find the css line box that the given word is in. + /// + /// the word to search for it's line box + /// line box that the word is in + public static CssLineBox GetCssLineBoxByWord(CssRect word) + { + var box = word.OwnerBox; + while (box.LineBoxes.Count == 0) + { + box = box.ParentBox; + } + foreach (var lineBox in box.LineBoxes) + { + foreach (var lineWord in lineBox.Words) + { + if (lineWord == word) + { + return lineBox; + } + } + } + return box.LineBoxes[0]; + } + + /// + /// Get selected plain text of the given html sub-tree. + /// + /// the DOM box to get selected text from its sub-tree + /// the selected plain text string + public static string GetSelectedPlainText(CssBox root) + { + var sb = new StringBuilder(); + var lastWordIndex = GetSelectedPlainText(sb, root); + return sb.ToString(0, lastWordIndex).Trim(); + } + + /// + /// Generate html from the given DOM tree.
+ /// Generate all the style inside the html, in header or for every tag depending on value. + ///
+ /// the box of the html generate html from + /// Optional: controls the way styles are generated when html is generated + /// Optional: true - generate only selected html subset, false - generate all (default - false) + /// generated html + public static string GenerateHtml(CssBox root, HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline, bool onlySelected = false) + { + var sb = new StringBuilder(); + if (root != null) + { + var selectedBoxes = onlySelected ? CollectSelectedBoxes(root) : null; + var selectionRoot = onlySelected ? GetSelectionRoot(root, selectedBoxes) : null; + WriteHtml(root.HtmlContainer.CssParser, sb, root, styleGen, selectedBoxes, selectionRoot); + } + return sb.ToString(); + } + + /// + /// Generate textual tree representation of the css boxes tree starting from the given root.
+ /// Used for debugging html parsing. + ///
+ /// the root to generate tree from + /// generated tree + public static string GenerateBoxTree(CssBox root) + { + var sb = new StringBuilder(); + GenerateBoxTree(root, sb, 0); + return sb.ToString(); + } + + + #region Private methods + + /// + /// Get selected plain text of the given html sub-tree.
+ /// Append all the selected words. + ///
+ /// the builder to append the selected text to + /// the DOM box to get selected text from its sub-tree + /// the index of the last word appended + private static int GetSelectedPlainText(StringBuilder sb, CssBox box) + { + int lastWordIndex = 0; + foreach (var boxWord in box.Words) + { + // append the text of selected word (handle partial selected words) + if (boxWord.Selected) + { + sb.Append(GetSelectedWord(boxWord, true)); + lastWordIndex = sb.Length; + } + } + + // empty span box + if (box.Boxes.Count < 1 && box.Text != null && box.Text.IsWhitespace()) + { + sb.Append(' '); + } + + // deep traversal + if (box.Visibility != CssConstants.Hidden && box.Display != CssConstants.None) + { + foreach (var childBox in box.Boxes) + { + var innerLastWordIdx = GetSelectedPlainText(sb, childBox); + lastWordIndex = Math.Max(lastWordIndex, innerLastWordIdx); + } + } + + if (sb.Length > 0) + { + // convert hr to line of dashes + if (box.HtmlTag != null && box.HtmlTag.Name == "hr") + { + if (sb.Length > 1 && sb[sb.Length - 1] != '\n') + sb.AppendLine(); + sb.AppendLine(new string('-', 80)); + } + + // new line for css block + if (box.Display == CssConstants.Block || box.Display == CssConstants.ListItem || box.Display == CssConstants.TableRow) + { + if (!(box.IsBrElement && sb.Length > 1 && sb[sb.Length - 1] == '\n')) + sb.AppendLine(); + } + + // space between table cells + if (box.Display == CssConstants.TableCell) + { + sb.Append(' '); + } + + // paragraphs has additional newline for nice formatting + if (box.HtmlTag != null && box.HtmlTag.Name == "p") + { + int newlines = 0; + for (int i = sb.Length - 1; i >= 0 && char.IsWhiteSpace(sb[i]); i--) + newlines += sb[i] == '\n' ? 1 : 0; + if (newlines < 2) + sb.AppendLine(); + } + } + + return lastWordIndex; + } + + /// + /// Collect the boxes that have at least one word down the hierarchy that is selected recursively.
+ ///
+ /// the box to check its sub-tree + /// the collection to add the selected tags to + private static Dictionary CollectSelectedBoxes(CssBox root) + { + var selectedBoxes = new Dictionary(); + var maybeBoxes = new Dictionary(); + CollectSelectedBoxes(root, selectedBoxes, maybeBoxes); + return selectedBoxes; + } + + /// + /// Collect the boxes that have at least one word down the hierarchy that is selected recursively.
+ /// Use to handle boxes that are between selected words but don't have selected word inside.
+ ///
+ /// the box to check its sub-tree + /// the hash to add the selected boxes to + /// used to handle boxes that are between selected words but don't have selected word inside + /// is the current box is in selected sub-tree + private static bool CollectSelectedBoxes(CssBox box, Dictionary selectedBoxes, Dictionary maybeBoxes) + { + bool isInSelection = false; + foreach (var word in box.Words) + { + if (word.Selected) + { + selectedBoxes[box] = true; + foreach (var maybeTag in maybeBoxes) + selectedBoxes[maybeTag.Key] = maybeTag.Value; + maybeBoxes.Clear(); + isInSelection = true; + } + } + + foreach (var childBox in box.Boxes) + { + var childInSelection = CollectSelectedBoxes(childBox, selectedBoxes, maybeBoxes); + if (childInSelection) + { + selectedBoxes[box] = true; + isInSelection = true; + } + } + + if (box.HtmlTag != null && selectedBoxes.Count > 0) + { + maybeBoxes[box] = true; + } + + return isInSelection; + } + + /// + /// find the box the is the root of selected boxes (the first box to contain multiple selected boxes) + /// + /// the root of the boxes tree + /// the selected boxes to find selection root in + /// the box that is the root of selected boxes + private static CssBox GetSelectionRoot(CssBox root, Dictionary selectedBoxes) + { + var selectionRoot = root; + var selectionRootRun = root; + while (true) + { + bool foundRoot = false; + CssBox selectedChild = null; + foreach (var childBox in selectionRootRun.Boxes) + { + if (selectedBoxes.ContainsKey(childBox)) + { + if (selectedChild != null) + { + foundRoot = true; + break; + } + selectedChild = childBox; + } + } + + if (foundRoot || selectedChild == null) + break; + + selectionRootRun = selectedChild; + + // the actual selection root must be a box with html tag + if (selectionRootRun.HtmlTag != null) + selectionRoot = selectionRootRun; + } + + // if the selection root doesn't contained any named boxes in it then we must go one level up, otherwise we will miss the selection root box formatting + if (!ContainsNamedBox(selectionRoot)) + { + selectionRootRun = selectionRoot.ParentBox; + while (selectionRootRun.ParentBox != null && selectionRootRun.HtmlTag == null) + selectionRootRun = selectionRootRun.ParentBox; + + if (selectionRootRun.HtmlTag != null) + selectionRoot = selectionRootRun; + } + + return selectionRoot; + } + + /// + /// Check if the given box has a names child box (has html tag) recursively. + /// + /// the box to check + /// true - in sub-tree there is a named box, false - otherwise + private static bool ContainsNamedBox(CssBox box) + { + foreach (var childBox in box.Boxes) + { + if (childBox.HtmlTag != null || ContainsNamedBox(childBox)) + return true; + } + return false; + } + + /// + /// Write the given html DOM sub-tree into the given string builder.
+ /// If are given write html only from those tags. + ///
+ /// used to parse CSS data + /// the string builder to write html into + /// the html sub-tree to write + /// Controls the way styles are generated when html is generated + /// Control if to generate only selected boxes, if given only boxes found in hash will be generated + /// the box the is the root of selected boxes (the first box to contain multiple selected boxes) + private static void WriteHtml(CssParser cssParser, StringBuilder sb, CssBox box, HtmlGenerationStyle styleGen, Dictionary selectedBoxes, CssBox selectionRoot) + { + if (box.HtmlTag == null || selectedBoxes == null || selectedBoxes.ContainsKey(box)) + { + if (box.HtmlTag != null) + { + if (box.HtmlTag.Name != "link" || !box.HtmlTag.Attributes.ContainsKey("href") || + (!box.HtmlTag.Attributes["href"].StartsWith("property") && !box.HtmlTag.Attributes["href"].StartsWith("method"))) + { + WriteHtmlTag(cssParser, sb, box, styleGen); + if (box == selectionRoot) + sb.Append(""); + } + + if (styleGen == HtmlGenerationStyle.InHeader && box.HtmlTag.Name == "html" && box.HtmlContainer.CssData != null) + { + sb.AppendLine(""); + WriteStylesheet(sb, box.HtmlContainer.CssData); + sb.AppendLine(""); + } + } + + if (box.Words.Count > 0) + { + foreach (var word in box.Words) + { + if (selectedBoxes == null || word.Selected) + { + var wordText = GetSelectedWord(word, selectedBoxes != null); + sb.Append(HtmlUtils.EncodeHtml(wordText)); + } + } + } + + foreach (var childBox in box.Boxes) + { + WriteHtml(cssParser, sb, childBox, styleGen, selectedBoxes, selectionRoot); + } + + if (box.HtmlTag != null && !box.HtmlTag.IsSingle) + { + if (box == selectionRoot) + sb.Append(""); + sb.AppendFormat("", box.HtmlTag.Name); + } + } + } + + /// + /// Write the given html tag with all its attributes and styles. + /// + /// used to parse CSS data + /// the string builder to write html into + /// the css box with the html tag to write + /// Controls the way styles are generated when html is generated + private static void WriteHtmlTag(CssParser cssParser, StringBuilder sb, CssBox box, HtmlGenerationStyle styleGen) + { + sb.AppendFormat("<{0}", box.HtmlTag.Name); + + // collect all element style properties including from stylesheet + var tagStyles = new Dictionary(); + var tagCssBlock = box.HtmlContainer.CssData.GetCssBlock(box.HtmlTag.Name); + if (tagCssBlock != null) + { + // TODO:a handle selectors + foreach (var cssBlock in tagCssBlock) + foreach (var prop in cssBlock.Properties) + tagStyles[prop.Key] = prop.Value; + } + + if (box.HtmlTag.HasAttributes()) + { + sb.Append(" "); + foreach (var att in box.HtmlTag.Attributes) + { + // handle image tags by inserting the image using base64 data + if (styleGen == HtmlGenerationStyle.Inline && att.Key == HtmlConstants.Style) + { + // if inline style add the styles to the collection + var block = cssParser.ParseCssBlock(box.HtmlTag.Name, box.HtmlTag.TryGetAttribute("style")); + foreach (var prop in block.Properties) + tagStyles[prop.Key] = prop.Value; + } + else if (styleGen == HtmlGenerationStyle.Inline && att.Key == HtmlConstants.Class) + { + // if inline style convert the style class to actual properties and add to collection + var cssBlocks = box.HtmlContainer.CssData.GetCssBlock("." + att.Value); + if (cssBlocks != null) + { + // TODO:a handle selectors + foreach (var cssBlock in cssBlocks) + foreach (var prop in cssBlock.Properties) + tagStyles[prop.Key] = prop.Value; + } + } + else + { + sb.AppendFormat("{0}=\"{1}\" ", att.Key, att.Value); + } + } + + sb.Remove(sb.Length - 1, 1); + } + + // if inline style insert the style tag with all collected style properties + if (styleGen == HtmlGenerationStyle.Inline && tagStyles.Count > 0) + { + var cleanTagStyles = StripDefaultStyles(box, tagStyles); + if (cleanTagStyles.Count > 0) + { + sb.Append(" style=\""); + foreach (var style in cleanTagStyles) + sb.AppendFormat("{0}: {1}; ", style.Key, style.Value); + sb.Remove(sb.Length - 1, 1); + sb.Append("\""); + } + } + + sb.AppendFormat("{0}>", box.HtmlTag.IsSingle ? "/" : ""); + } + + /// + /// Clean the given style collection by removing default styles so only custom styles remain.
+ /// Return new collection where the old remains unchanged. + ///
+ /// the box the styles apply to, used to know the default style + /// the collection of styles to clean + /// new cleaned styles collection + private static Dictionary StripDefaultStyles(CssBox box, Dictionary tagStyles) + { + // ReSharper disable PossibleMultipleEnumeration + var cleanTagStyles = new Dictionary(); + var defaultBlocks = box.HtmlContainer.Adapter.DefaultCssData.GetCssBlock(box.HtmlTag.Name); + foreach (var style in tagStyles) + { + bool isDefault = false; + foreach (var defaultBlock in defaultBlocks) + { + string value; + if (defaultBlock.Properties.TryGetValue(style.Key, out value) && value.Equals(style.Value, StringComparison.OrdinalIgnoreCase)) + { + isDefault = true; + break; + } + } + + if (!isDefault) + cleanTagStyles[style.Key] = style.Value; + } + return cleanTagStyles; + // ReSharper restore PossibleMultipleEnumeration + } + + /// + /// Write stylesheet data inline into the html. + /// + /// the string builder to write stylesheet into + /// the css data to write to the head + private static void WriteStylesheet(StringBuilder sb, CssData cssData) + { + sb.AppendLine(""); + } + + /// + /// Get the selected word with respect to partial selected words. + /// + /// the word to append + /// is to get selected text or all the text in the word + private static string GetSelectedWord(CssRect rect, bool selectedText) + { + if (selectedText && rect.SelectedStartIndex > -1 && rect.SelectedEndIndexOffset > -1) + { + return rect.Text.Substring(rect.SelectedStartIndex, rect.SelectedEndIndexOffset - rect.SelectedStartIndex); + } + else if (selectedText && rect.SelectedStartIndex > -1) + { + return rect.Text.Substring(rect.SelectedStartIndex) + (rect.HasSpaceAfter ? " " : ""); + } + else if (selectedText && rect.SelectedEndIndexOffset > -1) + { + return rect.Text.Substring(0, rect.SelectedEndIndexOffset); + } + else + { + var whitespaceBefore = rect.OwnerBox.Words[0] == rect ? IsBoxHasWhitespace(rect.OwnerBox) : rect.HasSpaceBefore; + return (whitespaceBefore ? " " : "") + rect.Text + (rect.HasSpaceAfter ? " " : ""); + } + } + + /// + /// Generate textual tree representation of the css boxes tree starting from the given root.
+ /// Used for debugging html parsing. + ///
+ /// the box to generate for + /// the string builder to generate to + /// the current indent level to set indent of generated text + private static void GenerateBoxTree(CssBox box, StringBuilder builder, int indent) + { + builder.AppendFormat("{0}<{1}", new string(' ', 2 * indent), box.Display); + if (box.HtmlTag != null) + builder.AppendFormat(" element=\"{0}\"", box.HtmlTag != null ? box.HtmlTag.Name : string.Empty); + if (box.Words.Count > 0) + builder.AppendFormat(" words=\"{0}\"", box.Words.Count); + builder.AppendFormat("{0}>\r\n", box.Boxes.Count > 0 ? "" : "/"); + if (box.Boxes.Count > 0) + { + foreach (var childBox in box.Boxes) + { + GenerateBoxTree(childBox, builder, indent + 1); + } + builder.AppendFormat("{0}\r\n", new string(' ', 2 * indent), box.Display); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/HtmlConstants.cs b/Source/HtmlRenderer.Core/Core/Utils/HtmlConstants.cs new file mode 100644 index 000000000..834cbe540 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/HtmlConstants.cs @@ -0,0 +1,229 @@ +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Defines HTML strings + /// + internal static class HtmlConstants + { + public const string A = "a"; + // public const string ABBR = "ABBR"; + // public const string ACRONYM = "ACRONYM"; + // public const string ADDRESS = "ADDRESS"; + // public const string APPLET = "APPLET"; + // public const string AREA = "AREA"; + // public const string B = "B"; + // public const string BASE = "BASE"; + // public const string BASEFONT = "BASEFONT"; + // public const string BDO = "BDO"; + // public const string BIG = "BIG"; + // public const string BLOCKQUOTE = "BLOCKQUOTE"; + // public const string BODY = "BODY"; + // public const string BR = "BR"; + // public const string BUTTON = "BUTTON"; + public const string Caption = "caption"; + // public const string CENTER = "CENTER"; + // public const string CITE = "CITE"; + // public const string CODE = "CODE"; + public const string Col = "col"; + public const string Colgroup = "colgroup"; + public const string Display = "display"; + // public const string DD = "DD"; + // public const string DEL = "DEL"; + // public const string DFN = "DFN"; + // public const string DIR = "DIR"; + // public const string DIV = "DIV"; + // public const string DL = "DL"; + // public const string DT = "DT"; + // public const string EM = "EM"; + // public const string FIELDSET = "FIELDSET"; + public const string Font = "font"; + // public const string FORM = "FORM"; + // public const string FRAME = "FRAME"; + // public const string FRAMESET = "FRAMESET"; + // public const string H1 = "H1"; + // public const string H2 = "H2"; + // public const string H3 = "H3"; + // public const string H4 = "H4"; + // public const string H5 = "H5"; + // public const string H6 = "H6"; + // public const string HEAD = "HEAD"; + public const string Hr = "hr"; + // public const string HTML = "HTML"; + // public const string I = "I"; + public const string Iframe = "iframe"; + public const string Img = "img"; + // public const string INPUT = "INPUT"; + // public const string INS = "INS"; + // public const string ISINDEX = "ISINDEX"; + // public const string KBD = "KBD"; + // public const string LABEL = "LABEL"; + // public const string LEGEND = "LEGEND"; + public const string Li = "li"; + // public const string LINK = "LINK"; + // public const string MAP = "MAP"; + // public const string MENU = "MENU"; + // public const string META = "META"; + // public const string NOFRAMES = "NOFRAMES"; + // public const string NOSCRIPT = "NOSCRIPT"; + // public const string OBJECT = "OBJECT"; + // public const string OL = "OL"; + // public const string OPTGROUP = "OPTGROUP"; + // public const string OPTION = "OPTION"; + // public const string P = "P"; + // public const string PARAM = "PARAM"; + // public const string PRE = "PRE"; + // public const string Q = "Q"; + // public const string S = "S"; + // public const string SAMP = "SAMP"; + // public const string SCRIPT = "SCRIPT"; + // public const string SELECT = "SELECT"; + // public const string SMALL = "SMALL"; + // public const string SPAN = "SPAN"; + // public const string STRIKE = "STRIKE"; + // public const string STRONG = "STRONG"; + public const string Style = "style"; + // public const string SUB = "SUB"; + // public const string SUP = "SUP"; + public const string Table = "table"; + public const string Tbody = "tbody"; + public const string Td = "td"; + // public const string TEXTAREA = "TEXTAREA"; + public const string Tfoot = "tfoot"; + public const string Th = "th"; + public const string Thead = "thead"; + // public const string TITLE = "TITLE"; + public const string Tr = "tr"; + // public const string TT = "TT"; + // public const string U = "U"; + // public const string UL = "UL"; + // public const string VAR = "VAR"; + + // public const string abbr = "abbr"; + // public const string accept = "accept"; + // public const string accesskey = "accesskey"; + // public const string action = "action"; + public const string Align = "align"; + // public const string alink = "alink"; + // public const string alt = "alt"; + // public const string archive = "archive"; + // public const string axis = "axis"; + public const string Background = "background"; + public const string Bgcolor = "bgcolor"; + public const string Border = "border"; + public const string Bordercolor = "bordercolor"; + public const string Cellpadding = "cellpadding"; + public const string Cellspacing = "cellspacing"; + // public const string char_ = "char"; + // public const string charoff = "charoff"; + // public const string charset = "charset"; + // public const string checked_ = "checked"; + // public const string cite = "cite"; + public const string Class = "class"; + // public const string classid = "classid"; + // public const string clear = "clear"; + // public const string code = "code"; + // public const string codebase = "codebase"; + // public const string codetype = "codetype"; + public const string Color = "color"; + // public const string cols = "cols"; + // public const string colspan = "colspan"; + // public const string compact = "compact"; + public const string content = "content"; + // public const string coords = "coords"; + // public const string data = "data"; + // public const string datetime = "datetime"; + // public const string declare = "declare"; + // public const string defer = "defer"; + public const string Dir = "dir"; + // public const string disabled = "disabled"; + // public const string enctype = "enctype"; + public const string Face = "face"; + // public const string for_ = "for"; + // public const string frame = "frame"; + // public const string frameborder = "frameborder"; + // public const string headers = "headers"; + public const string Height = "height"; + public const string Href = "href"; + // public const string hreflang = "hreflang"; + public const string Hspace = "hspace"; + // public const string http_equiv = "http-equiv"; + // public const string id = "id"; + // public const string ismap = "ismap"; + // public const string label = "label"; + // public const string lang = "lang"; + // public const string language = "language"; + // public const string link = "link"; + // public const string longdesc = "longdesc"; + // public const string marginheight = "marginheight"; + // public const string marginwidth = "marginwidth"; + // public const string maxlength = "maxlength"; + // public const string media = "media"; + // public const string method = "method"; + // public const string multiple = "multiple"; + // public const string name = "name"; + // public const string nohref = "nohref"; + // public const string noresize = "noresize"; + // public const string noshade = "noshade"; + public const string Nowrap = "nowrap"; + // public const string object_ = "object"; + // public const string onblur = "onblur"; + // public const string onchange = "onchange"; + // public const string onclick = "onclick"; + // public const string ondblclick = "ondblclick"; + // public const string onfocus = "onfocus"; + // public const string onkeydown = "onkeydown"; + // public const string onkeypress = "onkeypress"; + // public const string onkeyup = "onkeyup"; + // public const string onload = "onload"; + // public const string onmousedown = "onmousedown"; + // public const string onmousemove = "onmousemove"; + // public const string onmouseout = "onmouseout"; + // public const string onmouseover = "onmouseover"; + // public const string onmouseup = "onmouseup"; + // public const string onreset = "onreset"; + // public const string onselect = "onselect"; + // public const string onsubmit = "onsubmit"; + // public const string onunload = "onunload"; + // public const string profile = "profile"; + // public const string prompt = "prompt"; + // public const string readonly_ = "readonly"; + // public const string rel = "rel"; + // public const string rev = "rev"; + // public const string rows = "rows"; + // public const string rowspan = "rowspan"; + // public const string rules = "rules"; + // public const string scheme = "scheme"; + // public const string scope = "scope"; + // public const string scrolling = "scrolling"; + // public const string selected = "selected"; + // public const string shape = "shape"; + public const string Size = "size"; + // public const string span = "span"; + // public const string src = "src"; + // public const string standby = "standby"; + // public const string start = "start"; + // public const string style = "style"; + // public const string summary = "summary"; + // public const string tabindex = "tabindex"; + // public const string target = "target"; + // public const string text = "text"; + // public const string title = "title"; + // public const string type = "type"; + // public const string usemap = "usemap"; + public const string Valign = "valign"; + // public const string value = "value"; + // public const string valuetype = "valuetype"; + // public const string version = "version"; + // public const string vlink = "vlink"; + public const string Vspace = "vspace"; + public const string Width = "width"; + + public const string Left = "left"; + public const string Right = "right"; + // public const string top = "top"; + public const string Center = "center"; + // public const string middle = "middle"; + // public const string bottom = "bottom"; + public const string Justify = "justify"; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/HtmlUtils.cs b/Source/HtmlRenderer.Core/Core/Utils/HtmlUtils.cs new file mode 100644 index 000000000..960b719aa --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/HtmlUtils.cs @@ -0,0 +1,414 @@ +// "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.Utils +{ + internal static class HtmlUtils + { + #region Fields and Consts + + /// + /// List of html tags that don't have content + /// + private static readonly List _list = new List( + new[] + { + "area", "base", "basefont", "br", "col", + "frame", "hr", "img", "input", "isindex", + "link", "meta", "param" + } + ); + + /// + /// the html encode\decode pairs + /// + private static readonly KeyValuePair[] _encodeDecode = new[] + { + new KeyValuePair("<", "<"), + new KeyValuePair(">", ">"), + new KeyValuePair(""", "\""), + new KeyValuePair("&", "&"), + }; + + /// + /// the html decode only pairs + /// + private static readonly Dictionary _decodeOnly = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + #endregion + + + /// + /// Init. + /// + static HtmlUtils() + { + _decodeOnly["nbsp"] = ' '; + _decodeOnly["rdquo"] = '"'; + _decodeOnly["lsquo"] = '\''; + _decodeOnly["apos"] = '\''; + + // ISO 8859-1 Symbols + _decodeOnly["iexcl"] = Convert.ToChar(161); + _decodeOnly["cent"] = Convert.ToChar(162); + _decodeOnly["pound"] = Convert.ToChar(163); + _decodeOnly["curren"] = Convert.ToChar(164); + _decodeOnly["yen"] = Convert.ToChar(165); + _decodeOnly["brvbar"] = Convert.ToChar(166); + _decodeOnly["sect"] = Convert.ToChar(167); + _decodeOnly["uml"] = Convert.ToChar(168); + _decodeOnly["copy"] = Convert.ToChar(169); + _decodeOnly["ordf"] = Convert.ToChar(170); + _decodeOnly["laquo"] = Convert.ToChar(171); + _decodeOnly["not"] = Convert.ToChar(172); + _decodeOnly["shy"] = Convert.ToChar(173); + _decodeOnly["reg"] = Convert.ToChar(174); + _decodeOnly["macr"] = Convert.ToChar(175); + _decodeOnly["deg"] = Convert.ToChar(176); + _decodeOnly["plusmn"] = Convert.ToChar(177); + _decodeOnly["sup2"] = Convert.ToChar(178); + _decodeOnly["sup3"] = Convert.ToChar(179); + _decodeOnly["acute"] = Convert.ToChar(180); + _decodeOnly["micro"] = Convert.ToChar(181); + _decodeOnly["para"] = Convert.ToChar(182); + _decodeOnly["middot"] = Convert.ToChar(183); + _decodeOnly["cedil"] = Convert.ToChar(184); + _decodeOnly["sup1"] = Convert.ToChar(185); + _decodeOnly["ordm"] = Convert.ToChar(186); + _decodeOnly["raquo"] = Convert.ToChar(187); + _decodeOnly["frac14"] = Convert.ToChar(188); + _decodeOnly["frac12"] = Convert.ToChar(189); + _decodeOnly["frac34"] = Convert.ToChar(190); + _decodeOnly["iquest"] = Convert.ToChar(191); + _decodeOnly["times"] = Convert.ToChar(215); + _decodeOnly["divide"] = Convert.ToChar(247); + + // ISO 8859-1 Characters + _decodeOnly["Agrave"] = Convert.ToChar(192); + _decodeOnly["Aacute"] = Convert.ToChar(193); + _decodeOnly["Acirc"] = Convert.ToChar(194); + _decodeOnly["Atilde"] = Convert.ToChar(195); + _decodeOnly["Auml"] = Convert.ToChar(196); + _decodeOnly["Aring"] = Convert.ToChar(197); + _decodeOnly["AElig"] = Convert.ToChar(198); + _decodeOnly["Ccedil"] = Convert.ToChar(199); + _decodeOnly["Egrave"] = Convert.ToChar(200); + _decodeOnly["Eacute"] = Convert.ToChar(201); + _decodeOnly["Ecirc"] = Convert.ToChar(202); + _decodeOnly["Euml"] = Convert.ToChar(203); + _decodeOnly["Igrave"] = Convert.ToChar(204); + _decodeOnly["Iacute"] = Convert.ToChar(205); + _decodeOnly["Icirc"] = Convert.ToChar(206); + _decodeOnly["Iuml"] = Convert.ToChar(207); + _decodeOnly["ETH"] = Convert.ToChar(208); + _decodeOnly["Ntilde"] = Convert.ToChar(209); + _decodeOnly["Ograve"] = Convert.ToChar(210); + _decodeOnly["Oacute"] = Convert.ToChar(211); + _decodeOnly["Ocirc"] = Convert.ToChar(212); + _decodeOnly["Otilde"] = Convert.ToChar(213); + _decodeOnly["Ouml"] = Convert.ToChar(214); + _decodeOnly["Oslash"] = Convert.ToChar(216); + _decodeOnly["Ugrave"] = Convert.ToChar(217); + _decodeOnly["Uacute"] = Convert.ToChar(218); + _decodeOnly["Ucirc"] = Convert.ToChar(219); + _decodeOnly["Uuml"] = Convert.ToChar(220); + _decodeOnly["Yacute"] = Convert.ToChar(221); + _decodeOnly["THORN"] = Convert.ToChar(222); + _decodeOnly["szlig"] = Convert.ToChar(223); + _decodeOnly["agrave"] = Convert.ToChar(224); + _decodeOnly["aacute"] = Convert.ToChar(225); + _decodeOnly["acirc"] = Convert.ToChar(226); + _decodeOnly["atilde"] = Convert.ToChar(227); + _decodeOnly["auml"] = Convert.ToChar(228); + _decodeOnly["aring"] = Convert.ToChar(229); + _decodeOnly["aelig"] = Convert.ToChar(230); + _decodeOnly["ccedil"] = Convert.ToChar(231); + _decodeOnly["egrave"] = Convert.ToChar(232); + _decodeOnly["eacute"] = Convert.ToChar(233); + _decodeOnly["ecirc"] = Convert.ToChar(234); + _decodeOnly["euml"] = Convert.ToChar(235); + _decodeOnly["igrave"] = Convert.ToChar(236); + _decodeOnly["iacute"] = Convert.ToChar(237); + _decodeOnly["icirc"] = Convert.ToChar(238); + _decodeOnly["iuml"] = Convert.ToChar(239); + _decodeOnly["eth"] = Convert.ToChar(240); + _decodeOnly["ntilde"] = Convert.ToChar(241); + _decodeOnly["ograve"] = Convert.ToChar(242); + _decodeOnly["oacute"] = Convert.ToChar(243); + _decodeOnly["ocirc"] = Convert.ToChar(244); + _decodeOnly["otilde"] = Convert.ToChar(245); + _decodeOnly["ouml"] = Convert.ToChar(246); + _decodeOnly["oslash"] = Convert.ToChar(248); + _decodeOnly["ugrave"] = Convert.ToChar(249); + _decodeOnly["uacute"] = Convert.ToChar(250); + _decodeOnly["ucirc"] = Convert.ToChar(251); + _decodeOnly["uuml"] = Convert.ToChar(252); + _decodeOnly["yacute"] = Convert.ToChar(253); + _decodeOnly["thorn"] = Convert.ToChar(254); + _decodeOnly["yuml"] = Convert.ToChar(255); + + // Math Symbols Supported by HTML + _decodeOnly["forall"] = Convert.ToChar(8704); + _decodeOnly["part"] = Convert.ToChar(8706); + _decodeOnly["exist"] = Convert.ToChar(8707); + _decodeOnly["empty"] = Convert.ToChar(8709); + _decodeOnly["nabla"] = Convert.ToChar(8711); + _decodeOnly["isin"] = Convert.ToChar(8712); + _decodeOnly["notin"] = Convert.ToChar(8713); + _decodeOnly["ni"] = Convert.ToChar(8715); + _decodeOnly["prod"] = Convert.ToChar(8719); + _decodeOnly["sum"] = Convert.ToChar(8721); + _decodeOnly["minus"] = Convert.ToChar(8722); + _decodeOnly["lowast"] = Convert.ToChar(8727); + _decodeOnly["radic"] = Convert.ToChar(8730); + _decodeOnly["prop"] = Convert.ToChar(8733); + _decodeOnly["infin"] = Convert.ToChar(8734); + _decodeOnly["ang"] = Convert.ToChar(8736); + _decodeOnly["and"] = Convert.ToChar(8743); + _decodeOnly["or"] = Convert.ToChar(8744); + _decodeOnly["cap"] = Convert.ToChar(8745); + _decodeOnly["cup"] = Convert.ToChar(8746); + _decodeOnly["int"] = Convert.ToChar(8747); + _decodeOnly["there4"] = Convert.ToChar(8756); + _decodeOnly["sim"] = Convert.ToChar(8764); + _decodeOnly["cong"] = Convert.ToChar(8773); + _decodeOnly["asymp"] = Convert.ToChar(8776); + _decodeOnly["ne"] = Convert.ToChar(8800); + _decodeOnly["equiv"] = Convert.ToChar(8801); + _decodeOnly["le"] = Convert.ToChar(8804); + _decodeOnly["ge"] = Convert.ToChar(8805); + _decodeOnly["sub"] = Convert.ToChar(8834); + _decodeOnly["sup"] = Convert.ToChar(8835); + _decodeOnly["nsub"] = Convert.ToChar(8836); + _decodeOnly["sube"] = Convert.ToChar(8838); + _decodeOnly["supe"] = Convert.ToChar(8839); + _decodeOnly["oplus"] = Convert.ToChar(8853); + _decodeOnly["otimes"] = Convert.ToChar(8855); + _decodeOnly["perp"] = Convert.ToChar(8869); + _decodeOnly["sdot"] = Convert.ToChar(8901); + + // Greek Letters Supported by HTML + _decodeOnly["Alpha"] = Convert.ToChar(913); + _decodeOnly["Beta"] = Convert.ToChar(914); + _decodeOnly["Gamma"] = Convert.ToChar(915); + _decodeOnly["Delta"] = Convert.ToChar(916); + _decodeOnly["Epsilon"] = Convert.ToChar(917); + _decodeOnly["Zeta"] = Convert.ToChar(918); + _decodeOnly["Eta"] = Convert.ToChar(919); + _decodeOnly["Theta"] = Convert.ToChar(920); + _decodeOnly["Iota"] = Convert.ToChar(921); + _decodeOnly["Kappa"] = Convert.ToChar(922); + _decodeOnly["Lambda"] = Convert.ToChar(923); + _decodeOnly["Mu"] = Convert.ToChar(924); + _decodeOnly["Nu"] = Convert.ToChar(925); + _decodeOnly["Xi"] = Convert.ToChar(926); + _decodeOnly["Omicron"] = Convert.ToChar(927); + _decodeOnly["Pi"] = Convert.ToChar(928); + _decodeOnly["Rho"] = Convert.ToChar(929); + _decodeOnly["Sigma"] = Convert.ToChar(931); + _decodeOnly["Tau"] = Convert.ToChar(932); + _decodeOnly["Upsilon"] = Convert.ToChar(933); + _decodeOnly["Phi"] = Convert.ToChar(934); + _decodeOnly["Chi"] = Convert.ToChar(935); + _decodeOnly["Psi"] = Convert.ToChar(936); + _decodeOnly["Omega"] = Convert.ToChar(937); + _decodeOnly["alpha"] = Convert.ToChar(945); + _decodeOnly["beta"] = Convert.ToChar(946); + _decodeOnly["gamma"] = Convert.ToChar(947); + _decodeOnly["delta"] = Convert.ToChar(948); + _decodeOnly["epsilon"] = Convert.ToChar(949); + _decodeOnly["zeta"] = Convert.ToChar(950); + _decodeOnly["eta"] = Convert.ToChar(951); + _decodeOnly["theta"] = Convert.ToChar(952); + _decodeOnly["iota"] = Convert.ToChar(953); + _decodeOnly["kappa"] = Convert.ToChar(954); + _decodeOnly["lambda"] = Convert.ToChar(955); + _decodeOnly["mu"] = Convert.ToChar(956); + _decodeOnly["nu"] = Convert.ToChar(957); + _decodeOnly["xi"] = Convert.ToChar(958); + _decodeOnly["omicron"] = Convert.ToChar(959); + _decodeOnly["pi"] = Convert.ToChar(960); + _decodeOnly["rho"] = Convert.ToChar(961); + _decodeOnly["sigmaf"] = Convert.ToChar(962); + _decodeOnly["sigma"] = Convert.ToChar(963); + _decodeOnly["tau"] = Convert.ToChar(964); + _decodeOnly["upsilon"] = Convert.ToChar(965); + _decodeOnly["phi"] = Convert.ToChar(966); + _decodeOnly["chi"] = Convert.ToChar(967); + _decodeOnly["psi"] = Convert.ToChar(968); + _decodeOnly["omega"] = Convert.ToChar(969); + _decodeOnly["thetasym"] = Convert.ToChar(977); + _decodeOnly["upsih"] = Convert.ToChar(978); + _decodeOnly["piv"] = Convert.ToChar(982); + + // Other Entities Supported by HTML + _decodeOnly["OElig"] = Convert.ToChar(338); + _decodeOnly["oelig"] = Convert.ToChar(339); + _decodeOnly["Scaron"] = Convert.ToChar(352); + _decodeOnly["scaron"] = Convert.ToChar(353); + _decodeOnly["Yuml"] = Convert.ToChar(376); + _decodeOnly["fnof"] = Convert.ToChar(402); + _decodeOnly["circ"] = Convert.ToChar(710); + _decodeOnly["tilde"] = Convert.ToChar(732); + _decodeOnly["ndash"] = Convert.ToChar(8211); + _decodeOnly["mdash"] = Convert.ToChar(8212); + _decodeOnly["lsquo"] = Convert.ToChar(8216); + _decodeOnly["rsquo"] = Convert.ToChar(8217); + _decodeOnly["sbquo"] = Convert.ToChar(8218); + _decodeOnly["ldquo"] = Convert.ToChar(8220); + _decodeOnly["rdquo"] = Convert.ToChar(8221); + _decodeOnly["bdquo"] = Convert.ToChar(8222); + _decodeOnly["dagger"] = Convert.ToChar(8224); + _decodeOnly["Dagger"] = Convert.ToChar(8225); + _decodeOnly["bull"] = Convert.ToChar(8226); + _decodeOnly["hellip"] = Convert.ToChar(8230); + _decodeOnly["permil"] = Convert.ToChar(8240); + _decodeOnly["prime"] = Convert.ToChar(8242); + _decodeOnly["Prime"] = Convert.ToChar(8243); + _decodeOnly["lsaquo"] = Convert.ToChar(8249); + _decodeOnly["rsaquo"] = Convert.ToChar(8250); + _decodeOnly["oline"] = Convert.ToChar(8254); + _decodeOnly["euro"] = Convert.ToChar(8364); + _decodeOnly["trade"] = Convert.ToChar(153); + _decodeOnly["larr"] = Convert.ToChar(8592); + _decodeOnly["uarr"] = Convert.ToChar(8593); + _decodeOnly["rarr"] = Convert.ToChar(8594); + _decodeOnly["darr"] = Convert.ToChar(8595); + _decodeOnly["harr"] = Convert.ToChar(8596); + _decodeOnly["crarr"] = Convert.ToChar(8629); + _decodeOnly["lceil"] = Convert.ToChar(8968); + _decodeOnly["rceil"] = Convert.ToChar(8969); + _decodeOnly["lfloor"] = Convert.ToChar(8970); + _decodeOnly["rfloor"] = Convert.ToChar(8971); + _decodeOnly["loz"] = Convert.ToChar(9674); + _decodeOnly["spades"] = Convert.ToChar(9824); + _decodeOnly["clubs"] = Convert.ToChar(9827); + _decodeOnly["hearts"] = Convert.ToChar(9829); + _decodeOnly["diams"] = Convert.ToChar(9830); + } + + /// + /// Is the given html tag is single tag or can have content. + /// + /// the tag to check (must be lower case) + /// true - is single tag, false - otherwise + public static bool IsSingleTag(string tagName) + { + return _list.Contains(tagName); + } + + /// + /// Decode html encoded string to regular string.
+ /// Handles <, >, "&. + ///
+ /// the string to decode + /// decoded string + public static string DecodeHtml(string str) + { + if (!string.IsNullOrEmpty(str)) + { + str = DecodeHtmlCharByCode(str); + + str = DecodeHtmlCharByName(str); + + foreach (var encPair in _encodeDecode) + { + str = str.Replace(encPair.Key, encPair.Value); + } + } + return str; + } + + /// + /// Encode regular string into html encoded string.
+ /// Handles <, >, "&. + ///
+ /// the string to encode + /// encoded string + public static string EncodeHtml(string str) + { + if (!string.IsNullOrEmpty(str)) + { + for (int i = _encodeDecode.Length - 1; i >= 0; i--) + { + str = str.Replace(_encodeDecode[i].Value, _encodeDecode[i].Key); + } + } + return str; + } + + + #region Private methods + + /// + /// Decode html special charecters encoded using char entity code (€) + /// + /// the string to decode + /// decoded string + private static string DecodeHtmlCharByCode(string str) + { + var idx = str.IndexOf("&#", StringComparison.OrdinalIgnoreCase); + while (idx > -1) + { + bool hex = str.Length > idx + 3 && char.ToLower(str[idx + 2]) == 'x'; + var endIdx = idx + 2 + (hex ? 1 : 0); + + long num = 0; + while (endIdx < str.Length && CommonUtils.IsDigit(str[endIdx], hex)) + num = num * (hex ? 16 : 10) + CommonUtils.ToDigit(str[endIdx++], hex); + endIdx += (endIdx < str.Length && str[endIdx] == ';') ? 1 : 0; + + string repl = string.Empty; + if (num >= 0 && num <= 0x10ffff && !(num >= 0xd800 && num <= 0xdfff)) + repl = Char.ConvertFromUtf32((int)num); + + str = str.Remove(idx, endIdx - idx); + str = str.Insert(idx, repl); + + idx = str.IndexOf("&#", idx + 1); + } + return str; + } + + /// + /// Decode html special charecters encoded using char entity name (&#euro;) + /// + /// the string to decode + /// decoded string + private static string DecodeHtmlCharByName(string str) + { + var idx = str.IndexOf('&'); + while (idx > -1) + { + var endIdx = str.IndexOf(';', idx); + if (endIdx > -1 && endIdx - idx < 8) + { + var key = str.Substring(idx + 1, endIdx - idx - 1); + char c; + if (_decodeOnly.TryGetValue(key, out c)) + { + str = str.Remove(idx, endIdx - idx + 1); + str = str.Insert(idx, c.ToString()); + } + } + + idx = str.IndexOf('&', idx + 1); + } + return str; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/ImageError.png b/Source/HtmlRenderer.Core/Core/Utils/ImageError.png new file mode 100644 index 000000000..46b6c15fb Binary files /dev/null and b/Source/HtmlRenderer.Core/Core/Utils/ImageError.png differ diff --git a/Source/HtmlRenderer.Core/Core/Utils/ImageLoad.png b/Source/HtmlRenderer.Core/Core/Utils/ImageLoad.png new file mode 100644 index 000000000..2c132c679 Binary files /dev/null and b/Source/HtmlRenderer.Core/Core/Utils/ImageLoad.png differ diff --git a/Source/HtmlRenderer.Core/Core/Utils/RenderUtils.cs b/Source/HtmlRenderer.Core/Core/Utils/RenderUtils.cs new file mode 100644 index 000000000..2726b5fb2 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/RenderUtils.cs @@ -0,0 +1,140 @@ +// "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.Dom; + +namespace TheArtOfDev.HtmlRenderer.Core.Utils +{ + /// + /// Provides some drawing functionality + /// + internal static class RenderUtils + { + /// + /// Check if the given color is visible if painted (has alpha and color values) + /// + /// the color to check + /// true - visible, false - not visible + public static bool IsColorVisible(RColor color) + { + return color.A > 0; + } + + /// + /// Clip the region the graphics will draw on by the overflow style of the containing block.
+ /// Recursively travel up the tree to find containing block that has overflow style set to hidden. if not + /// block found there will be no clipping and null will be returned. + ///
+ /// the graphics to clip + /// the box that is rendered to get containing blocks + /// true - was clipped, false - not clipped + public static bool ClipGraphicsByOverflow(RGraphics g, CssBox box) + { + var containingBlock = box.ContainingBlock; + while (true) + { + if (containingBlock.Overflow == CssConstants.Hidden) + { + var prevClip = g.GetClip(); + var rect = box.ContainingBlock.ClientRectangle; + rect.X -= 2; // TODO:a find better way to fix it + rect.Width += 2; + + if (!box.IsFixed) + rect.Offset(box.HtmlContainer.ScrollOffset); + + rect.Intersect(prevClip); + g.PushClip(rect); + return true; + } + else + { + var cBlock = containingBlock.ContainingBlock; + if (cBlock == containingBlock) + return false; + containingBlock = cBlock; + } + } + } + + /// + /// Draw image loading icon. + /// + /// the device to draw into + /// + /// the rectangle to draw icon in + public static void DrawImageLoadingIcon(RGraphics g, HtmlContainerInt htmlContainer, RRect r) + { + g.DrawRectangle(g.GetPen(RColor.LightGray), r.Left + 3, r.Top + 3, 13, 14); + var image = htmlContainer.Adapter.GetLoadingImage(); + g.DrawImage(image, new RRect(r.Left + 4, r.Top + 4, image.Width, image.Height)); + } + + /// + /// Draw image failed to load icon. + /// + /// the device to draw into + /// + /// the rectangle to draw icon in + public static void DrawImageErrorIcon(RGraphics g, HtmlContainerInt htmlContainer, RRect r) + { + g.DrawRectangle(g.GetPen(RColor.LightGray), r.Left + 2, r.Top + 2, 15, 15); + var image = htmlContainer.Adapter.GetLoadingFailedImage(); + g.DrawImage(image, new RRect(r.Left + 3, r.Top + 3, image.Width, image.Height)); + } + + /// + /// Creates a rounded rectangle using the specified corner radius
+ /// NW-----NE + /// | | + /// | | + /// SW-----SE + ///
+ /// the device to draw into + /// Rectangle to round + /// Radius of the north east corner + /// Radius of the north west corner + /// Radius of the south east corner + /// Radius of the south west corner + /// GraphicsPath with the lines of the rounded rectangle ready to be painted + public static RGraphicsPath GetRoundRect(RGraphics g, RRect rect, double nwRadius, double neRadius, double seRadius, double swRadius) + { + var path = g.GetGraphicsPath(); + + path.Start(rect.Left + nwRadius, rect.Top); + + path.LineTo(rect.Right - neRadius, rect.Y); + + if (neRadius > 0f) + path.ArcTo(rect.Right, rect.Top + neRadius, neRadius, RGraphicsPath.Corner.TopRight); + + path.LineTo(rect.Right, rect.Bottom - seRadius); + + if (seRadius > 0f) + path.ArcTo(rect.Right - seRadius, rect.Bottom, seRadius, RGraphicsPath.Corner.BottomRight); + + path.LineTo(rect.Left + swRadius, rect.Bottom); + + if (swRadius > 0f) + path.ArcTo(rect.Left, rect.Bottom - swRadius, swRadius, RGraphicsPath.Corner.BottomLeft); + + path.LineTo(rect.Left, rect.Top + nwRadius); + + if (nwRadius > 0f) + path.ArcTo(rect.Left + nwRadius, rect.Top, nwRadius, RGraphicsPath.Corner.TopLeft); + + return path; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/Core/Utils/SubString.cs b/Source/HtmlRenderer.Core/Core/Utils/SubString.cs new file mode 100644 index 000000000..f06f740f2 --- /dev/null +++ b/Source/HtmlRenderer.Core/Core/Utils/SubString.cs @@ -0,0 +1,187 @@ +// "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.Utils +{ + /// + /// Represents sub-string of a full string starting at specific location with a specific length. + /// + internal sealed class SubString + { + #region Fields and Consts + + /// + /// the full string that this sub-string is part of + /// + private readonly string _fullString; + + /// + /// the start index of the sub-string + /// + private readonly int _startIdx; + + /// + /// the length of the sub-string starting at + /// + private readonly int _length; + + #endregion + + + /// + /// Init sub-string that is the full string. + /// + /// the full string that this sub-string is part of + public SubString(string fullString) + { + ArgChecker.AssertArgNotNull(fullString, "fullString"); + + _fullString = fullString; + _startIdx = 0; + _length = fullString.Length; + } + + /// + /// Init. + /// + /// the full string that this sub-string is part of + /// the start index of the sub-string + /// the length of the sub-string starting at + /// is null + public SubString(string fullString, int startIdx, int length) + { + ArgChecker.AssertArgNotNull(fullString, "fullString"); + if (startIdx < 0 || startIdx >= fullString.Length) + throw new ArgumentOutOfRangeException("startIdx", "Must within fullString boundries"); + if (length < 0 || startIdx + length > fullString.Length) + throw new ArgumentOutOfRangeException("length", "Must within fullString boundries"); + + _fullString = fullString; + _startIdx = startIdx; + _length = length; + } + + /// + /// the full string that this sub-string is part of + /// + public string FullString + { + get { return _fullString; } + } + + /// + /// the start index of the sub-string + /// + public int StartIdx + { + get { return _startIdx; } + } + + /// + /// the length of the sub-string starting at + /// + public int Length + { + get { return _length; } + } + + /// + /// Get string char at specific index. + /// + /// the idx to get the char at + /// char at index + public char this[int idx] + { + get + { + if (idx < 0 || idx > _length) + throw new ArgumentOutOfRangeException("idx", "must be within the string range"); + return _fullString[_startIdx + idx]; + } + } + + /// + /// Is the sub-string is empty string. + /// + /// true - empty string, false - otherwise + public bool IsEmpty() + { + return _length < 1; + } + + /// + /// Is the sub-string is empty string or contains only whitespaces. + /// + /// true - empty or whitespace string, false - otherwise + public bool IsEmptyOrWhitespace() + { + for (int i = 0; i < _length; i++) + { + if (!char.IsWhiteSpace(_fullString, _startIdx + i)) + return false; + } + return true; + } + + /// + /// Is the sub-string contains only whitespaces (at least one). + /// + /// true - empty or whitespace string, false - otherwise + public bool IsWhitespace() + { + if (_length < 1) + return false; + for (int i = 0; i < _length; i++) + { + if (!char.IsWhiteSpace(_fullString, _startIdx + i)) + return false; + } + return true; + } + + /// + /// Get a string of the sub-string.
+ /// This will create a new string object! + ///
+ /// new string that is the sub-string represented by this instance + public string CutSubstring() + { + return _length > 0 ? _fullString.Substring(_startIdx, _length) : string.Empty; + } + + /// + /// Retrieves a substring from this instance. The substring starts at a specified character position and has a specified length. + /// + /// The zero-based starting character position of a substring in this instance. + /// The number of characters in the substring. + /// A String equivalent to the substring of length length that begins at startIndex in this instance, or + /// Empty if startIndex is equal to the length of this instance and length is zero. + public string Substring(int startIdx, int length) + { + if (startIdx < 0 || startIdx > _length) + throw new ArgumentOutOfRangeException("startIdx"); + if (length > _length) + throw new ArgumentOutOfRangeException("length"); + if (startIdx + length > _length) + throw new ArgumentOutOfRangeException("length"); + + return _fullString.Substring(_startIdx + startIdx, length); + } + + public override string ToString() + { + return string.Format("Sub-string: {0}", _length > 0 ? _fullString.Substring(_startIdx, _length) : string.Empty); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj b/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj new file mode 100644 index 000000000..86ea3bbe7 --- /dev/null +++ b/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp2.1 + + + diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/BrushAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/BrushAdapter.cs new file mode 100644 index 000000000..4a3bfe242 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/BrushAdapter.cs @@ -0,0 +1,49 @@ +// "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 PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms brushes objects for core. + /// + internal sealed class BrushAdapter : RBrush + { + /// + /// The actual PdfSharp brush instance.
+ /// Should be but there is some fucking issue inheriting from it =/ + ///
+ private readonly Object _brush; + + /// + /// Init. + /// + public BrushAdapter(Object brush) + { + _brush = brush; + } + + /// + /// The actual WinForms brush instance. + /// + public Object Brush + { + get { return _brush; } + } + + public override void Dispose() + { } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontAdapter.cs new file mode 100644 index 000000000..d606567f5 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontAdapter.cs @@ -0,0 +1,105 @@ +// "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 PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms Font object for core. + /// + internal sealed class FontAdapter : RFont + { + #region Fields and Consts + + /// + /// the underline win-forms font. + /// + private readonly XFont _font; + + /// + /// the vertical offset of the font underline location from the top of the font. + /// + private double _underlineOffset = -1; + + /// + /// Cached font height. + /// + private double _height = -1; + + /// + /// Cached font whitespace width. + /// + private double _whitespaceWidth = -1; + + #endregion + + + /// + /// Init. + /// + public FontAdapter(XFont font) + { + _font = font; + } + + /// + /// the underline win-forms font. + /// + public XFont Font + { + get { return _font; } + } + + public override double Size + { + get { return _font.Size; } + } + + public override double UnderlineOffset + { + get { return _underlineOffset; } + } + + public override double Height + { + get { return _height; } + } + + public override double LeftPadding + { + get { return _height / 6f; } + } + + + public override double GetWhitespaceWidth(RGraphics graphics) + { + if (_whitespaceWidth < 0) + { + _whitespaceWidth = graphics.MeasureString(" ", this).Width; + } + return _whitespaceWidth; + } + + /// + /// Set font metrics to be cached for the font for future use. + /// + /// the full height of the font + /// the vertical offset of the font underline location from the top of the font. + internal void SetMetrics(int height, int underlineOffset) + { + _height = height; + _underlineOffset = underlineOffset; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontFamilyAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontFamilyAdapter.cs new file mode 100644 index 000000000..fdb5cb94b --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/FontFamilyAdapter.cs @@ -0,0 +1,49 @@ +// "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 PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms Font object for core. + /// + internal sealed class FontFamilyAdapter : RFontFamily + { + /// + /// the underline win-forms font. + /// + private readonly XFontFamily _fontFamily; + + /// + /// Init. + /// + public FontFamilyAdapter(XFontFamily fontFamily) + { + _fontFamily = fontFamily; + } + + /// + /// the underline win-forms font family. + /// + public XFontFamily FontFamily + { + get { return _fontFamily; } + } + + public override string Name + { + get { return _fontFamily.Name; } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsAdapter.cs new file mode 100644 index 000000000..f3e7f9d8c --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsAdapter.cs @@ -0,0 +1,204 @@ +// "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 PdfSharpCore.Drawing; +using System; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.PdfSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms Graphics for core. + /// + internal sealed class GraphicsAdapter : RGraphics + { + #region Fields and Consts + + /// + /// The wrapped WinForms graphics object + /// + private readonly XGraphics _g; + + /// + /// if to release the graphics object on dispose + /// + private readonly bool _releaseGraphics; + + /// + /// Used to measure and draw strings + /// + private static readonly XStringFormat _stringFormat; + + #endregion + + + static GraphicsAdapter() + { + _stringFormat = new XStringFormat(); + _stringFormat.Alignment = XStringAlignment.Near; + _stringFormat.LineAlignment = XLineAlignment.Near; + } + + /// + /// Init. + /// + /// the win forms graphics object to use + /// optional: if to release the graphics object on dispose (default - false) + public GraphicsAdapter(XGraphics g, bool releaseGraphics = false) + : base(PdfSharpAdapter.Instance, new RRect(0, 0, double.MaxValue, double.MaxValue)) + { + ArgChecker.AssertArgNotNull(g, "g"); + + _g = g; + _releaseGraphics = releaseGraphics; + } + + public override void PopClip() + { + _clipStack.Pop(); + _g.Restore(); + } + + public override void PushClip(RRect rect) + { + _clipStack.Push(rect); + _g.Save(); + _g.IntersectClip(Utils.Convert(rect)); + } + + public override void PushClipExclude(RRect rect) + { } + + public override Object SetAntiAliasSmoothingMode() + { + var prevMode = _g.SmoothingMode; + _g.SmoothingMode = XSmoothingMode.AntiAlias; + return prevMode; + } + + public override void ReturnPreviousSmoothingMode(Object prevMode) + { + if (prevMode != null) + { + _g.SmoothingMode = (XSmoothingMode)prevMode; + } + } + + public override RSize MeasureString(string str, RFont font) + { + var fontAdapter = (FontAdapter)font; + var realFont = fontAdapter.Font; + var size = _g.MeasureString(str, realFont, _stringFormat); + + if (font.Height < 0) + { + var height = realFont.Height; + var descent = realFont.Size * realFont.FontFamily.GetCellDescent(realFont.Style) / realFont.FontFamily.GetEmHeight(realFont.Style); + fontAdapter.SetMetrics(height, (int)Math.Round((height - descent + 1f))); + } + + return Utils.Convert(size); + } + + public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) + { + // there is no need for it - used for text selection + throw new NotSupportedException(); + } + + public override void DrawString(string str, RFont font, RColor color, RPoint point, RSize size, bool rtl) + { + var xBrush = ((BrushAdapter)_adapter.GetSolidBrush(color)).Brush; + _g.DrawString(str, ((FontAdapter)font).Font, (XBrush)xBrush, point.X, point.Y, _stringFormat); + } + + public override RBrush GetTextureBrush(RImage image, RRect dstRect, RPoint translateTransformLocation) + { + return new BrushAdapter(new XTextureBrush(((ImageAdapter)image).Image, Utils.Convert(dstRect), Utils.Convert(translateTransformLocation))); + } + + public override RGraphicsPath GetGraphicsPath() + { + return new GraphicsPathAdapter(); + } + + public override void Dispose() + { + if (_releaseGraphics) + _g.Dispose(); + } + + + #region Delegate graphics methods + + public override void DrawLine(RPen pen, double x1, double y1, double x2, double y2) + { + _g.DrawLine(((PenAdapter)pen).Pen, x1, y1, x2, y2); + } + + public override void DrawRectangle(RPen pen, double x, double y, double width, double height) + { + _g.DrawRectangle(((PenAdapter)pen).Pen, x, y, width, height); + } + + public override void DrawRectangle(RBrush brush, double x, double y, double width, double height) + { + var xBrush = ((BrushAdapter)brush).Brush; + var xTextureBrush = xBrush as XTextureBrush; + if (xTextureBrush != null) + { + xTextureBrush.DrawRectangle(_g, x, y, width, height); + } + else + { + _g.DrawRectangle((XBrush)xBrush, x, y, width, height); + + // handle bug in PdfSharp that keeps the brush color for next string draw + if (xBrush is XLinearGradientBrush) + _g.DrawRectangle(XBrushes.White, 0, 0, 0.1, 0.1); + } + } + + public override void DrawImage(RImage image, RRect destRect, RRect srcRect) + { + _g.DrawImage(((ImageAdapter)image).Image, Utils.Convert(destRect), Utils.Convert(srcRect), XGraphicsUnit.Point); + } + + public override void DrawImage(RImage image, RRect destRect) + { + _g.DrawImage(((ImageAdapter)image).Image, Utils.Convert(destRect)); + } + + public override void DrawPath(RPen pen, RGraphicsPath path) + { + _g.DrawPath(((PenAdapter)pen).Pen, ((GraphicsPathAdapter)path).GraphicsPath); + } + + public override void DrawPath(RBrush brush, RGraphicsPath path) + { + _g.DrawPath((XBrush)((BrushAdapter)brush).Brush, ((GraphicsPathAdapter)path).GraphicsPath); + } + + public override void DrawPolygon(RBrush brush, RPoint[] points) + { + if (points != null && points.Length > 0) + { + _g.DrawPolygon((XBrush)((BrushAdapter)brush).Brush, Utils.Convert(points), XFillMode.Winding); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsPathAdapter.cs new file mode 100644 index 000000000..6646987bb --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/GraphicsPathAdapter.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" + +using System; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms graphics path object for core. + /// + internal sealed class GraphicsPathAdapter : RGraphicsPath + { + /// + /// The actual PdfSharp graphics path instance. + /// + private readonly XGraphicsPath _graphicsPath = new XGraphicsPath(); + + /// + /// the last point added to the path to begin next segment from + /// + private RPoint _lastPoint; + + /// + /// The actual PdfSharp graphics path instance. + /// + public XGraphicsPath GraphicsPath + { + get { return _graphicsPath; } + } + + public override void Start(double x, double y) + { + _lastPoint = new RPoint(x, y); + } + + public override void LineTo(double x, double y) + { + _graphicsPath.AddLine((float)_lastPoint.X, (float)_lastPoint.Y, (float)x, (float)y); + _lastPoint = new RPoint(x, y); + } + + public override void ArcTo(double x, double y, double size, Corner corner) + { + float left = (float)(Math.Min(x, _lastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0)); + float top = (float)(Math.Min(y, _lastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0)); + _graphicsPath.AddArc(left, top, (float)size * 2, (float)size * 2, GetStartAngle(corner), 90); + _lastPoint = new RPoint(x, y); + } + + public override void Dispose() + { } + + /// + /// Get arc start angle for the given corner. + /// + private static int GetStartAngle(Corner corner) + { + int startAngle; + switch (corner) + { + case Corner.TopLeft: + startAngle = 180; + break; + case Corner.TopRight: + startAngle = 270; + break; + case Corner.BottomLeft: + startAngle = 90; + break; + case Corner.BottomRight: + startAngle = 0; + break; + default: + throw new ArgumentOutOfRangeException("corner"); + } + return startAngle; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/ImageAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/ImageAdapter.cs new file mode 100644 index 000000000..36f809334 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/ImageAdapter.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 TheArtOfDev.HtmlRenderer.Adapters; +using PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms Image object for core. + /// + internal sealed class ImageAdapter : RImage + { + /// + /// the underline win-forms image. + /// + private readonly XImage _image; + + /// + /// Initializes a new instance of the class. + /// + public ImageAdapter(XImage image) + { + _image = image; + } + + /// + /// the underline win-forms image. + /// + public XImage Image + { + get { return _image; } + } + + public override double Width + { + get { return _image.PixelWidth; } + } + + public override double Height + { + get { return _image.PixelHeight; } + } + + public override void Dispose() + { + _image.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/PdfSharpAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/PdfSharpAdapter.cs new file mode 100644 index 000000000..48b1a3eb0 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/PdfSharpAdapter.cs @@ -0,0 +1,136 @@ +// "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 PdfSharpCore.Drawing; +using PdfSharpCore.Pdf; +using System.Drawing; +using System.Drawing.Text; +using System.IO; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.PdfSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for PdfSharp library platform. + /// + internal sealed class PdfSharpAdapter : RAdapter + { + #region Fields and Consts + + /// + /// Singleton instance of global adapter. + /// + private static readonly PdfSharpAdapter _instance = new PdfSharpAdapter(); + + #endregion + + + /// + /// Init color resolve. + /// + private PdfSharpAdapter() + { + AddFontFamilyMapping("monospace", "Courier New"); + AddFontFamilyMapping("Helvetica", "Arial"); + + /* var families = new InstalledFontCollection(); + + foreach (var family in families.Families) + { + AddFontFamily(new FontFamilyAdapter(new XFontFamily(family.Name))); + }*/ + } + + /// + /// Singleton instance of global adapter. + /// + public static PdfSharpAdapter Instance + { + get { return _instance; } + } + + protected override RColor GetColorInt(string colorName) + { + try + { + var color = Color.FromKnownColor((KnownColor)System.Enum.Parse(typeof(KnownColor), colorName, true)); + return Utils.Convert(color); + } + catch + { + return RColor.Empty; + } + } + + protected override RPen CreatePen(RColor color) + { + return new PenAdapter(new XPen(Utils.Convert(color))); + } + + protected override RBrush CreateSolidBrush(RColor color) + { + XBrush solidBrush; + if (color == RColor.White) + solidBrush = XBrushes.White; + else if (color == RColor.Black) + solidBrush = XBrushes.Black; + else if (color.A < 1) + solidBrush = XBrushes.Transparent; + else + solidBrush = new XSolidBrush(Utils.Convert(color)); + + return new BrushAdapter(solidBrush); + } + + protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + { + XLinearGradientMode mode; + if (angle < 45) + mode = XLinearGradientMode.ForwardDiagonal; + else if (angle < 90) + mode = XLinearGradientMode.Vertical; + else if (angle < 135) + mode = XLinearGradientMode.BackwardDiagonal; + else + mode = XLinearGradientMode.Horizontal; + return new BrushAdapter(new XLinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), mode)); + } + + protected override RImage ConvertImageInt(object image) + { + return image != null ? new ImageAdapter((XImage)image) : null; + } + + protected override RImage ImageFromStreamInt(Stream memoryStream) + { + return new ImageAdapter(XImage.FromStream(() => memoryStream)); + } + + protected override RFont CreateFontInt(string family, double size, RFontStyle style) + { + var fontStyle = (XFontStyle)((int)style); + var xFont = new XFont(family, size, fontStyle, new XPdfFontOptions(PdfFontEncoding.Unicode)); + return new FontAdapter(xFont); + } + + protected override RFont CreateFontInt(RFontFamily family, double size, RFontStyle style) + { + var fontStyle = (XFontStyle)((int)style); + string fontName = family is FontFamilyAdapter adapter ? adapter.FontFamily.Name : family.Name; + var xFont = new XFont(fontName, size, fontStyle, new XPdfFontOptions(PdfFontEncoding.Unicode)); + return new FontAdapter(xFont); + } + + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/PenAdapter.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/PenAdapter.cs new file mode 100644 index 000000000..4391b339c --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/PenAdapter.cs @@ -0,0 +1,84 @@ +// "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 PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Adapter for WinForms pens objects for core. + /// + internal sealed class PenAdapter : RPen + { + /// + /// The actual WinForms brush instance. + /// + private readonly XPen _pen; + + /// + /// Init. + /// + public PenAdapter(XPen pen) + { + _pen = pen; + } + + /// + /// The actual WinForms brush instance. + /// + public XPen Pen + { + get { return _pen; } + } + + public override double Width + { + get { return _pen.Width; } + set { _pen.Width = value; } + } + + public override RDashStyle DashStyle + { + set + { + switch (value) + { + case RDashStyle.Solid: + _pen.DashStyle = XDashStyle.Solid; + break; + case RDashStyle.Dash: + _pen.DashStyle = XDashStyle.Dash; + if (Width < 2) + _pen.DashPattern = new[] { 4, 4d }; // better looking + break; + case RDashStyle.Dot: + _pen.DashStyle = XDashStyle.Dot; + break; + case RDashStyle.DashDot: + _pen.DashStyle = XDashStyle.DashDot; + break; + case RDashStyle.DashDotDot: + _pen.DashStyle = XDashStyle.DashDotDot; + break; + case RDashStyle.Custom: + _pen.DashStyle = XDashStyle.Custom; + break; + default: + _pen.DashStyle = XDashStyle.Solid; + break; + } + } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Adapters/XTextureBrush.cs b/Source/HtmlRenderer.PdfSharp.Core/Adapters/XTextureBrush.cs new file mode 100644 index 000000000..31ffe3e84 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Adapters/XTextureBrush.cs @@ -0,0 +1,76 @@ +// "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 PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Adapters +{ + /// + /// Because PdfSharp doesn't support texture brush we need to implement it ourselves. + /// + internal sealed class XTextureBrush + { + #region Fields/Consts + + /// + /// The image to draw in the brush + /// + private readonly XImage _image; + + /// + /// the + /// + private readonly XRect _dstRect; + + /// + /// the transform the location of the image to handle center alignment + /// + private readonly XPoint _translateTransformLocation; + + #endregion + + + /// + /// Init. + /// + public XTextureBrush(XImage image, XRect dstRect, XPoint translateTransformLocation) + { + _image = image; + _dstRect = dstRect; + _translateTransformLocation = translateTransformLocation; + } + + /// + /// Draw the texture image in the given graphics at the given location. + /// + public void DrawRectangle(XGraphics g, double x, double y, double width, double height) + { + var prevState = g.Save(); + g.IntersectClip(new XRect(x, y, width, height)); + + double rx = _translateTransformLocation.X; + double w = _image.PixelWidth, h = _image.PixelHeight; + while (rx < x + width) + { + double ry = _translateTransformLocation.Y; + while (ry < y + height) + { + g.DrawImage(_image, rx, ry, w, h); + ry += h; + } + rx += w; + } + + g.Restore(prevState); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/HtmlContainer.cs b/Source/HtmlRenderer.PdfSharp.Core/HtmlContainer.cs new file mode 100644 index 000000000..3c12f5fb4 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/HtmlContainer.cs @@ -0,0 +1,356 @@ +// "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 PdfSharpCore.Drawing; +using System; +using System.Collections.Generic; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.PdfSharp.Adapters; +using TheArtOfDev.HtmlRenderer.PdfSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp +{ + /// + /// Low level handling of Html Renderer logic, this class is used by . + /// + /// + public sealed class HtmlContainer : IDisposable + { + #region Fields and Consts + + /// + /// The internal core html container + /// + private readonly HtmlContainerInt _htmlContainerInt; + + #endregion + + + /// + /// Init. + /// + public HtmlContainer() + { + _htmlContainerInt = new HtmlContainerInt(PdfSharpAdapter.Instance); + _htmlContainerInt.AvoidAsyncImagesLoading = true; + _htmlContainerInt.AvoidImagesLateLoading = true; + } + + /// + /// Raised when the set html document has been fully loaded.
+ /// Allows manipulation of the html dom, scroll position, etc. + ///
+ public event EventHandler LoadComplete + { + add { _htmlContainerInt.LoadComplete += value; } + remove { _htmlContainerInt.LoadComplete -= value; } + } + + /// + /// Raised when an error occurred during html rendering.
+ ///
+ /// + /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. + /// + public event EventHandler RenderError + { + add { _htmlContainerInt.RenderError += value; } + remove { _htmlContainerInt.RenderError -= value; } + } + + /// + /// Raised when a stylesheet is about to be loaded by file path or URI by link element.
+ /// This event allows to provide the stylesheet manually or provide new source (file or Uri) to load from.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ public event EventHandler StylesheetLoad + { + add { _htmlContainerInt.StylesheetLoad += value; } + remove { _htmlContainerInt.StylesheetLoad -= value; } + } + + /// + /// Raised when an image is about to be loaded by file path or URI.
+ /// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI. + ///
+ public event EventHandler ImageLoad + { + add { _htmlContainerInt.ImageLoad += value; } + remove { _htmlContainerInt.ImageLoad -= value; } + } + + /// + /// The internal core html container + /// + internal HtmlContainerInt HtmlContainerInt + { + get { return _htmlContainerInt; } + } + + /// + /// the parsed stylesheet data used for handling the html + /// + public CssData CssData + { + get { return _htmlContainerInt.CssData; } + } + + /// + /// Gets or sets a value indicating if anti-aliasing should be avoided for geometry like backgrounds and borders (default - false). + /// + public bool AvoidGeometryAntialias + { + get { return _htmlContainerInt.AvoidGeometryAntialias; } + set { _htmlContainerInt.AvoidGeometryAntialias = value; } + } + + /// + /// The scroll offset of the html.
+ /// This will adjust the rendered html by the given offset so the content will be "scrolled".
+ ///
+ /// + /// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered as it + /// will be at -100 therefore outside the client rectangle. + /// + public XPoint ScrollOffset + { + get { return Utils.Convert(_htmlContainerInt.ScrollOffset); } + set { _htmlContainerInt.ScrollOffset = Utils.Convert(value); } + } + + /// + /// The top-left most location of the rendered html.
+ /// This will offset the top-left corner of the rendered html. + ///
+ public XPoint Location + { + get { return Utils.Convert(_htmlContainerInt.Location); } + set { _htmlContainerInt.Location = Utils.Convert(value); } + } + + /// + /// The max width and height of the rendered html.
+ /// The max width will effect the html layout wrapping lines, resize images and tables where possible.
+ /// The max height does NOT effect layout, but will not render outside it (clip).
+ /// can be exceed the max size by layout restrictions (unwrappable line, set image size, etc.).
+ /// Set zero for unlimited (width\height separately).
+ ///
+ public XSize MaxSize + { + get { return Utils.Convert(_htmlContainerInt.MaxSize); } + set { _htmlContainerInt.MaxSize = Utils.Convert(value); } + } + + /// + /// The actual size of the rendered html (after layout) + /// + public XSize ActualSize + { + get { return Utils.Convert(_htmlContainerInt.ActualSize); } + internal set { _htmlContainerInt.ActualSize = Utils.Convert(value); } + } + + public XSize PageSize { + get + { + return new XSize(_htmlContainerInt.PageSize.Width, _htmlContainerInt.PageSize.Height); + } + set + { + _htmlContainerInt.PageSize = new RSize(value.Width, value.Height); + } + } + + /// + /// the top margin between the page start and the text + /// + public int MarginTop + { + get { return _htmlContainerInt.MarginTop; } + set + { + if (value > -1) + _htmlContainerInt.MarginTop = value; + } + } + + /// + /// the bottom margin between the page end and the text + /// + public int MarginBottom + { + get { return _htmlContainerInt.MarginBottom; } + set + { + if (value > -1) + _htmlContainerInt.MarginBottom = value; + } + } + + /// + /// the left margin between the page start and the text + /// + public int MarginLeft + { + get { return _htmlContainerInt.MarginLeft; } + set + { + if (value > -1) + _htmlContainerInt.MarginLeft = value; + } + } + + /// + /// the right margin between the page end and the text + /// + public int MarginRight + { + get { return _htmlContainerInt.MarginRight; } + set + { + if (value > -1) + _htmlContainerInt.MarginRight = value; + } + } + + /// + /// Set all 4 margins to the given value. + /// + /// + public void SetMargins(int value) + { + if (value > -1) + _htmlContainerInt.SetMargins(value); + } + + /// + /// Get the currently selected text segment in the html. + /// + public string SelectedText + { + get { return _htmlContainerInt.SelectedText; } + } + + /// + /// Copy the currently selected html segment with style. + /// + public string SelectedHtml + { + get { return _htmlContainerInt.SelectedHtml; } + } + + /// + /// Init with optional document and stylesheet. + /// + /// the html to init with, init empty if not given + /// optional: the stylesheet to init with, init default if not given + public void SetHtml(string htmlSource, CssData baseCssData = null) + { + _htmlContainerInt.SetHtml(htmlSource, baseCssData); + } + + /// + /// Get html from the current DOM tree with style if requested. + /// + /// Optional: controls the way styles are generated when html is generated (default: ) + /// generated html + public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline) + { + return _htmlContainerInt.GetHtml(styleGen); + } + + /// + /// Get attribute value of element at the given x,y location by given key.
+ /// If more than one element exist with the attribute at the location the inner most is returned. + ///
+ /// the location to find the attribute at + /// the attribute key to get value by + /// found attribute value or null if not found + public string GetAttributeAt(XPoint location, string attribute) + { + return _htmlContainerInt.GetAttributeAt(Utils.Convert(location), attribute); + } + + /// + /// Get all the links in the HTML with the element rectangle and href data. + /// + /// collection of all the links in the HTML + public List> GetLinks() + { + var linkElements = new List>(); + foreach (var link in HtmlContainerInt.GetLinks()) + { + linkElements.Add(new LinkElementData(link.Id, link.Href, Utils.Convert(link.Rectangle))); + } + return linkElements; + } + + /// + /// Get css link href at the given x,y location. + /// + /// the location to find the link at + /// css link href if exists or null + public string GetLinkAt(XPoint location) + { + return _htmlContainerInt.GetLinkAt(Utils.Convert(location)); + } + + /// + /// Get the rectangle of html element as calculated by html layout.
+ /// Element if found by id (id attribute on the html element).
+ /// Note: to get the screen rectangle you need to adjust by the hosting control.
+ ///
+ /// the id of the element to get its rectangle + /// the rectangle of the element or null if not found + public XRect? GetElementRectangle(string elementId) + { + var r = _htmlContainerInt.GetElementRectangle(elementId); + return r.HasValue ? Utils.Convert(r.Value) : (XRect?)null; + } + + /// + /// Measures the bounds of box and children, recursively. + /// + /// Device context to draw + public void PerformLayout(XGraphics g) + { + ArgChecker.AssertArgNotNull(g, "g"); + + using (var ig = new GraphicsAdapter(g)) + { + _htmlContainerInt.PerformLayout(ig); + } + } + + /// + /// Render the html using the given device. + /// + /// the device to use to render + public void PerformPaint(XGraphics g) + { + ArgChecker.AssertArgNotNull(g, "g"); + + using (var ig = new GraphicsAdapter(g)) + { + _htmlContainerInt.PerformPaint(ig); + } + } + + public void Dispose() + { + _htmlContainerInt.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/HtmlRenderer.PdfSharp.Core.csproj b/Source/HtmlRenderer.PdfSharp.Core/HtmlRenderer.PdfSharp.Core.csproj new file mode 100644 index 000000000..48e613c08 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/HtmlRenderer.PdfSharp.Core.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.1 + + Library + + + + + + + + + + + + + + Always + + + + diff --git a/Source/HtmlRenderer.PdfSharp.Core/PdfGenerateConfig.cs b/Source/HtmlRenderer.PdfSharp.Core/PdfGenerateConfig.cs new file mode 100644 index 000000000..810570743 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/PdfGenerateConfig.cs @@ -0,0 +1,175 @@ +// "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 PdfSharpCore; +using PdfSharpCore.Drawing; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp +{ + /// + /// The settings for generating PDF using + /// + public sealed class PdfGenerateConfig + { + #region Fields/Consts + + /// + /// the page size to use for each page in the generated pdf + /// + private PageSize _pageSize; + + /// + /// if the page size is undefined this allow you to set manually the page size + /// + private XSize _xsize; + + /// + /// the orientation of each page of the generated pdf + /// + private PageOrientation _pageOrientation; + + /// + /// the top margin between the page start and the text + /// + private int _marginTop; + + /// + /// the bottom margin between the page end and the text + /// + private int _marginBottom; + + /// + /// the left margin between the page start and the text + /// + private int _marginLeft; + + /// + /// the right margin between the page end and the text + /// + private int _marginRight; + + #endregion + + + /// + /// the page size to use for each page in the generated pdf + /// + public PageSize PageSize + { + get { return _pageSize; } + set { _pageSize = value; } + } + + /// + /// if the page size is undefined this allow you to set manually the page size + /// + public XSize ManualPageSize { + get { return _xsize; } + set { _xsize = value; } + } + + /// + /// the orientation of each page of the generated pdf + /// + public PageOrientation PageOrientation + { + get { return _pageOrientation; } + set { _pageOrientation = value; } + } + + /// + /// the top margin between the page start and the text + /// + public int MarginTop + { + get { return _marginTop; } + set + { + if (value > -1) + _marginTop = value; + } + } + + /// + /// the bottom margin between the page end and the text + /// + public int MarginBottom + { + get { return _marginBottom; } + set + { + if (value > -1) + _marginBottom = value; + } + } + + /// + /// the left margin between the page start and the text + /// + public int MarginLeft + { + get { return _marginLeft; } + set + { + if (value > -1) + _marginLeft = value; + } + } + + /// + /// the right margin between the page end and the text + /// + public int MarginRight + { + get { return _marginRight; } + set + { + if (value > -1) + _marginRight = value; + } + } + + /// + /// Set all 4 margins to the given value. + /// + /// + public void SetMargins(int value) + { + if (value > -1) + _marginBottom = _marginLeft = _marginTop = _marginRight = value; + } + + // The international definitions are: + // 1 inch == 25.4 mm + // 1 inch == 72 point + + /// + /// Convert the units passed in milimiters to the units used in PdfSharp + /// + /// + /// + /// + public static XSize MilimitersToUnits(double width, double height) { + return new XSize(width / 25.4 * 72, height / 25.4 * 72); + } + + /// + /// Convert the units passed in inches to the units used in PdfSharp + /// + /// + /// + /// + public static XSize InchesToUnits(double width, double height) { + return new XSize(width * 72, height * 72); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/PdfGenerator.cs b/Source/HtmlRenderer.PdfSharp.Core/PdfGenerator.cs new file mode 100644 index 000000000..dfeb4a4ff --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/PdfGenerator.cs @@ -0,0 +1,242 @@ +// "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 PdfSharpCore; +using PdfSharpCore.Drawing; +using PdfSharpCore.Pdf; +using System; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.PdfSharp.Adapters; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp +{ + /// + /// TODO:a add doc + /// + public static class PdfGenerator + { + /// + /// 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 .
+ ///
+ /// + /// This fonts mapping can be used as a fallback in case the requested font is not installed in the client system. + /// + /// the font family to replace + /// the font family to replace with + public static void AddFontFamilyMapping(string fromFamily, string toFamily) + { + ArgChecker.AssertArgNotNullOrEmpty(fromFamily, "fromFamily"); + ArgChecker.AssertArgNotNullOrEmpty(toFamily, "toFamily"); + + PdfSharpAdapter.Instance.AddFontFamilyMapping(fromFamily, toFamily); + } + + public static void AddFontFamily(RFontFamily fontFamily) + { + PdfSharpAdapter.Instance.AddFontFamily(fontFamily); + } + + /// + /// 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. + ///
+ /// + /// 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 ParseStyleSheet(string stylesheet, bool combineWithDefault = true) + { + return CssData.Parse(PdfSharpAdapter.Instance, stylesheet, combineWithDefault); + } + + /// + /// Create PDF document from given HTML.
+ ///
+ /// HTML source to create PDF from + /// the page size to use for each page in the generated pdf + /// the margin to use between the HTML and the edges of each page + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static PdfDocument GeneratePdf(string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var config = new PdfGenerateConfig(); + config.PageSize = pageSize; + config.SetMargins(margin); + return GeneratePdf(html, config, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Create PDF document from given HTML.
+ ///
+ /// HTML source to create PDF from + /// the configuration to use for the PDF generation (page size/page orientation/margins/etc.) + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static PdfDocument GeneratePdf(string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + // create PDF document to render the HTML into + var document = new PdfDocument(); + + // add rendered PDF pages to document + AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); + + return document; + } + + /// + /// Create PDF pages from given HTML and appends them to the provided PDF document.
+ ///
+ /// PDF document to append pages to + /// HTML source to create PDF from + /// the page size to use for each page in the generated pdf + /// the margin to use between the HTML and the edges of each page + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static void AddPdfPages(PdfDocument document, string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var config = new PdfGenerateConfig(); + config.PageSize = pageSize; + config.SetMargins(margin); + AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Create PDF pages from given HTML and appends them to the provided PDF document.
+ ///
+ /// PDF document to append pages to + /// HTML source to create PDF from + /// the configuration to use for the PDF generation (page size/page orientation/margins/etc.) + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static void AddPdfPages(PdfDocument document, string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + XSize orgPageSize; + // get the size of each page to layout the HTML in + if (config.PageSize != PageSize.Undefined) + orgPageSize = PageSizeConverter.ToSize(config.PageSize); + else + orgPageSize = config.ManualPageSize; + + if (config.PageOrientation == PageOrientation.Landscape) + { + // invert pagesize for landscape + orgPageSize = new XSize(orgPageSize.Height, orgPageSize.Width); + } + + var pageSize = new XSize(orgPageSize.Width - config.MarginLeft - config.MarginRight, orgPageSize.Height - config.MarginTop - config.MarginBottom); + + if (!string.IsNullOrEmpty(html)) + { + using (var container = new HtmlContainer()) + { + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + + container.Location = new XPoint(config.MarginLeft, config.MarginTop); + container.MaxSize = new XSize(pageSize.Width, 0); + container.SetHtml(html, cssData); + container.PageSize = pageSize; + container.MarginBottom = config.MarginBottom; + container.MarginLeft = config.MarginLeft; + container.MarginRight = config.MarginRight; + container.MarginTop = config.MarginTop; + + // layout the HTML with the page width restriction to know how many pages are required + using (var measure = XGraphics.CreateMeasureContext(pageSize, XGraphicsUnit.Point, XPageDirection.Downwards)) + { + container.PerformLayout(measure); + } + + // while there is un-rendered HTML, create another PDF page and render with proper offset for the next page + double scrollOffset = 0; + while (scrollOffset > -container.ActualSize.Height) + { + var page = document.AddPage(); + page.Height = orgPageSize.Height; + page.Width = orgPageSize.Width; + + using (var g = XGraphics.FromPdfPage(page)) + { + //g.IntersectClip(new XRect(config.MarginLeft, config.MarginTop, pageSize.Width, pageSize.Height)); + g.IntersectClip(new XRect(0, 0, page.Width, page.Height)); + + container.ScrollOffset = new XPoint(0, scrollOffset); + container.PerformPaint(g); + } + scrollOffset -= pageSize.Height; + } + + // add web links and anchors + HandleLinks(document, container, orgPageSize, pageSize); + } + } + } + + + + #region Private/Protected methods + + /// + /// Handle HTML links by create PDF Documents link either to external URL or to another page in the document. + /// + private static void HandleLinks(PdfDocument document, HtmlContainer container, XSize orgPageSize, XSize pageSize) + { + foreach (var link in container.GetLinks()) + { + int i = (int)(link.Rectangle.Top / pageSize.Height); + for (; i < document.Pages.Count && pageSize.Height * i < link.Rectangle.Bottom; i++) + { + var offset = pageSize.Height * i; + + // fucking position is from the bottom of the page + var xRect = new XRect(link.Rectangle.Left, orgPageSize.Height - (link.Rectangle.Height + link.Rectangle.Top - offset), link.Rectangle.Width, link.Rectangle.Height); + + if (link.IsAnchor) + { + // create link to another page in the document + var anchorRect = container.GetElementRectangle(link.AnchorId); + if (anchorRect.HasValue) + { + // document links to the same page as the link is not allowed + int anchorPageIdx = (int)(anchorRect.Value.Top / pageSize.Height); + if (i != anchorPageIdx) + document.Pages[i].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx); + } + } + else + { + // create link to URL + document.Pages[i].AddWebLink(new PdfRectangle(xRect), link.Href); + } + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.PdfSharp.Core/Utilities/Utils.cs b/Source/HtmlRenderer.PdfSharp.Core/Utilities/Utils.cs new file mode 100644 index 000000000..106858645 --- /dev/null +++ b/Source/HtmlRenderer.PdfSharp.Core/Utilities/Utils.cs @@ -0,0 +1,100 @@ +// "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 PdfSharpCore.Drawing; +using System.Drawing; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.PdfSharp.Utilities +{ + /// + /// Utilities for converting WinForms entities to HtmlRenderer core entities. + /// + internal static class Utils + { + /// + /// Convert from WinForms point to core point. + /// + public static RPoint Convert(XPoint p) + { + return new RPoint(p.X, p.Y); + } + + /// + /// Convert from WinForms point to core point. + /// + public static XPoint[] Convert(RPoint[] points) + { + XPoint[] myPoints = new XPoint[points.Length]; + for (int i = 0; i < points.Length; i++) + myPoints[i] = Convert(points[i]); + return myPoints; + } + + /// + /// Convert from core point to WinForms point. + /// + public static XPoint Convert(RPoint p) + { + return new XPoint(p.X, p.Y); + } + + /// + /// Convert from WinForms size to core size. + /// + public static RSize Convert(XSize s) + { + return new RSize(s.Width, s.Height); + } + + /// + /// Convert from core size to WinForms size. + /// + public static XSize Convert(RSize s) + { + return new XSize(s.Width, s.Height); + } + + /// + /// Convert from WinForms rectangle to core rectangle. + /// + public static RRect Convert(XRect r) + { + return new RRect(r.X, r.Y, r.Width, r.Height); + } + + /// + /// Convert from core rectangle to WinForms rectangle. + /// + public static XRect Convert(RRect r) + { + return new XRect(r.X, r.Y, r.Width, r.Height); + } + + /// + /// Convert from core color to WinForms color. + /// + public static XColor Convert(RColor c) + { + return XColor.FromArgb(c.A, c.R, c.G, c.B); + } + + /// + /// Convert from color to WinForms color. + /// + public static RColor Convert(Color c) + { + return RColor.FromArgb(c.A, c.R, c.G, c.B); + } + + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.sln b/Source/HtmlRenderer.sln index cf7aaa634..21c55cd32 100644 --- a/Source/HtmlRenderer.sln +++ b/Source/HtmlRenderer.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30501.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{E263EA16-2E6A-4269-A319-AA2F97ADA8E1}" EndProject @@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{AA47D1 .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlRenderer.Core", "HtmlRenderer.Core\HtmlRenderer.Core.csproj", "{19984DE1-1F95-43D9-84B3-6BF233FCECA9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlRenderer.PdfSharp.Core", "HtmlRenderer.PdfSharp.Core\HtmlRenderer.PdfSharp.Core.csproj", "{D7BF9146-867B-472F-BB83-F72EA56E3945}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,6 +110,30 @@ Global {CA249F5D-9285-40A6-B217-5889EF79FD7E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {CA249F5D-9285-40A6-B217-5889EF79FD7E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {CA249F5D-9285-40A6-B217-5889EF79FD7E}.Release|x86.ActiveCfg = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|x86.ActiveCfg = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Debug|x86.Build.0 = Debug|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|Any CPU.Build.0 = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|x86.ActiveCfg = Release|Any CPU + {19984DE1-1F95-43D9-84B3-6BF233FCECA9}.Release|x86.Build.0 = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Debug|x86.Build.0 = Debug|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|Any CPU.Build.0 = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|x86.ActiveCfg = Release|Any CPU + {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -115,4 +143,7 @@ Global {8AD34FE8-8382-4A8A-B3AA-A0392ED42423} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} {F02E0216-4AE3-474F-9381-FCB93411CDB0} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2169EDEB-46C2-46B7-AB8E-C3ECC7B5568D} + EndGlobalSection EndGlobal