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