A lot of things has changed in the past year and it seems more is going to change in the next, unfortunately those changes are not aligned with the work required for HTML Renderer. That's why it took so long for v1.5 release to finally be ready and the reason not all features I originally planned made it.
With a heavy heart I'm going to hold all significant work on HTML Renderer until either my career path brings me back to .NET, I have more free time to work on it or the project will get more attentions. In the meanwhile I will continue to support the library with small fixes and answering question on the discussions/issues pages.
The goal of this version is to break the confinement of WinForms UI framework and allow the HTML Renderer to be used across all .NET supported platforms/frameworks1:
- WinForms – done.
- WPF – done.
- Mono – done.
- PdfSharp – done.
- MigraDoc, iTextSharp or others – couple days work.
- Silverlight – couple of days work.
- Metro – probably no more than a week work.
- Xamarin – hard to say but I think it is possible in 2-3 weeks.
- Unity, XNA – no idea, is it interesting?
The important part is not the frameworks already supported in v1.5 but the extensibility model baked into the core of HTML Renderer that allows reusing 90% of the code when adding support to more frameworks without effecting the already supported frameworks.
This update has a few breaking changes that were required to effectively move forward with the library.
The required adjustments are not significant but it is important to note that existing projects will not compile after upgrading to v1.5.
- Two assembly's instead of one.
- Changing all values to doubles.
- Object model changes to expose universal and not framework specific entities.
- Object model namespaces changes as part of assemblies separation.
Separating Core library
There are a few options available to separate core HTML Renderer code from framework specific code
- Conditional compilation
- Linked reference files
- Separated assemblies
The main difference between going with the first two and the separated assembly approach is simplicity to HTML Renderer developer vs. simplicity to the client developer. I decided that the price to the HTML Renderer developer was much higher than the price for the client developer and didn't want to see HTML Renderer development stall because of it. Separating the library into core assembly and framework specific assemblies allows clear separation between generic and framework specific code, preventing accidental usage of framework specific classes.
The core assembly has minimal dependencies, currently it's only dependency is "System" and the goal is to make it universal portable class library allowing it to run on frameworks like Windows Phone, Xamarin, Unity, etc. This will restrict the core even more than the current implementation and will require extending the adapters implementation to cover more parts of the library like resources (stylesheets/images) downloading and caching.
Adapters extensibility model
The create the cross-framework core library I had to remove all framework specific references and to replace them with custom classes. Those can be separated into two categories: basic entities and functional objects.
Basic entities, like Point, Size, Color, Mouse event, etc. do not have any functionality or complex state, therefore can be replaced by matching internal entities used in the core library, appropriately named RPoint, Rsize, Rcolor, RMouseEvent, etc. it is left to the specific framework libraries to convert between the external and internal entities so library client will not be aware of the internal, framework agnostic, entities.
Function objects, like Font, Image, Brush, Control, Graphics, etc. expose functionality themselves or concrete, framework specific, instance are required for other functional object to do its work. Therefore, framework specific functional objects are wrapped in framework agnostic adapters that the core library can use to execute functionality (measuring string width, font size, showing context menu, etc.) and the adapter class delegates it to the proper framework specific execution code.
Currently there are 10 adapters:
The main adapter, provide the global access point to framework specific functionality and caching. It doesn't wrap any framework specific functional object, passed in the constructor of the HtmlContainer it is available during all stages of html parsing, layout and rendering. Provides access to colors, brushes, fonts, images, clipboard and files functionality.
The rendering adapter, wraps framework specific graphics object (Graphics in WinForms, DrawingContext in WPF). Provides draw functionality to draw shapes and text.
UI control element adapter, wraps framework specific control object. Provide active UI elemetn functionality for the ability to handle keyboard, mouse, refresh, re-layout, set cursor, etc.
The rest of the adapters: RBrush, RPen, RImage, RFont, RFontFamily, RGraphicsPath and RContextMenu are simpler and less interesting, mostly wrap framework specific object to either delegate action to it or provide it when required in one of the core adapters above.
Unbelievably heavy, complex and unfriendly UI framework! I had numerous issues with clipping, blurry text rendering, fonts (typefaces), blurry line rendering and pixel snapping in general. I'm not sure that I handled those issues correctly or even if proper solution even exists, extremely frustrating experience all-in-all.
With the rant out of the way, I did managed to work out most of the issues to provide fully supported native WPF rendering library.
The obvious way to render text programmatically in WPF is to use FormattedText class that provides rich API for layout, alignment, fonts, etc. unfortunately it is not designed for rendering large amount of small strings, the performance is just unacceptable.
I have used a more low-level approach using GlyphRun class, that is used under the hood by the FormattedText class without many of the nice features it provides. But low-level has its price, I had to use the lower level GlyphTypeface class and handle scenarios where it is not available, manually map characters to glyph indexes and, again, handle scenario where simple mapping is not available or invalid, manually calculate each glyph width for text rectangle measurement, and I'm pretty sure this approach losses support for text kerning, fortunately it doesn't seems to be very significant visually.
The end result though is decent in my opinion, the time performance is roughly the same as native GDI text rendering and almost as sharp as it.
From all the annoyances of WPF this is by far the most frustrating. The core issue is straightforward, rendering 2.5 pixel width line or text at location (4.75, 2.2) won't work well on monitors as there is no way to light half pixel. The obvious solution is to round to the nearest pixel, unfortunately WPF, as GDI+, really wants to be device independent, which means that rendered object size and location won't change between devices with different resolution and pixel densities like monitor and printer. The result: unbelievably fuzzy/blurry rendering!
WPF tried to solve this issue by providing flags to favor sharp rendering over device independent rendering using SnapsToDevicePixels, UseLayoutRounding and TextFormattingMode unfortunately they either to high-level, not compatible to 3.0, 3.5 frameworks or just don't work as expected.
In the end I did multiple layers of hacks and pixel rounding to provide as sharp rendering as possible.
Execution time performance is in the same area of WinForms: average less than 10 millisecond for sample HTML rendering2, though you can see difference on specific hardware.
Memory, on the other hand, is an issue. Because everything in WPF requires multiple object allocation without any (as far as I know) way to reuse or recycle, the average sample HTML rendering memory allocation is x4 times that of WinForms. The higher memory usage may effect performance via higher GC pressure though I didn't notice any in my tests.
The nice thing about WPF is its ability to compose UI elements, so WPF Tooltip can embed HtmlLabel in it creating the same tooltip control that required special work in WinForms. For that reason I didn't see significant benefits in creating dedicated HtmlTooltip control, the usage example of HtmlLabel as content of WPF tooltip can be seen in WPF demo project.
Basically, Mono was always supported by HTML Renderer as it uses standard .NET code. It changed when I provided the ability to use GDI text rendering via P/Invoke and made it the default setting, HTML Renderer would not work out-of-the-box as the developer needed to manually to change the rendering method to GDI+.
To make things simpler I added conditional compilation that omits GDI text rendering when building with MONO flag, this way referencing mono targeted library will work out-of-the-box and there is no way to use unsupported code.
I have used PdfSharp library that is based on WinForms framework to simplify the work and provide the currently best rendering framework. As PdfSharp graphics closely match that of WinForms it was relatively straightforward to create the library, fortunately PDF document generation doesn't require any controls.
Major missing feature, one of the features I wish I had more time to work on, is paging support. Without it, it is possible for a line of text to be split between two pages, half at the end of the page and the other half at the begging of the next page.
I have decided to leave it as is for now, possibly for the community to fix or if the library will have usages I will invest the time in the future.
There are 4 new NuGet packages added to the family:
All packages, including the previously existing HtmlRenderer.WinForms, depends on platform independent HtmlRenderer.Core package. That, also, can be referenced independently to add custom platform support.
For 1.4 I used MSBuild XML script that proved to be hard to write and maintain, moving to 1.5 it was even more complex as I needed to handle multiple frameworks, demo applications and NuGet packages.
So for 1.5 I decided to go with batch file that executes MSBuild exe to build each framework release for each supported framework, that's more than 20 builds in the matrix!
After the builds the batch scripts continues to create NuGet packages, download the source code from GitHub and then ZIPs everything for CodePlex download page.
The batch file approach proved, so far, to be simpler and more straightforward than MSBuild script though it sure feels like a hack. I'm not a fan of playing with build stuff so if anyone knows of a better solution please come forward.
As with 1.4 I want to provide assembly for each supported framework and each version of .NET, including support for client profile for .NET 3.5 and 4.0. And as with 1.4 I don't want to support multiple project files as it adds overhead to handling the source files for each project separately.
My solution to this issue was to use single project file and specify the targeted framework in MSBuild command, this approach works perfectly for all projects except WPF as .NET 4.0 added "System.Xaml" reference, I solved it by making the reference conditional in the ".csproj" file:
<Reference Include="System.Xaml" Condition=" '$(TargetFrameworkVersion)' == 'v4.0' Or '$(TargetFrameworkVersion)' == 'v4.5'" />
Thought it created a problem when changing the targeted framework to 4.0 and then back to 3.0, so if this happens the conditional reference needs to be added back manually.
With Mono I didn't want to duplicate the code between WinForms and Mono frameworks, so I used conditional compilations removing the features not supported by Mono framework. The demo application, on the other hand, checks at runtime if it runs on Mono to disable unsupported features.
I wanted the demo applications, WinForms and WPF, to remain a single executable despite the fact I created a common assembly for demo and the 3 assemblies required for HTML Renderer (Core, WinForms, WPF).
For 1.4 I have used ILMerge to combine referenced assembly into the demo executable, unfortunately it doesn't work well for WPF and has other issues.
Therefore I wanted to use the technique described here but have it done automatically so I won't have to add the assemblies manually each time. Fortunately I found this absolutely awesome solution to embed the reference assemblies automatically via the ".csproj" file build script.
All in all I have a relatively simple build script that work most of the time creating everything I need to release a new version on CodePlex and NuGet.
For the long version see my blog post.
- Multi-framework support
- Native – no WinForms interop or hacks
- Native WPF controls (HtmlPanel, HtmlLabel)
- Feature parity with WinForms
- Dependency property and routed events
- Excellent performance
- Subset to WinForms HtmlRenderer
- GdiPlus text rendering only
- Text selection
- Links and anchors
- Extensibility support for other frameworks using Core library
- Metro (Win8/Windows RT/Windows Phone)
- Xamarin (iOS/Android)
- Split library into core assembly and additional assembly for each framework
- Core – dependency only on System
- Object model minor changes and namespace changes
- Improve WinForms demo to show image and PDF generation
- WinForms demo build that work on Mono
- Full WPF demo application
- Change build script to batch file
- Added clear current selection method
- Added LoadComplete event on when set html if fully parsed and layouted
- Fix table center/right alignment
- Fix sub, sup style text handling.
- Fix possible infinite loop in CorrectRelativeUrls
- Improve image downloading handling
- Fix collision of downloading the same image
- Don't cache failed or partial image downloads responses
- Synchronization issues
- Padding support on HtmlLabel and HtmlPanel controls
- URL attributes with double quotes are not processed correctly (thx Richard)
- Fix html clearing not resetting ActualSize
- Height calculation trimming last pixel
- Fix actual width calculation when location.X is set
- Fix WinForms horizonal scroll content clipping
- Fix high CPU usage while holding mouse down (thx Ryan)
- Handle possibility of no requested style exists for the font, use regular then
- Fix table cell width calculation too small when using multiple blocks with whitespaces
- Fix styles parsing for html and * wildcard
- Removed default body style (10pt Tahoma)
- I'm not sure the proper terminology to use to distinguish WinForms, WPF, Mono, PdfSharp, Metro and Xamarin as not all can be described as different platforms, I decided to use the more neutral term framework. ↩
- Machine dependent of course, but the results matched on multiple modern machines running different windows versions. ↩