Skip to content

Commit 8c0b745

Browse files
committed
Improve font caching for SkiaSharp.HarfBuzz
1 parent d41913e commit 8c0b745

File tree

4 files changed

+76
-51
lines changed

4 files changed

+76
-51
lines changed

UglyToad.PdfPig.Rendering.Skia/Helpers/FontCache.cs renamed to UglyToad.PdfPig.Rendering.Skia/Helpers/SkiaFontCache.cs

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,92 +17,121 @@
1717
using System.Text;
1818
using System.Threading;
1919
using SkiaSharp;
20+
using SkiaSharp.HarfBuzz;
2021
using UglyToad.PdfPig.Core;
2122
using UglyToad.PdfPig.Fonts.SystemFonts;
2223
using UglyToad.PdfPig.PdfFonts;
2324

2425
namespace UglyToad.PdfPig.Rendering.Skia.Helpers
2526
{
26-
internal sealed class FontCache : IDisposable
27+
internal sealed class SkiaFontCache : IDisposable
2728
{
2829
private readonly ConcurrentDictionary<IFont, ConcurrentDictionary<int, Lazy<SKPath>>> _cache =
2930
new ConcurrentDictionary<IFont, ConcurrentDictionary<int, Lazy<SKPath>>>();
3031

31-
private readonly ConcurrentDictionary<string, SKTypeface> _typefaces =
32-
new ConcurrentDictionary<string, SKTypeface>();
32+
private readonly ConcurrentDictionary<string, SkiaFontCacheItem> _typefaces =
33+
new ConcurrentDictionary<string, SkiaFontCacheItem>();
3334

3435
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
3536

3637
private readonly SKFontManager _skFontManager = SKFontManager.CreateDefault();
3738

38-
public SKTypeface GetTypefaceOrFallback(IFont font, string unicode)
39+
internal sealed class SkiaFontCacheItem : IDisposable
40+
{
41+
public SkiaFontCacheItem(SKTypeface typeface)
42+
{
43+
if (typeface is null)
44+
{
45+
throw new ArgumentNullException(nameof(typeface));
46+
}
47+
48+
Typeface = typeface;
49+
Shaper = new SKShaper(Typeface);
50+
}
51+
52+
public SKTypeface Typeface { get; }
53+
54+
public SKShaper Shaper { get; }
55+
56+
public void Dispose()
57+
{
58+
Typeface.Dispose();
59+
Shaper.Dispose();
60+
}
61+
}
62+
63+
private static string GetFontKey(IFont font)
64+
{
65+
if (string.IsNullOrEmpty(font.Name?.Data))
66+
{
67+
throw new NullReferenceException("The font's name is null.");
68+
}
69+
70+
return $"{font.Name.Data}|{(font.Details.IsBold ? (byte)1 : (byte)0)}|{(font.Details.IsItalic ? (byte)1 : (byte)0)}";
71+
}
72+
73+
public SkiaFontCacheItem GetTypefaceOrFallback(IFont font, string unicode)
3974
{
4075
if (IsDisposed())
4176
{
42-
throw new ObjectDisposedException(nameof(FontCache));
77+
throw new ObjectDisposedException(nameof(SkiaFontCache));
4378
}
4479

4580
_lock.EnterReadLock();
4681
try
4782
{
4883
if (IsDisposed())
4984
{
50-
throw new ObjectDisposedException(nameof(FontCache));
85+
throw new ObjectDisposedException(nameof(SkiaFontCache));
5186
}
5287

53-
using (var style = font.Details.GetFontStyle())
54-
{
55-
var codepoint = BitConverter.ToInt32(Encoding.UTF32.GetBytes(unicode), 0);
56-
57-
if (_typefaces.TryGetValue(font.Name, out SKTypeface drawTypeface) && drawTypeface != null &&
58-
(string.IsNullOrWhiteSpace(unicode) ||
59-
drawTypeface.ContainsGlyph(codepoint))) // Check if can render
60-
{
61-
if (FontStyleEquals(drawTypeface.FontStyle, style))
62-
{
63-
return drawTypeface;
64-
}
88+
string fontKey = GetFontKey(font);
6589

66-
drawTypeface = _skFontManager.MatchFamily(drawTypeface.FamilyName, style);
90+
var codepoint = BitConverter.ToInt32(Encoding.UTF32.GetBytes(unicode), 0);
6791

68-
if (drawTypeface != null)
69-
{
70-
return drawTypeface;
71-
}
72-
}
92+
if (_typefaces.TryGetValue(fontKey, out SkiaFontCacheItem skiaFontCacheItem) &&
93+
(string.IsNullOrWhiteSpace(unicode) || skiaFontCacheItem.Typeface.ContainsGlyph(codepoint))) // Check if can render
94+
{
95+
return skiaFontCacheItem;
96+
}
7397

98+
using (var style = font.Details.GetFontStyle())
99+
{
74100
string cleanFontName = font.GetCleanFontName();
75101

76-
drawTypeface = SKTypeface.FromFamilyName(cleanFontName, style);
77-
78-
if (drawTypeface.FamilyName.Equals(SKTypeface.Default.FamilyName))
102+
var typeface = SKTypeface.FromFamilyName(cleanFontName, style);
103+
if (typeface.FamilyName.Equals(SKTypeface.Default.FamilyName))
79104
{
80105
var trueTypeFont = SystemFontFinder.Instance.GetTrueTypeFont(cleanFontName);
81106

82-
if (trueTypeFont != null &&
83-
!string.IsNullOrEmpty(trueTypeFont.TableRegister.NameTable.FontFamilyName))
107+
string? fontFamilyName = trueTypeFont?.TableRegister?.NameTable?.FontFamilyName;
108+
109+
if (!string.IsNullOrEmpty(fontFamilyName))
84110
{
85-
drawTypeface.Dispose();
86-
drawTypeface =
87-
SKTypeface.FromFamilyName(trueTypeFont.TableRegister.NameTable.FontFamilyName, style);
111+
typeface.Dispose();
112+
typeface = SKTypeface.FromFamilyName(fontFamilyName, style);
88113
}
89114
}
90115

91116
// Fallback font
92117
// https://github.com/mono/SkiaSharp/issues/232
93-
if (!string.IsNullOrWhiteSpace(unicode) && !drawTypeface.ContainsGlyph(codepoint))
118+
if (!string.IsNullOrWhiteSpace(unicode) && !typeface.ContainsGlyph(codepoint))
94119
{
95120
var fallback = _skFontManager.MatchCharacter(codepoint); // Access violation here
96121
if (fallback != null)
97122
{
98-
drawTypeface.Dispose();
99-
drawTypeface = _skFontManager.MatchFamily(fallback.FamilyName, style);
123+
typeface.Dispose();
124+
typeface = _skFontManager.MatchFamily(fallback.FamilyName, style);
100125
}
101126
}
102127

103-
_typefaces[font.Name] = drawTypeface;
128+
skiaFontCacheItem = new SkiaFontCacheItem(typeface);
129+
130+
System.Diagnostics.Debug.Assert(!_typefaces.ContainsKey(fontKey));
104131

105-
return drawTypeface;
132+
_typefaces[fontKey] = skiaFontCacheItem;
133+
134+
return skiaFontCacheItem;
106135
}
107136
}
108137
finally
@@ -114,13 +143,6 @@ public SKTypeface GetTypefaceOrFallback(IFont font, string unicode)
114143
}
115144
}
116145

117-
private static bool FontStyleEquals(SKFontStyle fontStyle1, SKFontStyle fontStyle2)
118-
{
119-
return fontStyle1.Width == fontStyle2.Width &&
120-
fontStyle1.Weight == fontStyle2.Weight &&
121-
fontStyle1.Slant == fontStyle2.Slant;
122-
}
123-
124146
private static SKPath GetPathInternal(IFont font, int code)
125147
{
126148
// TODO - check if font can even have path info

UglyToad.PdfPig.Rendering.Skia/SkiaPageFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ namespace UglyToad.PdfPig.Rendering.Skia
3434
/// </summary>
3535
public sealed class SkiaPageFactory : BasePageFactory<SKPicture>, IDisposable
3636
{
37-
private readonly FontCache _fontCache;
37+
private readonly SkiaFontCache _fontCache;
3838

3939
/// <summary>
4040
/// <see cref="SkiaPageFactory"/> constructor.
@@ -47,7 +47,7 @@ public SkiaPageFactory(
4747
ParsingOptions parsingOptions)
4848
: base(pdfScanner, resourceStore, filterProvider, pageContentParser, parsingOptions)
4949
{
50-
_fontCache = new FontCache();
50+
_fontCache = new SkiaFontCache();
5151
}
5252

5353
/// <inheritdoc/>

UglyToad.PdfPig.Rendering.Skia/SkiaStreamProcessor.Glyph.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,18 @@ private void ShowNonVectorFontGlyph(IFont font, IColor strokingColor, IColor non
192192

193193
float skew = ComputeSkewX(transformedPdfBounds);
194194

195-
using (var drawTypeface = _fontCache.GetTypefaceOrFallback(font, unicode))
196-
using (var skFont = drawTypeface.ToFont((float)pointSize, 1f, -skew))
195+
var drawTypeface = _fontCache.GetTypefaceOrFallback(font, unicode);
196+
197+
using (var skFont = drawTypeface.Typeface.ToFont((float)pointSize, 1f, -skew))
197198
using (var fontPaint = new SKPaint(skFont))
198199
{
199200
fontPaint.Style = style.Value;
200201
fontPaint.Color = color.ToSKColor(GetCurrentState().AlphaConstantNonStroking);
201202
fontPaint.IsAntialias = _antiAliasing;
202203

203-
_canvas.DrawShapedText(unicode, startBaseLine, fontPaint);
204+
// TODO - Benchmark with SPARC - v9 Architecture Manual.pdf
205+
// as _canvas.DrawShapedText(unicode, startBaseLine, fontPaint); as very slow without 'Shaper' caching
206+
_canvas.DrawShapedText(drawTypeface.Shaper, unicode, startBaseLine, fontPaint);
204207
_canvas.ResetMatrix();
205208
}
206209
}

UglyToad.PdfPig.Rendering.Skia/SkiaStreamProcessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal partial class SkiaStreamProcessor : BaseStreamProcessor<SKPicture>
4747
/// </summary>
4848
private readonly SKMatrix _yAxisFlipMatrix;
4949

50-
private readonly FontCache _fontCache;
50+
private readonly SkiaFontCache _fontCache;
5151
private readonly SKPaintCache _paintCache = new SKPaintCache(_antiAliasing, _minimumLineWidth);
5252

5353
private readonly AnnotationProvider _annotationProvider;
@@ -64,7 +64,7 @@ public SkiaStreamProcessor(
6464
TransformationMatrix initialMatrix,
6565
ParsingOptions parsingOptions,
6666
AnnotationProvider annotationProvider,
67-
FontCache fontCache)
67+
SkiaFontCache fontCache)
6868
: base(pageNumber,
6969
resourceStore,
7070
pdfScanner,

0 commit comments

Comments
 (0)