question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Remove System.Drawing.Common dependency

See original GitHub issue

tl;dr; It’s a quagmire. The issues is font measurement. Options:

  1. Either dump net40/net46 target and use SixLabors.Fonts
  2. 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)
  3. 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).
  4. 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 font
    • FontStyle - 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:

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:

Prior art

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.

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:11
  • Comments:26 (12 by maintainers)

github_iconTop GitHub Comments

18reactions
jahavcommented, Sep 5, 2022

@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.

6reactions
Pankratycommented, Jun 8, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why unable to remove System.Drawing and replace with ...
Drawing , after I install System.Drawing.Common from NuGet Package Manager, I see System.Drawing is back in my references. I want to remove it ......
Read more >
System.Drawing.Common only supported on Windows - .NET
Learn about the .NET 6 breaking change where the System.Drawing.Common package is no longer supported on non-Windows operating systems.
Read more >
Removing System.Drawing.Common references from ...
Hi, We are removing the references to the System.Drawing.Common references to update the framework of some of our webApi project to NET7 ...
Read more >
Removal of System.Drawing (GDI+) dependency on non- ...
In the future, we plan to completely remove dependency on System.Drawing for non-Windows GemBox builds because .NET 6 introduced a breaking ...
Read more >
Conflicts between different versions of "System.Drawing. ...
Solved: I migrated a project from AGP 2.9 to AGP 3.0 and it builds fine, though it comes back with a single warning...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found