Closed jnyrup closed 1 year ago
Hi @jnyrup Can you please push some of the example output? Thanks!
I've moved the issue to the ImageSharp.Drawing repository as the behavior you are seeing will originate there.
Sure! See the difference in the amount of vertical space above the text.
Thanks!
It seems the problem might have been introduced or exposed in SixLabors.Fonts 1.0.0-beta17.
Using SixLabors.ImageSharp.Drawing 1.0.0-beta14 works. Upgrading only SixLabors.Fonts from 1.0.0-beta16 to 1.0.0-beta17 fails.
Bisected the commit to https://github.com/SixLabors/Fonts/commit/39773e680338b229cc285b52a3ba97ef9bf63182
Nice bit of detective work!
Unfortunately, I've given the commit a few reads over now, and I cannot spot what the introduced error would be. Something to do with the calculation of an ascender (likely within Textline/Textbox) but I'm not sure where.
I began with the previous commit and started adding parts of the failing commit until it failed.
Adding these few lines was enough.
if (metric.TopSideBearing < 0)
{
// Adjust for glyphs with a negative tsb. e.g. emoji.
ascender -= metric.TopSideBearing * scaleY;
}
Some more findings.
Removing ascender -= metric.TopSideBearing * scaleY;
from https://github.com/SixLabors/Fonts/commit/39773e680338b229cc285b52a3ba97ef9bf63182 fixes my problem and all SixLabor.Fonts test at that commit pass.
Similarly removing ascender -= tsbOffset * scaleY;
from current main fixes my problem and all SixLabor.Fonts test at that commit pass.
Great work!
Test coverage isn't great there unfortunately. That line only exists because the Segoe UI emoji font has a negative TSB for some bizarre reason. I'll have a dig and try to figure out what reference is shared across threads.
I tried a new approach: wrapping code in a lock and gradually move the lock inwards.
If I in StreamFontMetrics.GetGlyphMetrics
add a lock I no longer get an exception.
lock (this.glyphCache)
{
// We overwrite the cache entry for this type should the attributes change.
GlyphMetrics[]? cached = this.glyphCache[glyphId];
if (cached is null)
{
this.glyphCache[glyphId] = new[]
{
this.CreateGlyphMetrics(
codePoint,
glyphId,
glyphType)
};
}
}
Stack trace
SixLabors.Fonts.dll!SixLabors.Fonts.StreamFontMetrics.GetGlyphMetrics(SixLabors.Fonts.Unicode.CodePoint codePoint, ushort glyphId, SixLabors.Fonts.ColorFontSupport support)
SixLabors.Fonts.dll!SixLabors.Fonts.FileFontMetrics.GetGlyphMetrics(SixLabors.Fonts.Unicode.CodePoint codePoint, ushort glyphId, SixLabors.Fonts.ColorFontSupport support)
SixLabors.Fonts.dll!SixLabors.Fonts.GlyphPositioningCollection.TryAdd(SixLabors.Fonts.Font font, SixLabors.Fonts.GlyphSubstitutionCollection collection)
SixLabors.Fonts.dll!SixLabors.Fonts.TextLayout.DoFontRun(System.ReadOnlySpan<char> text, int start, System.Collections.Generic.IReadOnlyList<SixLabors.Fonts.TextRun> textRuns, ref int textRunIndex, ref int codePointIndex, ref int bidiRunIndex, bool isFallbackRun, SixLabors.Fonts.Font font, SixLabors.Fonts.Unicode.BidiRun[] bidiRuns, System.Collections.Generic.Dictionary<int, int> bidiMap, SixLabors.Fonts.GlyphSubstitutionCollection substitutions, SixLabors.Fonts.GlyphPositioningCollection positionings)
SixLabors.Fonts.dll!SixLabors.Fonts.TextLayout.ProcessText(System.ReadOnlySpan<char> text, SixLabors.Fonts.TextOptions options)
SixLabors.Fonts.dll!SixLabors.Fonts.TextLayout.GenerateLayout(System.ReadOnlySpan<char> text, SixLabors.Fonts.TextOptions options)
SixLabors.Fonts.dll!SixLabors.Fonts.TextRenderer.RenderText(System.ReadOnlySpan<char> text, SixLabors.Fonts.TextOptions options)
SixLabors.Fonts.dll!SixLabors.Fonts.TextRenderer.RenderText(string text, SixLabors.Fonts.TextOptions options)
SixLabors.ImageSharp.Drawing.dll!SixLabors.ImageSharp.Drawing.Processing.Processors.Text.DrawTextProcessor<SixLabors.ImageSharp.PixelFormats.Rgba32>.BeforeImageApply()
SixLabors.ImageSharp.dll!SixLabors.ImageSharp.Processing.Processors.ImageProcessor<SixLabors.ImageSharp.PixelFormats.Rgba32>.SixLabors.ImageSharp.Processing.Processors.IImageProcessor<SixLabors.ImageSharp.PixelFormats.Rgba32>.Execute() Unknown
SixLabors.ImageSharp.dll!SixLabors.ImageSharp.Processing.DefaultImageProcessorContext<SixLabors.ImageSharp.PixelFormats.Rgba32>.ApplyProcessor(SixLabors.ImageSharp.Processing.Processors.IImageProcessor processor, SixLabors.ImageSharp.Rectangle rectangle) Unknown
SixLabors.ImageSharp.dll!SixLabors.ImageSharp.Processing.DefaultImageProcessorContext<SixLabors.ImageSharp.PixelFormats.Rgba32>.ApplyProcessor(SixLabors.ImageSharp.Processing.Processors.IImageProcessor processor) Unknown
SixLabors.ImageSharp.Drawing.dll!SixLabors.ImageSharp.Drawing.Processing.DrawTextExtensions.DrawText(SixLabors.ImageSharp.Processing.IImageProcessingContext source, SixLabors.ImageSharp.Drawing.Processing.DrawingOptions drawingOptions, string text, SixLabors.Fonts.Font font, SixLabors.ImageSharp.Drawing.Processing.IBrush brush, SixLabors.ImageSharp.Drawing.Processing.IPen pen, SixLabors.ImageSharp.PointF location)
SixLabors.ImageSharp.Drawing.dll!SixLabors.ImageSharp.Drawing.Processing.DrawTextExtensions.DrawText(SixLabors.ImageSharp.Processing.IImageProcessingContext source, SixLabors.ImageSharp.Drawing.Processing.DrawingOptions drawingOptions, string text, SixLabors.Fonts.Font font, SixLabors.ImageSharp.Color color, SixLabors.ImageSharp.PointF location)
SixLabors.ImageSharp.Drawing.dll!SixLabors.ImageSharp.Drawing.Processing.DrawTextExtensions.DrawText(SixLabors.ImageSharp.Processing.IImageProcessingContext source, string text, SixLabors.Fonts.Font font, SixLabors.ImageSharp.Color color, SixLabors.ImageSharp.PointF location)
ConsoleApp4.dll!Program.<Main>$.AnonymousMethod__0_2(SixLabors.ImageSharp.Processing.IImageProcessingContext x)
Hmmm. That feels like a sticking plaster.
The worst thing that should happen is that there is a cache miss and that means that the metrics are parsed 2X.
We clone the metrics in GlyphPositioningCollection.TryAdd
and GlyphMetrics.TopSideBearing
is read-only. I wonder if the issue is replicable with a debugger attached? Then we can see why that property is different.
I guess we could use a concurrent dictionary as the cache and put a note saying that it requires further investigation.
I wonder if the issue is replicable with a debugger attached? Then we can see why that property is different.
It is.
Also by invoking Generate()
a single time to fill caches before the parallel loop, it also works.
_ = Generate();
Parallel.ForEach(Enumerable.Range(...)
Ah no, sorry I mean in the Fonts library itself.
I've managed to recreate the issue using the following test so should have enough to go on now.
[Fact]
public void RendererIsThreadsafe()
{
const int threadCount = 10;
Parallel.For(0, threadCount, _ =>
{
ColorGlyphRenderer renderer1 = new();
TextRenderer.RenderTextTo(renderer1, "A ", new TextOptions(SystemFonts.CreateFont("Arial", 15)));
ColorGlyphRenderer renderer2 = new();
TextRenderer.RenderTextTo(renderer2, "A ", new TextOptions(SystemFonts.CreateFont("Arial", 15)));
Assert.True(renderer1.ControlPoints.Count > 0);
Assert.True(renderer2.ControlPoints.Count > 0);
Assert.True(renderer1.ControlPoints.SequenceEqual(renderer2.ControlPoints));
});
}
Prerequisites
DEBUG
andRELEASE
modeImageSharp version
3.0.0-alpha.0.73
Other ImageSharp packages and versions
SixLabors.ImageSharp.Drawing: 1.0.0-beta15.3, SixLabors.Fonts: 1.0.0-beta19.5
Environment (Operating system, version and so on)
Windows 10, 22H2, Build 19045.2486
.NET Framework version
.NET 7
Description
In unit test I experienced that when running test locally, the text in images would sometimes move a tiny bit, but only when running the tests in parallel, which made me think there was a race-condition somewhere.
Below I have created the smallest reproducing example I could come up with and commented with what changes removes what looks like a race-condition to me.
To repeat to conditions here:
Rgba32
" A"
.Steps to Reproduce
Images
No response