Skip to content

Commit

Permalink
⚡️ Introduce cache for loaded fonts
Browse files Browse the repository at this point in the history
To reduce the number of calls to the font loader and reuse loaded fonts,
this commit introduces a cache for loaded fonts. The caching includes
fontkit fonts which are now also reused.
  • Loading branch information
ralfstx committed Nov 29, 2023
1 parent 2d6aa64 commit f65d87f
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 5 deletions.
38 changes: 36 additions & 2 deletions src/fonts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('fonts', () => {
fontLoader = {
loadFont: jest.fn(async (selector: FontSelector) => {
if (selector.fontFamily === 'Test') return testFont;
throw new Error('No font defined');
throw new Error('No such font defined');
}) as any,
};
jest.spyOn(fontkit, 'create').mockReturnValue({ fake: true } as any);
Expand All @@ -79,7 +79,7 @@ describe('fonts', () => {
const store = createFontStore(fontLoader);

await expect(store.selectFont({ fontFamily: 'foo' })).rejects.toThrowError(
"Could not load font for 'foo', style=normal, weight=normal: No font defined"
"Could not load font for 'foo', style=normal, weight=normal: No such font defined"
);
});

Expand All @@ -96,6 +96,40 @@ describe('fonts', () => {
fkFont: { fake: true },
});
});

it('calls font loader only once per selector', async () => {
const store = createFontStore(fontLoader);

await store.selectFont({ fontFamily: 'Test' });
await store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' });
await store.selectFont({ fontFamily: 'Test' });
await store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' });

expect(fontLoader.loadFont).toHaveBeenCalledTimes(2);
});

it('returns same font object for concurrent calls', async () => {
const store = createFontStore(fontLoader);

const [font1, font2] = await Promise.all([
store.selectFont({ fontFamily: 'Test' }),
store.selectFont({ fontFamily: 'Test' }),
]);

expect(font1).toBe(font2);
});

it('caches errors from font loader', async () => {
const store = createFontStore(fontLoader);

await expect(store.selectFont({ fontFamily: 'foo' })).rejects.toThrowError(
"Could not load font for 'foo', style=normal, weight=normal: No such font defined"
);
await expect(store.selectFont({ fontFamily: 'foo' })).rejects.toThrowError(
"Could not load font for 'foo', style=normal, weight=normal: No such font defined"
);
expect(fontLoader.loadFont).toHaveBeenCalledTimes(1);
});
});

describe('weightToNumber', () => {
Expand Down
17 changes: 14 additions & 3 deletions src/fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CustomFontSubsetEmbedder, PDFDocument, PDFFont, PDFRef } from 'pdf-lib'

import { parseBinaryData } from './binary-data.js';
import { FontStyle, FontWeight } from './content.js';
import { FontLoader } from './font-loader.js';
import { FontLoader, LoadedFont } from './font-loader.js';
import { printValue } from './print-value.js';
import {
optional,
Expand Down Expand Up @@ -74,12 +74,23 @@ export function registerFont(font: Font, pdfDoc: PDFDocument) {
}

export function createFontStore(fontLoader: FontLoader): FontStore {
const fontCache: Record<string, Promise<Font>> = {};

return {
selectFont,
};

async function selectFont(selector: FontSelector): Promise<Font> {
let loadedFont;
function selectFont(selector: FontSelector): Promise<Font> {
const cacheKey = [
selector.fontFamily ?? 'any',
selector.fontStyle ?? 'normal',
selector.fontWeight ?? 'normal',
].join(':');
return (fontCache[cacheKey] ??= loadFont(selector));
}

async function loadFont(selector: FontSelector): Promise<Font> {
let loadedFont: LoadedFont;
try {
loadedFont = await fontLoader.loadFont(selector);
} catch (error) {
Expand Down

0 comments on commit f65d87f

Please sign in to comment.