Remove System.Drawing.Common dependency
See original GitHub issuetl;dr; It’s a quagmire. The issues is font measurement. Options:
- Either dump net40/net46 target and use SixLabors.Fonts
- Use SkiaSharp wrapper (no net40, need to upgraed to net462) over native Skia library (with all baggage of various packages for OS/arch and no Blazor)
- Combine SixLabors.Fonts measurement for netstandard2.0 and System.Drawing.Common for net40/net46 (yay for two choices of one feature that need pixel perfect precision that must work with tests).
- Use simplified DIY font measurement mechanism that uses glyph typographic information (bad idea ™)
Option 3. is seems like the best choice. I made an attempt and it seems to work reasonably well. Tests already ignore dimensions for linux (default Calibri Windows fonts are not available), will use some percentage for allowed difference (e.g. 1-3%).
As for images, library is an option. There are several libraries, but we only need such a limiting subset that it might be better to just parse dimension information from images in ClosedXML directly (width, heigh, dpi).
Problem
ClosedXML is using System.Drawing for working with XLPicture. System.Drawing.Common
is dead and buried, since NET6, it only works on Windows, leaving other platforms, from Linux through Android to Blazor incapable of working with images. It is a source of several reported issues.
System.Drawing.Common usage
ClosedXML is using System.Drawing.Common
- to determine format and size of images (
Image.FromStream()
)- XLPictureFormat - for storing content type of Image part (15.2.14) that represents the image in workbook.
- Width and height - for storing extent of the image and its position (20.5.2.30, 19.3.1.53) in the drawing part.
- Measuring bounding box of a text
Font
- read the specification of fontFontStyle
- style of how to render font- MeasureString to autosize content
- DpiX, DpiY taken from GraphicsUtils to determine Dpi and measure stuff. This seems wrong and should be an part of image specification/option. Determining EMU from DPI of screen is not ideal
- Translate colors from HTML to standard Color (
ColorTranslator.FromHtml
)
Research
There are several ways to solve this issue:
-
Direct replacement through one of libraries recommended by Microsoft: ImageSharp, SkiaSharp, Windows Imaging Components, Microsoft.Maui.Graphics
-
Some other library found on nuget (search image or font and anything more than 1mil downloads or a devblogs post)
- ImageProcessor - dead/retired, readme says use ImageSharp.
- ImageResizer - not fit for purpose, no font resizing, net452 only
- Magick.NET - wrapper over a version of ImageMagick (native code, compiled to many platforms)
- SixLabors.Fonts - a new library for font manipulation, kept for further consideration.
- Splat https://github.com/reactiveui/splat not suitable for loading/saving, not font capability.
- https://www.nuget.org/packages/MetadataExtractor/ - no font
-
Font libraries
- https://github.com/LayoutFarm/Typography
- https://github.com/Robmaister/SharpFont wrapper for native
-
Don’t replace, just remove it and implement what is necessary.
How are images stores in xlsx?
Images are represented as a Drawings Part (12.3.8) of OOXML. It has several properties, the most interesting one is xfrm (19.3.1.53) which specifies dimensions (among other things) in the worksheet. Dimenstions of an image in worksheet are specified in English Metric Units(EMU, 20.1.2.1), not in pixels.
Expected format: Per 15.2.14, jpeg or png should be used, through DocumentFormat.OpenXml defines several others.
A producer that wants interoperability should use one of the following standard formats:
- image/png ISO/IEC 15948:2003, http://www.libpng.org/pub/png/spec/
- image/jpeg, http://www.w3.org/Graphics/JPEG
Prior art
- A fork of ClosedXML by @stesee that has replaced System.Drawing.Common with SkiaSharp https://github.com/stesee/ClosedXML. The fork just uses 12pt font for measuring everything = not suitable for merge.
- A PR that has adds methods to independent of Bitmap https://github.com/ClosedXML/ClosedXML/pull/1672
Considerations
- ClosedXMl doesn’t actually need to read image. It just needs a type, dimenstions of the picture, and its data stream.
- Specification doesn’t say what images to support. That is left up to the us, but DocumentFormat.OpenXml.dll has an enum.
- Blazor can’t use native code and is WASM is downloaded to the browser (library size can matter).
Font rendering/measurement differences
Font rendering is a very complex problem that must deal with all languages and do it reasonably well. Fonts deal with it by using few mechanisms in many different tables:
- Substituting glyphs where a glyph is replaced by different glyphs.
- arabic letters are drawn differently depending on a position in a word
- Changing position of glyphs
- kering where font has a list of pairs and each pair is moved moved closer together
- adding accents to letters even though accent is a separate char
Both of these obviously change the width and heigh of a rendered/measured text.
There are many tables (called features) that utilize glyph modification for a better text rendering/measurement.
As Microsoft documentatin succintly states
A feature definition may not provide all the information required to properly implement glyph substitution or positioning actions. Nothing in the feature’s lookup tables indicates when or where to apply this feature during text processing.
Information on lookups and lookup types is provided only as recommendations or suggestions; the set of lookups used to implement a feature may vary across platforms, applications, fonts, and font developers.
Basically each library decides what and how to implement features and there is over hundred of different standardized features. To give a concrete example, GDI+ (System.Drawing.Graphics.Draw/MeasureString) differs from SixLabors.Font for Calibri:
- 6L.Fonts takes ccmp feature and a rather large slash glyph (char
/
) with a smaller one. TBH, use of ccmp feature for 1:1 substitution seems strange, but it’s there. - GDI+ doesn’t, it just keeps a glyph for a large slash.
The result is different width between these two implementations for most of dates, becasue string representation (e.g. 7/4/2020
) that has a slash.
It’s clear DIY is not an option for font measurement.
Image/Font library evaluation
SkiaSharp
SkiaSharp is a managed wrapper over Skia library. The Skia is under active deveopment developed by Google, SkiaSharp is developed by single contributor.
Supports text bounding box calculation and image format detection.
It supports net462, but not net40. Probably easiest option from the point of replacement System.Drawing.Common.
It is a wrapper over a native library, so many different packages for OS/arch. The file is very large 8.5 MiB. Supports ARM.
(It has no support for WMF)[https://github.com/mono/SkiaSharp/issues/1314], but that is pretty standard. Due to native code, Blazor won’t support it.
Magic.NET
A wrapper for a native library, all disadvantages of SkiaSharp, but more limiting font api. No reason to choose when there is SkiaSharp.
SixLabors.ImageSharp
A library for image manipulation. Fully managed. ImageSharp has four significant active contributors and is constantly improved upon. Size of dll is 1.6MB. It supports .NET Core 2.1, netstandard2.0 and net472
Only image parsing/manipulation, nothing for fonts.
Microsoft.Maui.Graphics
It only supports netstandard2.0;net6
. ClosedXML supports netstandard2.0;net40;net46
. Microsoft doesn’t care about netfx, but ClosedXML definitely should keep at least one netfx target framework. Many enterprise webform applications are using ClosedXML to generate reports that will never be migrated to anything newer. Although netstandard2.0 is compatible with net4x, it’s not seamless.
Although the library itself doesn’t depend on Maui, I have some doubt about using a shiny-new library. Microsoft can easily drop support for netstandard2.0 in the future, just like they did for EntityFramework Core (e.g. they will want to use default interface methods or some cool new methods that was Core only in 3.1 or …).
Windows Imaging Components
An extensible COM-based imaging codec framework. Windows only. Not suitable for ClosedXML purposes.
LayoutFarm/Typography
A library that can layout glyphs, seems resonably mature, but doesn’t even have NuGet. It looks like it is only a dependency for different project. Based on contributions, seems dead.
SixLabors.Fonts
LIbrary that can calculate bounding box of a text. Still in preview, but used by some other projects and it seems wildly used by download count (1.0.0-beta0013 2,648,439 downloads on nuget). Since it is brand new, only fresh platforms (Core31+ and netstandard2.0) are supported.
MetadataExtractor
A library to extrat metadata (EXIF and such) from wide range of format. Works pretty much everywhere (net35, net45, netstandard1.3).
DIY
Determine fimage format, width, heigh and DPI by reading the stream. It’s not that hard and most of image libraries don’t support formats like EMF or WMF anyway.
Risk is unhandled edge cases would be handled by extra methods to manually specify the image properties:
IXLPicture Add(Stream stream, int width, int height, float dpiX, float dpiY, XLPictureFormat format);
IXLPicture Add(Stream stream, int width, int height, float dpiX, float dpiY, XLPictureFormat format, String name);
Most of image formats from XLPictureFormat (except TIFF, but what is new) have either fixed header or reasonably complex parsing.
Solution used by others
EPPlus has solved it by themselves:
To replace System.Drawing.Common we have implemented native code to handle the different image formats and text measurings. https://github.com/EPPlusSoftware/EPPlus/wiki/Breaking-Changes-in-EPPlus-6
NPOI seems to still use System.Drawing.Common.
Aspose.Cells is using SkiaSharp.
Options for fonts
Option 2: SixLabors.Fonts + Library
Basically same, as an option 1, but without extra coding of image parsing. There are two possible libraries to use (reasonably maintained):
- MetadataExtractor, library to get metadata from various files, from png to pdf. net35, netstandard1.3, maintained, wider possible number of formats. 700 KiB.
- Fewer formats, general purpose library. netcoreapp2.1, netstandard2.0 net472. 1.6MiB.
Recommendation
Image format
See list of libraries above. I lean toward DIY, seem reasonably simple (except tiff) and fewer dependencies are better. If that proves difficult - MetadataExtractor. Though it is of lesser importance.
Fonts
I would go with SixLabors.Fonts and replaced text bounding box first (while keeping System.Drawing.Common case for netfx).
SixLabors.Fonts only supports .NET Core 3.1 .NET Standard 2.0, it doesn’t support netfx platform. Calculation of text bounding box is reasonably contained. I would use conditional compiling and
- use System.Drawing.Common for netfx
- use SixLabors.Fonts for netstandard2.0 and others
It’s a beta, but with a long development and this basic API shouldn’t change.
Modify tests to be pixel perfect with SixLabors.Fonts, keep working net40/46 through some kind of tolerance in a test comparison (similar idea to StreamHelper.RemoveColumnWidths, but more flexible).
Although SkiaSharp seems to be easiest solution at this time, I just don’t see native code as a good long term solution (plus it’s huge).
Resources
Some interesting reading about fonts.
- GDI DrawString Configurator App - an app to understand flags of DrawString and MeasureStrign.
- The wonders of text rendering and GDI
- Fonts and Measuring Text - short intro into a basic of font metrics
- Why text appears different when drawn with GDIPlus versus GDI
- Text rendering methods comparison or GDI vs. GDI+ revised
Issue Analytics
- State:
- Created a year ago
- Reactions:11
- Comments:26 (12 by maintainers)
Top GitHub Comments
@Trigun27 I am the new maintainer of the project. I have finally finished CalcEngine redesign and I have moved on to this issue. This is the key thing to have in the next release, in a ~month. NET Core support is super important.
Option 5. Provide an interface for graphic manipulations (
IXLGraphicEngine
?) and include into ClosedXML a very basic, naïve implementation, measuring fonts very roughly, with no external dependencies. More precise versions, relying on different graphic libraries may be released as separate packages (and supported by other contributors). They may have smallers subsets of target frameworks supported (say, one for net40 and net46, another for netcoreapp31) - a user will be able to choose the one based on their needs. In majority of cases people do not care about the measurement precision, they just want to make it working on different platforms. Also, this will make ClosedXML bundle smaller, which is important for Blazor and serverless functions.PS. Naïve implementation may also be faster.