modern-pdf-lib 0.11.6 → 0.12.1
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/README.md +3 -3
- package/dist/{fflateAdapter-cT4YeY_h.cjs → fflateAdapter-AHC_S3cb.cjs} +1 -1
- package/dist/{fflateAdapter-D2mv_ttM.mjs → fflateAdapter-DX0VqT5k.mjs} +1 -1
- package/dist/fontSubset-ZpLoOZ2e.mjs +495 -0
- package/dist/fontSubset-pFc8Dueu.cjs +518 -0
- package/dist/index.cjs +19160 -94
- package/dist/index.d.cts +8 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +8 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +19071 -5
- package/dist/{libdeflateWasm-QVHmuzw-.mjs → libdeflateWasm-CRFwmrSl.mjs} +2 -2
- package/dist/{libdeflateWasm-to2bG6NG.cjs → libdeflateWasm-DeU6cupL.cjs} +2 -2
- package/dist/{loader-D9LTYmrX.mjs → loader-CZMj0gBy.mjs} +1 -1
- package/dist/{loader-mwt5wRJz.cjs → loader-DWwMj8jZ.cjs} +1 -1
- package/dist/{pngEmbed-BogDTEfL.cjs → pngEmbed-1RWu6KsO.cjs} +2 -2
- package/dist/{pngEmbed-D4Uig1LV.mjs → pngEmbed-CHyesD7i.mjs} +2 -2
- package/package.json +2 -2
- package/dist/documentMerge-CP2ECivx.mjs +0 -18361
- package/dist/documentMerge-lbeikuzo.cjs +0 -18888
- package/dist/fontSubset-0aTpGEDR.cjs +0 -226
- package/dist/fontSubset-b49QjYWn.mjs +0 -203
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Create, parse, fill, merge, sign, and manipulate PDF documents<br />in Node, Den
|
|
|
15
15
|
|
|
16
16
|
[](https://www.npmjs.com/package/modern-pdf-lib)
|
|
17
17
|
[](https://bundlephobia.com/package/modern-pdf-lib)
|
|
18
|
-
[](#)
|
|
19
19
|
[](#)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
@@ -429,7 +429,7 @@ modern-pdf-lib/
|
|
|
429
429
|
outline/ Bookmarks / document outline
|
|
430
430
|
metadata/ XMP metadata, viewer preferences
|
|
431
431
|
wasm/ Rust crate sources (4 modules)
|
|
432
|
-
tests/ 1,
|
|
432
|
+
tests/ 1,973 tests across 91 suites
|
|
433
433
|
docs/ VitePress documentation
|
|
434
434
|
```
|
|
435
435
|
|
|
@@ -441,7 +441,7 @@ modern-pdf-lib/
|
|
|
441
441
|
git clone https://github.com/ABCrimson/modern-pdf-lib.git
|
|
442
442
|
cd modern-pdf-lib
|
|
443
443
|
npm install
|
|
444
|
-
npm test # 1,
|
|
444
|
+
npm test # 1,973 tests
|
|
445
445
|
npm run typecheck # TypeScript 6.0 strict
|
|
446
446
|
npm run build # ESM + CJS + declarations
|
|
447
447
|
```
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/assets/font/ttfSubset.ts
|
|
4
|
+
/**
|
|
5
|
+
* @module assets/font/ttfSubset
|
|
6
|
+
*
|
|
7
|
+
* Pure TypeScript TrueType font subsetter.
|
|
8
|
+
*
|
|
9
|
+
* Reduces font file size by replacing glyph outline data for unused
|
|
10
|
+
* glyphs with zero-length entries. Keeps all glyph ID slots intact
|
|
11
|
+
* so that `CIDToGIDMap /Identity` (CID = GID) continues to work
|
|
12
|
+
* without any changes to the PDF embedding pipeline.
|
|
13
|
+
*
|
|
14
|
+
* The subsetter:
|
|
15
|
+
* 1. Parses the TrueType table directory and `loca`/`glyf` tables.
|
|
16
|
+
* 2. Resolves composite glyph dependencies (component glyphs).
|
|
17
|
+
* 3. Builds a new `glyf` table containing only data for retained glyphs.
|
|
18
|
+
* 4. Builds a new `loca` table with updated offsets.
|
|
19
|
+
* 5. Reassembles a valid TrueType font with correct checksums.
|
|
20
|
+
*
|
|
21
|
+
* No external dependencies. No Buffer — uses Uint8Array and DataView.
|
|
22
|
+
*/
|
|
23
|
+
/** Composite glyph flag: arguments are 16-bit instead of 8-bit. */
|
|
24
|
+
const ARG_1_AND_2_ARE_WORDS = 1;
|
|
25
|
+
/** Composite glyph flag: a single F2Dot14 scale follows. */
|
|
26
|
+
const WE_HAVE_A_SCALE = 8;
|
|
27
|
+
/** Composite glyph flag: more component records follow. */
|
|
28
|
+
const MORE_COMPONENTS = 32;
|
|
29
|
+
/** Composite glyph flag: separate X and Y scales follow. */
|
|
30
|
+
const WE_HAVE_AN_X_AND_Y_SCALE = 64;
|
|
31
|
+
/** Composite glyph flag: a 2×2 transformation matrix follows. */
|
|
32
|
+
const WE_HAVE_A_TWO_BY_TWO = 128;
|
|
33
|
+
function parseTableDir(data) {
|
|
34
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
35
|
+
const numTables = view.getUint16(4, false);
|
|
36
|
+
const tables = /* @__PURE__ */ new Map();
|
|
37
|
+
const requiredLen = 12 + numTables * 16;
|
|
38
|
+
if (data.length < requiredLen) throw new RangeError("Font data too small for table directory");
|
|
39
|
+
for (let i = 0; i < numTables; i++) {
|
|
40
|
+
const off = 12 + i * 16;
|
|
41
|
+
const tag = String.fromCharCode(data[off], data[off + 1], data[off + 2], data[off + 3]);
|
|
42
|
+
tables.set(tag, {
|
|
43
|
+
tag,
|
|
44
|
+
offset: view.getUint32(off + 8, false),
|
|
45
|
+
length: view.getUint32(off + 12, false)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return tables;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse the `loca` table to get glyph offsets within the `glyf` table.
|
|
52
|
+
*
|
|
53
|
+
* Returns an array of `numGlyphs + 1` offsets. The glyph data for
|
|
54
|
+
* glyph `i` spans from `offsets[i]` to `offsets[i+1]`. A zero-length
|
|
55
|
+
* span means the glyph has no outline (e.g. space or unused).
|
|
56
|
+
*/
|
|
57
|
+
function parseLoca(data, locaOffset, locaLength, numGlyphs, indexToLocFormat) {
|
|
58
|
+
const view = new DataView(data.buffer, data.byteOffset + locaOffset, locaLength);
|
|
59
|
+
const offsets = [];
|
|
60
|
+
const count = numGlyphs + 1;
|
|
61
|
+
if (indexToLocFormat === 0) for (let i = 0; i < count; i++) offsets.push(view.getUint16(i * 2, false) * 2);
|
|
62
|
+
else for (let i = 0; i < count; i++) offsets.push(view.getUint32(i * 4, false));
|
|
63
|
+
return offsets;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Scan composite glyphs and collect all component glyph IDs that are
|
|
67
|
+
* referenced (directly or transitively).
|
|
68
|
+
*/
|
|
69
|
+
function resolveCompositeDeps(data, glyfOffset, locaOffsets, usedGids) {
|
|
70
|
+
const retained = new Set(usedGids);
|
|
71
|
+
const queue = [...usedGids];
|
|
72
|
+
while (queue.length > 0) {
|
|
73
|
+
const gid = queue.pop();
|
|
74
|
+
const glyphStart = glyfOffset + locaOffsets[gid];
|
|
75
|
+
if (glyfOffset + locaOffsets[gid + 1] <= glyphStart) continue;
|
|
76
|
+
const view = new DataView(data.buffer, data.byteOffset);
|
|
77
|
+
if (view.getInt16(glyphStart, false) >= 0) continue;
|
|
78
|
+
let off = glyphStart + 10;
|
|
79
|
+
let flags;
|
|
80
|
+
do {
|
|
81
|
+
flags = view.getUint16(off, false);
|
|
82
|
+
const componentGid = view.getUint16(off + 2, false);
|
|
83
|
+
off += 4;
|
|
84
|
+
if (!retained.has(componentGid)) {
|
|
85
|
+
retained.add(componentGid);
|
|
86
|
+
queue.push(componentGid);
|
|
87
|
+
}
|
|
88
|
+
if (flags & ARG_1_AND_2_ARE_WORDS) off += 4;
|
|
89
|
+
else off += 2;
|
|
90
|
+
if (flags & WE_HAVE_A_SCALE) off += 2;
|
|
91
|
+
else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) off += 4;
|
|
92
|
+
else if (flags & WE_HAVE_A_TWO_BY_TWO) off += 8;
|
|
93
|
+
} while (flags & MORE_COMPONENTS);
|
|
94
|
+
}
|
|
95
|
+
return retained;
|
|
96
|
+
}
|
|
97
|
+
function calcTableChecksum(data, offset, length) {
|
|
98
|
+
const aligned = length + 3 & -4;
|
|
99
|
+
let sum = 0;
|
|
100
|
+
const view = new DataView(data.buffer, data.byteOffset + offset);
|
|
101
|
+
for (let i = 0; i < aligned; i += 4) if (i + 3 < length) sum = sum + view.getUint32(i, false) >>> 0;
|
|
102
|
+
else {
|
|
103
|
+
let val = 0;
|
|
104
|
+
for (let j = 0; j < 4; j++) val = val << 8 | (i + j < length ? data[offset + i + j] : 0);
|
|
105
|
+
sum = sum + val >>> 0;
|
|
106
|
+
}
|
|
107
|
+
return sum;
|
|
108
|
+
}
|
|
109
|
+
function writeTag(buf, offset, tag) {
|
|
110
|
+
buf[offset] = tag.charCodeAt(0);
|
|
111
|
+
buf[offset + 1] = tag.charCodeAt(1);
|
|
112
|
+
buf[offset + 2] = tag.charCodeAt(2);
|
|
113
|
+
buf[offset + 3] = tag.charCodeAt(3);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute `searchRange`, `entrySelector`, `rangeShift` for the offset
|
|
117
|
+
* table header.
|
|
118
|
+
*/
|
|
119
|
+
function offsetTableParams(numTables) {
|
|
120
|
+
let entrySelector = 0;
|
|
121
|
+
let searchRange = 1;
|
|
122
|
+
while (searchRange * 2 <= numTables) {
|
|
123
|
+
searchRange *= 2;
|
|
124
|
+
entrySelector++;
|
|
125
|
+
}
|
|
126
|
+
searchRange *= 16;
|
|
127
|
+
const rangeShift = numTables * 16 - searchRange;
|
|
128
|
+
return {
|
|
129
|
+
searchRange,
|
|
130
|
+
entrySelector,
|
|
131
|
+
rangeShift
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Subset a TrueType font, keeping only glyph outlines for the
|
|
136
|
+
* specified glyph IDs.
|
|
137
|
+
*
|
|
138
|
+
* Unused glyphs become zero-length entries in the `glyf` table.
|
|
139
|
+
* All glyph ID slots are preserved so `CIDToGIDMap /Identity` still
|
|
140
|
+
* works. Composite glyph dependencies are resolved automatically.
|
|
141
|
+
*
|
|
142
|
+
* @param fontData The raw TrueType font bytes.
|
|
143
|
+
* @param usedGlyphIds Set of glyph IDs to retain (GID 0 is always kept).
|
|
144
|
+
* @returns The subsetted font bytes.
|
|
145
|
+
*/
|
|
146
|
+
function subsetTtf(fontData, usedGlyphIds) {
|
|
147
|
+
if (fontData.length < 28) return new Uint8Array(fontData);
|
|
148
|
+
let tables;
|
|
149
|
+
try {
|
|
150
|
+
tables = parseTableDir(fontData);
|
|
151
|
+
} catch {
|
|
152
|
+
return new Uint8Array(fontData);
|
|
153
|
+
}
|
|
154
|
+
const headEntry = tables.get("head");
|
|
155
|
+
const locaEntry = tables.get("loca");
|
|
156
|
+
const glyfEntry = tables.get("glyf");
|
|
157
|
+
const maxpEntry = tables.get("maxp");
|
|
158
|
+
if (!headEntry || !locaEntry || !glyfEntry || !maxpEntry) return new Uint8Array(fontData);
|
|
159
|
+
const view = new DataView(fontData.buffer, fontData.byteOffset, fontData.byteLength);
|
|
160
|
+
const indexToLocFormat = view.getInt16(headEntry.offset + 50, false);
|
|
161
|
+
const numGlyphs = view.getUint16(maxpEntry.offset + 4, false);
|
|
162
|
+
const locaOffsets = parseLoca(fontData, locaEntry.offset, locaEntry.length, numGlyphs, indexToLocFormat);
|
|
163
|
+
const allUsed = new Set(usedGlyphIds);
|
|
164
|
+
allUsed.add(0);
|
|
165
|
+
const retained = resolveCompositeDeps(fontData, glyfEntry.offset, locaOffsets, allUsed);
|
|
166
|
+
let newGlyfSize = 0;
|
|
167
|
+
for (let gid = 0; gid < numGlyphs; gid++) if (retained.has(gid)) {
|
|
168
|
+
const glyphLen = locaOffsets[gid + 1] - locaOffsets[gid];
|
|
169
|
+
newGlyfSize += glyphLen + 1 & -2;
|
|
170
|
+
}
|
|
171
|
+
const newGlyf = new Uint8Array(newGlyfSize);
|
|
172
|
+
const newLocaOffsets = [];
|
|
173
|
+
let glyfPos = 0;
|
|
174
|
+
for (let gid = 0; gid < numGlyphs; gid++) {
|
|
175
|
+
newLocaOffsets.push(glyfPos);
|
|
176
|
+
if (retained.has(gid)) {
|
|
177
|
+
const srcStart = glyfEntry.offset + locaOffsets[gid];
|
|
178
|
+
const glyphLen = locaOffsets[gid + 1] - locaOffsets[gid];
|
|
179
|
+
if (glyphLen > 0) {
|
|
180
|
+
newGlyf.set(fontData.subarray(srcStart, srcStart + glyphLen), glyfPos);
|
|
181
|
+
glyfPos += glyphLen + 1 & -2;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
newLocaOffsets.push(glyfPos);
|
|
186
|
+
const useShortLoca = glyfPos <= 131070;
|
|
187
|
+
let newLocaSize;
|
|
188
|
+
let newLoca;
|
|
189
|
+
if (useShortLoca) {
|
|
190
|
+
newLocaSize = (numGlyphs + 1) * 2;
|
|
191
|
+
newLoca = new Uint8Array(newLocaSize);
|
|
192
|
+
const locaView = new DataView(newLoca.buffer);
|
|
193
|
+
for (let i = 0; i <= numGlyphs; i++) locaView.setUint16(i * 2, newLocaOffsets[i] >>> 1, false);
|
|
194
|
+
} else {
|
|
195
|
+
newLocaSize = (numGlyphs + 1) * 4;
|
|
196
|
+
newLoca = new Uint8Array(newLocaSize);
|
|
197
|
+
const locaView = new DataView(newLoca.buffer);
|
|
198
|
+
for (let i = 0; i <= numGlyphs; i++) locaView.setUint32(i * 4, newLocaOffsets[i], false);
|
|
199
|
+
}
|
|
200
|
+
const copyTags = [
|
|
201
|
+
"hhea",
|
|
202
|
+
"maxp",
|
|
203
|
+
"OS/2",
|
|
204
|
+
"name",
|
|
205
|
+
"cmap",
|
|
206
|
+
"post",
|
|
207
|
+
"cvt ",
|
|
208
|
+
"fpgm",
|
|
209
|
+
"prep",
|
|
210
|
+
"hmtx",
|
|
211
|
+
"gasp",
|
|
212
|
+
"GDEF",
|
|
213
|
+
"GPOS",
|
|
214
|
+
"GSUB"
|
|
215
|
+
];
|
|
216
|
+
const outputTables = [];
|
|
217
|
+
const newHead = new Uint8Array(headEntry.length);
|
|
218
|
+
newHead.set(fontData.subarray(headEntry.offset, headEntry.offset + headEntry.length));
|
|
219
|
+
const headView = new DataView(newHead.buffer);
|
|
220
|
+
headView.setInt16(50, useShortLoca ? 0 : 1, false);
|
|
221
|
+
headView.setUint32(8, 0, false);
|
|
222
|
+
outputTables.push({
|
|
223
|
+
tag: "head",
|
|
224
|
+
data: newHead
|
|
225
|
+
});
|
|
226
|
+
for (const tag of copyTags) {
|
|
227
|
+
const entry = tables.get(tag);
|
|
228
|
+
if (entry) outputTables.push({
|
|
229
|
+
tag,
|
|
230
|
+
data: fontData.subarray(entry.offset, entry.offset + entry.length)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
outputTables.push({
|
|
234
|
+
tag: "loca",
|
|
235
|
+
data: newLoca
|
|
236
|
+
});
|
|
237
|
+
outputTables.push({
|
|
238
|
+
tag: "glyf",
|
|
239
|
+
data: newGlyf
|
|
240
|
+
});
|
|
241
|
+
const numOutputTables = outputTables.length;
|
|
242
|
+
const { searchRange, entrySelector, rangeShift } = offsetTableParams(numOutputTables);
|
|
243
|
+
const headerSize = 12;
|
|
244
|
+
const dirSize = numOutputTables * 16;
|
|
245
|
+
let totalSize = headerSize + dirSize;
|
|
246
|
+
for (const t of outputTables) totalSize += t.data.length + 3 & -4;
|
|
247
|
+
const output = new Uint8Array(totalSize);
|
|
248
|
+
const outView = new DataView(output.buffer);
|
|
249
|
+
outView.setUint32(0, 65536, false);
|
|
250
|
+
outView.setUint16(4, numOutputTables, false);
|
|
251
|
+
outView.setUint16(6, searchRange, false);
|
|
252
|
+
outView.setUint16(8, entrySelector, false);
|
|
253
|
+
outView.setUint16(10, rangeShift, false);
|
|
254
|
+
let dataOffset = headerSize + dirSize;
|
|
255
|
+
for (let i = 0; i < outputTables.length; i++) {
|
|
256
|
+
const t = outputTables[i];
|
|
257
|
+
const dirOff = headerSize + i * 16;
|
|
258
|
+
const alignedLen = t.data.length + 3 & -4;
|
|
259
|
+
output.set(t.data, dataOffset);
|
|
260
|
+
const checksum = calcTableChecksum(output, dataOffset, t.data.length);
|
|
261
|
+
writeTag(output, dirOff, t.tag);
|
|
262
|
+
outView.setUint32(dirOff + 4, checksum, false);
|
|
263
|
+
outView.setUint32(dirOff + 8, dataOffset, false);
|
|
264
|
+
outView.setUint32(dirOff + 12, t.data.length, false);
|
|
265
|
+
dataOffset += alignedLen;
|
|
266
|
+
}
|
|
267
|
+
const adjustment = 2981146554 - calcTableChecksum(output, 0, output.length) >>> 0;
|
|
268
|
+
for (let i = 0; i < outputTables.length; i++) if (outputTables[i].tag === "head") {
|
|
269
|
+
const headDataOff = outView.getUint32(headerSize + i * 16 + 8, false);
|
|
270
|
+
outView.setUint32(headDataOff + 8, adjustment, false);
|
|
271
|
+
const newHeadChecksum = calcTableChecksum(output, headDataOff, outputTables[i].data.length);
|
|
272
|
+
outView.setUint32(headerSize + i * 16 + 4, newHeadChecksum, false);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
return output;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/assets/font/fontSubset.ts
|
|
280
|
+
/**
|
|
281
|
+
* @module assets/font/fontSubset
|
|
282
|
+
*
|
|
283
|
+
* Font subsetting — removes unused glyphs from a TrueType font to
|
|
284
|
+
* reduce file size. Offers WASM-accelerated subsetting with a pure
|
|
285
|
+
* JS subsetter as the default.
|
|
286
|
+
*
|
|
287
|
+
* The JS subsetter replaces unused glyph outlines with zero-length
|
|
288
|
+
* entries in the `glyf` table, preserving all glyph ID slots so that
|
|
289
|
+
* `CIDToGIDMap /Identity` continues to work without changes to the
|
|
290
|
+
* PDF embedding pipeline. Composite glyph dependencies are resolved
|
|
291
|
+
* automatically.
|
|
292
|
+
*
|
|
293
|
+
* Also builds CMap streams for the subset encoding, mapping CIDs to
|
|
294
|
+
* Unicode codepoints (required for PDF text extraction / copy-paste).
|
|
295
|
+
*
|
|
296
|
+
* No Buffer — uses Uint8Array exclusively.
|
|
297
|
+
* No fs — no file system access.
|
|
298
|
+
* No require() — ESM import only.
|
|
299
|
+
*/
|
|
300
|
+
var fontSubset_exports = /* @__PURE__ */ __exportAll({
|
|
301
|
+
buildSubsetCmap: () => buildSubsetCmap,
|
|
302
|
+
computeSubsetTag: () => computeSubsetTag,
|
|
303
|
+
initSubsetWasm: () => initSubsetWasm,
|
|
304
|
+
subsetFont: () => subsetFont
|
|
305
|
+
});
|
|
306
|
+
/**
|
|
307
|
+
* WASM subsetter is not available — the TTF WASM module (`src/wasm/ttf`)
|
|
308
|
+
* provides `parse_font()` for metric extraction but does NOT include a
|
|
309
|
+
* `subset_font()` export. The pure JS subsetter (`ttfSubset.ts`) is
|
|
310
|
+
* the primary subsetting path for all runtimes.
|
|
311
|
+
*
|
|
312
|
+
* This flag exists for forward-compatibility: a dedicated subsetting
|
|
313
|
+
* WASM module could be added in the future.
|
|
314
|
+
*/
|
|
315
|
+
let wasmSubsetReady = false;
|
|
316
|
+
/**
|
|
317
|
+
* Initialize the font subsetting WASM module.
|
|
318
|
+
*
|
|
319
|
+
* **Note:** No WASM subsetter currently exists — the TTF WASM module
|
|
320
|
+
* only parses fonts, it does not subset them. The pure JS subsetter
|
|
321
|
+
* (`subsetTtf`) handles all subsetting. This function is a no-op
|
|
322
|
+
* placeholder for forward-compatibility.
|
|
323
|
+
*
|
|
324
|
+
* @param _wasmSource - Unused. Reserved for a future WASM subsetter.
|
|
325
|
+
*/
|
|
326
|
+
async function initSubsetWasm(_wasmSource) {
|
|
327
|
+
wasmSubsetReady = false;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Subset a font using a WASM module.
|
|
331
|
+
*
|
|
332
|
+
* Currently no WASM subsetter exists — this delegates to the JS subsetter.
|
|
333
|
+
* Reserved for forward-compatibility with a future WASM subsetting module.
|
|
334
|
+
*
|
|
335
|
+
* @internal
|
|
336
|
+
*/
|
|
337
|
+
function subsetWithWasm(fontData, usedGlyphIds) {
|
|
338
|
+
return subsetJs(fontData, usedGlyphIds);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Subset a TrueType font using the pure JS subsetter.
|
|
342
|
+
*
|
|
343
|
+
* Replaces unused glyph outline data with zero-length entries in the
|
|
344
|
+
* `glyf` table while preserving all glyph ID slots. This keeps
|
|
345
|
+
* `CIDToGIDMap /Identity` working (CID = GID) while dramatically
|
|
346
|
+
* reducing the font program size.
|
|
347
|
+
*
|
|
348
|
+
* Composite glyph dependencies are resolved automatically — if a used
|
|
349
|
+
* glyph references component glyphs, those components are retained too.
|
|
350
|
+
*
|
|
351
|
+
* The `newToOldGid` array contains the used glyph IDs in sorted order.
|
|
352
|
+
* Since glyph IDs are not renumbered, each "new" GID equals the
|
|
353
|
+
* original GID (identity mapping for the used glyphs).
|
|
354
|
+
*
|
|
355
|
+
* @param fontData The raw TTF font file bytes.
|
|
356
|
+
* @param usedGlyphIds Set of glyph IDs that are actually used.
|
|
357
|
+
* @returns A {@link SubsetResult} with the subsetted font bytes and
|
|
358
|
+
* identity glyph-ID mappings for the used glyphs.
|
|
359
|
+
* @internal
|
|
360
|
+
*/
|
|
361
|
+
function subsetJs(fontData, usedGlyphIds) {
|
|
362
|
+
const allGids = new Set(usedGlyphIds);
|
|
363
|
+
allGids.add(0);
|
|
364
|
+
const subsetFontData = subsetTtf(fontData, allGids);
|
|
365
|
+
const sortedGids = allGids.values().toArray().sort((a, b) => a - b);
|
|
366
|
+
const newToOldGid = sortedGids;
|
|
367
|
+
const oldToNewGid = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const gid of sortedGids) oldToNewGid.set(gid, gid);
|
|
369
|
+
return {
|
|
370
|
+
fontData: subsetFontData,
|
|
371
|
+
newToOldGid,
|
|
372
|
+
oldToNewGid
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Subset a TrueType font to include only the specified glyphs.
|
|
377
|
+
*
|
|
378
|
+
* Uses WASM-accelerated subsetting if available (via {@link initSubsetWasm}),
|
|
379
|
+
* otherwise falls back to returning the full font with identity glyph
|
|
380
|
+
* mappings.
|
|
381
|
+
*
|
|
382
|
+
* Glyph ID 0 (.notdef) is always included automatically.
|
|
383
|
+
*
|
|
384
|
+
* @param fontData - The raw TTF font file bytes.
|
|
385
|
+
* @param usedGlyphIds - Set of glyph IDs that are actually used.
|
|
386
|
+
* @returns The subset result with the (possibly reduced) font and
|
|
387
|
+
* glyph-ID remapping tables.
|
|
388
|
+
*/
|
|
389
|
+
function subsetFont(fontData, usedGlyphIds) {
|
|
390
|
+
if (usedGlyphIds.size === 0) return subsetJs(fontData, new Set([0]));
|
|
391
|
+
if (wasmSubsetReady) return subsetWithWasm(fontData, usedGlyphIds);
|
|
392
|
+
return subsetJs(fontData, usedGlyphIds);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Build a PDF `/ToUnicode` CMap stream for a subsetted font.
|
|
396
|
+
*
|
|
397
|
+
* The CMap maps CIDs (new glyph IDs in the subset) back to Unicode
|
|
398
|
+
* codepoints, enabling text extraction and copy-paste in PDF viewers.
|
|
399
|
+
*
|
|
400
|
+
* @param subsetResult - The result from {@link subsetFont}.
|
|
401
|
+
* @param originalCmapTable - The original font's cmap table (Unicode
|
|
402
|
+
* codepoint → original glyph ID).
|
|
403
|
+
* @returns A CMap stream body and a CID-to-Unicode lookup.
|
|
404
|
+
*/
|
|
405
|
+
function buildSubsetCmap(subsetResult, originalCmapTable) {
|
|
406
|
+
const oldGidToUnicode = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const [codepoint, gid] of originalCmapTable) {
|
|
408
|
+
const existing = oldGidToUnicode.get(gid);
|
|
409
|
+
if (existing) existing.push(codepoint);
|
|
410
|
+
else oldGidToUnicode.set(gid, [codepoint]);
|
|
411
|
+
}
|
|
412
|
+
const cidToUnicode = /* @__PURE__ */ new Map();
|
|
413
|
+
for (let newGid = 0; newGid < subsetResult.newToOldGid.length; newGid++) {
|
|
414
|
+
const oldGid = subsetResult.newToOldGid[newGid];
|
|
415
|
+
const unicodes = oldGidToUnicode.get(oldGid);
|
|
416
|
+
if (unicodes && unicodes.length > 0) cidToUnicode.set(newGid, unicodes);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
cmapStream: generateToUnicodeCmap(cidToUnicode),
|
|
420
|
+
cidToUnicode
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Generate a PDF `/ToUnicode` CMap program.
|
|
425
|
+
*
|
|
426
|
+
* The CMap uses `beginbfchar` / `endbfchar` sections to map individual
|
|
427
|
+
* CIDs to Unicode values. For ranges of consecutive mappings it uses
|
|
428
|
+
* `beginbfrange` / `endbfrange`.
|
|
429
|
+
*
|
|
430
|
+
* @internal
|
|
431
|
+
*/
|
|
432
|
+
function generateToUnicodeCmap(cidToUnicode) {
|
|
433
|
+
const lines = [];
|
|
434
|
+
lines.push("/CIDInit /ProcSet findresource begin");
|
|
435
|
+
lines.push("12 dict begin");
|
|
436
|
+
lines.push("begincmap");
|
|
437
|
+
lines.push("/CIDSystemInfo");
|
|
438
|
+
lines.push("<< /Registry (Adobe)");
|
|
439
|
+
lines.push("/Ordering (UCS)");
|
|
440
|
+
lines.push("/Supplement 0");
|
|
441
|
+
lines.push(">> def");
|
|
442
|
+
lines.push("/CMapName /Adobe-Identity-UCS def");
|
|
443
|
+
lines.push("/CMapType 2 def");
|
|
444
|
+
lines.push("1 begincodespacerange");
|
|
445
|
+
lines.push("<0000> <FFFF>");
|
|
446
|
+
lines.push("endcodespacerange");
|
|
447
|
+
const entries = cidToUnicode.entries().toArray().sort((a, b) => a[0] - b[0]);
|
|
448
|
+
const CHUNK_SIZE = 100;
|
|
449
|
+
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
|
|
450
|
+
const chunk = entries.slice(i, i + CHUNK_SIZE);
|
|
451
|
+
lines.push(`${chunk.length} beginbfchar`);
|
|
452
|
+
for (const [cid, unicodes] of chunk) {
|
|
453
|
+
const cidHex = cid.toString(16).padStart(4, "0").toUpperCase();
|
|
454
|
+
if (unicodes.length === 1) {
|
|
455
|
+
const uniHex = unicodes[0].toString(16).padStart(4, "0").toUpperCase();
|
|
456
|
+
lines.push(`<${cidHex}> <${uniHex}>`);
|
|
457
|
+
} else {
|
|
458
|
+
let uniHex = "";
|
|
459
|
+
for (const cp of unicodes.slice(0, 1)) uniHex += cp.toString(16).padStart(4, "0").toUpperCase();
|
|
460
|
+
lines.push(`<${cidHex}> <${uniHex}>`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
lines.push("endbfchar");
|
|
464
|
+
}
|
|
465
|
+
lines.push("endcmap");
|
|
466
|
+
lines.push("CMapName currentdict /CMap defineresource pop");
|
|
467
|
+
lines.push("end");
|
|
468
|
+
lines.push("end");
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Generate a 6-letter uppercase tag for the subset font name.
|
|
473
|
+
*
|
|
474
|
+
* PDF spec recommends a tag prefix like `ABCDEF+FontName` to indicate
|
|
475
|
+
* the font is subsetted. This function generates a deterministic tag
|
|
476
|
+
* from the set of glyph IDs.
|
|
477
|
+
*
|
|
478
|
+
* @param usedGlyphIds - The set of glyph IDs in the subset.
|
|
479
|
+
* @returns A 6-character uppercase ASCII string (e.g. `"BCDEFG"`).
|
|
480
|
+
*/
|
|
481
|
+
function computeSubsetTag(usedGlyphIds) {
|
|
482
|
+
let hash = 0;
|
|
483
|
+
for (const gid of usedGlyphIds) hash = (hash << 5) - hash + gid | 0;
|
|
484
|
+
const tag = [];
|
|
485
|
+
let h = Math.abs(hash);
|
|
486
|
+
for (let i = 0; i < 6; i++) {
|
|
487
|
+
tag.push(String.fromCharCode(65 + h % 26));
|
|
488
|
+
h = Math.floor(h / 26);
|
|
489
|
+
}
|
|
490
|
+
return tag.join("");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
//#endregion
|
|
494
|
+
export { subsetFont as i, computeSubsetTag as n, fontSubset_exports as r, buildSubsetCmap as t };
|
|
495
|
+
//# sourceMappingURL=fontSubset-ZpLoOZ2e.mjs.map
|