@@ -48,17 +48,18 @@ internal sealed class RichTextGlyphRenderer : BaseGlyphBuilder, IColorGlyphRende
4848 // - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant)
4949 // - Cache hit ratio above 60%
5050 private const float AccuracyMultiple = 8 ;
51- private readonly Dictionary < ( GlyphRendererParameters Glyph , PointF SubPixelOffset ) , GlyphRenderData > glyphData = new ( ) ;
51+ private readonly Dictionary < ( GlyphRendererParameters Glyph , RectangleF Bounds ) , GlyphRenderData > glyphData = new ( ) ;
5252 private bool rasterizationRequired ;
5353 private readonly bool noCache ;
54- private ( GlyphRendererParameters Glyph , PointF SubPixelOffset ) currentCacheKey ;
54+ private ( GlyphRendererParameters Glyph , RectangleF Bounds ) currentCacheKey ;
5555
5656 public RichTextGlyphRenderer (
5757 RichTextOptions textOptions ,
5858 DrawingOptions drawingOptions ,
5959 MemoryAllocator memoryAllocator ,
6060 Pen pen ,
6161 Brush brush )
62+ : base ( drawingOptions . Transform )
6263 {
6364 this . textOptions = textOptions ;
6465 this . drawingOptions = drawingOptions ;
@@ -67,9 +68,6 @@ public RichTextGlyphRenderer(
6768 this . defaultBrush = brush ;
6869 this . DrawingOperations = new List < DrawingOperation > ( ) ;
6970
70- // Set the default transform.
71- this . Builder . SetTransform ( drawingOptions . Transform ) ;
72-
7371 IPath path = textOptions . Path ;
7472 if ( path is not null )
7573 {
@@ -138,15 +136,17 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
138136 if ( ! this . noCache )
139137 {
140138 // Create a cache entry for the glyph.
139+ // We need to apply the default transform to the bounds to get the correct size
140+ // for comparison with future glyphs. We can use this cached glyph anywhere in the text block.
141141 var currentBounds = RectangleF . Transform (
142- new RectangleF ( bounds . Location , new ( bounds . Width , bounds . Height ) ) ,
143- this . drawingOptions . Transform ) ;
142+ new RectangleF ( bounds . Location , new ( bounds . Width , bounds . Height ) ) ,
143+ this . drawingOptions . Transform ) ;
144144
145- PointF subPixelOffset = currentBounds . Location - ClampToPixel ( currentBounds . Location ) ;
146- subPixelOffset . X = MathF . Round ( subPixelOffset . X * AccuracyMultiple ) / AccuracyMultiple ;
147- subPixelOffset . Y = MathF . Round ( subPixelOffset . Y * AccuracyMultiple ) / AccuracyMultiple ;
145+ SizeF subPixelSize = new (
146+ MathF . Round ( currentBounds . Width * AccuracyMultiple ) / AccuracyMultiple ,
147+ MathF . Round ( currentBounds . Height * AccuracyMultiple ) / AccuracyMultiple ) ;
148148
149- this . currentCacheKey = ( parameters , subPixelOffset ) ;
149+ this . currentCacheKey = ( parameters , new RectangleF ( new ( 0 , 0 ) , subPixelSize ) ) ;
150150 if ( this . glyphData . ContainsKey ( this . currentCacheKey ) )
151151 {
152152 // We have already drawn the glyph vectors.
@@ -155,6 +155,8 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
155155 }
156156 }
157157
158+ // Transform the glyph vectors using the original bounds
159+ // The default transform will automatically be applied.
158160 this . TransformGlyph ( in bounds ) ;
159161 this . rasterizationRequired = true ;
160162 }
@@ -273,7 +275,7 @@ protected override void EndGlyph()
273275 GlyphRenderData renderData = default ;
274276
275277 // Fix up the text runs colors.
276- // Only if both brush and pen is null do we fallback to the defualt value
278+ // Only if both brush and pen is null do we fallback to the default value.
277279 if ( this . currentBrush == null && this . currentPen == null )
278280 {
279281 this . currentBrush = this . defaultBrush ;
@@ -284,8 +286,8 @@ protected override void EndGlyph()
284286 bool renderOutline = false ;
285287
286288 // If we are using the fonts color layers we ignore the request to draw an outline only
287- // cause that wont really work and instead force drawing with fill with the requested color
288- // if color fonts disabled then this.currentColor will always be null
289+ // because that won't really work. Instead we force drawing using fill with the requested color.
290+ // If color fonts are disabled then this.currentColor will always be null.
289291 if ( this . currentBrush != null || this . currentColor != null )
290292 {
291293 renderFill = true ;
@@ -310,16 +312,14 @@ protected override void EndGlyph()
310312
311313 // Path has already been added to the collection via the base class.
312314 IPath path = this . Paths . Last ( ) ;
315+ Point renderLocation = ClampToPixel ( path . Bounds . Location ) ;
313316 if ( this . noCache || this . rasterizationRequired )
314317 {
315318 if ( path . Bounds . Equals ( RectangleF . Empty ) )
316319 {
317320 return ;
318321 }
319322
320- // If we are using the fonts color layers we ignore the request to draw an outline only
321- // cause that wont really work and instead force drawing with fill with the requested color
322- // if color fonts disabled then this.currentColor will always be null
323323 if ( renderFill )
324324 {
325325 renderData . FillMap = this . Render ( path ) ;
@@ -331,6 +331,10 @@ protected override void EndGlyph()
331331 renderData . OutlineMap = this . Render ( path ) ;
332332 }
333333
334+ // Capture the delta between the location and the truncated render location.
335+ // We can use this to offset the render location on the next instance of this glyph.
336+ renderData . LocationDelta = ( Vector2 ) ( path . Bounds . Location - renderLocation ) ;
337+
334338 if ( ! this . noCache )
335339 {
336340 this . glyphData [ this . currentCacheKey ] = renderData ;
@@ -339,13 +343,16 @@ protected override void EndGlyph()
339343 else
340344 {
341345 renderData = this . glyphData [ this . currentCacheKey ] ;
346+
347+ // Offset the render location by the delta from the cached glyph and this one.
348+ renderLocation = ( Point ) ( path . Bounds . Location - ( PointF ) renderData . LocationDelta ) ;
342349 }
343350
344351 if ( renderData . FillMap != null )
345352 {
346353 this . DrawingOperations . Add ( new DrawingOperation
347354 {
348- Location = ClampToPixel ( path . Bounds . Location ) ,
355+ RenderLocation = renderLocation ,
349356 Map = renderData . FillMap ,
350357 Brush = this . currentBrush ,
351358 RenderPass = RenderOrderFill
@@ -356,7 +363,7 @@ protected override void EndGlyph()
356363 {
357364 this . DrawingOperations . Add ( new DrawingOperation
358365 {
359- Location = ClampToPixel ( path . Bounds . Location ) ,
366+ RenderLocation = renderLocation ,
360367 Map = renderData . OutlineMap ,
361368 Brush = this . currentPen ? . StrokeFill ?? this . currentBrush ,
362369 RenderPass = RenderOrderOutline
@@ -375,9 +382,9 @@ protected override void EndText()
375382 public void Dispose ( ) => this . Dispose ( true ) ;
376383
377384 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
378- private static Point ClampToPixel ( PointF point )
379- => Point . Truncate ( point ) ;
385+ private static Point ClampToPixel ( PointF point ) => Point . Truncate ( point ) ;
380386
387+ // Point.Truncate(point);
381388 private void FinalizeDecoration ( ref TextDecorationDetails ? decoration )
382389 {
383390 if ( decoration != null )
@@ -396,11 +403,11 @@ private void FinalizeDecoration(ref TextDecorationDetails? decoration)
396403
397404 if ( outline . Bounds . Width != 0 && outline . Bounds . Height != 0 )
398405 {
399- // Render the Path here
406+ // Render the path here. Decorations are uncached.
400407 this . DrawingOperations . Add ( new DrawingOperation
401408 {
402409 Brush = decoration . Value . Pen . StrokeFill ,
403- Location = ClampToPixel ( outline . Bounds . Location ) ,
410+ RenderLocation = ClampToPixel ( outline . Bounds . Location ) ,
404411 Map = this . Render ( outline ) ,
405412 RenderPass = RenderOrderDecoration
406413 } ) ;
@@ -444,14 +451,7 @@ private void AppendDecoration(ref TextDecorationDetails? decoration, Vector2 sta
444451
445452 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
446453 private void TransformGlyph ( in FontRectangle bounds )
447- {
448- if ( this . path is null )
449- {
450- return ;
451- }
452-
453- this . Builder . SetTransform ( this . ComputeTransform ( in bounds ) ) ;
454- }
454+ => this . Builder . SetTransform ( this . ComputeTransform ( in bounds ) ) ;
455455
456456 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
457457 private Matrix3x2 ComputeTransform ( in FontRectangle bounds )
@@ -482,15 +482,19 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds)
482482
483483 private Buffer2D < float > Render ( IPath path )
484484 {
485- // We need to offset the path now against 0,0 for rasterization.
486- IPath offsetPath = path . Translate ( - path . Bounds . Location ) ;
485+ // We need to offset the path now against the equivalent pixel position of [ 0,0] for rasterization.
486+ IPath offsetPath = path . Translate ( - ClampToPixel ( path . Bounds . Location ) ) ;
487487 Size size = Rectangle . Ceiling ( offsetPath . Bounds ) . Size ;
488+
489+ // Pad to prevent edge clipping.
490+ size += new Size ( 2 , 2 ) ;
491+
488492 int subpixelCount = FillPathProcessor . MinimumSubpixelCount ;
489493 float xOffset = .5F ;
490494 GraphicsOptions graphicsOptions = this . drawingOptions . GraphicsOptions ;
491495 if ( graphicsOptions . Antialias )
492496 {
493- xOffset = 0F ; // We are antialiasing skip offsetting as real antialiasing should take care of offset.
497+ xOffset = 0F ; // We are antialiasing. Skip offsetting as real antialiasing should take care of offset.
494498 subpixelCount = Math . Max ( subpixelCount , graphicsOptions . AntialiasSubpixelDepth ) ;
495499 }
496500
@@ -543,7 +547,7 @@ private void Dispose(bool disposing)
543547 {
544548 if ( disposing )
545549 {
546- foreach ( KeyValuePair < ( GlyphRendererParameters Glyph , PointF SubPixelOffset ) , GlyphRenderData > kv in this . glyphData )
550+ foreach ( KeyValuePair < ( GlyphRendererParameters Glyph , RectangleF Bounds ) , GlyphRenderData > kv in this . glyphData )
547551 {
548552 kv . Value . Dispose ( ) ;
549553 }
@@ -564,7 +568,8 @@ private void Dispose(bool disposing)
564568
565569 private struct GlyphRenderData : IDisposable
566570 {
567- // public Color? Color;
571+ public Vector2 LocationDelta ;
572+
568573 public Buffer2D < float > FillMap ;
569574
570575 public Buffer2D < float > OutlineMap ;
0 commit comments