Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optimised drawing path with cached glyph rendering #614

Merged
merged 25 commits into from
Jun 19, 2018

Conversation

tocsoft
Copy link
Member

@tocsoft tocsoft commented Jun 12, 2018

TODO

  • Expand test coverage if required (waiting on code cove)
  • Update reference images to match slightly updated rendering output.

Description

I've added a much more optimised text drawing path which makes use of caching individual glyphs the first time they are rendered. This has 2 benefits;

  1. we only have to create a glyph vector once per font/dpi/size.
  2. we only have to do the Intersection scanning once per unique glyph.

The cache could also be expanded in the future to live for a variety of extended life times (instead of the per drawing operation its using now) i.e. we could cache the glyphs for the life time of the ImageProcessingOperation or allow for a user to pass into the mutate/clone method some form of lifetime scope that can be used to manage the lifetime of the cache. Or could this be an operation on MemoryManager even??

This drastically reduce rendering time on large amount of text, with larger gains the large the corpus. In addition we also drastically reduce our allocations (we will only now allocate a ComplexPath once per unique glyph now as apposed to before we would allocate one per visible character in the text).

These are the numbers, in each case we are rendering a word wrapped piece of text with "Hello World" rendered "TextIterations" number of times as a single space separated string.

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-4690 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.300
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT

Method TextIterations Mean Error StdDev Scaled ScaledSD Gen 0 Gen 1 Gen 2 Allocated
'System.Drawing Draw Text' 10 961.9 us 19.00 us 30.14 us 1.00 0.00 - - - 680 B
'ImageSharp Draw Text - Cached Glyphs' 10 2,385.1 us 11.65 us 10.90 us 2.48 0.08 58.5938 - - 192955 B
'ImageSharp Draw Text - Nieve' 10 19,288.9 us 121.60 us 113.75 us 20.07 0.63 750.0000 281.2500 - 3920624 B
'System.Drawing Draw Text' 100 2,811.3 us 56.13 us 52.50 us 1.00 0.00 - - - 7506 B
'ImageSharp Draw Text - Cached Glyphs' 100 10,094.0 us 45.29 us 42.37 us 3.59 0.07 265.6250 - - 861721 B
'ImageSharp Draw Text - Nieve' 100 210,848.5 us 1,273.35 us 1,191.10 us 75.03 1.42 10250.0000 812.5000 250.0000 40888369 B
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-4690 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.300
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT

Method TextIterations Mean Error StdDev Scaled ScaledSD Gen 0 Gen 1 Gen 2 Allocated
'System.Drawing Draw Text Outline' 10 3.612 ms 0.0227 ms 0.0201 ms 1.00 0.00 - - - 728 B
'ImageSharp Draw Text Outline - Cached Glyphs' 10 6.741 ms 0.0208 ms 0.0195 ms 1.87 0.01 273.4375 - - 865368 B
'ImageSharp Draw Text Outline - Nieve' 10 60.993 ms 0.2344 ms 0.1957 ms 16.89 0.10 4000.0000 250.0000 - 15793256 B
'System.Drawing Draw Text Outline' 100 31.409 ms 0.1844 ms 0.1725 ms 1.00 0.00 - - - 7460 B
'ImageSharp Draw Text Outline - Cached Glyphs' 100 28.109 ms 0.1337 ms 0.1250 ms 0.89 0.01 656.2500 - - 2047190 B
'ImageSharp Draw Text Outline - Nieve' 100 622.104 ms 3.0993 ms 2.8991 ms 19.81 0.14 48250.0000 1437.5000 437.5000 160002918 B

Before and after changes to visual output

Before

image

After

image

Issues fixed

fixes #421

@JimBobSquarePants
Copy link
Member

JimBobSquarePants commented Jun 13, 2018

Looks like the FontShapesAreRenderedCorrectly test is failing.

Ah you know this already though, didn't spot the unticked checkbox.

@codecov
Copy link

codecov bot commented Jun 13, 2018

Codecov Report

Merging #614 into master will increase coverage by 0.01%.
The diff coverage is 95.44%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #614      +/-   ##
==========================================
+ Coverage   88.55%   88.56%   +0.01%     
==========================================
  Files         881      883       +2     
  Lines       36994    37114     +120     
  Branches     2627     2665      +38     
==========================================
+ Hits        32759    32869     +110     
- Misses       3452     3456       +4     
- Partials      783      789       +6
Impacted Files Coverage Δ
tests/ImageSharp.Tests/Drawing/BeziersTests.cs 100% <ø> (ø) ⬆️
...ocessing/Drawing/Processors/FillRegionProcessor.cs 97.26% <100%> (+1.22%) ⬆️
src/ImageSharp.Drawing/Primitives/ShapePath.cs 100% <100%> (ø) ⬆️
...harp.Drawing/Processing/Text/DrawTextExtensions.cs 100% <100%> (ø) ⬆️
...s/ImageSharp.Tests/Drawing/Utils/QuickSortTests.cs 100% <100%> (ø)
src/ImageSharp.Drawing/Utils/QuickSort.cs 100% <100%> (ø)
tests/ImageSharp.Tests/Drawing/Text/DrawText.cs 100% <100%> (ø) ⬆️
...arp.Drawing/Processing/Text/TextGraphicsOptions.cs 100% <100%> (ø) ⬆️
...ageSharp.Tests/Drawing/Paths/DrawPathCollection.cs 100% <100%> (ø)
...geSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs 100% <100%> (ø) ⬆️
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2e62c60...2f52b39. Read the comment docs.

@antonfirsov
Copy link
Member

@tocsoft I think what you need is to increase the tolerance a bit for the tests still failing. There are floating point implementation differences, leading to super-minor differences in the output.

@tocsoft
Copy link
Member Author

tocsoft commented Jun 15, 2018

when outlining large numbers of characters we now even beat system.drawing 😃

/// <summary>
/// Gets or sets a value indicating the DPI to render text along the X axis.
/// </summary>
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I snuck this one in... this will allow users to render at DPIs other that 72.

@tocsoft tocsoft changed the title [WIP] Add optimised drawing path with cached glyph rendering Add optimised drawing path with cached glyph rendering Jun 15, 2018
Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few questions/concerns to address, otherwise looks good.

/// </summary>
public IBrush<TPixel> Brush { get; }
public IBrush<TPixel> Brush { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, in most cases image processor properties are readonly and initialized from the constructors. Why are there exceptions, and are we sure we need them?

I think we need a guideline for this.

Hint:
Readonly properties will allow input check at constructor time + readonly logic is more in sync with the "one-shot" nature of our processors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Processors should be immutable. I've already gone through and changed all others before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.. i'll make them immutable again... was trying to find ways of reducing allocations by allow users to reuse as required, but its not a major gain really

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the processors were not designed to be reusable, they cache stuff of their current execution. Making them reusable would mean that we need to implement and unit test proper reset logic for them, which is a statefulness hell IMO.

TPixel colorOutline = NamedColors<TPixel>.Black;
TPixel colorFill = NamedColors<TPixel>.Gray;

provider.VerifyOperation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the expected output right?

image

Don't we need another test? More complex path (eg. circle) but simpler brush/pen. I'm happy to add it.

}
}

if (this.Pen != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if Brush and Pen are both null? Shouldn't we throw?

(Same for all text processors.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

this.builder.StartFigure();
}

public bool BeginGlyph(RectangleF bounds, int cacheKey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need a dedicated GlyphParameters struct/class with proper equals + gethashcode implementations instead of an integer key here? Collisions may lead to bugs!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your right... i'll add it to Fonts get it wired up.

TPixel color = NamedColors<TPixel>.Black;

provider.VerifyOperation(
ImageComparer.Tolerant(imageThreshold: 0.1f, perPixelManhattanThreshold: 20),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this threshold is too high now, will do some experiments lowering it.

{
int w = (int)(img.Width * 0.6);
int h = (int)(img.Height * 0.6);
IPath path = new EllipsePolygon(img.Width/2, img.Height/2, w, h);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tocsoft can you confirm, if the output is correct?

image

The first point in path is X=120, Y=300, but the text starts on the top, so I'm a bit confused. Is it center aligned?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this and think we should just remove this set of APIs... its just a thin wrapper around TextBuilder.GenerateGlyphs from Shapes and you can already draw shapes... it would also encourage users to investigate the shapes apis and also allow us to drop the SixLabors.Shapes.Text package and switch down to just Fonts & Shapes directly, then is users want to do this we can direct them to the optional package that they can use.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't harm that its one less API to make sure is displaying correctly before we launch too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the .DrawText() overloads taking IPath? That might be fine, it's an advanced scenario.

Having these image-based tests for TextBuilder.GenerateGlyphs(text, path, style) here might be still useful!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TextBuilder.GenerateGlyphs are really tests that belong in the Shapes project and not really Imagesharp concerns... as far and Imagesharp is concerned they will be paths and that it renders paths correctly.

@tocsoft
Copy link
Member Author

tocsoft commented Jun 17, 2018

I've dropped the Draw text along a path code as its not really that big, I am not 100% confident that its drawing the text in quite the expected location and in reality it is something that a small tutorial/article would be enough to cover and not something a whole dedicated processor is required for.

@antonfirsov
Copy link
Member

@tocsoft I pushed a few refactors, if you're fine with them, I'm happy to merge this.

@antonfirsov
Copy link
Member

antonfirsov commented Jun 18, 2018

Btw. defining a library-wide cache infrastructure is a good idea! (Not easy though.)

I discourage mixing caching concern with the allocator concern however. A short proposal:

public interface ICache
{
	bool TryLookup<TKey, TResult>(TKey key, out TResult);
	void Clear();
}

public class Configuration
{
	// ...
	public ICache Cache { get; set; } = new NullCache(); // no caching by default?
	// ...
}

public interface IImageProcessingContext<TPixel>
{
	// ...
	public ICache Cache { get; set; } = new DefaultCache();
	// ...
}

@JimBobSquarePants
Copy link
Member

where TKey : IEquatable<TKey> nothing worse that slow lookups.

@tocsoft tocsoft merged commit f8dba5f into master Jun 19, 2018
@JimBobSquarePants JimBobSquarePants deleted the sw/draw-text-optermizations branch September 3, 2019 11:12
antonfirsov pushed a commit to antonfirsov/ImageSharp that referenced this pull request Nov 11, 2019
…ations

Add optimised drawing path with cached glyph rendering
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Inconsistent behaviour between TextMeasurer and the result text size when DPI is specified
3 participants