From b340fb93d7b8aa1955068b57e870ebef49fa31ef Mon Sep 17 00:00:00 2001 From: ManniAT Date: Sat, 10 Jul 2021 12:46:11 +0200 Subject: [PATCH 1/2] added fix for https://github.com/ArthurHub/HTML-Renderer/issues/94 Included .NET 5 version of HtmlRenderer and HtmlRenderer.WPF --- .vs/ProjectSettings.json | 3 + .vs/slnx.sqlite | Bin 0 -> 1908736 bytes .../Adapters/BrushAdapter.cs | 47 + .../Adapters/ContextMenuAdapter.cs | 88 + .../Adapters/ControlAdapter.cs | 100 ++ .../Adapters/FontAdapter.cs | 125 ++ .../Adapters/FontFamilyAdapter.cs | 66 + .../Adapters/GraphicsAdapter.cs | 321 ++++ .../Adapters/GraphicsPathAdapter.cs | 67 + .../Adapters/ImageAdapter.cs | 60 + .../Adapters/PenAdapter.cs | 91 + .../Adapters/WpfAdapter.cs | 229 +++ .../HTMLRendererCore.WPF.csproj | 12 + Source/HTMLRendererCore.WPF/HtmlContainer.cs | 470 +++++ Source/HTMLRendererCore.WPF/HtmlControl.cs | 559 ++++++ Source/HTMLRendererCore.WPF/HtmlLabel.cs | 136 ++ Source/HTMLRendererCore.WPF/HtmlPanel.cs | 370 ++++ Source/HTMLRendererCore.WPF/HtmlRender.cs | 424 +++++ .../HTMLRendererCore.WPF/RoutedEventArgs.cs | 62 + .../Utilities/ClipboardHelper.cs | 255 +++ .../HTMLRendererCore.WPF/Utilities/Utils.cs | 151 ++ Source/HtmlRenderer.sln | 35 +- .../Adapters/Entities/RColor.cs | 273 +++ .../Adapters/Entities/RDashStyle.cs | 27 + .../Adapters/Entities/RFontStyle.cs | 29 + .../Adapters/Entities/RKeyEvent.cs | 71 + .../Adapters/Entities/RMouseEvent.cs | 43 + .../Adapters/Entities/RPoint.cs | 293 +++ .../Adapters/Entities/RRect.cs | 506 ++++++ .../Adapters/Entities/RSize.cs | 341 ++++ Source/HtmlRendererCore/Adapters/RAdapter.cs | 459 +++++ Source/HtmlRendererCore/Adapters/RBrush.cs | 25 + .../HtmlRendererCore/Adapters/RContextMenu.cs | 52 + Source/HtmlRendererCore/Adapters/RControl.cs | 97 + Source/HtmlRendererCore/Adapters/RFont.cs | 42 + .../HtmlRendererCore/Adapters/RFontFamily.cs | 26 + Source/HtmlRendererCore/Adapters/RGraphics.cs | 272 +++ .../Adapters/RGraphicsPath.cs | 53 + Source/HtmlRendererCore/Adapters/RImage.cs | 34 + Source/HtmlRendererCore/Adapters/RPen.cs | 32 + Source/HtmlRendererCore/Core/CssData.cs | 214 +++ Source/HtmlRendererCore/Core/CssDefaults.cs | 128 ++ Source/HtmlRendererCore/Core/Dom/Border.cs | 25 + Source/HtmlRendererCore/Core/Dom/CssBox.cs | 1559 ++++++++++++++++ .../HtmlRendererCore/Core/Dom/CssBoxFrame.cs | 609 +++++++ Source/HtmlRendererCore/Core/Dom/CssBoxHr.cs | 122 ++ .../HtmlRendererCore/Core/Dom/CssBoxImage.cs | 210 +++ .../Core/Dom/CssBoxProperties.cs | 1567 +++++++++++++++++ .../Core/Dom/CssLayoutEngine.cs | 718 ++++++++ .../Core/Dom/CssLayoutEngineTable.cs | 1039 +++++++++++ Source/HtmlRendererCore/Core/Dom/CssLength.cs | 250 +++ .../HtmlRendererCore/Core/Dom/CssLineBox.cs | 293 +++ Source/HtmlRendererCore/Core/Dom/CssRect.cs | 291 +++ .../HtmlRendererCore/Core/Dom/CssRectImage.cs | 81 + .../HtmlRendererCore/Core/Dom/CssRectWord.cs | 113 ++ .../Core/Dom/CssSpacingBox.cs | 60 + Source/HtmlRendererCore/Core/Dom/CssUnit.cs | 33 + .../Core/Dom/HoverBoxBlock.cs | 57 + Source/HtmlRendererCore/Core/Dom/HtmlTag.cs | 115 ++ .../Core/Entities/CssBlock.cs | 235 +++ .../Core/Entities/CssBlockSelectorItem.cs | 74 + .../Core/Entities/HtmlGenerationStyle.cs | 35 + .../Core/Entities/HtmlImageLoadEventArgs.cs | 176 ++ .../Core/Entities/HtmlLinkClickedEventArgs.cs | 78 + .../Core/Entities/HtmlLinkClickedException.cs | 44 + .../Core/Entities/HtmlRefreshEventArgs.cs | 51 + .../Core/Entities/HtmlRenderErrorEventArgs.cs | 79 + .../Core/Entities/HtmlRenderErrorType.cs | 30 + .../Core/Entities/HtmlScrollEventArgs.cs | 59 + .../Entities/HtmlStylesheetLoadEventArgs.cs | 110 ++ .../Core/Entities/LinkElementData.cs | 91 + .../Handlers/BackgroundImageDrawHandler.cs | 165 ++ .../Core/Handlers/BordersDrawHandler.cs | 371 ++++ .../Core/Handlers/ContextMenuHandler.cs | 509 ++++++ .../Core/Handlers/FontsHandler.cs | 196 +++ .../Core/Handlers/ImageDownloader.cs | 264 +++ .../Core/Handlers/ImageLoadHandler.cs | 393 +++++ .../Core/Handlers/SelectionHandler.cs | 693 ++++++++ .../Core/Handlers/StylesheetLoadHandler.cs | 192 ++ .../HtmlRendererCore/Core/HtmlContainerInt.cs | 1062 +++++++++++ .../Core/HtmlRendererUtils.cs | 123 ++ .../HtmlRendererCore/Core/Parse/CssParser.cs | 963 ++++++++++ .../Core/Parse/CssValueParser.cs | 543 ++++++ .../HtmlRendererCore/Core/Parse/DomParser.cs | 900 ++++++++++ .../HtmlRendererCore/Core/Parse/HtmlParser.cs | 263 +++ .../Core/Parse/RegexParserHelper.cs | 227 +++ .../Core/Parse/RegexParserUtils.cs | 197 +++ .../HtmlRendererCore/Core/Utils/ArgChecker.cs | 117 ++ .../Core/Utils/CommonUtils.cs | 505 ++++++ .../Core/Utils/CssConstants.cs | 169 ++ .../HtmlRendererCore/Core/Utils/CssUtils.cs | 414 +++++ .../HtmlRendererCore/Core/Utils/DomUtils.cs | 919 ++++++++++ .../Core/Utils/HtmlConstants.cs | 229 +++ .../HtmlRendererCore/Core/Utils/HtmlUtils.cs | 414 +++++ .../Core/Utils/ImageError.png | Bin 0 -> 428 bytes .../HtmlRendererCore/Core/Utils/ImageLoad.png | Bin 0 -> 414 bytes .../Core/Utils/RenderUtils.cs | 140 ++ .../HtmlRendererCore/Core/Utils/SubString.cs | 187 ++ .../HtmlRendererCore/HtmlRendererCore.csproj | 17 + 99 files changed, 25062 insertions(+), 2 deletions(-) create mode 100644 .vs/ProjectSettings.json create mode 100644 .vs/slnx.sqlite create mode 100644 Source/HTMLRendererCore.WPF/Adapters/BrushAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/ContextMenuAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/ControlAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/FontAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/FontFamilyAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/GraphicsAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/GraphicsPathAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/ImageAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/PenAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/Adapters/WpfAdapter.cs create mode 100644 Source/HTMLRendererCore.WPF/HTMLRendererCore.WPF.csproj create mode 100644 Source/HTMLRendererCore.WPF/HtmlContainer.cs create mode 100644 Source/HTMLRendererCore.WPF/HtmlControl.cs create mode 100644 Source/HTMLRendererCore.WPF/HtmlLabel.cs create mode 100644 Source/HTMLRendererCore.WPF/HtmlPanel.cs create mode 100644 Source/HTMLRendererCore.WPF/HtmlRender.cs create mode 100644 Source/HTMLRendererCore.WPF/RoutedEventArgs.cs create mode 100644 Source/HTMLRendererCore.WPF/Utilities/ClipboardHelper.cs create mode 100644 Source/HTMLRendererCore.WPF/Utilities/Utils.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RColor.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RDashStyle.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RFontStyle.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RKeyEvent.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RMouseEvent.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RPoint.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RRect.cs create mode 100644 Source/HtmlRendererCore/Adapters/Entities/RSize.cs create mode 100644 Source/HtmlRendererCore/Adapters/RAdapter.cs create mode 100644 Source/HtmlRendererCore/Adapters/RBrush.cs create mode 100644 Source/HtmlRendererCore/Adapters/RContextMenu.cs create mode 100644 Source/HtmlRendererCore/Adapters/RControl.cs create mode 100644 Source/HtmlRendererCore/Adapters/RFont.cs create mode 100644 Source/HtmlRendererCore/Adapters/RFontFamily.cs create mode 100644 Source/HtmlRendererCore/Adapters/RGraphics.cs create mode 100644 Source/HtmlRendererCore/Adapters/RGraphicsPath.cs create mode 100644 Source/HtmlRendererCore/Adapters/RImage.cs create mode 100644 Source/HtmlRendererCore/Adapters/RPen.cs create mode 100644 Source/HtmlRendererCore/Core/CssData.cs create mode 100644 Source/HtmlRendererCore/Core/CssDefaults.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/Border.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssBox.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssBoxFrame.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssBoxHr.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssBoxImage.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssBoxProperties.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssLayoutEngine.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssLayoutEngineTable.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssLength.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssLineBox.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssRect.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssRectImage.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssRectWord.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssSpacingBox.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/CssUnit.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/HoverBoxBlock.cs create mode 100644 Source/HtmlRendererCore/Core/Dom/HtmlTag.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/CssBlock.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/CssBlockSelectorItem.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlGenerationStyle.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlImageLoadEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlLinkClickedEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlLinkClickedException.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlRefreshEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlRenderErrorEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlRenderErrorType.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlScrollEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/HtmlStylesheetLoadEventArgs.cs create mode 100644 Source/HtmlRendererCore/Core/Entities/LinkElementData.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/BackgroundImageDrawHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/BordersDrawHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/ContextMenuHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/FontsHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/ImageDownloader.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/ImageLoadHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/SelectionHandler.cs create mode 100644 Source/HtmlRendererCore/Core/Handlers/StylesheetLoadHandler.cs create mode 100644 Source/HtmlRendererCore/Core/HtmlContainerInt.cs create mode 100644 Source/HtmlRendererCore/Core/HtmlRendererUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/CssParser.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/CssValueParser.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/DomParser.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/HtmlParser.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/RegexParserHelper.cs create mode 100644 Source/HtmlRendererCore/Core/Parse/RegexParserUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/ArgChecker.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/CommonUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/CssConstants.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/CssUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/DomUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/HtmlConstants.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/HtmlUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/ImageError.png create mode 100644 Source/HtmlRendererCore/Core/Utils/ImageLoad.png create mode 100644 Source/HtmlRendererCore/Core/Utils/RenderUtils.cs create mode 100644 Source/HtmlRendererCore/Core/Utils/SubString.cs create mode 100644 Source/HtmlRendererCore/HtmlRendererCore.csproj diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 000000000..f8b488856 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..b97c8ece7598b4a9ed02d5cadac10579b6c08b53 GIT binary patch literal 1908736 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®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
70pG>13*lpgj}ksYc%JZK!iNaY5&oI*Ea9IB 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 literal 0 HcmV?d00001 diff --git a/Source/HTMLRendererCore.WPF/Adapters/BrushAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/BrushAdapter.cs new file mode 100644 index 000000000..b6ed00931 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/BrushAdapter.cs @@ -0,0 +1,47 @@ +// "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.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF brushes. + /// + internal sealed class BrushAdapter : RBrush + { + /// + /// The actual WPF brush instance. + /// + private readonly Brush _brush; + + /// + /// Init. + /// + public BrushAdapter(Brush brush) + { + _brush = brush; + } + + /// + /// The actual WPF brush instance. + /// + public Brush Brush + { + get { return _brush; } + } + + public override void Dispose() + { } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/ContextMenuAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/ContextMenuAdapter.cs new file mode 100644 index 000000000..214b1ef5a --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/ContextMenuAdapter.cs @@ -0,0 +1,88 @@ +// "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.Windows; +using System.Windows.Controls; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF context menu for core. + /// + internal sealed class ContextMenuAdapter : RContextMenu + { + #region Fields and Consts + + /// + /// the underline WPF context menu + /// + private readonly ContextMenu _contextMenu; + + #endregion + + + /// + /// Init. + /// + public ContextMenuAdapter() + { + _contextMenu = new ContextMenu(); + } + + public override int ItemsCount + { + get { return _contextMenu.Items.Count; } + } + + public override void AddDivider() + { + _contextMenu.Items.Add(new Separator()); + } + + public override void AddItem(string text, bool enabled, EventHandler onClick) + { + ArgChecker.AssertArgNotNullOrEmpty(text, "text"); + ArgChecker.AssertArgNotNull(onClick, "onClick"); + + var item = new MenuItem(); + item.Header = text; + item.IsEnabled = enabled; + item.Click += new RoutedEventHandler(onClick); + _contextMenu.Items.Add(item); + } + + public override void RemoveLastDivider() + { + if (_contextMenu.Items[_contextMenu.Items.Count - 1].GetType() == typeof(Separator)) + _contextMenu.Items.RemoveAt(_contextMenu.Items.Count - 1); + } + + public override void Show(RControl parent, RPoint location) + { + _contextMenu.PlacementTarget = ((ControlAdapter)parent).Control; + _contextMenu.PlacementRectangle = new Rect(Utils.ConvertRound(location), Size.Empty); + _contextMenu.IsOpen = true; + } + + public override void Dispose() + { + _contextMenu.IsOpen = false; + _contextMenu.PlacementTarget = null; + _contextMenu.Items.Clear(); + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/ControlAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/ControlAdapter.cs new file mode 100644 index 000000000..9c93900e7 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/ControlAdapter.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 System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF Control for core. + /// + internal sealed class ControlAdapter : RControl + { + /// + /// the underline WPF control. + /// + private readonly Control _control; + + /// + /// Init. + /// + public ControlAdapter(Control control) + : base(WpfAdapter.Instance) + { + ArgChecker.AssertArgNotNull(control, "control"); + + _control = control; + } + + /// + /// Get the underline WPF control + /// + public Control Control + { + get { return _control; } + } + + public override RPoint MouseLocation + { + get { return Utils.Convert(_control.PointFromScreen(Mouse.GetPosition(_control))); } + } + + public override bool LeftMouseButton + { + get { return Mouse.LeftButton == MouseButtonState.Pressed; } + } + + public override bool RightMouseButton + { + get { return Mouse.RightButton == MouseButtonState.Pressed; } + } + + public override void SetCursorDefault() + { + _control.Cursor = Cursors.Arrow; + } + + public override void SetCursorHand() + { + _control.Cursor = Cursors.Hand; + } + + public override void SetCursorIBeam() + { + _control.Cursor = Cursors.IBeam; + } + + public override void DoDragDropCopy(object dragDropData) + { + DragDrop.DoDragDrop(_control, dragDropData, DragDropEffects.Copy); + } + + public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) + { + using (var g = new GraphicsAdapter()) + { + g.MeasureString(str, font, maxWidth, out charFit, out charFitWidth); + } + } + + public override void Invalidate() + { + _control.InvalidateVisual(); + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/FontAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/FontAdapter.cs new file mode 100644 index 000000000..ed693374c --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/FontAdapter.cs @@ -0,0 +1,125 @@ +// "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.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF Font. + /// + internal sealed class FontAdapter : RFont + { + #region Fields and Consts + + /// + /// the underline win-forms font. + /// + private readonly Typeface _font; + + /// + /// The glyph font for the font + /// + private readonly GlyphTypeface _glyphTypeface; + + /// + /// the size of the font + /// + private readonly double _size; + + /// + /// the vertical offset of the font underline location from the top of the font. + /// + private readonly double _underlineOffset = -1; + + /// + /// Cached font height. + /// + private readonly double _height = -1; + + /// + /// Cached font whitespace width. + /// + private double _whitespaceWidth = -1; + + #endregion + + + /// + /// Init. + /// + public FontAdapter(Typeface font, double size) + { + _font = font; + _size = size; + _height = 96d / 72d * _size * _font.FontFamily.LineSpacing; + _underlineOffset = 96d / 72d * _size * (_font.FontFamily.LineSpacing + font.UnderlinePosition); + + GlyphTypeface typeface; + if (font.TryGetGlyphTypeface(out typeface)) + { + _glyphTypeface = typeface; + } + else + { + foreach (var sysTypeface in Fonts.SystemTypefaces) + { + if (sysTypeface.TryGetGlyphTypeface(out typeface)) + break; + } + } + } + + /// + /// the underline win-forms font. + /// + public Typeface Font + { + get { return _font; } + } + + public GlyphTypeface GlyphTypeface + { + get { return _glyphTypeface; } + } + + public override double Size + { + get { return _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; + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/FontFamilyAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/FontFamilyAdapter.cs new file mode 100644 index 000000000..a3ec7734d --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/FontFamilyAdapter.cs @@ -0,0 +1,66 @@ +// "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.Windows.Markup; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF Font family object for core. + /// + internal sealed class FontFamilyAdapter : RFontFamily + { + /// + /// Default language to get font family name by + /// + private static readonly XmlLanguage _xmlLanguage = XmlLanguage.GetLanguage("en-us"); + + /// + /// the underline win-forms font. + /// + private readonly FontFamily _fontFamily; + + /// + /// Init. + /// + public FontFamilyAdapter(FontFamily fontFamily) + { + _fontFamily = fontFamily; + } + + /// + /// the underline WPF font family. + /// + public FontFamily FontFamily + { + get { return _fontFamily; } + } + + public override string Name + { + get + { + string name = _fontFamily.FamilyNames[_xmlLanguage]; + if (string.IsNullOrEmpty(name)) + { + foreach (var familyName in _fontFamily.FamilyNames) + { + return familyName.Value; + } + } + return name; + } + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/GraphicsAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/GraphicsAdapter.cs new file mode 100644 index 000000000..d61bef4ca --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/GraphicsAdapter.cs @@ -0,0 +1,321 @@ +// "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.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF Graphics. + /// + internal sealed class GraphicsAdapter : RGraphics + { + #region Fields and Consts + + /// + /// The wrapped WPF graphics object + /// + private readonly DrawingContext _g; + + /// + /// if to release the graphics object on dispose + /// + private readonly bool _releaseGraphics; + + #endregion + + + /// + /// Init. + /// + /// the WPF graphics object to use + /// the initial clip of the graphics + /// optional: if to release the graphics object on dispose (default - false) + public GraphicsAdapter(DrawingContext g, RRect initialClip, bool releaseGraphics = false) + : base(WpfAdapter.Instance, initialClip) + { + ArgChecker.AssertArgNotNull(g, "g"); + + _g = g; + _releaseGraphics = releaseGraphics; + } + + /// + /// Init. + /// + public GraphicsAdapter() + : base(WpfAdapter.Instance, RRect.Empty) + { + _g = null; + _releaseGraphics = false; + } + + public override void PopClip() + { + _g.Pop(); + _clipStack.Pop(); + } + + public override void PushClip(RRect rect) + { + _clipStack.Push(rect); + _g.PushClip(new RectangleGeometry(Utils.Convert(rect))); + } + + public override void PushClipExclude(RRect rect) + { + var geometry = new CombinedGeometry(); + geometry.Geometry1 = new RectangleGeometry(Utils.Convert(_clipStack.Peek())); + geometry.Geometry2 = new RectangleGeometry(Utils.Convert(rect)); + geometry.GeometryCombineMode = GeometryCombineMode.Exclude; + + _clipStack.Push(_clipStack.Peek()); + _g.PushClip(geometry); + } + + public override Object SetAntiAliasSmoothingMode() + { + return null; + } + + public override void ReturnPreviousSmoothingMode(Object prevMode) + { } + + public override RSize MeasureString(string str, RFont font) + { + double width = 0; + GlyphTypeface glyphTypeface = ((FontAdapter)font).GlyphTypeface; + if (glyphTypeface != null) + { + for (int i = 0; i < str.Length; i++) + { + if (glyphTypeface.CharacterToGlyphMap.ContainsKey(str[i])) + { + ushort glyph = glyphTypeface.CharacterToGlyphMap[str[i]]; + double advanceWidth = glyphTypeface.AdvanceWidths[glyph]; + width += advanceWidth; + } + else + { + width = 0; + break; + } + } + } + + if (width <= 0) + { + var formattedText = new FormattedText(str, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, ((FontAdapter)font).Font, 96d / 72d * font.Size, Brushes.Red); + return new RSize(formattedText.WidthIncludingTrailingWhitespace, formattedText.Height); + } + + return new RSize(width * font.Size * 96d / 72d, font.Height); + } + + public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) + { + charFit = 0; + charFitWidth = 0; + bool handled = false; + GlyphTypeface glyphTypeface = ((FontAdapter)font).GlyphTypeface; + if (glyphTypeface != null) + { + handled = true; + double width = 0; + for (int i = 0; i < str.Length; i++) + { + if (glyphTypeface.CharacterToGlyphMap.ContainsKey(str[i])) + { + ushort glyph = glyphTypeface.CharacterToGlyphMap[str[i]]; + double advanceWidth = glyphTypeface.AdvanceWidths[glyph] * font.Size * 96d / 72d; + + if (!(width + advanceWidth < maxWidth)) + { + charFit = i; + charFitWidth = width; + break; + } + width += advanceWidth; + } + else + { + handled = false; + break; + } + } + } + + if (!handled) + { + var formattedText = new FormattedText(str, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, ((FontAdapter)font).Font, 96d / 72d * font.Size, Brushes.Red); + charFit = str.Length; + charFitWidth = formattedText.WidthIncludingTrailingWhitespace; + } + } + + public override void DrawString(string str, RFont font, RColor color, RPoint point, RSize size, bool rtl) + { + var colorConv = ((BrushAdapter)_adapter.GetSolidBrush(color)).Brush; + + bool glyphRendered = false; + GlyphTypeface glyphTypeface = ((FontAdapter)font).GlyphTypeface; + if (glyphTypeface != null) + { + double width = 0; + ushort[] glyphs = new ushort[str.Length]; + double[] widths = new double[str.Length]; + + int i = 0; + for (; i < str.Length; i++) + { + ushort glyph; + if (!glyphTypeface.CharacterToGlyphMap.TryGetValue(str[i], out glyph)) + break; + + glyphs[i] = glyph; + width += glyphTypeface.AdvanceWidths[glyph]; + widths[i] = 96d / 72d * font.Size * glyphTypeface.AdvanceWidths[glyph]; + } + + if (i >= str.Length) + { + point.Y += glyphTypeface.Baseline * font.Size * 96d / 72d; + point.X += rtl ? 96d / 72d * font.Size * width : 0; + + glyphRendered = true; + var glyphRun = new GlyphRun(glyphTypeface, rtl ? 1 : 0, false, 96d / 72d * font.Size, glyphs, Utils.ConvertRound(point), widths, null, null, null, null, null, null); + _g.DrawGlyphRun(colorConv, glyphRun); + } + } + + if (!glyphRendered) + { + var formattedText = new FormattedText(str, CultureInfo.CurrentCulture, rtl ? FlowDirection.RightToLeft : FlowDirection.LeftToRight, ((FontAdapter)font).Font, 96d / 72d * font.Size, colorConv); + point.X += rtl ? formattedText.Width : 0; + _g.DrawText(formattedText, Utils.ConvertRound(point)); + } + } + + public override RBrush GetTextureBrush(RImage image, RRect dstRect, RPoint translateTransformLocation) + { + var brush = new ImageBrush(((ImageAdapter)image).Image); + brush.Stretch = Stretch.None; + brush.TileMode = TileMode.Tile; + brush.Viewport = Utils.Convert(dstRect); + brush.ViewportUnits = BrushMappingMode.Absolute; + brush.Transform = new TranslateTransform(translateTransformLocation.X, translateTransformLocation.Y); + brush.Freeze(); + return new BrushAdapter(brush); + } + + public override RGraphicsPath GetGraphicsPath() + { + return new GraphicsPathAdapter(); + } + + public override void Dispose() + { + if (_releaseGraphics) + _g.Close(); + } + + + #region Delegate graphics methods + + public override void DrawLine(RPen pen, double x1, double y1, double x2, double y2) + { + x1 = (int)x1; + x2 = (int)x2; + y1 = (int)y1; + y2 = (int)y2; + + var adj = pen.Width; + if (Math.Abs(x1 - x2) < .1 && Math.Abs(adj % 2 - 1) < .1) + { + x1 += .5; + x2 += .5; + } + if (Math.Abs(y1 - y2) < .1 && Math.Abs(adj % 2 - 1) < .1) + { + y1 += .5; + y2 += .5; + } + + _g.DrawLine(((PenAdapter)pen).CreatePen(), new Point(x1, y1), new Point(x2, y2)); + } + + public override void DrawRectangle(RPen pen, double x, double y, double width, double height) + { + var adj = pen.Width; + if (Math.Abs(adj % 2 - 1) < .1) + { + x += .5; + y += .5; + } + + _g.DrawRectangle(null, ((PenAdapter)pen).CreatePen(), new Rect(x, y, width, height)); + } + + public override void DrawRectangle(RBrush brush, double x, double y, double width, double height) + { + _g.DrawRectangle(((BrushAdapter)brush).Brush, null, new Rect(x, y, width, height)); + } + + public override void DrawImage(RImage image, RRect destRect, RRect srcRect) + { + CroppedBitmap croppedImage = new CroppedBitmap(((ImageAdapter)image).Image, new Int32Rect((int)srcRect.X, (int)srcRect.Y, (int)srcRect.Width, (int)srcRect.Height)); + _g.DrawImage(croppedImage, Utils.ConvertRound(destRect)); + } + + public override void DrawImage(RImage image, RRect destRect) + { + _g.DrawImage(((ImageAdapter)image).Image, Utils.ConvertRound(destRect)); + } + + public override void DrawPath(RPen pen, RGraphicsPath path) + { + _g.DrawGeometry(null, ((PenAdapter)pen).CreatePen(), ((GraphicsPathAdapter)path).GetClosedGeometry()); + } + + public override void DrawPath(RBrush brush, RGraphicsPath path) + { + _g.DrawGeometry(((BrushAdapter)brush).Brush, null, ((GraphicsPathAdapter)path).GetClosedGeometry()); + } + + public override void DrawPolygon(RBrush brush, RPoint[] points) + { + if (points != null && points.Length > 0) + { + var g = new StreamGeometry(); + using (var context = g.Open()) + { + context.BeginFigure(Utils.Convert(points[0]), true, true); + for (int i = 1; i < points.Length; i++) + context.LineTo(Utils.Convert(points[i]), true, true); + } + g.Freeze(); + + _g.DrawGeometry(((BrushAdapter)brush).Brush, null, g); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/GraphicsPathAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/GraphicsPathAdapter.cs new file mode 100644 index 000000000..a6ffb66bb --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/GraphicsPathAdapter.cs @@ -0,0 +1,67 @@ +// "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.Windows; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF graphics path object for core. + /// + internal sealed class GraphicsPathAdapter : RGraphicsPath + { + /// + /// The actual WPF graphics geometry instance. + /// + private readonly StreamGeometry _geometry = new StreamGeometry(); + + /// + /// The context used in WPF geometry to render path + /// + private readonly StreamGeometryContext _geometryContext; + + public GraphicsPathAdapter() + { + _geometryContext = _geometry.Open(); + } + + public override void Start(double x, double y) + { + _geometryContext.BeginFigure(new Point(x, y), true, false); + } + + public override void LineTo(double x, double y) + { + _geometryContext.LineTo(new Point(x, y), true, true); + } + + public override void ArcTo(double x, double y, double size, Corner corner) + { + _geometryContext.ArcTo(new Point(x, y), new Size(size, size), 0, false, SweepDirection.Clockwise, true, true); + } + + /// + /// Close the geometry to so no more path adding is allowed and return the instance so it can be rendered. + /// + public StreamGeometry GetClosedGeometry() + { + _geometryContext.Close(); + _geometry.Freeze(); + return _geometry; + } + + public override void Dispose() + { } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/ImageAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/ImageAdapter.cs new file mode 100644 index 000000000..858b00968 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/ImageAdapter.cs @@ -0,0 +1,60 @@ +// "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.Windows.Media.Imaging; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF Image object for core. + /// + internal sealed class ImageAdapter : RImage + { + /// + /// the underline WPF image. + /// + private readonly BitmapImage _image; + + /// + /// Init. + /// + public ImageAdapter(BitmapImage image) + { + _image = image; + } + + /// + /// the underline WPF image. + /// + public BitmapImage Image + { + get { return _image; } + } + + public override double Width + { + get { return _image.PixelWidth; } + } + + public override double Height + { + get { return _image.PixelHeight; } + } + + public override void Dispose() + { + if (_image.StreamSource != null) + _image.StreamSource.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/PenAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/PenAdapter.cs new file mode 100644 index 000000000..2d3b391aa --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/PenAdapter.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.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF pens objects for core. + /// + internal sealed class PenAdapter : RPen + { + /// + /// The actual WPF brush instance. + /// + private readonly Brush _brush; + + /// + /// the width of the pen + /// + private double _width; + + /// + /// the dash style of the pen + /// + private DashStyle _dashStyle = DashStyles.Solid; + + /// + /// Init. + /// + public PenAdapter(Brush brush) + { + _brush = brush; + } + + public override double Width + { + get { return _width; } + set { _width = value; } + } + + public override RDashStyle DashStyle + { + set + { + switch (value) + { + case RDashStyle.Solid: + _dashStyle = DashStyles.Solid; + break; + case RDashStyle.Dash: + _dashStyle = DashStyles.Dash; + break; + case RDashStyle.Dot: + _dashStyle = DashStyles.Dot; + break; + case RDashStyle.DashDot: + _dashStyle = DashStyles.DashDot; + break; + case RDashStyle.DashDotDot: + _dashStyle = DashStyles.DashDotDot; + break; + default: + _dashStyle = DashStyles.Solid; + break; + } + } + } + + /// + /// Create the actual WPF pen instance. + /// + public Pen CreatePen() + { + var pen = new Pen(_brush, _width); + pen.DashStyle = _dashStyle; + return pen; + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Adapters/WpfAdapter.cs b/Source/HTMLRendererCore.WPF/Adapters/WpfAdapter.cs new file mode 100644 index 000000000..3ea88d9c4 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Adapters/WpfAdapter.cs @@ -0,0 +1,229 @@ +// "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.Reflection; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; +using Microsoft.Win32; + +namespace TheArtOfDev.HtmlRenderer.WPF.Adapters +{ + /// + /// Adapter for WPF platform. + /// + internal sealed class WpfAdapter : RAdapter + { + #region Fields and Consts + + /// + /// Singleton instance of global adapter. + /// + private static readonly WpfAdapter _instance = new WpfAdapter(); + + /// + /// List of valid predefined color names in lower-case + /// + private static readonly List ValidColorNamesLc; + + #endregion + + static WpfAdapter() + { + ValidColorNamesLc = new List(); + var colorList = new List(typeof(Colors).GetProperties()); + foreach (var colorProp in colorList) + { + ValidColorNamesLc.Add(colorProp.Name.ToLower()); + } + } + + /// + /// Init installed font families and set default font families mapping. + /// + private WpfAdapter() + { + AddFontFamilyMapping("monospace", "Courier New"); + AddFontFamilyMapping("Helvetica", "Arial"); + + foreach (var family in Fonts.SystemFontFamilies) + { + try + { + AddFontFamily(new FontFamilyAdapter(family)); + } + catch + { + } + } + } + + /// + /// Singleton instance of global adapter. + /// + public static WpfAdapter Instance + { + get { return _instance; } + } + + protected override RColor GetColorInt(string colorName) + { + // check if color name is valid to avoid ColorConverter throwing an exception + if (!ValidColorNamesLc.Contains(colorName.ToLower())) + return RColor.Empty; + + var convertFromString = ColorConverter.ConvertFromString(colorName) ?? Colors.Black; + return Utils.Convert((Color)convertFromString); + } + + protected override RPen CreatePen(RColor color) + { + return new PenAdapter(GetSolidColorBrush(color)); + } + + protected override RBrush CreateSolidBrush(RColor color) + { + var solidBrush = GetSolidColorBrush(color); + return new BrushAdapter(solidBrush); + } + + protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + { + var startColor = angle <= 180 ? Utils.Convert(color1) : Utils.Convert(color2); + var endColor = angle <= 180 ? Utils.Convert(color2) : Utils.Convert(color1); + angle = angle <= 180 ? angle : angle - 180; + double x = angle < 135 ? Math.Max((angle - 45) / 90, 0) : 1; + double y = angle <= 45 ? Math.Max(0.5 - angle / 90, 0) : angle > 135 ? Math.Abs(1.5 - angle / 90) : 0; + return new BrushAdapter(new LinearGradientBrush(startColor, endColor, new Point(x, y), new Point(1 - x, 1 - y))); + } + + protected override RImage ConvertImageInt(object image) + { + return image != null ? new ImageAdapter((BitmapImage)image) : null; + } + + protected override RImage ImageFromStreamInt(Stream memoryStream) + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = memoryStream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + return new ImageAdapter(bitmap); + } + + protected override RFont CreateFontInt(string family, double size, RFontStyle style) + { + var fontFamily = (FontFamily)new FontFamilyConverter().ConvertFromString(family) ?? new FontFamily(); + return new FontAdapter(new Typeface(fontFamily, GetFontStyle(style), GetFontWidth(style), FontStretches.Normal), size); + } + + protected override RFont CreateFontInt(RFontFamily family, double size, RFontStyle style) + { + return new FontAdapter(new Typeface(((FontFamilyAdapter)family).FontFamily, GetFontStyle(style), GetFontWidth(style), FontStretches.Normal), size); + } + + protected override object GetClipboardDataObjectInt(string html, string plainText) + { + return ClipboardHelper.CreateDataObject(html, plainText); + } + + protected override void SetToClipboardInt(string text) + { + ClipboardHelper.CopyToClipboard(text); + } + + protected override void SetToClipboardInt(string html, string plainText) + { + ClipboardHelper.CopyToClipboard(html, plainText); + } + + protected override void SetToClipboardInt(RImage image) + { + Clipboard.SetImage(((ImageAdapter)image).Image); + } + + protected override RContextMenu CreateContextMenuInt() + { + return new ContextMenuAdapter(); + } + + protected override void SaveToFileInt(RImage image, string name, string extension, RControl control = null) + { + var saveDialog = new SaveFileDialog(); + saveDialog.Filter = "Images|*.png;*.bmp;*.jpg;*.tif;*.gif;*.wmp;"; + saveDialog.FileName = name; + saveDialog.DefaultExt = extension; + + var dialogResult = saveDialog.ShowDialog(); + if (dialogResult.GetValueOrDefault()) + { + var encoder = Utils.GetBitmapEncoder(Path.GetExtension(saveDialog.FileName)); + encoder.Frames.Add(BitmapFrame.Create(((ImageAdapter)image).Image)); + using (FileStream stream = new FileStream(saveDialog.FileName, FileMode.OpenOrCreate)) + encoder.Save(stream); + } + } + + + #region Private/Protected methods + + /// + /// Get solid color brush for the given color. + /// + private static Brush GetSolidColorBrush(RColor color) + { + Brush solidBrush; + if (color == RColor.White) + solidBrush = Brushes.White; + else if (color == RColor.Black) + solidBrush = Brushes.Black; + else if (color.A < 1) + solidBrush = Brushes.Transparent; + else + solidBrush = new SolidColorBrush(Utils.Convert(color)); + return solidBrush; + } + + /// + /// Get WPF font style for the given style. + /// + private static FontStyle GetFontStyle(RFontStyle style) + { + if ((style & RFontStyle.Italic) == RFontStyle.Italic) + return FontStyles.Italic; + + return FontStyles.Normal; + } + + /// + /// Get WPF font style for the given style. + /// + private static FontWeight GetFontWidth(RFontStyle style) + { + if ((style & RFontStyle.Bold) == RFontStyle.Bold) + return FontWeights.Bold; + + return FontWeights.Normal; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/HTMLRendererCore.WPF.csproj b/Source/HTMLRendererCore.WPF/HTMLRendererCore.WPF.csproj new file mode 100644 index 000000000..17f8b2bad --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HTMLRendererCore.WPF.csproj @@ -0,0 +1,12 @@ + + + + net5.0-windows + true + + + + + + + diff --git a/Source/HTMLRendererCore.WPF/HtmlContainer.cs b/Source/HTMLRendererCore.WPF/HtmlContainer.cs new file mode 100644 index 000000000..d7d37d5be --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HtmlContainer.cs @@ -0,0 +1,470 @@ +// "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.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.WPF.Adapters; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Low level handling of Html Renderer logic, this class is used by , + /// , and .
+ ///
+ /// + 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(WpfAdapter.Instance); + _htmlContainerInt.PageSize = new RSize(99999, 99999); + } + + /// + /// 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 the user clicks on a link in the html.
+ /// Allows canceling the execution of the link. + ///
+ public event EventHandler LinkClicked + { + add { _htmlContainerInt.LinkClicked += value; } + remove { _htmlContainerInt.LinkClicked -= value; } + } + + /// + /// 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 + { + add { _htmlContainerInt.Refresh += value; } + remove { _htmlContainerInt.Refresh -= value; } + } + + /// + /// Raised when Html Renderer request scroll to specific location.
+ /// This can occur on document anchor click. + ///
+ public event EventHandler ScrollChange + { + add { _htmlContainerInt.ScrollChange += value; } + remove { _htmlContainerInt.ScrollChange -= 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 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 _htmlContainerInt.AvoidAsyncImagesLoading; } + set { _htmlContainerInt.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 _htmlContainerInt.AvoidImagesLateLoading; } + set { _htmlContainerInt.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 _htmlContainerInt.IsSelectionEnabled; } + set { _htmlContainerInt.IsSelectionEnabled = value; } + } + + /// + /// Is the build-in context menu enabled and will be shown on mouse right click (default - true) + /// + public bool IsContextMenuEnabled + { + get { return _htmlContainerInt.IsContextMenuEnabled; } + set { _htmlContainerInt.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 Rect. + /// + public Point 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 Point 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 Size MaxSize + { + get { return Utils.Convert(_htmlContainerInt.MaxSize); } + set { _htmlContainerInt.MaxSize = Utils.Convert(value); } + } + + /// + /// The actual size of the rendered html (after layout) + /// + public Size ActualSize + { + get { return Utils.Convert(_htmlContainerInt.ActualSize); } + internal set { _htmlContainerInt.ActualSize = Utils.Convert(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; } + } + + /// + /// Clear the current selection. + /// + public void ClearSelection() + { + HtmlContainerInt.ClearSelection(); + } + + /// + /// 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); + } + + /// + /// Clear the content of the HTML container releasing any resources used to render previously existing content. + /// + public void Clear() + { + _htmlContainerInt.Clear(); + } + + /// + /// 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(Point location, string attribute) + { + return _htmlContainerInt.GetAttributeAt(Utils.Convert(location), attribute); + } + + /// + /// Get all the links in the HTML with the element Rect 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(Point location) + { + return _htmlContainerInt.GetLinkAt(Utils.Convert(location)); + } + + /// + /// Get the Rect of html element as calculated by html layout.
+ /// Element if found by id (id attribute on the html element).
+ /// Note: to get the screen Rect you need to adjust by the hosting control.
+ ///
+ /// the id of the element to get its Rect + /// the Rect of the element or null if not found + public Rect? GetElementRectangle(string elementId) + { + var r = _htmlContainerInt.GetElementRectangle(elementId); + return r.HasValue ? Utils.Convert(r.Value) : (Rect?)null; + } + + /// + /// Measures the bounds of box and children, recursively. + /// + public void PerformLayout() + { + using (var ig = new GraphicsAdapter()) + { + _htmlContainerInt.PerformLayout(ig); + } + } + + /// + /// Render the html using the given device. + /// + /// the device to use to render + /// the clip rectangle of the html container + public void PerformPaint(DrawingContext g, Rect clip) + { + ArgChecker.AssertArgNotNull(g, "g"); + + using (var ig = new GraphicsAdapter(g, Utils.Convert(clip))) + { + _htmlContainerInt.PerformPaint(ig); + } + } + + /// + /// Handle mouse down to handle selection. + /// + /// the control hosting the html to invalidate + /// the mouse event args + public void HandleMouseDown(Control parent, MouseEventArgs e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + ArgChecker.AssertArgNotNull(e, "e"); + + _htmlContainerInt.HandleMouseDown(new ControlAdapter(parent), Utils.Convert(e.GetPosition(parent))); + } + + /// + /// Handle mouse up to handle selection and link click. + /// + /// the control hosting the html to invalidate + /// the mouse event args + public void HandleMouseUp(Control parent, MouseButtonEventArgs e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + ArgChecker.AssertArgNotNull(e, "e"); + + var mouseEvent = new RMouseEvent(e.ChangedButton == MouseButton.Left); + _htmlContainerInt.HandleMouseUp(new ControlAdapter(parent), Utils.Convert(e.GetPosition(parent)), mouseEvent); + } + + /// + /// Handle mouse double click to select word under the mouse. + /// + /// the control hosting the html to set cursor and invalidate + /// mouse event args + public void HandleMouseDoubleClick(Control parent, MouseEventArgs e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + ArgChecker.AssertArgNotNull(e, "e"); + + _htmlContainerInt.HandleMouseDoubleClick(new ControlAdapter(parent), Utils.Convert(e.GetPosition(parent))); + } + + /// + /// Handle mouse move to handle hover cursor and text selection. + /// + /// the control hosting the html to set cursor and invalidate + /// the mouse event args + public void HandleMouseMove(Control parent, Point mousePos) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + _htmlContainerInt.HandleMouseMove(new ControlAdapter(parent), Utils.Convert(mousePos)); + } + + /// + /// Handle mouse leave to handle hover cursor. + /// + /// the control hosting the html to set cursor and invalidate + public void HandleMouseLeave(Control parent) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + + _htmlContainerInt.HandleMouseLeave(new ControlAdapter(parent)); + } + + /// + /// Handle key down event for selection and copy. + /// + /// the control hosting the html to invalidate + /// the pressed key + public void HandleKeyDown(Control parent, KeyEventArgs e) + { + ArgChecker.AssertArgNotNull(parent, "parent"); + ArgChecker.AssertArgNotNull(e, "e"); + + _htmlContainerInt.HandleKeyDown(new ControlAdapter(parent), CreateKeyEevent(e)); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _htmlContainerInt.Dispose(); + } + + + #region Private methods + + /// + /// Create HtmlRenderer key event from WPF key event. + /// + private static RKeyEvent CreateKeyEevent(KeyEventArgs e) + { + var control = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; + return new RKeyEvent(control, e.Key == Key.A, e.Key == Key.C); + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/HtmlControl.cs b/Source/HTMLRendererCore.WPF/HtmlControl.cs new file mode 100644 index 000000000..be4e241fd --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HtmlControl.cs @@ -0,0 +1,559 @@ +// "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.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Provides HTML rendering using the text property.
+ /// WPF control that will render html content in it's client rectangle.
+ /// The control will handle mouse and keyboard events on it to support html text selection, copy-paste and mouse clicks.
+ /// + /// The major differential to use HtmlPanel or HtmlLabel is size and scrollbars.
+ /// If the size of the control depends on the html content the HtmlLabel should be used.
+ /// If the size is set by some kind of layout then HtmlPanel is more suitable, also shows scrollbars if the html contents is larger than the control client rectangle.
+ ///
+ /// + ///

LinkClicked event:

+ /// Raised when the user clicks on a link in the html.
+ /// Allows canceling the execution of the link. + ///
+ /// + ///

StylesheetLoad event:

+ /// 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.
+ ///
+ /// + ///

ImageLoad event:

+ /// 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. + ///
+ /// + ///

RenderError event:

+ /// Raised when an error occurred during html rendering.
+ ///
+ ///
+ public class HtmlControl : Control + { + #region Fields and Consts + + /// + /// Underline html container instance. + /// + protected readonly HtmlContainer _htmlContainer; + + /// + /// the base stylesheet data used in the control + /// + protected CssData _baseCssData; + + /// + /// The last position of the scrollbars to know if it has changed to update mouse + /// + protected Point _lastScrollOffset; + + #endregion + + + #region Dependency properties / routed events + + public static readonly DependencyProperty AvoidImagesLateLoadingProperty = DependencyProperty.Register("AvoidImagesLateLoading", typeof(bool), typeof(HtmlControl), new PropertyMetadata(false, OnDependencyProperty_valueChanged)); + public static readonly DependencyProperty IsSelectionEnabledProperty = DependencyProperty.Register("IsSelectionEnabled", typeof(bool), typeof(HtmlControl), new PropertyMetadata(true, OnDependencyProperty_valueChanged)); + public static readonly DependencyProperty IsContextMenuEnabledProperty = DependencyProperty.Register("IsContextMenuEnabled", typeof(bool), typeof(HtmlControl), new PropertyMetadata(true, OnDependencyProperty_valueChanged)); + public static readonly DependencyProperty BaseStylesheetProperty = DependencyProperty.Register("BaseStylesheet", typeof(string), typeof(HtmlControl), new PropertyMetadata(null, OnDependencyProperty_valueChanged)); + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(HtmlControl), new PropertyMetadata(null, OnDependencyProperty_valueChanged)); + + public static readonly RoutedEvent LoadCompleteEvent = EventManager.RegisterRoutedEvent("LoadComplete", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + public static readonly RoutedEvent LinkClickedEvent = EventManager.RegisterRoutedEvent("LinkClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + public static readonly RoutedEvent RenderErrorEvent = EventManager.RegisterRoutedEvent("RenderError", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + public static readonly RoutedEvent RefreshEvent = EventManager.RegisterRoutedEvent("Refresh", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + public static readonly RoutedEvent StylesheetLoadEvent = EventManager.RegisterRoutedEvent("StylesheetLoad", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + public static readonly RoutedEvent ImageLoadEvent = EventManager.RegisterRoutedEvent("ImageLoad", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HtmlControl)); + + #endregion + + + /// + /// Creates a new HtmlPanel and sets a basic css for it's styling. + /// + protected HtmlControl() + { + // shitty WPF rendering, have no idea why this actually makes everything sharper =/ + SnapsToDevicePixels = false; + + _htmlContainer = new HtmlContainer(); + _htmlContainer.LoadComplete += OnLoadComplete; + _htmlContainer.LinkClicked += OnLinkClicked; + _htmlContainer.RenderError += OnRenderError; + _htmlContainer.Refresh += OnRefresh; + _htmlContainer.StylesheetLoad += OnStylesheetLoad; + _htmlContainer.ImageLoad += OnImageLoad; + } + + /// + /// Raised when the set html document has been fully loaded.
+ /// Allows manipulation of the html dom, scroll position, etc. + ///
+ public event RoutedEventHandler LoadComplete + { + add { AddHandler(LoadCompleteEvent, value); } + remove { RemoveHandler(LoadCompleteEvent, value); } + } + + /// + /// Raised when the user clicks on a link in the html.
+ /// Allows canceling the execution of the link. + ///
+ public event RoutedEventHandler LinkClicked + { + add { AddHandler(LinkClickedEvent, value); } + remove { RemoveHandler(LinkClickedEvent, value); } + } + + /// + /// Raised when an error occurred during html rendering.
+ ///
+ public event RoutedEventHandler RenderError + { + add { AddHandler(RenderErrorEvent, value); } + remove { RemoveHandler(RenderErrorEvent, 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 RoutedEventHandler StylesheetLoad + { + add { AddHandler(StylesheetLoadEvent, value); } + remove { RemoveHandler(StylesheetLoadEvent, 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 RoutedEventHandler ImageLoad + { + add { AddHandler(ImageLoadEvent, value); } + remove { RemoveHandler(ImageLoadEvent, 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. + ///
+ [Category("Behavior")] + [Description("If image loading only when visible should be avoided")] + public bool AvoidImagesLateLoading + { + get { return (bool)GetValue(AvoidImagesLateLoadingProperty); } + set { SetValue(AvoidImagesLateLoadingProperty, 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. + ///
+ [Category("Behavior")] + [Description("Is content selection is enabled for the rendered html.")] + public bool IsSelectionEnabled + { + get { return (bool)GetValue(IsSelectionEnabledProperty); } + set { SetValue(IsSelectionEnabledProperty, value); } + } + + /// + /// Is the build-in context menu enabled and will be shown on mouse right click (default - true) + /// + [Category("Behavior")] + [Description("Is the build-in context menu enabled and will be shown on mouse right click.")] + public bool IsContextMenuEnabled + { + get { return (bool)GetValue(IsContextMenuEnabledProperty); } + set { SetValue(IsContextMenuEnabledProperty, value); } + } + + /// + /// Set base stylesheet to be used by html rendered in the panel. + /// + [Category("Appearance")] + [Description("Set base stylesheet to be used by html rendered in the control.")] + public string BaseStylesheet + { + get { return (string)GetValue(BaseStylesheetProperty); } + set { SetValue(BaseStylesheetProperty, value); } + } + + /// + /// Gets or sets the text of this panel + /// + [Description("Sets the html of this control.")] + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + /// + /// Get the currently selected text segment in the html. + /// + [Browsable(false)] + public virtual string SelectedText + { + get { return _htmlContainer.SelectedText; } + } + + /// + /// Copy the currently selected html segment with style. + /// + [Browsable(false)] + public virtual string SelectedHtml + { + get { return _htmlContainer.SelectedHtml; } + } + + /// + /// Get html from the current DOM tree with inline style. + /// + /// generated html + public virtual string GetHtml() + { + return _htmlContainer != null ? _htmlContainer.GetHtml() : 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 virtual Rect? GetElementRectangle(string elementId) + { + return _htmlContainer != null ? _htmlContainer.GetElementRectangle(elementId) : null; + } + + /// + /// Clear the current selection. + /// + public void ClearSelection() + { + if (_htmlContainer != null) + _htmlContainer.ClearSelection(); + } + + + #region Private methods + + /// + /// Perform paint of the html in the control. + /// + protected override void OnRender(DrawingContext context) + { + if (Background.Opacity > 0) + context.DrawRectangle(Background, null, new Rect(RenderSize)); + + if (BorderThickness != new Thickness(0)) + { + var brush = BorderBrush ?? SystemColors.ControlDarkBrush; + if (BorderThickness.Top > 0) + context.DrawRectangle(brush, null, new Rect(0, 0, RenderSize.Width, BorderThickness.Top)); + if (BorderThickness.Bottom > 0) + context.DrawRectangle(brush, null, new Rect(0, RenderSize.Height - BorderThickness.Bottom, RenderSize.Width, BorderThickness.Bottom)); + if (BorderThickness.Left > 0) + context.DrawRectangle(brush, null, new Rect(0, 0, BorderThickness.Left, RenderSize.Height)); + if (BorderThickness.Right > 0) + context.DrawRectangle(brush, null, new Rect(RenderSize.Width - BorderThickness.Right, 0, BorderThickness.Right, RenderSize.Height)); + } + + var htmlWidth = HtmlWidth(RenderSize); + var htmlHeight = HtmlHeight(RenderSize); + if (_htmlContainer != null && htmlWidth > 0 && htmlHeight > 0) + { + var windows = Window.GetWindow(this); + if (windows != null) + { + // adjust render location to round point so we won't get anti-alias smugness + var wPoint = TranslatePoint(new Point(0, 0), windows); + wPoint.Offset(-(int)wPoint.X, -(int)wPoint.Y); + var xTrans = wPoint.X < .5 ? -wPoint.X : 1 - wPoint.X; + var yTrans = wPoint.Y < .5 ? -wPoint.Y : 1 - wPoint.Y; + context.PushTransform(new TranslateTransform(xTrans, yTrans)); + } + + context.PushClip(new RectangleGeometry(new Rect(Padding.Left + BorderThickness.Left, Padding.Top + BorderThickness.Top, htmlWidth, (int)htmlHeight))); + _htmlContainer.Location = new Point(Padding.Left + BorderThickness.Left, Padding.Top + BorderThickness.Top); + _htmlContainer.PerformPaint(context, new Rect(Padding.Left + BorderThickness.Left, Padding.Top + BorderThickness.Top, htmlWidth, htmlHeight)); + context.Pop(); + + if (!_lastScrollOffset.Equals(_htmlContainer.ScrollOffset)) + { + _lastScrollOffset = _htmlContainer.ScrollOffset; + InvokeMouseMove(); + } + } + } + + /// + /// Handle mouse move to handle hover cursor and text selection. + /// + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + if (_htmlContainer != null) + _htmlContainer.HandleMouseMove(this, e.GetPosition(this)); + } + + /// + /// Handle mouse leave to handle cursor change. + /// + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + if (_htmlContainer != null) + _htmlContainer.HandleMouseLeave(this); + } + + /// + /// Handle mouse down to handle selection. + /// + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + if (_htmlContainer != null) + _htmlContainer.HandleMouseDown(this, e); + } + + /// + /// Handle mouse up to handle selection and link click. + /// + protected override void OnMouseUp(MouseButtonEventArgs e) + { + base.OnMouseUp(e); + if (_htmlContainer != null) + _htmlContainer.HandleMouseUp(this, e); + } + + /// + /// Handle mouse double click to select word under the mouse. + /// + protected override void OnMouseDoubleClick(MouseButtonEventArgs e) + { + base.OnMouseDoubleClick(e); + if (_htmlContainer != null) + _htmlContainer.HandleMouseDoubleClick(this, e); + } + + /// + /// Handle key down event for selection, copy and scrollbars handling. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (_htmlContainer != null) + _htmlContainer.HandleKeyDown(this, e); + } + + /// + /// Propagate the LoadComplete event from root container. + /// + protected virtual void OnLoadComplete(EventArgs e) + { + RoutedEventArgs newEventArgs = new RoutedEventArgs(LoadCompleteEvent, this, e); + RaiseEvent(newEventArgs); + } + + /// + /// Propagate the LinkClicked event from root container. + /// + protected virtual void OnLinkClicked(HtmlLinkClickedEventArgs e) + { + RoutedEventArgs newEventArgs = new RoutedEventArgs(LinkClickedEvent, this, e); + RaiseEvent(newEventArgs); + } + + /// + /// Propagate the Render Error event from root container. + /// + protected virtual void OnRenderError(HtmlRenderErrorEventArgs e) + { + RoutedEventArgs newEventArgs = new RoutedEventArgs(RenderErrorEvent, this, e); + RaiseEvent(newEventArgs); + } + + /// + /// Propagate the stylesheet load event from root container. + /// + protected virtual void OnStylesheetLoad(HtmlStylesheetLoadEventArgs e) + { + RoutedEventArgs newEventArgs = new RoutedEventArgs(StylesheetLoadEvent, this, e); + RaiseEvent(newEventArgs); + } + + /// + /// Propagate the image load event from root container. + /// + protected virtual void OnImageLoad(HtmlImageLoadEventArgs e) + { + RoutedEventArgs newEventArgs = new RoutedEventArgs(ImageLoadEvent, this, e); + RaiseEvent(newEventArgs); + } + + /// + /// Handle html renderer invalidate and re-layout as requested. + /// + protected virtual void OnRefresh(HtmlRefreshEventArgs e) + { + if (e.Layout) + InvalidateMeasure(); + InvalidateVisual(); + } + + /// + /// Get the width the HTML has to render in (not including vertical scroll iff it is visible) + /// + protected virtual double HtmlWidth(Size size) + { + return size.Width - Padding.Left - Padding.Right - BorderThickness.Left - BorderThickness.Right; + } + + /// + /// Get the width the HTML has to render in (not including vertical scroll iff it is visible) + /// + protected virtual double HtmlHeight(Size size) + { + return size.Height - Padding.Top - Padding.Bottom - BorderThickness.Top - BorderThickness.Bottom; + } + + /// + /// call mouse move to handle paint after scroll or html change affecting mouse cursor. + /// + protected virtual void InvokeMouseMove() + { + _htmlContainer.HandleMouseMove(this, Mouse.GetPosition(this)); + } + + /// + /// Handle when dependency property value changes to update the underline HtmlContainer with the new value. + /// + private static void OnDependencyProperty_valueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + var control = dependencyObject as HtmlControl; + if (control != null) + { + var htmlContainer = control._htmlContainer; + if (e.Property == AvoidImagesLateLoadingProperty) + { + htmlContainer.AvoidImagesLateLoading = (bool)e.NewValue; + } + else if (e.Property == IsSelectionEnabledProperty) + { + htmlContainer.IsSelectionEnabled = (bool)e.NewValue; + } + else if (e.Property == IsContextMenuEnabledProperty) + { + htmlContainer.IsContextMenuEnabled = (bool)e.NewValue; + } + else if (e.Property == BaseStylesheetProperty) + { + var baseCssData = HtmlRender.ParseStyleSheet((string)e.NewValue); + control._baseCssData = baseCssData; + htmlContainer.SetHtml(control.Text, baseCssData); + } + else if (e.Property == TextProperty) + { + htmlContainer.ScrollOffset = new Point(0, 0); + htmlContainer.SetHtml((string)e.NewValue, control._baseCssData); + control.InvalidateMeasure(); + control.InvalidateVisual(); + control.InvokeMouseMove(); + } + } + } + + + #region Private event handlers + + private void OnLoadComplete(object sender, EventArgs e) + { + if (CheckAccess()) + OnLoadComplete(e); + else + Dispatcher.Invoke(new Action(OnLinkClicked), e); + } + + private void OnLinkClicked(object sender, HtmlLinkClickedEventArgs e) + { + if (CheckAccess()) + OnLinkClicked(e); + else + Dispatcher.Invoke(new Action(OnLinkClicked), e); + } + + private void OnRenderError(object sender, HtmlRenderErrorEventArgs e) + { + if (CheckAccess()) + OnRenderError(e); + else + Dispatcher.Invoke(new Action(OnRenderError), e); + } + + private void OnStylesheetLoad(object sender, HtmlStylesheetLoadEventArgs e) + { + if (CheckAccess()) + OnStylesheetLoad(e); + else + Dispatcher.Invoke(new Action(OnStylesheetLoad), e); + } + + private void OnImageLoad(object sender, HtmlImageLoadEventArgs e) + { + if (CheckAccess()) + OnImageLoad(e); + else + Dispatcher.Invoke(new Action(OnImageLoad), e); + } + + private void OnRefresh(object sender, HtmlRefreshEventArgs e) + { + if (CheckAccess()) + OnRefresh(e); + else + Dispatcher.Invoke(new Action(OnRefresh), e); + } + + #endregion + + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/HtmlLabel.cs b/Source/HTMLRendererCore.WPF/HtmlLabel.cs new file mode 100644 index 000000000..01429c872 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HtmlLabel.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 System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.WPF.Adapters; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Provides HTML rendering using the text property.
+ /// WPF control that will render html content in it's client rectangle.
+ /// Using and client can control how the html content effects the + /// size of the label. Either case scrollbars are never shown and html content outside of client bounds will be clipped. + /// MaxWidth/MaxHeight and MinWidth/MinHeight with AutoSize can limit the max/min size of the control
+ /// The control will handle mouse and keyboard events on it to support html text selection, copy-paste and mouse clicks.
+ ///
+ /// + /// See for more info. + /// + public class HtmlLabel : HtmlControl + { + #region Dependency properties + + public static readonly DependencyProperty AutoSizeProperty = DependencyProperty.Register("AutoSize", typeof(bool), typeof(HtmlLabel), new PropertyMetadata(true, OnDependencyProperty_valueChanged)); + public static readonly DependencyProperty AutoSizeHeightOnlyProperty = DependencyProperty.Register("AutoSizeHeightOnly", typeof(bool), typeof(HtmlLabel), new PropertyMetadata(false, OnDependencyProperty_valueChanged)); + + #endregion + + + /// + /// Init. + /// + static HtmlLabel() + { + BackgroundProperty.OverrideMetadata(typeof(HtmlLabel), new FrameworkPropertyMetadata(Brushes.Transparent)); + } + + /// + /// Automatically sets the size of the label by content size + /// + [Category("Layout")] + [Description("Automatically sets the size of the label by content size.")] + public bool AutoSize + { + get { return (bool)GetValue(AutoSizeProperty); } + set { SetValue(AutoSizeProperty, value); } + } + + /// + /// Automatically sets the height of the label by content height (width is not effected). + /// + [Category("Layout")] + [Description("Automatically sets the height of the label by content height (width is not effected)")] + public virtual bool AutoSizeHeightOnly + { + get { return (bool)GetValue(AutoSizeHeightOnlyProperty); } + set { SetValue(AutoSizeHeightOnlyProperty, value); } + } + + + #region Private methods + + /// + /// Perform the layout of the html in the control. + /// + protected override Size MeasureOverride(Size constraint) + { + if (_htmlContainer != null) + { + using (var ig = new GraphicsAdapter()) + { + var horizontal = Padding.Left + Padding.Right + BorderThickness.Left + BorderThickness.Right; + var vertical = Padding.Top + Padding.Bottom + BorderThickness.Top + BorderThickness.Bottom; + + var size = new RSize(constraint.Width < Double.PositiveInfinity ? constraint.Width - horizontal : 0, constraint.Height < Double.PositiveInfinity ? constraint.Height - vertical : 0); + var minSize = new RSize(MinWidth < Double.PositiveInfinity ? MinWidth - horizontal : 0, MinHeight < Double.PositiveInfinity ? MinHeight - vertical : 0); + var maxSize = new RSize(MaxWidth < Double.PositiveInfinity ? MaxWidth - horizontal : 0, MaxHeight < Double.PositiveInfinity ? MaxHeight - vertical : 0); + + var newSize = HtmlRendererUtils.Layout(ig, _htmlContainer.HtmlContainerInt, size, minSize, maxSize, AutoSize, AutoSizeHeightOnly); + + constraint = new Size(newSize.Width + horizontal, newSize.Height + vertical); + } + } + + if (double.IsPositiveInfinity(constraint.Width) || double.IsPositiveInfinity(constraint.Height)) + constraint = Size.Empty; + + return constraint; + } + + /// + /// Handle when dependency property value changes to update the underline HtmlContainer with the new value. + /// + private static void OnDependencyProperty_valueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + var control = dependencyObject as HtmlLabel; + if (control != null) + { + if (e.Property == AutoSizeProperty) + { + if ((bool)e.NewValue) + { + dependencyObject.SetValue(AutoSizeHeightOnlyProperty, false); + control.InvalidateMeasure(); + control.InvalidateVisual(); + } + } + else if (e.Property == AutoSizeHeightOnlyProperty) + { + if ((bool)e.NewValue) + { + dependencyObject.SetValue(AutoSizeProperty, false); + control.InvalidateMeasure(); + control.InvalidateVisual(); + } + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/HtmlPanel.cs b/Source/HTMLRendererCore.WPF/HtmlPanel.cs new file mode 100644 index 000000000..44fea7028 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HtmlPanel.cs @@ -0,0 +1,370 @@ +// "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.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Provides HTML rendering using the text property.
+ /// WPF control that will render html content in it's client rectangle.
+ /// If the layout of the html resulted in its content beyond the client bounds of the panel it will show scrollbars (horizontal/vertical) allowing to scroll the content.
+ /// The control will handle mouse and keyboard events on it to support html text selection, copy-paste and mouse clicks.
+ ///
+ /// + /// See for more info. + /// + public class HtmlPanel : HtmlControl + { + #region Fields and Consts + + /// + /// the vertical scroll bar for the control to scroll to html content out of view + /// + protected ScrollBar _verticalScrollBar; + + /// + /// the horizontal scroll bar for the control to scroll to html content out of view + /// + protected ScrollBar _horizontalScrollBar; + + #endregion + + + static HtmlPanel() + { + BackgroundProperty.OverrideMetadata(typeof(HtmlPanel), new FrameworkPropertyMetadata(SystemColors.WindowBrush)); + + TextProperty.OverrideMetadata(typeof(HtmlPanel), new PropertyMetadata(null, OnTextProperty_change)); + } + + /// + /// Creates a new HtmlPanel and sets a basic css for it's styling. + /// + public HtmlPanel() + { + _verticalScrollBar = new ScrollBar(); + _verticalScrollBar.Orientation = Orientation.Vertical; + _verticalScrollBar.Width = 18; + _verticalScrollBar.Scroll += OnScrollBarScroll; + AddVisualChild(_verticalScrollBar); + AddLogicalChild(_verticalScrollBar); + + _horizontalScrollBar = new ScrollBar(); + _horizontalScrollBar.Orientation = Orientation.Horizontal; + _horizontalScrollBar.Height = 18; + _horizontalScrollBar.Scroll += OnScrollBarScroll; + AddVisualChild(_horizontalScrollBar); + AddLogicalChild(_horizontalScrollBar); + + _htmlContainer.ScrollChange += OnScrollChange; + } + + /// + /// Adjust the scrollbar of the panel on html element by the given id.
+ /// The top of the html element rectangle will be at the top of the panel, if there + /// is not enough height to scroll to the top the scroll will be at maximum.
+ ///
+ /// the id of the element to scroll to + public virtual void ScrollToElement(string elementId) + { + ArgChecker.AssertArgNotNullOrEmpty(elementId, "elementId"); + + if (_htmlContainer != null) + { + var rect = _htmlContainer.GetElementRectangle(elementId); + if (rect.HasValue) + { + ScrollToPoint(rect.Value.Location.X, rect.Value.Location.Y); + _htmlContainer.HandleMouseMove(this, Mouse.GetPosition(this)); + } + } + } + + + #region Private methods + + protected override int VisualChildrenCount + { + get { return 2; } + } + + protected override Visual GetVisualChild(int index) + { + if (index == 0) + return _verticalScrollBar; + else if (index == 1) + return _horizontalScrollBar; + return null; + } + + /// + /// Perform the layout of the html in the control. + /// + protected override Size MeasureOverride(Size constraint) + { + Size size = PerformHtmlLayout(constraint); + + // to handle if scrollbar is appearing or disappearing + bool relayout = false; + var htmlWidth = HtmlWidth(constraint); + var htmlHeight = HtmlHeight(constraint); + + if ((_verticalScrollBar.Visibility == Visibility.Hidden && size.Height > htmlHeight) || + (_verticalScrollBar.Visibility == Visibility.Visible && size.Height <= htmlHeight)) + { + _verticalScrollBar.Visibility = _verticalScrollBar.Visibility == Visibility.Visible ? Visibility.Hidden : Visibility.Visible; + relayout = true; + } + + if ((_horizontalScrollBar.Visibility == Visibility.Hidden && size.Width > htmlWidth) || + (_horizontalScrollBar.Visibility == Visibility.Visible && size.Width <= htmlWidth)) + { + _horizontalScrollBar.Visibility = _horizontalScrollBar.Visibility == Visibility.Visible ? Visibility.Hidden : Visibility.Visible; + relayout = true; + } + + if (relayout) + PerformHtmlLayout(constraint); + + if (double.IsPositiveInfinity(constraint.Width) || double.IsPositiveInfinity(constraint.Height)) + constraint = size; + + return constraint; + } + + /// + /// After measurement arrange the scrollbars of the panel. + /// + protected override Size ArrangeOverride(Size bounds) + { + var scrollHeight = HtmlHeight(bounds) + Padding.Top + Padding.Bottom; + scrollHeight = scrollHeight > 1 ? scrollHeight : 1; + var scrollWidth = HtmlWidth(bounds) + Padding.Left + Padding.Right; + scrollWidth = scrollWidth > 1 ? scrollWidth : 1; + _verticalScrollBar.Arrange(new Rect(System.Math.Max(bounds.Width - _verticalScrollBar.Width - BorderThickness.Right, 0), BorderThickness.Top, _verticalScrollBar.Width, scrollHeight)); + _horizontalScrollBar.Arrange(new Rect(BorderThickness.Left, System.Math.Max(bounds.Height - _horizontalScrollBar.Height - BorderThickness.Bottom, 0), scrollWidth, _horizontalScrollBar.Height)); + + if (_htmlContainer != null) + { + if (_verticalScrollBar.Visibility == Visibility.Visible) + { + _verticalScrollBar.ViewportSize = HtmlHeight(bounds); + _verticalScrollBar.SmallChange = 25; + _verticalScrollBar.LargeChange = _verticalScrollBar.ViewportSize * .9; + _verticalScrollBar.Maximum = _htmlContainer.ActualSize.Height - _verticalScrollBar.ViewportSize; + } + + if (_horizontalScrollBar.Visibility == Visibility.Visible) + { + _horizontalScrollBar.ViewportSize = HtmlWidth(bounds); + _horizontalScrollBar.SmallChange = 25; + _horizontalScrollBar.LargeChange = _horizontalScrollBar.ViewportSize * .9; + _horizontalScrollBar.Maximum = _htmlContainer.ActualSize.Width - _horizontalScrollBar.ViewportSize; + } + + // update the scroll offset because the scroll values may have changed + UpdateScrollOffsets(); + } + + return bounds; + } + + /// + /// Perform html container layout by the current panel client size. + /// + protected Size PerformHtmlLayout(Size constraint) + { + if (_htmlContainer != null) + { + _htmlContainer.MaxSize = new Size(HtmlWidth(constraint), 0); + _htmlContainer.PerformLayout(); + return _htmlContainer.ActualSize; + } + return Size.Empty; + } + + /// + /// Handle minor case where both scroll are visible and create a rectangle at the bottom right corner between them. + /// + protected override void OnRender(DrawingContext context) + { + base.OnRender(context); + + // render rectangle in right bottom corner where both scrolls meet + if (_horizontalScrollBar.Visibility == Visibility.Visible && _verticalScrollBar.Visibility == Visibility.Visible) + context.DrawRectangle(SystemColors.ControlBrush, null, new Rect(BorderThickness.Left + HtmlWidth(RenderSize), BorderThickness.Top + HtmlHeight(RenderSize), _verticalScrollBar.Width, _horizontalScrollBar.Height)); + } + + /// + /// Handle mouse up to set focus on the control. + /// + protected override void OnMouseUp(MouseButtonEventArgs e) + { + base.OnMouseUp(e); + Focus(); + } + + /// + /// Handle mouse wheel for scrolling. + /// + protected override void OnMouseWheel(MouseWheelEventArgs e) + { + base.OnMouseWheel(e); + if (_verticalScrollBar.Visibility == Visibility.Visible) + { + _verticalScrollBar.Value -= e.Delta; + UpdateScrollOffsets(); + e.Handled = true; + } + } + + /// + /// Handle key down event for selection, copy and scrollbars handling. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (_verticalScrollBar.Visibility == Visibility.Visible) + { + if (e.Key == Key.Up) + { + _verticalScrollBar.Value -= _verticalScrollBar.SmallChange; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.Down) + { + _verticalScrollBar.Value += _verticalScrollBar.SmallChange; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.PageUp) + { + _verticalScrollBar.Value -= _verticalScrollBar.LargeChange; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.PageDown) + { + _verticalScrollBar.Value += _verticalScrollBar.LargeChange; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.Home) + { + _verticalScrollBar.Value = 0; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.End) + { + _verticalScrollBar.Value = _verticalScrollBar.Maximum; + UpdateScrollOffsets(); + e.Handled = true; + } + } + + if (_horizontalScrollBar.Visibility == Visibility.Visible) + { + if (e.Key == Key.Left) + { + _horizontalScrollBar.Value -= _horizontalScrollBar.SmallChange; + UpdateScrollOffsets(); + e.Handled = true; + } + else if (e.Key == Key.Right) + { + _horizontalScrollBar.Value += _horizontalScrollBar.SmallChange; + UpdateScrollOffsets(); + e.Handled = true; + } + } + } + + /// + /// Get the width the HTML has to render in (not including vertical scroll iff it is visible) + /// + protected override double HtmlWidth(Size size) + { + var width = base.HtmlWidth(size) - (_verticalScrollBar.Visibility == Visibility.Visible ? _verticalScrollBar.Width : 0); + return width > 1 ? width : 1; + } + + /// + /// Get the width the HTML has to render in (not including vertical scroll iff it is visible) + /// + protected override double HtmlHeight(Size size) + { + var height = base.HtmlHeight(size) - (_horizontalScrollBar.Visibility == Visibility.Visible ? _horizontalScrollBar.Height : 0); + return height > 1 ? height : 1; + } + + /// + /// On HTML container scroll change request scroll to the requested location. + /// + private void OnScrollChange(object sender, HtmlScrollEventArgs e) + { + ScrollToPoint(e.X, e.Y); + } + + /// + /// Set the control scroll offset to the given values. + /// + private void ScrollToPoint(double x, double y) + { + _horizontalScrollBar.Value = x; + _verticalScrollBar.Value = y; + UpdateScrollOffsets(); + } + + /// + /// On scrollbar scroll update the scroll offsets and invalidate. + /// + private void OnScrollBarScroll(object sender, ScrollEventArgs e) + { + UpdateScrollOffsets(); + } + + /// + /// Update the scroll offset of the HTML container and invalidate visual to re-render. + /// + private void UpdateScrollOffsets() + { + var newScrollOffset = new Point(-_horizontalScrollBar.Value, -_verticalScrollBar.Value); + if (!newScrollOffset.Equals(_htmlContainer.ScrollOffset)) + { + _htmlContainer.ScrollOffset = newScrollOffset; + InvalidateVisual(); + } + } + + /// + /// On text property change reset the scrollbars to zero. + /// + private static void OnTextProperty_change(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var panel = d as HtmlPanel; + if (panel != null) + panel._horizontalScrollBar.Value = panel._verticalScrollBar.Value = 0; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/HtmlRender.cs b/Source/HTMLRendererCore.WPF/HtmlRender.cs new file mode 100644 index 000000000..77eb4a046 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/HtmlRender.cs @@ -0,0 +1,424 @@ +// "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.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.WPF.Adapters; +using TheArtOfDev.HtmlRenderer.WPF.Utilities; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Standalone static class for simple and direct HTML rendering.
+ /// For WPF UI prefer using HTML controls: or .
+ /// For low-level control and performance consider using .
+ ///
+ /// + /// + /// Rendering to image
+ /// // TODO:a update! + /// See https://htmlrenderer.codeplex.com/wikipage?title=Image%20generation
+ /// Because of GDI text rendering issue with alpha channel clear type text rendering rendering to image requires special handling.
+ /// Solid color background - generate an image where the background is filled with solid color and all the html is rendered on top + /// of the background color, GDI text rendering will be used. (RenderToImage method where the first argument is html string)
+ /// Image background - render html on top of existing image with whatever currently exist but it cannot have transparent pixels, + /// GDI text rendering will be used. (RenderToImage method where the first argument is Image object)
+ /// Transparent background - render html to empty image using GDI+ text rendering, the generated image can be transparent. + ///
+ /// + /// Overwrite stylesheet resolution
+ /// Exposed by optional "stylesheetLoad" delegate argument.
+ /// 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.
+ ///
+ /// + /// Overwrite image resolution
+ /// Exposed by optional "imageLoad" delegate argument.
+ /// 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.
+ /// If no alternative data is provided the original source will be used.
+ /// Note: Cannot use asynchronous scheme overwrite scheme.
+ ///
+ ///
+ /// + /// + /// Simple rendering
+ /// HtmlRender.Render(g, "Hello World]]>");
+ /// HtmlRender.Render(g, "Hello World]]>", 10, 10, 500, CssData.Parse("body {font-size: 20px}")");
+ ///
+ /// + /// Image rendering
+ /// HtmlRender.RenderToImage("Hello World]]>", new Size(600,400));
+ /// HtmlRender.RenderToImage("Hello World]]>", 600);
+ /// HtmlRender.RenderToImage(existingImage, "Hello World]]>");
+ ///
+ ///
+ public static class HtmlRender + { + /// + /// Adds a font family to be used in html rendering.
+ /// The added font will be used by all rendering function including and all WPF controls. + ///
+ /// + /// The given font family instance must be remain alive while the renderer is in use.
+ /// If loaded from file then the file must not be deleted. + ///
+ /// The font family to add. + public static void AddFontFamily(FontFamily fontFamily) + { + ArgChecker.AssertArgNotNull(fontFamily, "fontFamily"); + + WpfAdapter.Instance.AddFontFamily(new FontFamilyAdapter(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 .
+ ///
+ /// + /// 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"); + + WpfAdapter.Instance.AddFontFamilyMapping(fromFamily, toFamily); + } + + /// + /// 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(WpfAdapter.Instance, stylesheet, combineWithDefault); + } + + /// + /// Measure the size (width and height) required to draw the given html under given max width restriction.
+ /// If no max width restriction is given the layout will use the maximum possible width required by the content, + /// it can be the longest text line or full image width.
+ ///
+ /// HTML source to render + /// optional: bound the width of the html to render in (default - 0, unlimited) + /// 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 size required for the html + public static Size Measure(string html, double maxWidth = 0, CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + Size actualSize = Size.Empty; + if (!string.IsNullOrEmpty(html)) + { + using (var container = new HtmlContainer()) + { + container.MaxSize = new Size(maxWidth, 0); + container.AvoidAsyncImagesLoading = true; + container.AvoidImagesLateLoading = true; + + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + + container.SetHtml(html, cssData); + container.PerformLayout(); + + actualSize = container.ActualSize; + } + } + return actualSize; + } + + /// + /// Renders the specified HTML source on the specified location and max width restriction.
+ /// If is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// Returned is the actual width and height of the rendered html.
+ ///
+ /// Device to render with + /// HTML source to render + /// optional: the left most location to start render the html at (default - 0) + /// optional: the top most location to start render the html at (default - 0) + /// optional: bound the width of the html to render in (default - 0, unlimited) + /// 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 actual size of the rendered html + public static Size Render(DrawingContext g, string html, double left = 0, double top = 0, double maxWidth = 0, CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + ArgChecker.AssertArgNotNull(g, "g"); + return RenderClip(g, html, new Point(left, top), new Size(maxWidth, 0), cssData, stylesheetLoad, imageLoad); + } + + /// + /// Renders the specified HTML source on the specified location and max size restriction.
+ /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// If .Height is zero the html will use all the required height, otherwise it will clip at the + /// given max height not rendering the html below it.
+ /// Returned is the actual width and height of the rendered html.
+ ///
+ /// Device to render with + /// HTML source to render + /// the top-left most location to start render the html at + /// the max size of the rendered html (if height above zero it will be clipped) + /// 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 actual size of the rendered html + public static Size Render(DrawingContext g, string html, Point location, Size maxSize, CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + ArgChecker.AssertArgNotNull(g, "g"); + return RenderClip(g, html, location, maxSize, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Renders the specified HTML into a new image of the requested size.
+ /// The HTML will be layout by the given size but will be clipped if cannot fit.
+ ///
+ /// HTML source to render + /// The size of the image to render into, layout html by width and clipped by height + /// 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 BitmapFrame RenderToImage(string html, Size size, CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var renderTarget = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32); + + if (!string.IsNullOrEmpty(html)) + { + // render HTML into the visual + DrawingVisual drawingVisual = new DrawingVisual(); + using (DrawingContext g = drawingVisual.RenderOpen()) + { + RenderHtml(g, html, new Point(), size, cssData, stylesheetLoad, imageLoad); + } + + // render visual into target bitmap + renderTarget.Render(drawingVisual); + } + + return BitmapFrame.Create(renderTarget); + } + + /// + /// Renders the specified HTML into a new image of unknown size that will be determined by max width/height and HTML layout.
+ /// If is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// If is zero the html will use all the required height, otherwise it will clip at the + /// given max height not rendering the html below it.
+ ///

+ /// Limitation: The image cannot have transparent background, by default it will be white.
+ /// See "Rendering to image" remarks section on .
+ ///

+ ///
+ /// HTML source to render + /// optional: the max width of the rendered html, if not zero and html cannot be layout within the limit it will be clipped + /// optional: the max height of the rendered html, if not zero and html cannot be layout within the limit it will be clipped + /// optional: the color to fill the image with (default - white) + /// 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 BitmapFrame RenderToImage(string html, int maxWidth = 0, int maxHeight = 0, Color backgroundColor = new Color(), CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + return RenderToImage(html, Size.Empty, new Size(maxWidth, maxHeight), backgroundColor, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Renders the specified HTML into a new image of unknown size that will be determined by min/max width/height and HTML layout.
+ /// If is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// If is zero the html will use all the required height, otherwise it will clip at the + /// given max height not rendering the html below it.
+ /// If (Width/Height) is above zero the rendered image will not be smaller than the given min size.
+ ///

+ /// Limitation: The image cannot have transparent background, by default it will be white.
+ /// See "Rendering to image" remarks section on .
+ ///

+ ///
+ /// HTML source to render + /// optional: the min size of the rendered html (zero - not limit the width/height) + /// optional: the max 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) + /// optional: the color to fill the image with (default - white) + /// 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 BitmapFrame RenderToImage(string html, Size minSize, Size maxSize, Color backgroundColor = new Color(), CssData cssData = null, + EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + RenderTargetBitmap renderTarget; + if (!string.IsNullOrEmpty(html)) + { + using (var container = new HtmlContainer()) + { + container.AvoidAsyncImagesLoading = true; + container.AvoidImagesLateLoading = true; + + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + container.SetHtml(html, cssData); + + var finalSize = MeasureHtmlByRestrictions(container, minSize, maxSize); + container.MaxSize = finalSize; + + renderTarget = new RenderTargetBitmap((int)finalSize.Width, (int)finalSize.Height, 96, 96, PixelFormats.Pbgra32); + + // render HTML into the visual + DrawingVisual drawingVisual = new DrawingVisual(); + using (DrawingContext g = drawingVisual.RenderOpen()) + { + container.PerformPaint(g, new Rect(new Size(maxSize.Width > 0 ? maxSize.Width : double.MaxValue, maxSize.Height > 0 ? maxSize.Height : double.MaxValue))); + } + + // render visual into target bitmap + renderTarget.Render(drawingVisual); + } + } + else + { + renderTarget = new RenderTargetBitmap(0, 0, 96, 96, PixelFormats.Pbgra32); + } + + return BitmapFrame.Create(renderTarget); + } + + + #region Private methods + + /// + /// Measure the size of the html by performing layout under the given restrictions. + /// + /// 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 + private static Size MeasureHtmlByRestrictions(HtmlContainer htmlContainer, Size minSize, Size maxSize) + { + // use desktop created graphics to measure the HTML + using (var mg = new GraphicsAdapter()) + { + var sizeInt = HtmlRendererUtils.MeasureHtmlByRestrictions(mg, htmlContainer.HtmlContainerInt, Utils.Convert(minSize), Utils.Convert(maxSize)); + if (maxSize.Width < 1 && sizeInt.Width > 4096) + sizeInt.Width = 4096; + return Utils.ConvertRound(sizeInt); + } + } + + /// + /// Renders the specified HTML source on the specified location and max size restriction.
+ /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// If .Height is zero the html will use all the required height, otherwise it will clip at the + /// given max height not rendering the html below it.
+ /// Clip the graphics so the html will not be rendered outside the max height bound given.
+ /// Returned is the actual width and height of the rendered html.
+ ///
+ /// Device to render with + /// HTML source to render + /// the top-left most location to start render the html at + /// the max size of the rendered html (if height above zero it will be clipped) + /// 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 actual size of the rendered html + private static Size RenderClip(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) + { + if (maxSize.Height > 0) + g.PushClip(new RectangleGeometry(new Rect(location, maxSize))); + + var actualSize = RenderHtml(g, html, location, maxSize, cssData, stylesheetLoad, imageLoad); + + if (maxSize.Height > 0) + g.Pop(); + + return actualSize; + } + + /// + /// Renders the specified HTML source on the specified location and max size restriction.
+ /// If .Width is zero the html will use all the required width, otherwise it will perform line + /// wrap as specified in the html
+ /// If .Height is zero the html will use all the required height, otherwise it will clip at the + /// given max height not rendering the html below it.
+ /// Returned is the actual width and height of the rendered html.
+ ///
+ /// Device to render with + /// HTML source to render + /// the top-left most location to start render the html at + /// the max size of the rendered html (if height above zero it will be clipped) + /// 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 actual size of the rendered html + private static Size RenderHtml(DrawingContext g, string html, Point location, Size maxSize, CssData cssData, EventHandler stylesheetLoad, EventHandler imageLoad) + { + Size actualSize = Size.Empty; + + if (!string.IsNullOrEmpty(html)) + { + using (var container = new HtmlContainer()) + { + container.Location = location; + container.MaxSize = maxSize; + container.AvoidAsyncImagesLoading = true; + container.AvoidImagesLateLoading = true; + + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + + container.SetHtml(html, cssData); + container.PerformLayout(); + container.PerformPaint(g, new Rect(0, 0, double.MaxValue, double.MaxValue)); + + actualSize = container.ActualSize; + } + } + + return actualSize; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/RoutedEventArgs.cs b/Source/HTMLRendererCore.WPF/RoutedEventArgs.cs new file mode 100644 index 000000000..32dd0c65a --- /dev/null +++ b/Source/HTMLRendererCore.WPF/RoutedEventArgs.cs @@ -0,0 +1,62 @@ +// "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.Windows; +using TheArtOfDev.HtmlRenderer.Core.Utils; + +namespace TheArtOfDev.HtmlRenderer.WPF +{ + /// + /// Handler for HTML renderer routed events. + /// + /// the event arguments object + /// the type of the routed events args data + public delegate void RoutedEventHandler(object sender, RoutedEventArgs args) where T : class; + + /// + /// HTML Renderer routed event arguments containing event data. + /// + public sealed class RoutedEventArgs : RoutedEventArgs where T : class + { + /// + /// the argument data of the routed event + /// + private readonly T _data; + + public RoutedEventArgs(RoutedEvent routedEvent, T data) + : base(routedEvent) + { + ArgChecker.AssertArgNotNull(data, "args"); + _data = data; + } + + public RoutedEventArgs(RoutedEvent routedEvent, object source, T data) + : base(routedEvent, source) + { + ArgChecker.AssertArgNotNull(data, "args"); + _data = data; + } + + /// + /// the argument data of the routed event + /// + public T Data + { + get { return _data; } + } + + public override string ToString() + { + return string.Format("RoutedEventArgs({0})", _data); + } + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Utilities/ClipboardHelper.cs b/Source/HTMLRendererCore.WPF/Utilities/ClipboardHelper.cs new file mode 100644 index 000000000..aac84e249 --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Utilities/ClipboardHelper.cs @@ -0,0 +1,255 @@ +// "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.Text; +using System.Windows; + +namespace TheArtOfDev.HtmlRenderer.WPF.Utilities +{ + /// + /// Helper to encode and set HTML fragment to clipboard.
+ /// See http://theartofdev.com/2012/11/11/setting-html-and-plain-text-formatting-to-clipboard/.
+ /// . + ///
+ /// + /// The MIT License (MIT) Copyright (c) 2014 Arthur Teplitzki. + /// + internal static class ClipboardHelper + { + #region Fields and Consts + + /// + /// The string contains index references to other spots in the string, so we need placeholders so we can compute the offsets.
+ /// The _ strings are just placeholders. We'll back-patch them actual values afterwards.
+ /// The string layout () also ensures that it can't appear in the body of the html because the
+ /// character must be escaped.
+ ///
+ private const string Header = @"Version:0.9 +StartHTML:<<<<<<<<1 +EndHTML:<<<<<<<<2 +StartFragment:<<<<<<<<3 +EndFragment:<<<<<<<<4 +StartSelection:<<<<<<<<3 +EndSelection:<<<<<<<<4"; + + /// + /// html comment to point the beginning of html fragment + /// + public const string StartFragment = ""; + + /// + /// html comment to point the end of html fragment + /// + public const string EndFragment = @""; + + /// + /// Used to calculate characters byte count in UTF-8 + /// + private static readonly char[] _byteCount = new char[1]; + + #endregion + + + /// + /// Create with given html and plain-text ready to be used for clipboard or drag and drop.
+ /// Handle missing ]]> tags, specified start\end segments and Unicode characters. + ///
+ /// + /// + /// Windows Clipboard works with UTF-8 Unicode encoding while .NET strings use with UTF-16 so for clipboard to correctly + /// decode Unicode string added to it from .NET we needs to be re-encoded it using UTF-8 encoding. + /// + /// + /// Builds the CF_HTML header correctly for all possible HTMLs
+ /// If given html contains start/end fragments then it will use them in the header: + /// hello world]]> + /// If given html contains html/body tags then it will inject start/end fragments to exclude html/body tags: + /// hello world]]> + /// If given html doesn't contain html/body tags then it will inject the tags and start/end fragments properly: + /// world
]]> + /// In all cases creating a proper CF_HTML header:
+ /// + /// + /// hello world + /// ]]> + /// + /// See format specification here: http://msdn.microsoft.com/library/default.asp?url=/workshop/networking/clipboard/htmlclipboard.asp + /// + /// + /// a html fragment + /// the plain text + public static DataObject CreateDataObject(string html, string plainText) + { + html = html ?? String.Empty; + var htmlFragment = GetHtmlDataString(html); + + // re-encode the string so it will work correctly (fixed in CLR 4.0) + if (Environment.Version.Major < 4 && html.Length != Encoding.UTF8.GetByteCount(html)) + htmlFragment = Encoding.Default.GetString(Encoding.UTF8.GetBytes(htmlFragment)); + + var dataObject = new DataObject(); + dataObject.SetData(DataFormats.Html, htmlFragment); + dataObject.SetData(DataFormats.Text, plainText); + dataObject.SetData(DataFormats.UnicodeText, plainText); + return dataObject; + } + + /// + /// Clears clipboard and sets the given HTML and plain text fragment to the clipboard, providing additional meta-information for HTML.
+ /// See for HTML fragment details.
+ ///
+ /// + /// ClipboardHelper.CopyToClipboard("Hello World", "Hello World"); + /// + /// a html fragment + /// the plain text + public static void CopyToClipboard(string html, string plainText) + { + var dataObject = CreateDataObject(html, plainText); + Clipboard.SetDataObject(dataObject, true); + } + + /// + /// Clears clipboard and sets the given plain text fragment to the clipboard.
+ ///
+ /// the plain text + public static void CopyToClipboard(string plainText) + { + var dataObject = new DataObject(); + dataObject.SetData(DataFormats.Text, plainText); + dataObject.SetData(DataFormats.UnicodeText, plainText); + Clipboard.SetDataObject(dataObject, true); + } + + + #region Private/Protected methods + + /// + /// Generate HTML fragment data string with header that is required for the clipboard. + /// + /// the html to generate for + /// the resulted string + private static string GetHtmlDataString(string html) + { + var sb = new StringBuilder(); + sb.AppendLine(Header); + sb.AppendLine(@""); + + // if given html already provided the fragments we won't add them + int fragmentStart, fragmentEnd; + int fragmentStartIdx = html.IndexOf(StartFragment, StringComparison.OrdinalIgnoreCase); + int fragmentEndIdx = html.LastIndexOf(EndFragment, StringComparison.OrdinalIgnoreCase); + + // if html tag is missing add it surrounding the given html (critical) + int htmlOpenIdx = html.IndexOf(" -1 ? html.IndexOf('>', htmlOpenIdx) + 1 : -1; + int htmlCloseIdx = html.LastIndexOf(" -1 ? html.IndexOf('>', bodyOpenIdx) + 1 : -1; + + if (htmlOpenEndIdx < 0 && bodyOpenEndIdx < 0) + { + // the given html doesn't contain html or body tags so we need to add them and place start/end fragments around the given html only + sb.Append(""); + sb.Append(StartFragment); + fragmentStart = GetByteCount(sb); + sb.Append(html); + fragmentEnd = GetByteCount(sb); + sb.Append(EndFragment); + sb.Append(""); + } + else + { + // insert start/end fragments in the proper place (related to html/body tags if exists) so the paste will work correctly + int bodyCloseIdx = html.LastIndexOf(""); + else + sb.Append(html, 0, htmlOpenEndIdx); + + if (bodyOpenEndIdx > -1) + sb.Append(html, htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0, bodyOpenEndIdx - (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0)); + + sb.Append(StartFragment); + fragmentStart = GetByteCount(sb); + + var innerHtmlStart = bodyOpenEndIdx > -1 ? bodyOpenEndIdx : (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0); + var innerHtmlEnd = bodyCloseIdx > -1 ? bodyCloseIdx : (htmlCloseIdx > -1 ? htmlCloseIdx : html.Length); + sb.Append(html, innerHtmlStart, innerHtmlEnd - innerHtmlStart); + + fragmentEnd = GetByteCount(sb); + sb.Append(EndFragment); + + if (innerHtmlEnd < html.Length) + sb.Append(html, innerHtmlEnd, html.Length - innerHtmlEnd); + + if (htmlCloseIdx < 0) + sb.Append(""); + } + } + else + { + // handle html with existing start\end fragments just need to calculate the correct bytes offset (surround with html tag if missing) + if (htmlOpenEndIdx < 0) + sb.Append(""); + int start = GetByteCount(sb); + sb.Append(html); + fragmentStart = start + GetByteCount(sb, start, start + fragmentStartIdx) + StartFragment.Length; + fragmentEnd = start + GetByteCount(sb, start, start + fragmentEndIdx); + if (htmlCloseIdx < 0) + sb.Append(""); + } + + // Back-patch offsets (scan only the header part for performance) + sb.Replace("<<<<<<<<1", Header.Length.ToString("D9"), 0, Header.Length); + sb.Replace("<<<<<<<<2", GetByteCount(sb).ToString("D9"), 0, Header.Length); + sb.Replace("<<<<<<<<3", fragmentStart.ToString("D9"), 0, Header.Length); + sb.Replace("<<<<<<<<4", fragmentEnd.ToString("D9"), 0, Header.Length); + + return sb.ToString(); + } + + /// + /// Calculates the number of bytes produced by encoding the string in the string builder in UTF-8 and not .NET default string encoding. + /// + /// the string builder to count its string + /// optional: the start index to calculate from (default - start of string) + /// optional: the end index to calculate to (default - end of string) + /// the number of bytes required to encode the string in UTF-8 + private static int GetByteCount(StringBuilder sb, int start = 0, int end = -1) + { + int count = 0; + end = end > -1 ? end : sb.Length; + for (int i = start; i < end; i++) + { + _byteCount[0] = sb[i]; + count += Encoding.UTF8.GetByteCount(_byteCount); + } + return count; + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HTMLRendererCore.WPF/Utilities/Utils.cs b/Source/HTMLRendererCore.WPF/Utilities/Utils.cs new file mode 100644 index 000000000..9cb755f9d --- /dev/null +++ b/Source/HTMLRendererCore.WPF/Utilities/Utils.cs @@ -0,0 +1,151 @@ +// "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.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.WPF.Utilities +{ + /// + /// Utilities for converting WPF entities to HtmlRenderer core entities. + /// + internal static class Utils + { + /// + /// Convert from WPF point to core point. + /// + public static RPoint Convert(Point p) + { + return new RPoint(p.X, p.Y); + } + + /// + /// Convert from WPF point to core point. + /// + public static Point[] Convert(RPoint[] points) + { + Point[] myPoints = new Point[points.Length]; + for (int i = 0; i < points.Length; i++) + myPoints[i] = Convert(points[i]); + return myPoints; + } + + /// + /// Convert from core point to WPF point. + /// + public static Point Convert(RPoint p) + { + return new Point(p.X, p.Y); + } + + /// + /// Convert from core point to WPF point. + /// + public static Point ConvertRound(RPoint p) + { + return new Point((int)p.X, (int)p.Y); + } + + /// + /// Convert from WPF size to core size. + /// + public static RSize Convert(Size s) + { + return new RSize(s.Width, s.Height); + } + + /// + /// Convert from core size to WPF size. + /// + public static Size Convert(RSize s) + { + return new Size(s.Width, s.Height); + } + + /// + /// Convert from core point to WPF point. + /// + public static Size ConvertRound(RSize s) + { + return new Size((int)s.Width, (int)s.Height); + } + + /// + /// Convert from WPF rectangle to core rectangle. + /// + public static RRect Convert(Rect r) + { + return new RRect(r.X, r.Y, r.Width, r.Height); + } + + /// + /// Convert from core rectangle to WPF rectangle. + /// + public static Rect Convert(RRect r) + { + return new Rect(r.X, r.Y, r.Width, r.Height); + } + + /// + /// Convert from core rectangle to WPF rectangle. + /// + public static Rect ConvertRound(RRect r) + { + return new Rect((int)r.X, (int)r.Y, (int)r.Width, (int)r.Height); + } + + /// + /// Convert from WPF color to core color. + /// + public static RColor Convert(Color c) + { + return RColor.FromArgb(c.A, c.R, c.G, c.B); + } + + /// + /// Convert from core color to WPF color. + /// + public static Color Convert(RColor c) + { + return Color.FromArgb(c.A, c.R, c.G, c.B); + } + + /// + /// Get encoder to be used for encoding bitmap frame by given file extension.
+ /// Default is PNG encoder. + ///
+ /// the file extension to select encoder by + /// encoder instance + public static BitmapEncoder GetBitmapEncoder(string ext) + { + switch (ext.ToLower()) + { + case ".jpg": + case ".jpeg": + return new JpegBitmapEncoder(); + case ".bmp": + return new BmpBitmapEncoder(); + case ".tif": + case ".tiff": + return new TiffBitmapEncoder(); + case ".gif": + return new GifBitmapEncoder(); + case ".wmp": + return new WmpBitmapEncoder(); + default: + return new PngBitmapEncoder(); + } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.sln b/Source/HtmlRenderer.sln index cf7aaa634..39b9806e0 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.31424.327 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}") = "HtmlRendererCore", "HtmlRendererCore\HtmlRendererCore.csproj", "{7B17721F-E938-48D2-A7BB-2A489510D52E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTMLRendererCore.WPF", "HTMLRendererCore.WPF\HTMLRendererCore.WPF.csproj", "{F55B7081-FAE0-4100-BBE9-D36DD298732C}" +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 + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Debug|x86.Build.0 = Debug|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|Any CPU.Build.0 = Release|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|x86.ActiveCfg = Release|Any CPU + {7B17721F-E938-48D2-A7BB-2A489510D52E}.Release|x86.Build.0 = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Debug|x86.Build.0 = Debug|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Release|Any CPU.Build.0 = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.Release|x86.ActiveCfg = Release|Any CPU + {F55B7081-FAE0-4100-BBE9-D36DD298732C}.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 = {BA93753D-09BB-4D10-88D6-DDA1FB5ADB3E} + EndGlobalSection EndGlobal diff --git a/Source/HtmlRendererCore/Adapters/Entities/RColor.cs b/Source/HtmlRendererCore/Adapters/Entities/RColor.cs new file mode 100644 index 000000000..83fb26a12 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RDashStyle.cs b/Source/HtmlRendererCore/Adapters/Entities/RDashStyle.cs new file mode 100644 index 000000000..d569303eb --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RFontStyle.cs b/Source/HtmlRendererCore/Adapters/Entities/RFontStyle.cs new file mode 100644 index 000000000..f2c62b29d --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RKeyEvent.cs b/Source/HtmlRendererCore/Adapters/Entities/RKeyEvent.cs new file mode 100644 index 000000000..3f8f49a68 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RMouseEvent.cs b/Source/HtmlRendererCore/Adapters/Entities/RMouseEvent.cs new file mode 100644 index 000000000..e0aa88d19 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RPoint.cs b/Source/HtmlRendererCore/Adapters/Entities/RPoint.cs new file mode 100644 index 000000000..eb36c8b47 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RRect.cs b/Source/HtmlRendererCore/Adapters/Entities/RRect.cs new file mode 100644 index 000000000..3ae148655 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/Entities/RSize.cs b/Source/HtmlRendererCore/Adapters/Entities/RSize.cs new file mode 100644 index 000000000..521bce308 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RAdapter.cs b/Source/HtmlRendererCore/Adapters/RAdapter.cs new file mode 100644 index 000000000..6b13459c1 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RBrush.cs b/Source/HtmlRendererCore/Adapters/RBrush.cs new file mode 100644 index 000000000..439eb4e7b --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RContextMenu.cs b/Source/HtmlRendererCore/Adapters/RContextMenu.cs new file mode 100644 index 000000000..53c61f36c --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RControl.cs b/Source/HtmlRendererCore/Adapters/RControl.cs new file mode 100644 index 000000000..40acdeeaa --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RFont.cs b/Source/HtmlRendererCore/Adapters/RFont.cs new file mode 100644 index 000000000..43f171d80 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RFontFamily.cs b/Source/HtmlRendererCore/Adapters/RFontFamily.cs new file mode 100644 index 000000000..724d9d96f --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RGraphics.cs b/Source/HtmlRendererCore/Adapters/RGraphics.cs new file mode 100644 index 000000000..af54f2ae4 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RGraphicsPath.cs b/Source/HtmlRendererCore/Adapters/RGraphicsPath.cs new file mode 100644 index 000000000..21c86bc1a --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RImage.cs b/Source/HtmlRendererCore/Adapters/RImage.cs new file mode 100644 index 000000000..6c184e2d1 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Adapters/RPen.cs b/Source/HtmlRendererCore/Adapters/RPen.cs new file mode 100644 index 000000000..5804ab895 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/CssData.cs b/Source/HtmlRendererCore/Core/CssData.cs new file mode 100644 index 000000000..f0e4a8106 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/CssDefaults.cs b/Source/HtmlRendererCore/Core/CssDefaults.cs new file mode 100644 index 000000000..bb313c223 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/Border.cs b/Source/HtmlRendererCore/Core/Dom/Border.cs new file mode 100644 index 000000000..c4ee107eb --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssBox.cs b/Source/HtmlRendererCore/Core/Dom/CssBox.cs new file mode 100644 index 000000000..b2cd3d984 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssBoxFrame.cs b/Source/HtmlRendererCore/Core/Dom/CssBoxFrame.cs new file mode 100644 index 000000000..7bb6ed57f --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssBoxHr.cs b/Source/HtmlRendererCore/Core/Dom/CssBoxHr.cs new file mode 100644 index 000000000..8280f47c3 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssBoxImage.cs b/Source/HtmlRendererCore/Core/Dom/CssBoxImage.cs new file mode 100644 index 000000000..a33627c83 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssBoxProperties.cs b/Source/HtmlRendererCore/Core/Dom/CssBoxProperties.cs new file mode 100644 index 000000000..7abfb53ed --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssLayoutEngine.cs b/Source/HtmlRendererCore/Core/Dom/CssLayoutEngine.cs new file mode 100644 index 000000000..a24f9e708 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssLayoutEngineTable.cs b/Source/HtmlRendererCore/Core/Dom/CssLayoutEngineTable.cs new file mode 100644 index 000000000..79627161a --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssLength.cs b/Source/HtmlRendererCore/Core/Dom/CssLength.cs new file mode 100644 index 000000000..6b7eebf20 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssLineBox.cs b/Source/HtmlRendererCore/Core/Dom/CssLineBox.cs new file mode 100644 index 000000000..dd2db8925 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssRect.cs b/Source/HtmlRendererCore/Core/Dom/CssRect.cs new file mode 100644 index 000000000..d7ff14ac1 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssRectImage.cs b/Source/HtmlRendererCore/Core/Dom/CssRectImage.cs new file mode 100644 index 000000000..c2f53800f --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssRectWord.cs b/Source/HtmlRendererCore/Core/Dom/CssRectWord.cs new file mode 100644 index 000000000..cc5499fad --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssSpacingBox.cs b/Source/HtmlRendererCore/Core/Dom/CssSpacingBox.cs new file mode 100644 index 000000000..0ea65d99b --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/CssUnit.cs b/Source/HtmlRendererCore/Core/Dom/CssUnit.cs new file mode 100644 index 000000000..269893e8d --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/HoverBoxBlock.cs b/Source/HtmlRendererCore/Core/Dom/HoverBoxBlock.cs new file mode 100644 index 000000000..1f919ec66 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Dom/HtmlTag.cs b/Source/HtmlRendererCore/Core/Dom/HtmlTag.cs new file mode 100644 index 000000000..ca282fc29 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/CssBlock.cs b/Source/HtmlRendererCore/Core/Entities/CssBlock.cs new file mode 100644 index 000000000..2982b3159 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/CssBlockSelectorItem.cs b/Source/HtmlRendererCore/Core/Entities/CssBlockSelectorItem.cs new file mode 100644 index 000000000..76ad35919 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlGenerationStyle.cs b/Source/HtmlRendererCore/Core/Entities/HtmlGenerationStyle.cs new file mode 100644 index 000000000..1d9225a69 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlImageLoadEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlImageLoadEventArgs.cs new file mode 100644 index 000000000..5d5a4c5af --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlLinkClickedEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlLinkClickedEventArgs.cs new file mode 100644 index 000000000..f39c70a44 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlLinkClickedException.cs b/Source/HtmlRendererCore/Core/Entities/HtmlLinkClickedException.cs new file mode 100644 index 000000000..eb04c6eb1 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlRefreshEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlRefreshEventArgs.cs new file mode 100644 index 000000000..7ab630ccc --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlRenderErrorEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlRenderErrorEventArgs.cs new file mode 100644 index 000000000..4696ed3b9 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlRenderErrorType.cs b/Source/HtmlRendererCore/Core/Entities/HtmlRenderErrorType.cs new file mode 100644 index 000000000..681d90be1 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlScrollEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlScrollEventArgs.cs new file mode 100644 index 000000000..c43f96c92 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/HtmlStylesheetLoadEventArgs.cs b/Source/HtmlRendererCore/Core/Entities/HtmlStylesheetLoadEventArgs.cs new file mode 100644 index 000000000..66d520583 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Entities/LinkElementData.cs b/Source/HtmlRendererCore/Core/Entities/LinkElementData.cs new file mode 100644 index 000000000..5f10399ed --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/BackgroundImageDrawHandler.cs b/Source/HtmlRendererCore/Core/Handlers/BackgroundImageDrawHandler.cs new file mode 100644 index 000000000..275f23320 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/BordersDrawHandler.cs b/Source/HtmlRendererCore/Core/Handlers/BordersDrawHandler.cs new file mode 100644 index 000000000..b20c331cb --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/ContextMenuHandler.cs b/Source/HtmlRendererCore/Core/Handlers/ContextMenuHandler.cs new file mode 100644 index 000000000..051884ed6 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/FontsHandler.cs b/Source/HtmlRendererCore/Core/Handlers/FontsHandler.cs new file mode 100644 index 000000000..83c037eac --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/ImageDownloader.cs b/Source/HtmlRendererCore/Core/Handlers/ImageDownloader.cs new file mode 100644 index 000000000..2f8b0f5e6 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/ImageLoadHandler.cs b/Source/HtmlRendererCore/Core/Handlers/ImageLoadHandler.cs new file mode 100644 index 000000000..eb9ace4fd --- /dev/null +++ b/Source/HtmlRendererCore/Core/Handlers/ImageLoadHandler.cs @@ -0,0 +1,393 @@ +// "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 + { + 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) + { + 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) + { + _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/HtmlRendererCore/Core/Handlers/SelectionHandler.cs b/Source/HtmlRendererCore/Core/Handlers/SelectionHandler.cs new file mode 100644 index 000000000..9913a9c5d --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Handlers/StylesheetLoadHandler.cs b/Source/HtmlRendererCore/Core/Handlers/StylesheetLoadHandler.cs new file mode 100644 index 000000000..719f3cebb --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/HtmlContainerInt.cs b/Source/HtmlRendererCore/Core/HtmlContainerInt.cs new file mode 100644 index 000000000..1d71de9fb --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/HtmlRendererUtils.cs b/Source/HtmlRendererCore/Core/HtmlRendererUtils.cs new file mode 100644 index 000000000..9a414a7fe --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Parse/CssParser.cs b/Source/HtmlRendererCore/Core/Parse/CssParser.cs new file mode 100644 index 000000000..640152854 --- /dev/null +++ b/Source/HtmlRendererCore/Core/Parse/CssParser.cs @@ -0,0 +1,963 @@ +// "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 < propValue.Length) { + while(start < propValue.Length && (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/HtmlRendererCore/Core/Parse/CssValueParser.cs b/Source/HtmlRendererCore/Core/Parse/CssValueParser.cs new file mode 100644 index 000000000..b7c721b28 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Parse/DomParser.cs b/Source/HtmlRendererCore/Core/Parse/DomParser.cs new file mode 100644 index 000000000..d841485f0 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Parse/RegexParserHelper.cs b/Source/HtmlRendererCore/Core/Parse/RegexParserHelper.cs new file mode 100644 index 000000000..86a690b50 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Parse/RegexParserUtils.cs b/Source/HtmlRendererCore/Core/Parse/RegexParserUtils.cs new file mode 100644 index 000000000..806fca4cf --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/ArgChecker.cs b/Source/HtmlRendererCore/Core/Utils/ArgChecker.cs new file mode 100644 index 000000000..1b8b3e72d --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/CommonUtils.cs b/Source/HtmlRendererCore/Core/Utils/CommonUtils.cs new file mode 100644 index 000000000..12c03ad32 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/CssConstants.cs b/Source/HtmlRendererCore/Core/Utils/CssConstants.cs new file mode 100644 index 000000000..655849256 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/CssUtils.cs b/Source/HtmlRendererCore/Core/Utils/CssUtils.cs new file mode 100644 index 000000000..69f949390 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/DomUtils.cs b/Source/HtmlRendererCore/Core/Utils/DomUtils.cs new file mode 100644 index 000000000..6d00a4abc --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/HtmlConstants.cs b/Source/HtmlRendererCore/Core/Utils/HtmlConstants.cs new file mode 100644 index 000000000..834cbe540 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/HtmlUtils.cs b/Source/HtmlRendererCore/Core/Utils/HtmlUtils.cs new file mode 100644 index 000000000..960b719aa --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/Core/Utils/ImageError.png b/Source/HtmlRendererCore/Core/Utils/ImageError.png new file mode 100644 index 0000000000000000000000000000000000000000..46b6c15fb29fd4057e9c195748ed390be513376c GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*bK+V@cm~ldWhJyShH1A{L`3xPIqc)B=-SorUq6rI;>AmAG5HT_V~rYU;8 zvLULufz4Bdmhzjb{$QM;l8~1klg0hCyY9;@0GGDiw-{cz8YRGR>{!{j~I&5kv56-^H5iWO8S}I{}y7kyjd-e|>f6lveBuAjOzEPG<+*|GW zw0q_}wG8vkvU4*2?0;SH`^MwXAN(HMJFGIe$-VRDMYe*MpZ?1Ie|!34{%^Jo>Z=vC T`u(JUA;;k9>gTe~DWM4ftkA6h literal 0 HcmV?d00001 diff --git a/Source/HtmlRendererCore/Core/Utils/ImageLoad.png b/Source/HtmlRendererCore/Core/Utils/ImageLoad.png new file mode 100644 index 0000000000000000000000000000000000000000..2c132c67974fac0ede093625cd097166297b471c GIT binary patch literal 414 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6T7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!Gca%qfiUBxyLEqng6t)pzOL+7Ik>p=B#wy9Fa-)pmAFQf1m~xf zlqVLYG6W=M=9TFAxrQi|8S9zq85$UTDOw0r^~=-6F+?MH>V&(#haCi3V$~+JTwqq= zb@0%((Eag7Oecqn1P7Z4P)U&kg(h_Dx=E{l2b$%QAh61Ufk86srmH zf0X+d{Y9+d@I$VQs~Ty1Y%H^%vg|l!b?vKk*lTy4X~vhIeU*HFm_eSYX_be?%=GT) zH}>CWEIWDR|KGzh%}(JAyVqayPurk3mGyRVuczV>Z>QAmN(RAbRGYk*wl@1_W*-0KVPz0Emzf12EJLCs}J?5tK-S6si5VgAnf z + /// 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/HtmlRendererCore/Core/Utils/SubString.cs b/Source/HtmlRendererCore/Core/Utils/SubString.cs new file mode 100644 index 000000000..f06f740f2 --- /dev/null +++ b/Source/HtmlRendererCore/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/HtmlRendererCore/HtmlRendererCore.csproj b/Source/HtmlRendererCore/HtmlRendererCore.csproj new file mode 100644 index 000000000..5b0108d94 --- /dev/null +++ b/Source/HtmlRendererCore/HtmlRendererCore.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + From a8d3ee48bde06a50e8fa3f4f43341214de9c7c97 Mon Sep 17 00:00:00 2001 From: ManniAT Date: Sat, 10 Jul 2021 12:49:28 +0200 Subject: [PATCH 2/2] Updated Readme.MD --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5a21ca388..c5fd7769c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ HTML Renderer [![Build status](https://ci.appveyor.com/api/projects/status/cm8xpf8ebt3hyi3e)](https://ci.appveyor.com/project/ArthurHub/html-renderer) ============= +## .net 5 version of HtmlRendererCore and HtmlRendererCore.WPF added +* I didn't add nuget build targets +* included fix for https://github.com/ArthurHub/HTML-Renderer/issues/94 ## Help Wanted * Looking for a contributor(s) to take this project forward as I'm unable to continue supporting it.