diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs index 478358191..f9a1d8739 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs @@ -165,9 +165,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text private class CachingGlyphRenderer : IGlyphRenderer, IDisposable { - // just enough accuracy to allow for half pixel differences which - // later are componded into full pixel offsets while rendering. - private const float AccuracyMultiple = 2; + // just enough accuracy to allow for 1/8 pixel differences which + // later are accumulated while rendering, but do not grow into full pixel offsets + // The value 8 is benchmarked to: + // - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant) + // - Cache hit ratio above 60% + private const float AccuracyMultiple = 8; private readonly PathBuilder builder; @@ -222,14 +225,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Text public bool BeginGlyph(RectangleF bounds, GlyphRendererParameters parameters) { this.currentRenderPosition = Point.Truncate(bounds.Location); - PointF dif = bounds.Location - this.currentRenderPosition; + PointF subPixelOffset = bounds.Location - this.currentRenderPosition; - dif.X = ((int)(dif.X * AccuracyMultiple)) / AccuracyMultiple; - dif.Y = ((int)(dif.Y * AccuracyMultiple)) / AccuracyMultiple; + subPixelOffset.X = MathF.Round(subPixelOffset.X * AccuracyMultiple) / AccuracyMultiple; + subPixelOffset.Y = MathF.Round(subPixelOffset.Y * AccuracyMultiple) / AccuracyMultiple; // we have offset our rendering origion a little bit down to prevent edge cropping, move the draw origin up to compensate this.currentRenderPosition = new Point(this.currentRenderPosition.X - this.offset, this.currentRenderPosition.Y - this.offset); - this.currentGlyphRenderParams = (parameters, dif); + this.currentGlyphRenderParams = (parameters, subPixelOffset); + if (this.glyphData.ContainsKey(this.currentGlyphRenderParams)) { // we have already drawn the glyph vectors skip trying again diff --git a/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs index 09aeef35c..c1aeebed8 100644 --- a/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/Text/DrawTextOnImageTests.cs @@ -11,6 +11,8 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; +using Xunit.Abstractions; + // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Drawing.Text @@ -25,6 +27,13 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text public static ImageComparer TextDrawingComparer = ImageComparer.TolerantPercentage(1e-5f); public static ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(1e-4f); + public DrawTextOnImageTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + [Theory] [WithSolidFilledImages(200, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "SixLaborsSampleAB.woff", AB)] [WithSolidFilledImages(900, 100, "White", PixelTypes.Rgba32, 50, 0, 0, "OpenSans-Regular.ttf", TestText)] @@ -87,8 +96,12 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text TPixel color = NamedColors.Black; + // Based on the reported 0.0270% difference with AccuracyMultiple = 8 + // We should avoid quality regressions leading to higher difference! + var comparer = ImageComparer.TolerantPercentage(0.03f); + provider.VerifyOperation( - TextDrawingComparer, + comparer, img => { img.Mutate(c => c.DrawText(textOptions, sb.ToString(), font, color, new PointF(10, 5))); @@ -152,8 +165,9 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text } [Theory] - [WithSolidFilledImages(1000, 1500, "White", PixelTypes.Rgba32)] - public static void TextPositioningIsRobust(TestImageProvider provider) + [WithSolidFilledImages(1000, 1500, "White", PixelTypes.Rgba32, "Baskerville Old Face")] + [WithSolidFilledImages(1000, 1500, "White", PixelTypes.Rgba32, "Arial")] + public void TextPositioningIsRobust(TestImageProvider provider, string fontName) where TPixel : struct, IPixel { if (!TestEnvironment.IsWindows) @@ -162,14 +176,24 @@ namespace SixLabors.ImageSharp.Tests.Drawing.Text return; } - Font font = SystemFonts.CreateFont("Baskerville Old Face", 30, FontStyle.Regular); + Font font = SystemFonts.CreateFont(fontName, 30, FontStyle.Regular); string text = Repeat("Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!\n", 20); var textOptions = new TextGraphicsOptions(true) { WrapTextWidth = 1000 }; + string details = fontName.Replace(" ", ""); + + // Based on the reported 0.1755% difference with AccuracyMultiple = 8 + // We should avoid quality regressions leading to higher difference! + var comparer = ImageComparer.TolerantPercentage(0.2f); + provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, font, NamedColors.Black, new PointF(10, 50)), appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + x => x.DrawText(textOptions, text, font, NamedColors.Black, new PointF(10, 50)), + details, + comparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times)); diff --git a/tests/Images/External b/tests/Images/External index 5634b8b2a..b7a7eacc0 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 5634b8b2affa1158cec31ccbb67f9583e9137234 +Subproject commit b7a7eacc0446a1acc266f30ddadbda40fccde231