Skip to content

Commit 89e0c67

Browse files
Add caching
1 parent ab96c2e commit 89e0c67

1 file changed

Lines changed: 70 additions & 16 deletions

File tree

src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder, IColorGlyphRende
4040
private TextDecorationDetails? currentStrikout;
4141
private TextDecorationDetails? currentOverline;
4242

43+
// Just enough accuracy to allow for 1/8 px differences which later are accumulated while rendering,
44+
// but do not grow into full px offsets.
45+
// The value 8 is benchmarked to:
46+
// - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant)
47+
// - Cache hit ratio above 60%
48+
private const float AccuracyMultiple = 8;
49+
private readonly Dictionary<(GlyphRendererParameters Glyph, PointF SubPixelOffset), GlyphRenderData> glyphData = new();
50+
private bool rasterizationRequired;
51+
private readonly bool noCache;
52+
private (GlyphRendererParameters Glyph, PointF SubPixelOffset) currentCacheKey;
53+
4354
public RichTextGlyphRenderer(
4455
TextDrawingOptions textOptions,
4556
DrawingOptions drawingOptions,
@@ -60,6 +71,10 @@ public RichTextGlyphRenderer(
6071
IPath path = textOptions.Path;
6172
if (path is not null)
6273
{
74+
// Turn of caching. The chances of a hit are near-zero.
75+
this.rasterizationRequired = true;
76+
this.noCache = true;
77+
6378
if (path is IPathInternals internals)
6479
{
6580
this.path = internals;
@@ -118,7 +133,28 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
118133
this.currentPen = null;
119134
}
120135

136+
if (!this.noCache)
137+
{
138+
// Create a cache entry for the glyph.
139+
var currentBounds = RectangleF.Transform(
140+
new RectangleF(bounds.Location, new(bounds.Width, bounds.Height)),
141+
this.drawingOptions.Transform);
142+
143+
PointF subPixelOffset = currentBounds.Location - Point.Truncate(currentBounds.Location);
144+
subPixelOffset.X = MathF.Round(subPixelOffset.X * AccuracyMultiple) / AccuracyMultiple;
145+
subPixelOffset.Y = MathF.Round(subPixelOffset.Y * AccuracyMultiple) / AccuracyMultiple;
146+
147+
this.currentCacheKey = (parameters, subPixelOffset);
148+
if (this.glyphData.ContainsKey(this.currentCacheKey))
149+
{
150+
// We have already drawn the glyph vectors.
151+
this.rasterizationRequired = false;
152+
return;
153+
}
154+
}
155+
121156
this.TransformGlyph(in bounds);
157+
this.rasterizationRequired = true;
122158
}
123159

124160
/// <inheritdoc/>
@@ -214,8 +250,8 @@ protected override void EndGlyph()
214250
{
215251
GlyphRenderData renderData = default;
216252

217-
// fix up the text runs colors
218-
// only if both brush and pen is null do we fallback to the defualt value
253+
// Fix up the text runs colors.
254+
// Only if both brush and pen is null do we fallback to the defualt value
219255
if (this.currentBrush == null && this.currentPen == null)
220256
{
221257
this.currentBrush = this.defaultBrush;
@@ -252,23 +288,32 @@ protected override void EndGlyph()
252288

253289
// Path has already been added to the collection via the base class.
254290
IPath path = this.Paths.Last();
255-
if (path.Bounds.Equals(RectangleF.Empty))
291+
if (this.noCache || this.rasterizationRequired)
256292
{
257-
return;
258-
}
293+
if (path.Bounds.Equals(RectangleF.Empty))
294+
{
295+
return;
296+
}
259297

260-
// If we are using the fonts color layers we ignore the request to draw an outline only
261-
// cause that wont really work and instead force drawing with fill with the requested color
262-
// if color fonts disabled then this.currentColor will always be null
263-
if (renderFill)
264-
{
265-
renderData.FillMap = this.Render(path);
266-
}
298+
// If we are using the fonts color layers we ignore the request to draw an outline only
299+
// cause that wont really work and instead force drawing with fill with the requested color
300+
// if color fonts disabled then this.currentColor will always be null
301+
if (renderFill)
302+
{
303+
renderData.FillMap = this.Render(path);
304+
}
305+
306+
if (renderOutline)
307+
{
308+
path = this.currentPen.GeneratePath(path);
309+
renderData.OutlineMap = this.Render(path);
310+
}
267311

268-
if (renderOutline)
312+
this.glyphData[this.currentCacheKey] = renderData;
313+
}
314+
else
269315
{
270-
path = this.currentPen.GeneratePath(path);
271-
renderData.OutlineMap = this.Render(path);
316+
renderData = this.glyphData[this.currentCacheKey];
272317
}
273318

274319
if (renderData.FillMap != null)
@@ -302,7 +347,7 @@ protected override void EndText()
302347
this.FinalizeDecoration(ref this.currentStrikout);
303348
}
304349

305-
public void Dispose() => this.Dispose(disposing: true);
350+
public void Dispose() => this.Dispose(true);
306351

307352
private void FinalizeDecoration(ref TextDecorationDetails? decoration)
308353
{
@@ -469,10 +514,19 @@ private void Dispose(bool disposing)
469514
{
470515
if (disposing)
471516
{
517+
foreach (KeyValuePair<(GlyphRendererParameters Glyph, PointF SubPixelOffset), GlyphRenderData> kv in this.glyphData)
518+
{
519+
kv.Value.Dispose();
520+
}
521+
522+
this.glyphData.Clear();
523+
472524
foreach (DrawingOperation operation in this.DrawingOperations)
473525
{
474526
operation.Map.Dispose();
475527
}
528+
529+
this.DrawingOperations.Clear();
476530
}
477531

478532
this.isDisposed = true;

0 commit comments

Comments
 (0)