postext-pdf 1.29.2 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/__tests__/canvasShaping.test.d.ts +2 -0
  2. package/dist/__tests__/canvasShaping.test.d.ts.map +1 -0
  3. package/dist/__tests__/canvasShaping.test.js +258 -0
  4. package/dist/__tests__/canvasShaping.test.js.map +1 -0
  5. package/dist/__tests__/canvasWords.test.d.ts +2 -0
  6. package/dist/__tests__/canvasWords.test.d.ts.map +1 -0
  7. package/dist/__tests__/canvasWords.test.js +66 -0
  8. package/dist/__tests__/canvasWords.test.js.map +1 -0
  9. package/dist/__tests__/declaredFiles.test.d.ts +2 -0
  10. package/dist/__tests__/declaredFiles.test.d.ts.map +1 -0
  11. package/dist/__tests__/declaredFiles.test.js +435 -0
  12. package/dist/__tests__/declaredFiles.test.js.map +1 -0
  13. package/dist/__tests__/designTextRtl.test.js +17 -6
  14. package/dist/__tests__/designTextRtl.test.js.map +1 -1
  15. package/dist/__tests__/glyphPositions.d.ts +25 -0
  16. package/dist/__tests__/glyphPositions.d.ts.map +1 -0
  17. package/dist/__tests__/glyphPositions.js +288 -0
  18. package/dist/__tests__/glyphPositions.js.map +1 -0
  19. package/dist/__tests__/japaneseShaping.test.js +3 -3
  20. package/dist/__tests__/japaneseShaping.test.js.map +1 -1
  21. package/dist/__tests__/listMarkerShaping.test.d.ts +2 -0
  22. package/dist/__tests__/listMarkerShaping.test.d.ts.map +1 -0
  23. package/dist/__tests__/listMarkerShaping.test.js +64 -0
  24. package/dist/__tests__/listMarkerShaping.test.js.map +1 -0
  25. package/dist/__tests__/pdfWorker.test.js +27 -0
  26. package/dist/__tests__/pdfWorker.test.js.map +1 -1
  27. package/dist/__tests__/scriptContextHan.test.js +3 -3
  28. package/dist/__tests__/scriptContextHan.test.js.map +1 -1
  29. package/dist/__tests__/shapingLanguageDocument.test.js +1 -1
  30. package/dist/__tests__/shapingLanguageDocument.test.js.map +1 -1
  31. package/dist/__tests__/shapingLanguageRunsDocument.test.js +158 -8
  32. package/dist/__tests__/shapingLanguageRunsDocument.test.js.map +1 -1
  33. package/dist/__tests__/trackedLetters.test.d.ts +2 -0
  34. package/dist/__tests__/trackedLetters.test.d.ts.map +1 -0
  35. package/dist/__tests__/trackedLetters.test.js +77 -0
  36. package/dist/__tests__/trackedLetters.test.js.map +1 -0
  37. package/dist/canvasWords.d.ts +55 -0
  38. package/dist/canvasWords.d.ts.map +1 -0
  39. package/dist/canvasWords.js +160 -0
  40. package/dist/canvasWords.js.map +1 -0
  41. package/dist/faceFiles.d.ts +93 -5
  42. package/dist/faceFiles.d.ts.map +1 -1
  43. package/dist/faceFiles.js +256 -10
  44. package/dist/faceFiles.js.map +1 -1
  45. package/dist/fontCache.d.ts +92 -11
  46. package/dist/fontCache.d.ts.map +1 -1
  47. package/dist/fontCache.js +82 -17
  48. package/dist/fontCache.js.map +1 -1
  49. package/dist/index.d.ts +2 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/pdf-backend/blockRender.d.ts.map +1 -1
  54. package/dist/pdf-backend/blockRender.js +63 -23
  55. package/dist/pdf-backend/blockRender.js.map +1 -1
  56. package/dist/pdf-backend/index.d.ts +3 -2
  57. package/dist/pdf-backend/index.d.ts.map +1 -1
  58. package/dist/pdf-backend/index.js +9 -3
  59. package/dist/pdf-backend/index.js.map +1 -1
  60. package/dist/pdf-backend/primitives.d.ts +55 -5
  61. package/dist/pdf-backend/primitives.d.ts.map +1 -1
  62. package/dist/pdf-backend/primitives.js +206 -25
  63. package/dist/pdf-backend/primitives.js.map +1 -1
  64. package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -1
  65. package/dist/pdf-backend/renderResourceBlock.js +27 -7
  66. package/dist/pdf-backend/renderResourceBlock.js.map +1 -1
  67. package/dist/pdf-backend/shapedText.d.ts +4 -0
  68. package/dist/pdf-backend/shapedText.d.ts.map +1 -1
  69. package/dist/pdf-backend/shapedText.js +15 -4
  70. package/dist/pdf-backend/shapedText.js.map +1 -1
  71. package/dist/pdf-backend/svgVector.js +1 -1
  72. package/dist/pdf-backend/svgVector.js.map +1 -1
  73. package/dist/pdf-backend/verticalText.d.ts.map +1 -1
  74. package/dist/pdf-backend/verticalText.js +7 -4
  75. package/dist/pdf-backend/verticalText.js.map +1 -1
  76. package/dist/pdf-backend/wordPieces.d.ts +22 -41
  77. package/dist/pdf-backend/wordPieces.d.ts.map +1 -1
  78. package/dist/pdf-backend/wordPieces.js +75 -62
  79. package/dist/pdf-backend/wordPieces.js.map +1 -1
  80. package/dist/shapingLanguage.d.ts +16 -0
  81. package/dist/shapingLanguage.d.ts.map +1 -1
  82. package/dist/shapingLanguage.js +50 -20
  83. package/dist/shapingLanguage.js.map +1 -1
  84. package/dist/unicodeRange.d.ts +19 -0
  85. package/dist/unicodeRange.d.ts.map +1 -0
  86. package/dist/unicodeRange.js +68 -0
  87. package/dist/unicodeRange.js.map +1 -0
  88. package/dist/worker/client.d.ts +1 -1
  89. package/dist/worker/client.d.ts.map +1 -1
  90. package/dist/worker/client.js +5 -2
  91. package/dist/worker/client.js.map +1 -1
  92. package/dist/worker/pdf.worker.js +7 -0
  93. package/dist/worker/pdf.worker.js.map +1 -1
  94. package/dist/worker/protocol.d.ts +6 -0
  95. package/dist/worker/protocol.d.ts.map +1 -1
  96. package/package.json +3 -3
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The words a browser's canvas shapes a string in (postext #770).
3
+ *
4
+ * Chrome does not hand a string to its shaper whole. It cuts it into words
5
+ * and shapes each alone, so that a word met again is answered from a cache
6
+ * (Blink's `CachingWordShapeIterator`): nothing a font does between two
7
+ * glyphs crosses from one word to the next, neither a kerned pair nor a
8
+ * ligature, and each word is given the script of its own letters. The
9
+ * layout was measured that way, and the canvas paints that way. A shaper
10
+ * handed the whole string does otherwise:
11
+ *
12
+ * - Source Serif 4 kerns a space against the `W` after it: `Wang Wei` in a
13
+ * running head came out 0.06 em narrower than its measure;
14
+ * - Noto Sans JP kerns kana against kana (`える` 0.03 em, `われ` 0.05 em):
15
+ * a line of 36 characters ended 1.4 pt short of its measure, where the
16
+ * canvas kerns only a character and the mark after it (`が、`, `す。`);
17
+ * - Noto Sans SC draws the dash of `9704—2012` in its Chinese form, a full
18
+ * em, when the word is shaped with the Latin words beside it, and 0.89
19
+ * em wide alone, as measured;
20
+ * - Noto Serif JP draws `“` in its Japanese form beside a Han character
21
+ * in one run, and in its default form as the word of its own it is on
22
+ * the canvas;
23
+ * - Aref Ruqaa sets a space 0.146 em wide in Arabic text and 0.333 em in
24
+ * any other: the space between two Arabic words is a word of its own on
25
+ * the canvas, with no letter and so no script, and a title of three
26
+ * words came out 0.37 em short.
27
+ *
28
+ * The cuts, as measured in Chrome 154 (widths of strings against the sum
29
+ * of their parts, in faces that kern across the cut or draw a mark
30
+ * otherwise by script):
31
+ *
32
+ * - a space (U+0020) and a tab are words of their own. No other space
33
+ * cuts: a no-break space stays in its word;
34
+ * - a string with no character above U+00FF is cut there only;
35
+ * - a CJK character (a Han character, a kana, a CJK mark or symbol, a
36
+ * full-width form, an enclosed number, an emoji) ends the word before
37
+ * it and opens one that holds it, the marks and modifier letters after
38
+ * it (`ー`, `々`, a voicing mark) and the CJK characters of no script
39
+ * after it (`、`, `。`, `」`): `が、` is one word, `える` two. A word that
40
+ * opens with such a mark (`「`, `ー`) takes the first character with a
41
+ * script after it as well.
42
+ *
43
+ * The symbols counted as CJK were read off the canvas ({@link CJK_BASE}):
44
+ * the characters of no script after which `n` + character + `—` keeps the
45
+ * default dash in Noto Serif SC under `zh`, whose dash takes the Chinese
46
+ * form anywhere in a Latin word. The letters (Han, kana, Bopomofo, the
47
+ * full-width and half-width forms, Roman numerals) cannot be read that
48
+ * way and are Blink's own ranges; Hangul is not among them.
49
+ */
50
+ /** The characters with no script, by block, that end a word (measured). */
51
+ const SYMBOLS = '2020-2021 2030 203B-203C 2042 2047-2049 2051 2100 2103 2105 2109-210A 2113 2116 2121 213B 2150-2152 2156-215A 2189 2194-2195 2307 2312 231A-231B 2329 23BE-23CC 23CE 23E9-23EC 23F0 23F3 2423 2460-2492 249C-24FF 25A0-25A2 25AA-25AB 25B1-25B3 25B6-25B7 25BC-25BD 25C0-25C1 25C6-25C7 25C9 25CB-25CC 25CE-25D3 25E2-25E6 25EF 25FD-25FE 2600-2603 2605-2606 260E 2614-2617 261D 2620 2640 2642 2648-2653 2660-266F 2672-267D 267F 2693 2695-2696 26A0-26A1 26A7 26AA-26AB 26BD-26BE 26C4-26C5 26CE 26D3-26D4 26EA 26F2-26F3 26F5 26F9-26FA 26FD 2705 2708 270A-270D 2713 271A 2728 273F-2740 2744 274C 274E 2753-2757 2763-2764 2776-277F 2795-2797 27A1 27B0 27BF 2B1A-2B1C 2B50 2B55 2FF0-3004 3006 3008-3020 3030 3036-3037 303C-303F 3190-319F 31C0-31E5 31EF 3220-325F 327F-32CF 32FF 3358-33FF FE10-FE12 FE19 FE30-FE52 FE54-FE66 FE68-FE6B FF01-FF0C FF0E-FF1A FF1D FF1F-FF20 FF3B-FF3D FF3F FF5B-FF65 FFE0-FFE2 FFE4-FFE6 FFE8-FFEE';
52
+ /** The letters and numbers: Å, Roman numerals, Han, kana, Bopomofo,
53
+ * circled and squared kana, full-width Latin, half-width kana and Hangul,
54
+ * the emoji and the Han of the upper planes. */
55
+ const LETTERS = '212B 2160-216B 2170-217B 217F 2E80-2FDF 3007 3021-3029 3038-303A 3041-3096 309F 30A1-30FA 30FF 3105-312F 31A0-31BF 32D0-32FE 3300-3357 3400-4DBF 4E00-9FFF F900-FAFF FF21-FF3A FF41-FF5A FF66-FF6F FF71-FF9D FFA0-FFDC 1F000-1FAFF 20000-3FFFF';
56
+ /** The marks and modifier letters of the CJK blocks: they open a CJK word
57
+ * when a string starts with one, and never end a word. */
58
+ const MODIFIERS = '3005 302A-302D 3031-3035 303B 3099-309E 30FC-30FE FF3E FF40 FF70 FF9E-FF9F FFE3';
59
+ function ranges(list) {
60
+ const out = [];
61
+ for (const part of list.split(' ')) {
62
+ const [lo, hi = lo] = part.split('-');
63
+ out.push(parseInt(lo, 16), parseInt(hi, 16));
64
+ }
65
+ // By first code point, for the search.
66
+ const pairs = [];
67
+ for (let i = 0; i < out.length; i += 2)
68
+ pairs.push([out[i], out[i + 1]]);
69
+ pairs.sort((a, b) => a[0] - b[0]);
70
+ return pairs.flat();
71
+ }
72
+ const CJK_BASE = ranges(`${SYMBOLS} ${LETTERS}`);
73
+ const CJK_MODIFIERS = ranges(MODIFIERS);
74
+ function inRanges(table, cp) {
75
+ let lo = 0;
76
+ let hi = table.length / 2 - 1;
77
+ while (lo <= hi) {
78
+ const mid = (lo + hi) >> 1;
79
+ if (cp < table[mid * 2])
80
+ hi = mid - 1;
81
+ else if (cp > table[mid * 2 + 1])
82
+ lo = mid + 1;
83
+ else
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+ /** No CJK character is below this. */
89
+ const FIRST_CJK = 0x2020;
90
+ /** A CJK character that ends the word before it. */
91
+ function isCjkBase(cp) {
92
+ return cp >= FIRST_CJK && inRanges(CJK_BASE, cp);
93
+ }
94
+ /** A CJK character: one that opens a CJK word when a word starts with it. */
95
+ function isCjk(cp) {
96
+ return cp >= FIRST_CJK && (inRanges(CJK_BASE, cp) || inRanges(CJK_MODIFIERS, cp));
97
+ }
98
+ function isDelimiter(cp) {
99
+ return cp === 0x20 || cp === 0x09;
100
+ }
101
+ /** A mark, a modifier letter or a modifier symbol, and the zero width
102
+ * joiner: they stay in the word of the character before them. */
103
+ const JOINS_RE = /^[\p{M}\p{Lm}\p{Sk}\u200D]$/u;
104
+ /** A character with no script of its own. */
105
+ const NO_SCRIPT_RE = /^[\p{Script=Common}\p{Script=Inherited}]$/u;
106
+ /** A string a browser keeps in eight bits. */
107
+ const LATIN_1_RE = /^[\u0000-\u00FF]*$/;
108
+ /** Anything a string may be cut at: a space, a tab, a character from the
109
+ * first CJK one on. */
110
+ const MAY_CUT_RE = /[ \t\u2020-\uFFFF]/;
111
+ /**
112
+ * `text` cut into the words the canvas shapes it in (see the module
113
+ * comment); the string itself, alone, when it is one word.
114
+ */
115
+ export function canvasWords(text) {
116
+ if (text.length < 2 || !MAY_CUT_RE.test(text))
117
+ return [text];
118
+ const latin1 = LATIN_1_RE.test(text);
119
+ const words = [];
120
+ const n = text.length;
121
+ const width = (cp) => (cp > 0xFFFF ? 2 : 1);
122
+ for (let start = 0; start < n;) {
123
+ const first = text.codePointAt(start);
124
+ let end = start + width(first);
125
+ if (isDelimiter(first)) {
126
+ // A word of its own.
127
+ }
128
+ else if (latin1 || !isCjk(first)) {
129
+ while (end < n) {
130
+ const cp = text.codePointAt(end);
131
+ if (isDelimiter(cp) || (!latin1 && isCjkBase(cp)))
132
+ break;
133
+ end += width(cp);
134
+ }
135
+ }
136
+ else {
137
+ let scripted = !NO_SCRIPT_RE.test(String.fromCodePoint(first));
138
+ while (end < n) {
139
+ const cp = text.codePointAt(end);
140
+ const ch = String.fromCodePoint(cp);
141
+ if (JOINS_RE.test(ch)) {
142
+ end += width(cp);
143
+ continue;
144
+ }
145
+ if (!isCjk(cp))
146
+ break;
147
+ if (!NO_SCRIPT_RE.test(ch)) {
148
+ if (scripted)
149
+ break;
150
+ scripted = true;
151
+ }
152
+ end += width(cp);
153
+ }
154
+ }
155
+ words.push(text.slice(start, end));
156
+ start = end;
157
+ }
158
+ return words.length === 1 ? [text] : words;
159
+ }
160
+ //# sourceMappingURL=canvasWords.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasWords.js","sourceRoot":"","sources":["../src/canvasWords.ts"],"names":[],"mappings":"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"}
@@ -12,30 +12,92 @@
12
12
  * text object, so a line of Han with a Latin word paints from three or four
13
13
  * slices and each slice keeps only the glyphs set in it.
14
14
  *
15
+ * Which file sets a character two files hold (postext #770): a browser
16
+ * takes it from the `@font-face` rule declared last among those whose
17
+ * `unicode-range` holds it, and Fontsource's files overlap. A Japanese
18
+ * family repeats the ASCII letters in its `latin` file and in its numbered
19
+ * slices, and a pen declares the slices after `latin`: Chrome 154 sets the
20
+ * letters of `Shift_JIS` from slice 118 and the `_` from slice 113 of Noto
21
+ * Serif JP, 4252 thousandths of the em wide, where the `latin` file alone
22
+ * kerns `t_` and `_J` and sets it 4318 wide. A provider that hands each
23
+ * file with its range and its place in the declaration (`PdfFontFile`)
24
+ * gets each character from the file the browser took it from
25
+ * ({@link FaceFiles.declaredFile}), in a document shaped in its own
26
+ * language; one that hands bare files gets it from the first file with a
27
+ * glyph, as before, and so does a document that keeps the host's language.
28
+ *
15
29
  * Characters a face lacks in every file are recorded as they are shaped
16
30
  * ({@link noteMissingGlyphs}), for the `missingGlyph` warning.
17
31
  */
18
32
  import type { PDFFont } from 'pdf-lib';
33
+ /** What a provider says of a file of a face (`PdfFontFile`): the
34
+ * characters its `@font-face` rule answers for, and where the rule is
35
+ * declared among the rules of the face. */
36
+ export interface FileDeclaration {
37
+ unicodeRange?: string;
38
+ order?: number;
39
+ }
19
40
  /** The files of one face, in the order their glyphs are looked up. */
20
41
  export declare class FaceFiles {
21
42
  readonly files: PDFFont[];
22
43
  private readonly faces;
23
44
  /** First file holding each code point, or null for none. */
24
45
  private readonly first;
25
- constructor(files: readonly PDFFont[]);
26
- /** Append a file (a slice fetched later); a file already listed is kept
27
- * where it is. */
28
- add(file: PDFFont): void;
46
+ /** The range each file was given with; a file with none answers for
47
+ * every character. */
48
+ private readonly ranges;
49
+ /** Where each file is declared: the number it was given with, else its
50
+ * place in the list counted down, the first file being the one
51
+ * declared last. */
52
+ private readonly orders;
53
+ /** The files from the one declared last to the one declared first. */
54
+ private lastDeclaredFirst;
55
+ /** The file a browser sets each code point in, or null for none. */
56
+ private readonly declared;
57
+ private hasRanges;
58
+ constructor(files: readonly PDFFont[], declarations?: ReadonlyMap<PDFFont, FileDeclaration>);
59
+ /** Append a file (a slice fetched later), with what the provider says
60
+ * of it; a file already listed is kept where it is, as it was given. */
61
+ add(file: PDFFont, declaration?: FileDeclaration): void;
62
+ /** Whether the provider gave a file of the face with its range: the
63
+ * face is then read as a browser reads its `@font-face` rules
64
+ * ({@link declaredFile}). */
65
+ get ranged(): boolean;
29
66
  /** Whether `file` has a glyph for `cp`. */
30
67
  covers(file: PDFFont, cp: number): boolean;
68
+ /** Whether `file` answers for `cp` as a browser reads it: its range
69
+ * holds the character and the file has a glyph for it. Chrome 154 skips
70
+ * a file whose range claims a character it has no glyph for and takes
71
+ * the next one down, and never draws a glyph a file has outside its
72
+ * range. */
73
+ private answers;
31
74
  /** The first file with a glyph for `cp`, or null when none has one. */
32
75
  fileFor(cp: number): PDFFont | null;
76
+ /** The files a browser tries in turn: the one declared last first.
77
+ * Files of one number keep the order they were given in. */
78
+ private byDeclaration;
79
+ /**
80
+ * The file a browser sets `cp` in: the one declared last among those
81
+ * whose range holds it and that have a glyph for it. Null when none
82
+ * does: the browser then leaves the family, for the next one of the
83
+ * `font-family` list or a font of the system.
84
+ */
85
+ declaredFile(cp: number): PDFFont | null;
86
+ /**
87
+ * The file a browser sets a letter and the marks on it in (`cps`, the
88
+ * letter first): the one declared last that answers for every one of
89
+ * them. With Source Serif 4 declared first and Noto Serif JP after it
90
+ * for `a` alone, Chrome sets `a` in the second (557 thousandths) and
91
+ * `a` + U+0308 whole in the first (541). Null when no file sets them
92
+ * all.
93
+ */
94
+ declaredFileOfAll(cps: readonly number[]): PDFFont | null;
33
95
  }
34
96
  /** Register the files of a face under its first file, the font the
35
97
  * renderers are handed. A first file already registered (two faces the
36
98
  * provider answered with the same files) keeps its list, which takes the
37
99
  * new files too; the list in force is returned. */
38
- export declare function registerFaceFiles(primary: PDFFont, files: readonly PDFFont[]): FaceFiles;
100
+ export declare function registerFaceFiles(primary: PDFFont, files: readonly PDFFont[], declarations?: ReadonlyMap<PDFFont, FileDeclaration>): FaceFiles;
39
101
  /** The files of the face whose first file is `font`, when it has several. */
40
102
  export declare function faceFilesOf(font: PDFFont): FaceFiles | undefined;
41
103
  /** A run of text and the file it is set in. */
@@ -46,6 +108,9 @@ export interface FileRun {
46
108
  * and the text it was cut out of does (`besideLetterOf`, #764): the
47
109
  * caller shapes the run inside `withBesideLetter`. */
48
110
  beside?: number;
111
+ /** The run is a word, or part of one, of a string with a character
112
+ * above U+00FF ({@link wordRuns}): with no letter it has no script. */
113
+ wide?: true;
49
114
  }
50
115
  /**
51
116
  * `text` cut into runs by the file of `font`'s face that has a glyph for
@@ -85,8 +150,31 @@ export interface FileRun {
85
150
  * `خالة.»`, which the `latin` file draws), says the letter whose script
86
151
  * the browser shaped it in (`beside`, #764), in a document shaped in its
87
152
  * own language.
153
+ *
154
+ * A face given with the ranges of its files is cut as a browser cuts it
155
+ * ({@link declaredRuns}, #770) in a document shaped in its own language,
156
+ * and none of the rules above that guess at the browser's file apply.
88
157
  */
89
158
  export declare function fileRuns(font: PDFFont, text: string, rtl?: boolean): FileRun[];
159
+ /**
160
+ * `text`, a string the canvas was handed whole (a line of a running head,
161
+ * a word of the body), cut into the runs it is shaped in: its words
162
+ * (`canvasWords.ts`, #770), each cut by file ({@link fileRuns}). The
163
+ * canvas shapes every word alone, so no kerned pair and no ligature
164
+ * crosses a space or stands between two CJK characters, and a word takes
165
+ * the script of its own letters: one with no letter, a space among them,
166
+ * has none in a string not kept in eight bits (`wide`).
167
+ *
168
+ * {@link fileRuns} itself, the string shaped whole as up to postext-pdf
169
+ * 1.29, for a string of one word, in a document that keeps the host's
170
+ * language, and with a script in force (#725: a word with no letter the
171
+ * layout measured in the script of its context).
172
+ */
173
+ export declare function wordRuns(font: PDFFont, text: string, rtl?: boolean): FileRun[];
174
+ /** Run `fn`, the shaping of `run` ({@link wordRuns}), with what the run
175
+ * says of its script in force: the letter it was cut from beside, and
176
+ * whether it is a word of a string not kept in eight bits. */
177
+ export declare function inRun<T>(run: FileRun, fn: () => T): T;
90
178
  /** `text`'s width at `size` (points), each run measured in its file. */
91
179
  export declare function widthOfTextAtSize(font: PDFFont, text: string, size: number): number;
92
180
  /** Record the characters of `text` that `font` has no glyph for (it draws
@@ -1 +1 @@
1
- {"version":3,"file":"faceFiles.d.ts","sourceRoot":"","sources":["../src/faceFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAsBvC,sEAAsE;AACtE,qBAAa,SAAS;IACpB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgD;IACtE,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;gBAE/C,KAAK,EAAE,SAAS,OAAO,EAAE;IAKrC;uBACmB;IACnB,GAAG,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAQxB,2CAA2C;IAC3C,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAI1C,uEAAuE;IACvE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;CAapC;AAID;;;oDAGoD;AACpD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,CASxF;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAGhE;AAED,+CAA+C;AAC/C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb;;2DAEuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,UAAQ,GAAG,OAAO,EAAE,CAmE5E;AAgCD,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnF;AAQD;;;6EAG6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAanE;AAED;yEACyE;AACzE,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAEtF;AAED;4BAC4B;AAC5B,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE9C"}
1
+ {"version":3,"file":"faceFiles.d.ts","sourceRoot":"","sources":["../src/faceFiles.ts"],"names":[],"mappings":"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"}
package/dist/faceFiles.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { mirroredCodePoint } from 'postext';
2
2
  import { isDefaultIgnorable, isFallbackHandled, substituteGlyph } from './pdf-backend/fallbackSpaces';
3
- import { besideLetterOf, shapingScriptLetter } from './shapingLanguage';
3
+ import { besideLetterOf, shapesAsCanvas, shapingScript, shapingScriptLetter, withBesideLetter, withinWideText } from './shapingLanguage';
4
+ import { canvasWords } from './canvasWords';
5
+ import { parseUnicodeRange, rangesHold } from './unicodeRange';
4
6
  function coverageFace(font) {
5
7
  const face = font.embedder?.font;
6
8
  return face && typeof face.hasGlyphForCodePoint === 'function' ? face : undefined;
@@ -17,27 +19,63 @@ export class FaceFiles {
17
19
  faces = new Map();
18
20
  /** First file holding each code point, or null for none. */
19
21
  first = new Map();
20
- constructor(files) {
22
+ /** The range each file was given with; a file with none answers for
23
+ * every character. */
24
+ ranges = new Map();
25
+ /** Where each file is declared: the number it was given with, else its
26
+ * place in the list counted down, the first file being the one
27
+ * declared last. */
28
+ orders = new Map();
29
+ /** The files from the one declared last to the one declared first. */
30
+ lastDeclaredFirst;
31
+ /** The file a browser sets each code point in, or null for none. */
32
+ declared = new Map();
33
+ hasRanges = false;
34
+ constructor(files, declarations) {
21
35
  this.files = [];
22
36
  for (const file of files)
23
- this.add(file);
37
+ this.add(file, declarations?.get(file));
24
38
  }
25
- /** Append a file (a slice fetched later); a file already listed is kept
26
- * where it is. */
27
- add(file) {
39
+ /** Append a file (a slice fetched later), with what the provider says
40
+ * of it; a file already listed is kept where it is, as it was given. */
41
+ add(file, declaration) {
28
42
  if (this.faces.has(file))
29
43
  return;
44
+ this.orders.set(file, declaration?.order ?? -this.files.length);
30
45
  this.files.push(file);
31
46
  this.faces.set(file, coverageFace(file));
32
- // A code point no file had may be in this one.
47
+ const ranges = declaration?.unicodeRange === undefined ? undefined : parseUnicodeRange(declaration.unicodeRange);
48
+ if (ranges) {
49
+ this.ranges.set(file, ranges);
50
+ this.hasRanges = true;
51
+ }
52
+ // A code point no file had may be in this one, and a file declared
53
+ // later than the one that set a character takes it.
33
54
  for (const [cp, hit] of this.first)
34
55
  if (hit === null)
35
56
  this.first.delete(cp);
57
+ this.declared.clear();
58
+ this.lastDeclaredFirst = undefined;
59
+ }
60
+ /** Whether the provider gave a file of the face with its range: the
61
+ * face is then read as a browser reads its `@font-face` rules
62
+ * ({@link declaredFile}). */
63
+ get ranged() {
64
+ return this.hasRanges;
36
65
  }
37
66
  /** Whether `file` has a glyph for `cp`. */
38
67
  covers(file, cp) {
39
68
  return this.faces.get(file)?.hasGlyphForCodePoint(cp) ?? false;
40
69
  }
70
+ /** Whether `file` answers for `cp` as a browser reads it: its range
71
+ * holds the character and the file has a glyph for it. Chrome 154 skips
72
+ * a file whose range claims a character it has no glyph for and takes
73
+ * the next one down, and never draws a glyph a file has outside its
74
+ * range. */
75
+ answers(file, cp) {
76
+ const ranges = this.ranges.get(file);
77
+ return (ranges === undefined || rangesHold(ranges, cp)) && this.covers(file, cp);
78
+ }
41
79
  /** The first file with a glyph for `cp`, or null when none has one. */
42
80
  fileFor(cp) {
43
81
  const hit = this.first.get(cp);
@@ -53,20 +91,54 @@ export class FaceFiles {
53
91
  this.first.set(cp, found);
54
92
  return found;
55
93
  }
94
+ /** The files a browser tries in turn: the one declared last first.
95
+ * Files of one number keep the order they were given in. */
96
+ byDeclaration() {
97
+ if (!this.lastDeclaredFirst) {
98
+ const at = new Map(this.files.map((file, i) => [file, i]));
99
+ this.lastDeclaredFirst = [...this.files].sort((a, b) => this.orders.get(b) - this.orders.get(a) || at.get(a) - at.get(b));
100
+ }
101
+ return this.lastDeclaredFirst;
102
+ }
103
+ /**
104
+ * The file a browser sets `cp` in: the one declared last among those
105
+ * whose range holds it and that have a glyph for it. Null when none
106
+ * does: the browser then leaves the family, for the next one of the
107
+ * `font-family` list or a font of the system.
108
+ */
109
+ declaredFile(cp) {
110
+ const hit = this.declared.get(cp);
111
+ if (hit !== undefined)
112
+ return hit;
113
+ const found = this.byDeclaration().find((file) => this.answers(file, cp)) ?? null;
114
+ this.declared.set(cp, found);
115
+ return found;
116
+ }
117
+ /**
118
+ * The file a browser sets a letter and the marks on it in (`cps`, the
119
+ * letter first): the one declared last that answers for every one of
120
+ * them. With Source Serif 4 declared first and Noto Serif JP after it
121
+ * for `a` alone, Chrome sets `a` in the second (557 thousandths) and
122
+ * `a` + U+0308 whole in the first (541). Null when no file sets them
123
+ * all.
124
+ */
125
+ declaredFileOfAll(cps) {
126
+ return this.byDeclaration().find((file) => cps.every((cp) => this.answers(file, cp))) ?? null;
127
+ }
56
128
  }
57
129
  const faceFiles = new WeakMap();
58
130
  /** Register the files of a face under its first file, the font the
59
131
  * renderers are handed. A first file already registered (two faces the
60
132
  * provider answered with the same files) keeps its list, which takes the
61
133
  * new files too; the list in force is returned. */
62
- export function registerFaceFiles(primary, files) {
134
+ export function registerFaceFiles(primary, files, declarations) {
63
135
  const known = faceFiles.get(primary);
64
136
  if (known) {
65
137
  for (const file of files)
66
- known.add(file);
138
+ known.add(file, declarations?.get(file));
67
139
  return known;
68
140
  }
69
- const list = new FaceFiles([primary, ...files]);
141
+ const list = new FaceFiles([primary, ...files], declarations);
70
142
  faceFiles.set(primary, list);
71
143
  return list;
72
144
  }
@@ -113,11 +185,17 @@ export function faceFilesOf(font) {
113
185
  * `خالة.»`, which the `latin` file draws), says the letter whose script
114
186
  * the browser shaped it in (`beside`, #764), in a document shaped in its
115
187
  * own language.
188
+ *
189
+ * A face given with the ranges of its files is cut as a browser cuts it
190
+ * ({@link declaredRuns}, #770) in a document shaped in its own language,
191
+ * and none of the rules above that guess at the browser's file apply.
116
192
  */
117
193
  export function fileRuns(font, text, rtl = false) {
118
194
  const list = faceFilesOf(font);
119
195
  if (!list)
120
196
  return [{ font, text }];
197
+ if (setsAsDeclared(list))
198
+ return besideLetters(declaredRuns(list, font, text, rtl), text, font);
121
199
  const letter = shapingScriptLetter();
122
200
  if (letter !== undefined) {
123
201
  const file = list.fileFor(letter);
@@ -177,6 +255,11 @@ export function fileRuns(font, text, rtl = false) {
177
255
  else
178
256
  runs.push({ font, text: pending });
179
257
  }
258
+ return besideLetters(runs, text, font);
259
+ }
260
+ /** `runs`, the runs `text` was cut into, each saying the letter it was
261
+ * cut from beside when it has none; one run in `font` for no run. */
262
+ function besideLetters(runs, text, font) {
180
263
  if (runs.length > 1) {
181
264
  // The runs follow each other through the text.
182
265
  let at = 0;
@@ -189,6 +272,169 @@ export function fileRuns(font, text, rtl = false) {
189
272
  }
190
273
  return runs.length > 0 ? runs : [{ font, text }];
191
274
  }
275
+ /**
276
+ * Whether the text being painted takes each character from the file a
277
+ * browser takes it from: a face whose provider gave its files with their
278
+ * ranges, in a document shaped in its own language.
279
+ *
280
+ * The page chose the file of a character the same way whatever language
281
+ * it shaped the text in, but a document that keeps the host's language is
282
+ * shaped string by string, each run in the script it shows, as it always
283
+ * was, and prints the same bytes. Cut by the browser's files, its strings
284
+ * fall into more runs with no letter, which that shaping gives no script:
285
+ * in Noto Serif SC the space is another slice's than the letters and the
286
+ * digits, so `GB/T 15834` became `GB/T`, a space and `15834`, and the
287
+ * digits alone took the proportional forms of the default script, 0.80 pt
288
+ * short of the tabular ones the page measured in a Latin string (15.75
289
+ * pt, the Cookbook's Chinese official document). `renderToPdf` drops what
290
+ * the files say when no document of the render is shaped in its own
291
+ * language (`withBareFiles`); this is for a page of one that is not, in a
292
+ * render that has one.
293
+ */
294
+ function setsAsDeclared(list) {
295
+ return list.ranged && shapesAsCanvas();
296
+ }
297
+ /** A combining mark: it is set in the file of the letter it stands on. */
298
+ const MARK_RE = /^\p{M}$/u;
299
+ /**
300
+ * `text` cut into runs by the file a browser sets each character in, for
301
+ * a face given with the ranges of its files (postext #770). As measured
302
+ * in Chrome 154 (two files that draw the same letters at other widths,
303
+ * declared as one family, on a canvas in a page and in a worker):
304
+ *
305
+ * - a character is set in the file declared last among those whose range
306
+ * holds it: of two `FontFace` objects, the one added to the font set
307
+ * last, whichever was made or loaded first; of two `@font-face` rules,
308
+ * the later one in the document; and a `FontFace` added by script
309
+ * rather than any rule of a style sheet, whichever came first;
310
+ * - a file with no range answers for every character;
311
+ * - a file whose range claims a character it has no glyph for is passed
312
+ * over for the next one down, and a glyph a file has outside its range
313
+ * is never drawn;
314
+ * - nothing stays in a file because the character before it was set
315
+ * there: `Aa` with the capitals in one file is `A` from it and `a` from
316
+ * the other, 1269 thousandths, where either file alone gives 1253 or
317
+ * 1274. A space goes to the file whose range holds it, though every
318
+ * file has one;
319
+ * - a letter and the marks on it are set in one file, the one declared
320
+ * last that answers for them all. (Chrome also composes the pair where
321
+ * a file answers for the composed character: `o` + U+0304 is the `ō` of
322
+ * `latin-ext`. The PDF's shapers compose nothing, so here the pair goes
323
+ * to a file that has both.)
324
+ *
325
+ * So no character follows the one before it here, an opening bracket does
326
+ * not go with what it opens and a word shaped in a script is not looked up
327
+ * by a letter of that script: the browser did none of that. What stays is
328
+ * what the shapers need: a zero width joiner or non-joiner with the letter
329
+ * it joins, an invisible character in the run it stands in.
330
+ *
331
+ * A character no file answers for was drawn by the browser in another
332
+ * family; here it is set as in a face given without ranges: in the run
333
+ * before it when that file has a glyph, else in the first file that has
334
+ * one (its mirror's, in a right-to-left run), and reported as missing
335
+ * when none has.
336
+ */
337
+ function declaredRuns(list, font, text, rtl) {
338
+ const runs = [];
339
+ let current;
340
+ // Joining controls waiting for the letter after them.
341
+ let pending = '';
342
+ let previous = 0;
343
+ // The file of the letter whose marks follow, up to `wholeEnd`.
344
+ let whole = null;
345
+ let wholeEnd = 0;
346
+ const chars = [...text];
347
+ for (let at = 0; at < chars.length; at++) {
348
+ const ch = chars[at];
349
+ const cp = ch.codePointAt(0);
350
+ if (isJoiningControl(cp) && (!current || !joinsOnto(previous))) {
351
+ pending += ch;
352
+ continue;
353
+ }
354
+ let file;
355
+ if (at < wholeEnd) {
356
+ file = whole;
357
+ }
358
+ else if (current && isDefaultIgnorable(cp)) {
359
+ file = current.font;
360
+ }
361
+ else if (MARK_RE.test(ch)) {
362
+ // A mark whose letter no file sets with it: beside its letter when
363
+ // that file has a glyph for it.
364
+ file = current && list.covers(current.font, cp) ? current.font : list.declaredFile(cp);
365
+ }
366
+ else {
367
+ let end = at + 1;
368
+ while (end < chars.length && MARK_RE.test(chars[end]))
369
+ end++;
370
+ if (end > at + 1) {
371
+ whole = list.declaredFileOfAll(chars.slice(at, end).map((c) => c.codePointAt(0)));
372
+ wholeEnd = whole ? end : 0;
373
+ }
374
+ file = (at < wholeEnd ? whole : null) ?? list.declaredFile(cp);
375
+ }
376
+ if (!file) {
377
+ const mirror = rtl ? mirroredCodePoint(cp) : cp;
378
+ const shown = mirror !== cp && list.fileFor(mirror) ? mirror : cp;
379
+ file = current && (list.covers(current.font, shown) || isDefaultIgnorable(cp)) ? current.font : list.fileFor(shown) ?? current?.font ?? font;
380
+ }
381
+ if (current && current.font === file) {
382
+ current.text += pending + ch;
383
+ }
384
+ else {
385
+ current = { font: file, text: pending + ch };
386
+ runs.push(current);
387
+ }
388
+ pending = '';
389
+ previous = cp;
390
+ }
391
+ if (pending) {
392
+ if (current)
393
+ current.text += pending;
394
+ else
395
+ runs.push({ font, text: pending });
396
+ }
397
+ return runs;
398
+ }
399
+ /** A string a browser keeps in eight bits: no character above U+00FF. */
400
+ const LATIN_1_RE = /^[\u0000-\u00FF]*$/;
401
+ /**
402
+ * `text`, a string the canvas was handed whole (a line of a running head,
403
+ * a word of the body), cut into the runs it is shaped in: its words
404
+ * (`canvasWords.ts`, #770), each cut by file ({@link fileRuns}). The
405
+ * canvas shapes every word alone, so no kerned pair and no ligature
406
+ * crosses a space or stands between two CJK characters, and a word takes
407
+ * the script of its own letters: one with no letter, a space among them,
408
+ * has none in a string not kept in eight bits (`wide`).
409
+ *
410
+ * {@link fileRuns} itself, the string shaped whole as up to postext-pdf
411
+ * 1.29, for a string of one word, in a document that keeps the host's
412
+ * language, and with a script in force (#725: a word with no letter the
413
+ * layout measured in the script of its context).
414
+ */
415
+ export function wordRuns(font, text, rtl = false) {
416
+ if (!shapesAsCanvas() || shapingScript() !== undefined)
417
+ return fileRuns(font, text, rtl);
418
+ const words = canvasWords(text);
419
+ if (words.length === 1)
420
+ return fileRuns(font, text, rtl);
421
+ const wide = !LATIN_1_RE.test(text);
422
+ const runs = [];
423
+ for (const word of words) {
424
+ for (const run of fileRuns(font, word, rtl)) {
425
+ if (wide)
426
+ run.wide = true;
427
+ runs.push(run);
428
+ }
429
+ }
430
+ return runs;
431
+ }
432
+ /** Run `fn`, the shaping of `run` ({@link wordRuns}), with what the run
433
+ * says of its script in force: the letter it was cut from beside, and
434
+ * whether it is a word of a string not kept in eight bits. */
435
+ export function inRun(run, fn) {
436
+ return run.wide ? withinWideText(true, () => withBesideLetter(run.beside, fn)) : withBesideLetter(run.beside, fn);
437
+ }
192
438
  /** Opening brackets and initial quotation marks (`Ps`, `Pi`). */
193
439
  const OPENING_RE = /^[\p{Ps}\p{Pi}]$/u;
194
440
  /** The file the first character after a row of opening marks (from `from`)