Skip to content

Commit fbe4259

Browse files
committed
refactor: unify font substitution engines
1 parent 24690f5 commit fbe4259

File tree

7 files changed

+146
-270
lines changed

7 files changed

+146
-270
lines changed

.changeset/soft-camels-visit.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-pdf/textkit": minor
3+
"@react-pdf/layout": minor
4+
---
5+
6+
refactor: unify font substitution engines

packages/layout/src/svg/layoutText.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import layoutEngine, {
66
justification,
77
scriptItemizer,
88
wordHyphenation,
9+
fontSubstitution,
910
textDecoration,
1011
fromFragments,
1112
Fragment,
1213
Font,
1314
} from '@react-pdf/textkit';
1415

1516
import transformText from '../text/transformText';
16-
import fontSubstitution from '../text/fontSubstitution';
1717
import {
1818
SafeNode,
1919
SafeTextNode,

packages/layout/src/text/fontSubstitution.ts

-102
This file was deleted.

packages/layout/src/text/layoutText.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import layoutEngine, {
55
scriptItemizer,
66
wordHyphenation,
77
textDecoration,
8+
fontSubstitution,
89
} from '@react-pdf/textkit';
910
import FontStore from '@react-pdf/font';
1011

11-
import fontSubstitution from './fontSubstitution';
1212
import getAttributedString from './getAttributedString';
1313
import { SafeTextNode } from '../types';
1414

packages/layout/tests/text/fontSubstitution.test.ts

-117
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,81 @@
11
import { last } from '@react-pdf/fns';
2-
3-
import empty from '../../attributedString/empty';
42
import { AttributedString, Run } from '../../types';
53

6-
/**
7-
* @param run - Run
8-
* @returns Font size
9-
*/
10-
const getFontSize = (run: Run) => {
11-
return run.attributes.fontSize || 12;
12-
};
4+
const IGNORED_CODE_POINTS = [173];
5+
6+
const getFontSize = (run: Run) => run.attributes.fontSize || 12;
137

14-
/**
15-
* Resolve font runs in an AttributedString, grouping equal
16-
* runs and performing font substitution where necessary.
17-
*/
18-
const fontSubstitution = () => {
19-
/**
20-
* @param attributedString - Attributed string
21-
* @returns Attributed string
22-
*/
23-
return (attributedString: AttributedString) => {
24-
const { string, runs } = attributedString;
8+
const pickFontFromFontStack = (codePoint, fontStack, lastFont) => {
9+
const fontStackWithFallback = [...fontStack, lastFont];
10+
for (let i = 0; i < fontStackWithFallback.length; i += 1) {
11+
const font = fontStackWithFallback[i];
12+
if (
13+
!IGNORED_CODE_POINTS.includes(codePoint) &&
14+
font &&
15+
font.hasGlyphForCodePoint &&
16+
font.hasGlyphForCodePoint(codePoint)
17+
) {
18+
return font;
19+
}
20+
}
21+
return fontStack.at(-1);
22+
};
2523

24+
const fontSubstitution =
25+
() =>
26+
({ string, runs }: AttributedString) => {
2627
let lastFont = null;
28+
let lastFontSize = null;
2729
let lastIndex = 0;
2830
let index = 0;
29-
const res: Run[] = [];
3031

31-
if (!string) return empty();
32+
const res: Run[] = [];
3233

33-
for (const run of runs) {
34-
const fontSize = getFontSize(run);
35-
const defaultFont = run.attributes.font;
34+
for (let i = 0; i < runs.length; i += 1) {
35+
const run = runs[i];
3636

3737
if (string.length === 0) {
38-
res.push({ start: 0, end: 0, attributes: { font: defaultFont } });
38+
res.push({
39+
start: 0,
40+
end: 0,
41+
attributes: { font: run.attributes.font },
42+
});
3943
break;
4044
}
4145

42-
for (const char of string.slice(run.start, run.end)) {
43-
const font = defaultFont;
46+
const chars = string.slice(run.start, run.end);
47+
48+
for (let j = 0; j < chars.length; j += 1) {
49+
const char = chars[j];
50+
const codePoint = char.codePointAt(0);
51+
// If the default font does not have a glyph and the fallback font does, we use it
52+
const font = pickFontFromFontStack(
53+
codePoint,
54+
run.attributes.font,
55+
lastFont,
56+
);
4457

45-
if (font !== lastFont) {
58+
const fontSize = getFontSize(run);
59+
60+
// If anything that would impact res has changed, update it
61+
if (
62+
font !== lastFont ||
63+
fontSize !== lastFontSize ||
64+
font.unitsPerEm !== lastFont.unitsPerEm
65+
) {
4666
if (lastFont) {
4767
res.push({
4868
start: lastIndex,
4969
end: index,
5070
attributes: {
5171
font: lastFont,
52-
scale: lastFont ? fontSize / lastFont.unitsPerEm : 0,
72+
scale: lastFontSize / lastFont.unitsPerEm,
5373
},
5474
});
5575
}
5676

5777
lastFont = font;
78+
lastFontSize = fontSize;
5879
lastIndex = index;
5980
}
6081

@@ -70,13 +91,12 @@ const fontSubstitution = () => {
7091
end: string.length,
7192
attributes: {
7293
font: lastFont,
73-
scale: lastFont ? fontSize / lastFont.unitsPerEm : 0,
94+
scale: fontSize / lastFont.unitsPerEm,
7495
},
7596
});
7697
}
7798

78-
return { string, runs: res };
99+
return { string, runs: res } as AttributedString;
79100
};
80-
};
81101

82102
export default fontSubstitution;

0 commit comments

Comments
 (0)