pagyra-js 0.0.13 → 0.0.15
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.
- package/dist/browser/pagyra.min.js +32 -32
- package/dist/browser/pagyra.min.js.map +4 -4
- package/dist/src/pdf/font/binary-writer.d.ts +18 -0
- package/dist/src/pdf/font/binary-writer.js +67 -0
- package/dist/src/pdf/font/builtin-fonts.browser.js +1 -1
- package/dist/src/pdf/font/embedder.d.ts +1 -0
- package/dist/src/pdf/font/embedder.js +20 -23
- package/dist/src/pdf/font/font-registry.js +4 -25
- package/dist/src/pdf/font/font-subset.d.ts +7 -4
- package/dist/src/pdf/font/font-subset.js +329 -124
- package/dist/src/pdf/font/managers/subset-resource-manager.js +14 -2
- package/dist/src/pdf/font/ttf-lite.js +2 -1
- package/dist/src/pdf/font/ttf-table-parser.d.ts +1 -0
- package/dist/src/pdf/font/ttf-table-parser.js +7 -0
- package/dist/src/pdf/renderers/text-renderer.js +17 -16
- package/dist/src/pdf/renderers/text-shadow-renderer.d.ts +4 -0
- package/dist/src/pdf/renderers/text-shadow-renderer.js +34 -74
- package/dist/src/types/fonts.d.ts +9 -1
- package/dist/src/types/fonts.js +6 -1
- package/dist/tests/verify-subset-multi.spec.d.ts +1 -0
- package/dist/tests/verify-subset-multi.spec.js +36 -0
- package/dist/tests/verify-subset.spec.d.ts +1 -0
- package/dist/tests/verify-subset.spec.js +35 -0
- package/package.json +1 -1
|
@@ -1,71 +1,90 @@
|
|
|
1
1
|
import { createToUnicodeCMapText } from "./to-unicode.js";
|
|
2
|
+
import { BinaryWriter } from "./binary-writer.js";
|
|
3
|
+
import { LocaTableReader } from "./loca-reader.js";
|
|
4
|
+
import { DefaultDataViewReader } from "./ttf-table-provider.js";
|
|
2
5
|
/**
|
|
3
6
|
* Creates a PDF font subset for TTF fonts.
|
|
4
7
|
*/
|
|
5
8
|
export function createPdfFontSubset(options) {
|
|
6
9
|
const { baseName, fontMetrics, fontProgram, usedGlyphIds } = options;
|
|
7
10
|
const encoding = options.encoding ?? "identity";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
// 1. Compute closure of used glyphs (checking composite glyphs)
|
|
12
|
+
const initialGids = Array.from(usedGlyphIds);
|
|
13
|
+
if (!initialGids.includes(0)) {
|
|
14
|
+
initialGids.push(0); // Always include .notdef
|
|
11
15
|
}
|
|
12
|
-
const
|
|
13
|
-
if (encoding === "identity") {
|
|
14
|
-
for (const gid of glyphIds) {
|
|
15
|
-
gidToCharCode.set(gid, gid);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
for (let i = 0; i < glyphIds.length; i++) {
|
|
20
|
-
gidToCharCode.set(glyphIds[i], i);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const firstChar = encoding === "identity" ? Math.min(...glyphIds) : 0;
|
|
24
|
-
const lastChar = encoding === "identity" ? Math.max(...glyphIds) : glyphIds.length - 1;
|
|
16
|
+
const { glyphIds, gidMap } = computeSubsetClosure(fontProgram, initialGids);
|
|
25
17
|
const unitsPerEm = fontMetrics.metrics.unitsPerEm;
|
|
26
18
|
const widths = [];
|
|
27
|
-
|
|
19
|
+
const usedGidSet = new Set(glyphIds);
|
|
20
|
+
let firstChar;
|
|
21
|
+
let lastChar;
|
|
22
|
+
if (encoding === "sequential") {
|
|
23
|
+
firstChar = 0;
|
|
24
|
+
lastChar = glyphIds.length - 1;
|
|
28
25
|
for (const gid of glyphIds) {
|
|
29
26
|
const glyphMetric = fontMetrics.glyphMetrics.get(gid);
|
|
30
27
|
const advanceWidth = glyphMetric?.advanceWidth ?? 0;
|
|
31
|
-
|
|
32
|
-
widths.push(pdfWidth);
|
|
28
|
+
widths.push(Math.round((advanceWidth / unitsPerEm) * 1000));
|
|
33
29
|
}
|
|
34
30
|
}
|
|
35
31
|
else {
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
// Identity: CIDs = Original GIDs.
|
|
33
|
+
// We use a sparse range from 0 to maxGid to ensure stability.
|
|
34
|
+
const maxGid = Math.max(...glyphIds);
|
|
35
|
+
firstChar = 0;
|
|
36
|
+
lastChar = maxGid;
|
|
37
|
+
for (let i = 0; i <= maxGid; i++) {
|
|
38
|
+
const glyphMetric = fontMetrics.glyphMetrics.get(i);
|
|
38
39
|
const advanceWidth = glyphMetric?.advanceWidth ?? 0;
|
|
39
|
-
|
|
40
|
-
widths.push(pdfWidth);
|
|
40
|
+
widths.push(Math.round((advanceWidth / unitsPerEm) * 1000));
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Generate ToUnicode CMap
|
|
44
|
+
// we map CID -> Unicode.
|
|
43
45
|
const cmapEntries = [];
|
|
44
46
|
const unicodeMap = fontMetrics.cmap.unicodeMap;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const gidToUnicodes = new Map();
|
|
48
|
+
for (const [cp, mappedGid] of unicodeMap.entries()) {
|
|
49
|
+
const list = gidToUnicodes.get(mappedGid) || [];
|
|
50
|
+
list.push(cp);
|
|
51
|
+
gidToUnicodes.set(mappedGid, list);
|
|
52
|
+
}
|
|
53
|
+
if (encoding === "sequential") {
|
|
54
|
+
for (let i = 0; i < glyphIds.length; i++) {
|
|
55
|
+
const originalGid = glyphIds[i];
|
|
56
|
+
const unicodes = gidToUnicodes.get(originalGid);
|
|
57
|
+
if (unicodes) {
|
|
58
|
+
for (const u of unicodes) {
|
|
59
|
+
cmapEntries.push({ gid: i, unicode: u });
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Identity: CID = Original GID
|
|
66
|
+
for (const gid of glyphIds) {
|
|
67
|
+
const unicodes = gidToUnicodes.get(gid);
|
|
68
|
+
if (unicodes) {
|
|
69
|
+
for (const u of unicodes) {
|
|
70
|
+
cmapEntries.push({ gid: gid, unicode: u });
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
const toUnicodeCMap = createToUnicodeCMapText(cmapEntries);
|
|
62
|
-
|
|
76
|
+
// Generate the binary TTF
|
|
77
|
+
const fontFile = subsetFont(fontProgram, glyphIds, encoding);
|
|
63
78
|
const encodeGlyph = (gid) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (encoding === "sequential") {
|
|
80
|
+
const index = gidMap.get(gid);
|
|
81
|
+
if (index === undefined)
|
|
82
|
+
throw new Error(`Glyph ID ${gid} not found in subset`);
|
|
83
|
+
return index;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return gid;
|
|
67
87
|
}
|
|
68
|
-
return charCode;
|
|
69
88
|
};
|
|
70
89
|
return {
|
|
71
90
|
name: `/${baseName}`,
|
|
@@ -76,113 +95,299 @@ export function createPdfFontSubset(options) {
|
|
|
76
95
|
fontFile,
|
|
77
96
|
encodeGlyph,
|
|
78
97
|
glyphIds,
|
|
98
|
+
gidMap
|
|
79
99
|
};
|
|
80
100
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
function computeSubsetClosure(fontProgram, initialGids) {
|
|
102
|
+
const reader = new DefaultDataViewReader();
|
|
103
|
+
const glyfData = fontProgram.getRawTableData?.("glyf");
|
|
104
|
+
const locaData = fontProgram.getRawTableData?.("loca");
|
|
105
|
+
const headData = fontProgram.getRawTableData?.("head");
|
|
106
|
+
const used = new Set(initialGids);
|
|
107
|
+
const stack = [...initialGids];
|
|
108
|
+
if (!glyfData || !locaData || !headData) {
|
|
109
|
+
const sorted = Array.from(used).sort((a, b) => a - b);
|
|
110
|
+
const map = new Map();
|
|
111
|
+
sorted.forEach((gid, i) => map.set(gid, i));
|
|
112
|
+
return { glyphIds: sorted, gidMap: map };
|
|
113
|
+
}
|
|
114
|
+
const headView = new DataView(headData.buffer, headData.byteOffset, headData.byteLength);
|
|
115
|
+
const locaView = new DataView(locaData.buffer, locaData.byteOffset, locaData.byteLength);
|
|
116
|
+
const glyfView = new DataView(glyfData.buffer, glyfData.byteOffset, glyfData.byteLength);
|
|
117
|
+
const indexToLocFormat = reader.getUint16(headView, 50);
|
|
118
|
+
const locaReader = new LocaTableReader(locaView, indexToLocFormat, reader);
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
const gid = stack.pop();
|
|
121
|
+
const offset = locaReader.getGlyphOffset(gid);
|
|
122
|
+
if (!offset || offset.start === offset.end)
|
|
123
|
+
continue;
|
|
124
|
+
if (offset.start + 2 > glyfView.byteLength)
|
|
125
|
+
continue;
|
|
126
|
+
const numberOfContours = reader.getInt16(glyfView, offset.start);
|
|
127
|
+
if (numberOfContours < 0) {
|
|
128
|
+
let pos = offset.start + 10;
|
|
129
|
+
let flags;
|
|
130
|
+
do {
|
|
131
|
+
if (pos + 4 > glyfView.byteLength)
|
|
132
|
+
break;
|
|
133
|
+
flags = reader.getUint16(glyfView, pos);
|
|
134
|
+
const componentGid = reader.getUint16(glyfView, pos + 2);
|
|
135
|
+
if (!used.has(componentGid)) {
|
|
136
|
+
used.add(componentGid);
|
|
137
|
+
stack.push(componentGid);
|
|
138
|
+
}
|
|
139
|
+
pos += 4;
|
|
140
|
+
const arg1And2AreWords = (flags & 0x0001) !== 0;
|
|
141
|
+
if (arg1And2AreWords)
|
|
142
|
+
pos += 4;
|
|
143
|
+
else
|
|
144
|
+
pos += 2;
|
|
145
|
+
const weHaveAScale = (flags & 0x0008) !== 0;
|
|
146
|
+
const weHaveAnXAndYScale = (flags & 0x0040) !== 0;
|
|
147
|
+
const weHaveATwoByTwo = (flags & 0x0080) !== 0;
|
|
148
|
+
if (weHaveAScale)
|
|
149
|
+
pos += 2;
|
|
150
|
+
else if (weHaveAnXAndYScale)
|
|
151
|
+
pos += 4;
|
|
152
|
+
else if (weHaveATwoByTwo)
|
|
153
|
+
pos += 8;
|
|
154
|
+
} while (flags & 0x0020);
|
|
155
|
+
}
|
|
88
156
|
}
|
|
89
|
-
|
|
90
|
-
|
|
157
|
+
const sorted = Array.from(used).sort((a, b) => a - b);
|
|
158
|
+
const map = new Map();
|
|
159
|
+
sorted.forEach((gid, i) => map.set(gid, i));
|
|
160
|
+
return { glyphIds: sorted, gidMap: map };
|
|
91
161
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
162
|
+
function subsetFont(fontProgram, glyphIds, encoding) {
|
|
163
|
+
const reader = new DefaultDataViewReader();
|
|
164
|
+
const getTable = (tag) => fontProgram.getRawTableData?.(tag);
|
|
165
|
+
const head = getTable("head");
|
|
166
|
+
const hhea = getTable("hhea");
|
|
167
|
+
const maxp = getTable("maxp");
|
|
168
|
+
const hmtx = getTable("hmtx");
|
|
169
|
+
const loca = getTable("loca");
|
|
170
|
+
const glyf = getTable("glyf");
|
|
171
|
+
const OS2 = getTable("OS/2");
|
|
172
|
+
if (!head || !hhea || !maxp || !hmtx || !loca || !glyf) {
|
|
173
|
+
throw new Error("Missing required tables for subsetting");
|
|
174
|
+
}
|
|
175
|
+
const headView = new DataView(head.buffer, head.byteOffset, head.byteLength);
|
|
176
|
+
const locaView = new DataView(loca.buffer, loca.byteOffset, loca.byteLength);
|
|
177
|
+
const glyfView = new DataView(glyf.buffer, glyf.byteOffset, glyf.byteLength);
|
|
178
|
+
const hmtxView = new DataView(hmtx.buffer, hmtx.byteOffset, hmtx.byteLength);
|
|
179
|
+
const hheaView = new DataView(hhea.buffer, hhea.byteOffset, hhea.byteLength);
|
|
180
|
+
const indexToLocFormat = reader.getUint16(headView, 50);
|
|
181
|
+
const locaReader = new LocaTableReader(locaView, indexToLocFormat, reader);
|
|
182
|
+
const numberOfHMetrics = reader.getUint16(hheaView, 34);
|
|
183
|
+
const newLoca = new BinaryWriter();
|
|
184
|
+
const newGlyf = new BinaryWriter();
|
|
185
|
+
const usedGidSet = new Set(glyphIds);
|
|
186
|
+
let maxGidToEmit;
|
|
187
|
+
if (encoding === "sequential") {
|
|
188
|
+
maxGidToEmit = glyphIds.length - 1;
|
|
189
|
+
const gidMap = new Map();
|
|
190
|
+
glyphIds.forEach((gid, i) => gidMap.set(gid, i));
|
|
191
|
+
for (const originalGid of glyphIds) {
|
|
192
|
+
newLoca.writeUint32(newGlyf.byteLength());
|
|
193
|
+
const range = locaReader.getGlyphOffset(originalGid);
|
|
194
|
+
if (range && range.end > range.start) {
|
|
195
|
+
const glyphData = new Uint8Array(glyf.buffer, glyf.byteOffset + range.start, range.end - range.start);
|
|
196
|
+
const numberOfContours = reader.getInt16(new DataView(glyphData.buffer, glyphData.byteOffset, glyphData.byteLength), 0);
|
|
197
|
+
if (numberOfContours < 0) {
|
|
198
|
+
const newGlyphData = new Uint8Array(glyphData);
|
|
199
|
+
const view = new DataView(newGlyphData.buffer);
|
|
200
|
+
let pos = 10;
|
|
201
|
+
let flags;
|
|
202
|
+
do {
|
|
203
|
+
flags = reader.getUint16(view, pos);
|
|
204
|
+
const oldCompGid = reader.getUint16(view, pos + 2);
|
|
205
|
+
view.setUint16(pos + 2, gidMap.get(oldCompGid) ?? 0, false);
|
|
206
|
+
pos += 4;
|
|
207
|
+
const arg1And2AreWords = (flags & 0x0001) !== 0;
|
|
208
|
+
if (arg1And2AreWords)
|
|
209
|
+
pos += 4;
|
|
210
|
+
else
|
|
211
|
+
pos += 2;
|
|
212
|
+
const weHaveAScale = (flags & 0x0008) !== 0;
|
|
213
|
+
const weHaveAnXAndYScale = (flags & 0x0040) !== 0;
|
|
214
|
+
const weHaveATwoByTwo = (flags & 0x0080) !== 0;
|
|
215
|
+
if (weHaveAScale)
|
|
216
|
+
pos += 2;
|
|
217
|
+
else if (weHaveAnXAndYScale)
|
|
218
|
+
pos += 4;
|
|
219
|
+
else if (weHaveATwoByTwo)
|
|
220
|
+
pos += 8;
|
|
221
|
+
} while (flags & 0x0020);
|
|
222
|
+
newGlyf.writeBytes(newGlyphData);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
newGlyf.writeBytes(glyphData);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Identity Mode: Sparse TTF
|
|
232
|
+
maxGidToEmit = Math.max(...glyphIds);
|
|
233
|
+
for (let gid = 0; gid <= maxGidToEmit; gid++) {
|
|
234
|
+
newLoca.writeUint32(newGlyf.byteLength());
|
|
235
|
+
if (usedGidSet.has(gid)) {
|
|
236
|
+
const range = locaReader.getGlyphOffset(gid);
|
|
237
|
+
if (range && range.end > range.start) {
|
|
238
|
+
const glyphData = new Uint8Array(glyf.buffer, glyf.byteOffset + range.start, range.end - range.start);
|
|
239
|
+
newGlyf.writeBytes(glyphData);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
newLoca.writeUint32(newGlyf.byteLength());
|
|
245
|
+
// 2. MAXP
|
|
246
|
+
const newMaxp = new BinaryWriter();
|
|
247
|
+
newMaxp.writeBytes(new Uint8Array(maxp.buffer, maxp.byteOffset, 6));
|
|
248
|
+
const maxpView = new DataView(newMaxp.getData().buffer);
|
|
249
|
+
maxpView.setUint16(4, maxGidToEmit + 1, false);
|
|
250
|
+
if (maxp.byteLength > 6) {
|
|
251
|
+
newMaxp.writeBytes(new Uint8Array(maxp.buffer, maxp.byteOffset + 6, Math.min(maxp.byteLength - 6, 26)));
|
|
252
|
+
}
|
|
253
|
+
// 3. HMTX
|
|
254
|
+
const newHmtx = new BinaryWriter();
|
|
255
|
+
if (encoding === "sequential") {
|
|
256
|
+
for (const originalGid of glyphIds) {
|
|
257
|
+
let advance = 0, lsb = 0;
|
|
258
|
+
if (originalGid < numberOfHMetrics) {
|
|
259
|
+
advance = reader.getUint16(hmtxView, originalGid * 4);
|
|
260
|
+
lsb = reader.getInt16(hmtxView, originalGid * 4 + 2);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
advance = reader.getUint16(hmtxView, (numberOfHMetrics - 1) * 4);
|
|
264
|
+
const lsbOffset = numberOfHMetrics * 4 + (originalGid - numberOfHMetrics) * 2;
|
|
265
|
+
if (lsbOffset + 2 <= hmtxView.byteLength)
|
|
266
|
+
lsb = reader.getInt16(hmtxView, lsbOffset);
|
|
267
|
+
}
|
|
268
|
+
newHmtx.writeUint16(advance);
|
|
269
|
+
newHmtx.writeInt16(lsb);
|
|
105
270
|
}
|
|
106
271
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
272
|
+
else {
|
|
273
|
+
for (let gid = 0; gid <= maxGidToEmit; gid++) {
|
|
274
|
+
let advance = 0, lsb = 0;
|
|
275
|
+
if (gid < numberOfHMetrics) {
|
|
276
|
+
advance = reader.getUint16(hmtxView, gid * 4);
|
|
277
|
+
lsb = reader.getInt16(hmtxView, gid * 4 + 2);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
advance = reader.getUint16(hmtxView, (numberOfHMetrics - 1) * 4);
|
|
281
|
+
const lsbOffset = numberOfHMetrics * 4 + (gid - numberOfHMetrics) * 2;
|
|
282
|
+
if (lsbOffset + 2 <= hmtxView.byteLength)
|
|
283
|
+
lsb = reader.getInt16(hmtxView, lsbOffset);
|
|
284
|
+
}
|
|
285
|
+
newHmtx.writeUint16(advance);
|
|
286
|
+
newHmtx.writeInt16(lsb);
|
|
287
|
+
}
|
|
110
288
|
}
|
|
111
|
-
|
|
289
|
+
// 4. HHEA
|
|
290
|
+
const newHhea = new BinaryWriter();
|
|
291
|
+
newHhea.writeBytes(new Uint8Array(hhea));
|
|
292
|
+
const newHheaView = new DataView(newHhea.getData().buffer);
|
|
293
|
+
newHheaView.setUint16(34, maxGidToEmit + 1, false);
|
|
294
|
+
// 5. CMAP (Minimal Dummy)
|
|
295
|
+
const newCmap = new BinaryWriter();
|
|
296
|
+
newCmap.writeUint16(0);
|
|
297
|
+
newCmap.writeUint16(1);
|
|
298
|
+
newCmap.writeUint16(3);
|
|
299
|
+
newCmap.writeUint16(1);
|
|
300
|
+
newCmap.writeUint32(12);
|
|
301
|
+
const startCmap = newCmap.byteLength();
|
|
302
|
+
newCmap.writeUint16(4);
|
|
303
|
+
newCmap.writeUint16(0);
|
|
304
|
+
newCmap.writeUint16(0);
|
|
305
|
+
newCmap.writeUint16(0);
|
|
306
|
+
newCmap.writeUint16(0);
|
|
307
|
+
newCmap.writeUint16(0);
|
|
308
|
+
newCmap.writeUint16(0);
|
|
309
|
+
newCmap.writeUint16(0xFFFF);
|
|
310
|
+
newCmap.writeUint16(0);
|
|
311
|
+
newCmap.writeUint16(0);
|
|
312
|
+
newCmap.writeUint16(0);
|
|
313
|
+
newCmap.writeUint16(0);
|
|
314
|
+
const endCmap = newCmap.byteLength();
|
|
315
|
+
const cmapView = new DataView(newCmap.getData().buffer);
|
|
316
|
+
cmapView.setUint16(startCmap + 2, endCmap - startCmap, false);
|
|
317
|
+
// 6. HEAD
|
|
318
|
+
const newHead = new BinaryWriter();
|
|
319
|
+
newHead.writeBytes(new Uint8Array(head));
|
|
320
|
+
const newHeadView = new DataView(newHead.getData().buffer);
|
|
321
|
+
newHeadView.setUint16(50, 1, false);
|
|
322
|
+
newHeadView.setUint32(8, 0, false);
|
|
323
|
+
const tables = [
|
|
324
|
+
["head", newHead.getData()],
|
|
325
|
+
["hhea", newHhea.getData()],
|
|
326
|
+
["maxp", newMaxp.getData()],
|
|
327
|
+
["hmtx", newHmtx.getData()],
|
|
328
|
+
["loca", newLoca.getData()],
|
|
329
|
+
["glyf", newGlyf.getData()],
|
|
330
|
+
["cmap", newCmap.getData()]
|
|
331
|
+
];
|
|
332
|
+
if (OS2)
|
|
333
|
+
tables.push(["OS/2", new Uint8Array(OS2)]);
|
|
334
|
+
tables.sort((a, b) => a[0].localeCompare(b[0]));
|
|
335
|
+
const numTables = tables.length;
|
|
112
336
|
const searchRange = Math.pow(2, Math.floor(Math.log2(numTables))) * 16;
|
|
113
337
|
const entrySelector = Math.floor(Math.log2(numTables));
|
|
114
338
|
const rangeShift = numTables * 16 - searchRange;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const buffer = new ArrayBuffer(totalSize);
|
|
123
|
-
const view = new DataView(buffer);
|
|
124
|
-
const bytes = new Uint8Array(buffer);
|
|
125
|
-
view.setUint32(0, 0x00010000, false);
|
|
126
|
-
view.setUint16(4, numTables, false);
|
|
127
|
-
view.setUint16(6, searchRange, false);
|
|
128
|
-
view.setUint16(8, entrySelector, false);
|
|
129
|
-
view.setUint16(10, rangeShift, false);
|
|
130
|
-
let directoryOffset = 12;
|
|
131
|
-
let dataOffset = headerSize + directorySize;
|
|
339
|
+
const fullTtf = new BinaryWriter();
|
|
340
|
+
fullTtf.writeUint32(0x00010000);
|
|
341
|
+
fullTtf.writeUint16(numTables);
|
|
342
|
+
fullTtf.writeUint16(searchRange);
|
|
343
|
+
fullTtf.writeUint16(entrySelector);
|
|
344
|
+
fullTtf.writeUint16(rangeShift);
|
|
345
|
+
let offset = 12 + numTables * 16;
|
|
132
346
|
for (const [tag, data] of tables) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
347
|
+
fullTtf.writeBytes(new TextEncoder().encode(tag.padEnd(4)).slice(0, 4));
|
|
348
|
+
fullTtf.writeUint32(calculateChecksum(data));
|
|
349
|
+
fullTtf.writeUint32(offset);
|
|
350
|
+
fullTtf.writeUint32(data.length);
|
|
351
|
+
let paddedLength = data.length;
|
|
352
|
+
if (data.length % 4 !== 0)
|
|
353
|
+
paddedLength += 4 - (data.length % 4);
|
|
354
|
+
offset += paddedLength;
|
|
355
|
+
}
|
|
356
|
+
for (const [_, data] of tables) {
|
|
357
|
+
fullTtf.writeBytes(data);
|
|
358
|
+
const padding = (4 - (data.length % 4)) % 4;
|
|
359
|
+
for (let k = 0; k < padding; k++)
|
|
360
|
+
fullTtf.writeUint8(0);
|
|
361
|
+
}
|
|
362
|
+
const ttfBytes = fullTtf.getData();
|
|
363
|
+
const ttfView = new DataView(ttfBytes.buffer);
|
|
364
|
+
const fileChecksum = calculateChecksum(ttfBytes);
|
|
365
|
+
const adjustment = (0xB1B0AFBA - fileChecksum) >>> 0;
|
|
366
|
+
let headOffset = 0;
|
|
367
|
+
for (let i = 0; i < numTables; i++) {
|
|
368
|
+
const tag = new TextDecoder().decode(ttfBytes.slice(12 + i * 16, 12 + i * 16 + 4));
|
|
369
|
+
if (tag === "head") {
|
|
370
|
+
headOffset = ttfView.getUint32(12 + i * 16 + 8, false);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (headOffset > 0)
|
|
375
|
+
ttfView.setUint32(headOffset + 8, adjustment, false);
|
|
376
|
+
return ttfBytes;
|
|
146
377
|
}
|
|
147
|
-
|
|
148
|
-
* Calculate TTF table checksum
|
|
149
|
-
*/
|
|
150
|
-
function calculateTableChecksum(data) {
|
|
378
|
+
function calculateChecksum(data) {
|
|
151
379
|
let sum = 0;
|
|
152
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
153
380
|
const nLongs = Math.floor(data.length / 4);
|
|
154
|
-
|
|
381
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
382
|
+
for (let i = 0; i < nLongs; i++)
|
|
155
383
|
sum = (sum + view.getUint32(i * 4, false)) >>> 0;
|
|
156
|
-
}
|
|
157
384
|
const remaining = data.length % 4;
|
|
158
385
|
if (remaining > 0) {
|
|
159
386
|
let last = 0;
|
|
160
|
-
for (let i = 0; i < remaining; i++)
|
|
387
|
+
for (let i = 0; i < remaining; i++)
|
|
161
388
|
last = (last << 8) | data[nLongs * 4 + i];
|
|
162
|
-
}
|
|
163
389
|
last = last << ((4 - remaining) * 8);
|
|
164
390
|
sum = (sum + last) >>> 0;
|
|
165
391
|
}
|
|
166
392
|
return sum >>> 0;
|
|
167
393
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Update the checkSumAdjustment field in the head table
|
|
170
|
-
*/
|
|
171
|
-
function updateHeadChecksum(bytes, view, tables) {
|
|
172
|
-
const numTables = tables.size;
|
|
173
|
-
let headOffset = -1;
|
|
174
|
-
for (let i = 0; i < numTables; i++) {
|
|
175
|
-
const dirOffset = 12 + i * 16;
|
|
176
|
-
const tag = new TextDecoder().decode(bytes.slice(dirOffset, dirOffset + 4));
|
|
177
|
-
if (tag.trim() === 'head') {
|
|
178
|
-
headOffset = view.getUint32(dirOffset + 8, false);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (headOffset === -1)
|
|
183
|
-
return;
|
|
184
|
-
view.setUint32(headOffset + 8, 0, false);
|
|
185
|
-
const fileChecksum = calculateTableChecksum(bytes);
|
|
186
|
-
const adjustment = (0xB1B0AFBA - fileChecksum) >>> 0;
|
|
187
|
-
view.setUint32(headOffset + 8, adjustment, false);
|
|
188
|
-
}
|
|
@@ -62,12 +62,24 @@ export class SubsetResourceManager {
|
|
|
62
62
|
scaleTo1000(metrics.headBBox[3]),
|
|
63
63
|
]
|
|
64
64
|
: [-1000, -1000, 1000, 1000];
|
|
65
|
-
|
|
65
|
+
// Ensure we use the subset font file if available, falling back to full font only if subset is empty
|
|
66
|
+
const fontFile = (subset.fontFile && subset.fontFile.length > 0) ? subset.fontFile : (font.embedded?.subset ?? new Uint8Array(0));
|
|
66
67
|
if (!fontFile || fontFile.length === 0) {
|
|
67
68
|
log("font", "warn", "missing-font-file-for-subset", { baseFont: font.baseFont, alias: subset.name });
|
|
68
69
|
return font.ref;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
// Use widths from subset if available, otherwise compute from metrics
|
|
72
|
+
let DW = 1000;
|
|
73
|
+
let W = [];
|
|
74
|
+
if (subset.widths && subset.widths.length > 0) {
|
|
75
|
+
// For sequential subsets starting at CID 0
|
|
76
|
+
W = [subset.firstChar, subset.widths];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const result = computeWidths(metrics);
|
|
80
|
+
DW = result.DW;
|
|
81
|
+
W = result.W;
|
|
82
|
+
}
|
|
71
83
|
const subsetTag = subset.name.startsWith("/") ? subset.name.slice(1) : subset.name;
|
|
72
84
|
const subsetBaseName = `${subsetTag}+${font.baseFont}`;
|
|
73
85
|
const fontFileRef = this.doc.registerStream(fontFile, {});
|
|
@@ -26,7 +26,8 @@ export function parseTtfBuffer(buffer) {
|
|
|
26
26
|
const cmap = new CmapParser(parser, cmapTable);
|
|
27
27
|
const kerning = mergeKerningMaps(parseKerningTable(parser), parseGposKerning(parser));
|
|
28
28
|
const glyfProvider = createGlyfOutlineProvider(parser);
|
|
29
|
-
|
|
29
|
+
const getRawTableData = (tag) => parser.getRawTableDataString(tag);
|
|
30
|
+
return new TtfFontMetrics(metrics, glyphMetrics, cmap, headBBox, glyfProvider, kerning, getRawTableData);
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Parse the 'kern' table (format 0) into a nested map of adjustments in font units.
|
|
@@ -107,4 +107,11 @@ export class TtfTableParser {
|
|
|
107
107
|
throw new Error("Read beyond table bounds (getUint32)");
|
|
108
108
|
return table.getUint32(offset, false); // big-endian
|
|
109
109
|
}
|
|
110
|
+
getRawTableDataString(tagStr) {
|
|
111
|
+
const tag = tagStr.split('').reduce((acc, char) => (acc << 8) | char.charCodeAt(0), 0) >>> 0;
|
|
112
|
+
const entry = this.tableDirectory.get(tag);
|
|
113
|
+
if (!entry)
|
|
114
|
+
return null;
|
|
115
|
+
return new Uint8Array(this.dataView.buffer, this.dataView.byteOffset + entry.offset, entry.length);
|
|
116
|
+
}
|
|
110
117
|
}
|
|
@@ -103,22 +103,6 @@ export class TextRenderer {
|
|
|
103
103
|
const wordSpacingPx = run.wordSpacing ?? 0;
|
|
104
104
|
const wordSpacingPt = this.coordinateTransformer.convertPxToPt(wordSpacingPx);
|
|
105
105
|
const appliedWordSpacing = wordSpacingPx !== 0 && wordSpacingPt !== 0;
|
|
106
|
-
// Shadows use the base font resource (non-subset) for compatibility with Identity-H encoding
|
|
107
|
-
this.registerFont(font);
|
|
108
|
-
if (run.textShadows && run.textShadows.length > 0) {
|
|
109
|
-
const shadowCommands = await this.shadowRenderer.render({
|
|
110
|
-
run,
|
|
111
|
-
font,
|
|
112
|
-
encoded,
|
|
113
|
-
Tm: run.lineMatrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
|
|
114
|
-
fontSizePt,
|
|
115
|
-
fontSizePx: run.fontSize,
|
|
116
|
-
wordSpacingPt,
|
|
117
|
-
appliedWordSpacing,
|
|
118
|
-
fontResourceName: font.resourceName,
|
|
119
|
-
});
|
|
120
|
-
this.commands.push(...shadowCommands);
|
|
121
|
-
}
|
|
122
106
|
const subsetResource = this.fontRegistry.ensureSubsetForGlyphRun(glyphRun, font);
|
|
123
107
|
this.registerSubsetFont(subsetResource.alias, subsetResource.ref);
|
|
124
108
|
const gradientBackground = run.textGradient;
|
|
@@ -153,6 +137,22 @@ export class TextRenderer {
|
|
|
153
137
|
}
|
|
154
138
|
}
|
|
155
139
|
}
|
|
140
|
+
if (run.textShadows && run.textShadows.length > 0) {
|
|
141
|
+
const shadowCommands = await this.shadowRenderer.render({
|
|
142
|
+
run,
|
|
143
|
+
font,
|
|
144
|
+
encoded,
|
|
145
|
+
Tm: run.lineMatrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 },
|
|
146
|
+
fontSizePt,
|
|
147
|
+
fontSizePx: run.fontSize,
|
|
148
|
+
wordSpacingPt,
|
|
149
|
+
appliedWordSpacing,
|
|
150
|
+
fontResourceName: font.resourceName,
|
|
151
|
+
subset: subsetResource.subset,
|
|
152
|
+
subsetAlias: subsetResource.alias
|
|
153
|
+
});
|
|
154
|
+
this.commands.push(...shadowCommands);
|
|
155
|
+
}
|
|
156
156
|
const glyphCommands = drawGlyphRun(glyphRun, subsetResource.subset, 0, 0, fontSizePt, color, this.graphicsStateManager, wordSpacingPt, { tm: textMatrix });
|
|
157
157
|
this.commands.push(...glyphCommands);
|
|
158
158
|
if (run.decorations) {
|
|
@@ -276,6 +276,7 @@ function buildUnifiedFontFromResource(run, font) {
|
|
|
276
276
|
unitsPerEm: metrics.metrics.unitsPerEm,
|
|
277
277
|
glyphCount: metrics.glyphMetrics.size,
|
|
278
278
|
getGlyphOutline: metrics.getGlyphOutline,
|
|
279
|
+
getRawTableData: metrics.getRawTableData,
|
|
279
280
|
},
|
|
280
281
|
css: {
|
|
281
282
|
family: run.fontFamily,
|
|
@@ -13,6 +13,10 @@ export interface TextShadowRenderContext {
|
|
|
13
13
|
wordSpacingPt: number;
|
|
14
14
|
appliedWordSpacing: boolean;
|
|
15
15
|
fontResourceName?: string;
|
|
16
|
+
subset?: {
|
|
17
|
+
gidMap: Map<number, number>;
|
|
18
|
+
};
|
|
19
|
+
subsetAlias?: string;
|
|
16
20
|
}
|
|
17
21
|
export declare class TextShadowRenderer {
|
|
18
22
|
private readonly coordinateTransformer;
|