@file-viewer/renderer-pdf 2.1.25 → 2.1.27

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.
@@ -0,0 +1,452 @@
1
+ import { decodePDFRawStream, PDFArray, PDFDict, PDFDocument, PDFHexString, PDFName, PDFNumber, PDFRawStream, PDFString, } from 'pdf-lib';
2
+ const MAX_REPAIR_SOURCE_BYTES = 64 * 1024 * 1024;
3
+ const MAX_TTF_TABLES = 256;
4
+ const MAX_CMAP_GLYPHS = 0xffff;
5
+ const MAX_CMAP_CODEPOINT_VISITS = 0x10000;
6
+ const MAX_DECODED_FONT_BYTES = 64 * 1024 * 1024;
7
+ const CMAP_BFCHAR_CHUNK_SIZE = 100;
8
+ const BASE_FONT = PDFName.of('BaseFont');
9
+ const CID_TO_GID_MAP = PDFName.of('CIDToGIDMap');
10
+ const DESCENDANT_FONTS = PDFName.of('DescendantFonts');
11
+ const ENCODING = PDFName.of('Encoding');
12
+ const FONT = PDFName.of('Font');
13
+ const FONT_DESCRIPTOR = PDFName.of('FontDescriptor');
14
+ const FONT_FAMILY = PDFName.of('FontFamily');
15
+ const FONT_FILE_2 = PDFName.of('FontFile2');
16
+ const LENGTH_1 = PDFName.of('Length1');
17
+ const RESOURCES = PDFName.of('Resources');
18
+ const SUBTYPE = PDFName.of('Subtype');
19
+ const TO_UNICODE = PDFName.of('ToUnicode');
20
+ const X_OBJECT = PDFName.of('XObject');
21
+ const CJK_FONT_FAMILY_ALIASES = {
22
+ '微软雅黑': 'microsoftyahei',
23
+ '宋体': 'simsun',
24
+ '黑体': 'simhei',
25
+ '楷体': 'kaiti',
26
+ '仿宋': 'fangsong',
27
+ };
28
+ const CJK_FONT_FAMILY_MARKERS = [
29
+ 'microsoftyahei',
30
+ 'simsun',
31
+ 'nsimsun',
32
+ 'simhei',
33
+ 'kaiti',
34
+ 'fangsong',
35
+ 'pingfang',
36
+ 'songti',
37
+ 'heiti',
38
+ 'hiragino',
39
+ 'notosanscjk',
40
+ 'sourcehansans',
41
+ 'sourcehanserif',
42
+ ];
43
+ const readUint16 = (bytes, offset) => {
44
+ if (offset < 0 || offset + 2 > bytes.length) {
45
+ throw new Error('Unexpected end of TrueType font data.');
46
+ }
47
+ return (bytes[offset] << 8) | bytes[offset + 1];
48
+ };
49
+ const readUint32 = (bytes, offset) => {
50
+ if (offset < 0 || offset + 4 > bytes.length) {
51
+ throw new Error('Unexpected end of TrueType font data.');
52
+ }
53
+ return (bytes[offset] * 0x1000000 +
54
+ (bytes[offset + 1] << 16) +
55
+ (bytes[offset + 2] << 8) +
56
+ bytes[offset + 3]) >>> 0;
57
+ };
58
+ const readTag = (bytes, offset) => {
59
+ if (offset < 0 || offset + 4 > bytes.length) {
60
+ return '';
61
+ }
62
+ return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
63
+ };
64
+ const normalizeFontFamily = (value) => {
65
+ const withoutSubset = value.replace(/^[A-Z]{6}\+/i, '');
66
+ const withoutStyle = withoutSubset.replace(/(?:[\s,_-]+)(?:bold|regular|italic|oblique|medium|semibold|demibold|light|black|thin)(?:mt)?(?:[\s,_-].*)?$/i, '');
67
+ const normalized = withoutStyle
68
+ .normalize('NFKC')
69
+ .toLowerCase()
70
+ .replace(/[\s,_-]+/g, '');
71
+ return CJK_FONT_FAMILY_ALIASES[normalized] || normalized;
72
+ };
73
+ const isCjkFontFamily = (family) => {
74
+ const key = normalizeFontFamily(family);
75
+ return CJK_FONT_FAMILY_MARKERS.some(marker => key.includes(marker)) ||
76
+ /[\u3400-\u9fff]/.test(family);
77
+ };
78
+ const readFontName = (font) => {
79
+ var _a;
80
+ return ((_a = font.lookupMaybe(BASE_FONT, PDFName)) === null || _a === void 0 ? void 0 : _a.decodeText()) || '';
81
+ };
82
+ const readFontFamily = (descriptor) => {
83
+ var _a;
84
+ if (!descriptor) {
85
+ return '';
86
+ }
87
+ return ((_a = descriptor.lookupMaybe(FONT_FAMILY, PDFString, PDFHexString)) === null || _a === void 0 ? void 0 : _a.decodeText()) || '';
88
+ };
89
+ const getDescendantFont = (font) => {
90
+ var _a;
91
+ return (_a = font.lookupMaybe(DESCENDANT_FONTS, PDFArray)) === null || _a === void 0 ? void 0 : _a.lookupMaybe(0, PDFDict);
92
+ };
93
+ const getEmbeddedTrueTypeFont = (descriptor) => {
94
+ var _a;
95
+ if (!descriptor) {
96
+ return undefined;
97
+ }
98
+ const fontFile = descriptor.get(FONT_FILE_2);
99
+ if (!fontFile) {
100
+ return undefined;
101
+ }
102
+ const resolved = descriptor.context.lookup(fontFile);
103
+ if (!(resolved instanceof PDFRawStream)) {
104
+ return undefined;
105
+ }
106
+ const declaredLength = (_a = resolved.dict.lookupMaybe(LENGTH_1, PDFNumber)) === null || _a === void 0 ? void 0 : _a.asNumber();
107
+ if (typeof declaredLength !== 'number' ||
108
+ !Number.isSafeInteger(declaredLength) ||
109
+ declaredLength < 1 ||
110
+ declaredLength > MAX_DECODED_FONT_BYTES ||
111
+ resolved.contents.byteLength > MAX_DECODED_FONT_BYTES) {
112
+ return undefined;
113
+ }
114
+ return resolved;
115
+ };
116
+ const createFontRecord = (font) => {
117
+ var _a;
118
+ const descendant = getDescendantFont(font);
119
+ if (!descendant) {
120
+ return null;
121
+ }
122
+ const encoding = (_a = font.lookupMaybe(ENCODING, PDFName)) === null || _a === void 0 ? void 0 : _a.decodeText();
123
+ if (encoding !== 'Identity-H' && encoding !== 'Identity-V') {
124
+ return null;
125
+ }
126
+ const descriptor = descendant.lookupMaybe(FONT_DESCRIPTOR, PDFDict);
127
+ const baseFont = readFontName(font);
128
+ const descriptorFamily = readFontFamily(descriptor);
129
+ const cidToGidMapObject = descendant.get(CID_TO_GID_MAP);
130
+ const cidToGidMap = cidToGidMapObject
131
+ ? descendant.context.lookup(cidToGidMapObject)
132
+ : undefined;
133
+ const familyKeys = new Set([baseFont, descriptorFamily]
134
+ .filter(Boolean)
135
+ .map(normalizeFontFamily)
136
+ .filter(Boolean));
137
+ return {
138
+ font,
139
+ descendant,
140
+ familyKeys,
141
+ baseFont,
142
+ identityCidToGidMap: !cidToGidMap ||
143
+ (cidToGidMap instanceof PDFName && cidToGidMap.decodeText() === 'Identity'),
144
+ embeddedFont: getEmbeddedTrueTypeFont(descriptor),
145
+ };
146
+ };
147
+ const collectFontRecords = (pdfDocument) => {
148
+ const records = new Map();
149
+ const visitedResources = new Set();
150
+ const visitedXObjects = new Set();
151
+ const visitResources = (resources) => {
152
+ var _a;
153
+ if (!resources || visitedResources.has(resources)) {
154
+ return;
155
+ }
156
+ visitedResources.add(resources);
157
+ const fonts = resources.lookupMaybe(FONT, PDFDict);
158
+ for (const fontObject of (fonts === null || fonts === void 0 ? void 0 : fonts.values()) || []) {
159
+ const font = pdfDocument.context.lookup(fontObject);
160
+ if (!(font instanceof PDFDict) || records.has(font)) {
161
+ continue;
162
+ }
163
+ const record = createFontRecord(font);
164
+ if (record) {
165
+ records.set(font, record);
166
+ }
167
+ }
168
+ const xObjects = resources.lookupMaybe(X_OBJECT, PDFDict);
169
+ for (const xObjectRef of (xObjects === null || xObjects === void 0 ? void 0 : xObjects.values()) || []) {
170
+ const xObject = pdfDocument.context.lookup(xObjectRef);
171
+ if (!(xObject instanceof PDFRawStream) || visitedXObjects.has(xObject)) {
172
+ continue;
173
+ }
174
+ visitedXObjects.add(xObject);
175
+ if (((_a = xObject.dict.lookupMaybe(SUBTYPE, PDFName)) === null || _a === void 0 ? void 0 : _a.decodeText()) !== 'Form') {
176
+ continue;
177
+ }
178
+ visitResources(xObject.dict.lookupMaybe(RESOURCES, PDFDict));
179
+ }
180
+ };
181
+ for (const page of pdfDocument.getPages()) {
182
+ visitResources(page.node.Resources());
183
+ }
184
+ return [...records.values()];
185
+ };
186
+ const findCmapTable = (fontBytes) => {
187
+ if (fontBytes.length < 12) {
188
+ throw new Error('Invalid TrueType font header.');
189
+ }
190
+ const tableCount = readUint16(fontBytes, 4);
191
+ if (tableCount < 1 || tableCount > MAX_TTF_TABLES) {
192
+ throw new Error(`Invalid TrueType table count: ${tableCount}.`);
193
+ }
194
+ let cmapOffset = -1;
195
+ for (let index = 0; index < tableCount; index += 1) {
196
+ const entryOffset = 12 + index * 16;
197
+ if (entryOffset + 16 > fontBytes.length) {
198
+ throw new Error('Invalid TrueType table directory.');
199
+ }
200
+ if (readTag(fontBytes, entryOffset) === 'cmap') {
201
+ cmapOffset = readUint32(fontBytes, entryOffset + 8);
202
+ break;
203
+ }
204
+ }
205
+ if (cmapOffset < 0 || cmapOffset + 4 > fontBytes.length) {
206
+ throw new Error('TrueType font does not contain a readable cmap table.');
207
+ }
208
+ const cmapCount = readUint16(fontBytes, cmapOffset + 2);
209
+ const tables = [];
210
+ for (let index = 0; index < cmapCount; index += 1) {
211
+ const recordOffset = cmapOffset + 4 + index * 8;
212
+ if (recordOffset + 8 > fontBytes.length) {
213
+ break;
214
+ }
215
+ const offset = cmapOffset + readUint32(fontBytes, recordOffset + 4);
216
+ if (offset + 2 > fontBytes.length) {
217
+ continue;
218
+ }
219
+ const format = readUint16(fontBytes, offset);
220
+ if (format === 4 || format === 12) {
221
+ tables.push({
222
+ offset,
223
+ format,
224
+ platformId: readUint16(fontBytes, recordOffset),
225
+ encodingId: readUint16(fontBytes, recordOffset + 2),
226
+ });
227
+ }
228
+ }
229
+ const score = (table) => (table.format === 12 ? 100 : 0) +
230
+ (table.platformId === 3 ? 20 : 0) +
231
+ (table.platformId === 0 ? 10 : 0) +
232
+ (table.encodingId === 10 ? 4 : 0) +
233
+ (table.encodingId === 1 ? 2 : 0);
234
+ const selected = tables.sort((left, right) => score(right) - score(left))[0];
235
+ if (!selected) {
236
+ throw new Error('TrueType font does not contain a supported Unicode cmap.');
237
+ }
238
+ return selected;
239
+ };
240
+ const setGlyphMapping = (map, glyphId, codePoint) => {
241
+ if (glyphId > 0 &&
242
+ glyphId <= MAX_CMAP_GLYPHS &&
243
+ codePoint > 0 &&
244
+ codePoint <= 0x10ffff &&
245
+ !map.has(glyphId)) {
246
+ map.set(glyphId, codePoint);
247
+ }
248
+ };
249
+ const parseFormat12Cmap = (fontBytes, table, glyphToUnicode) => {
250
+ const tableLength = readUint32(fontBytes, table.offset + 4);
251
+ const tableEnd = table.offset + tableLength;
252
+ const groupCount = readUint32(fontBytes, table.offset + 12);
253
+ if (tableLength < 16 ||
254
+ tableEnd > fontBytes.length ||
255
+ table.offset + 16 + groupCount * 12 > tableEnd) {
256
+ throw new Error('Invalid TrueType cmap format 12 groups.');
257
+ }
258
+ let visitedCodePoints = 0;
259
+ for (let index = 0; index < groupCount; index += 1) {
260
+ const offset = table.offset + 16 + index * 12;
261
+ const startCodePoint = readUint32(fontBytes, offset);
262
+ const endCodePoint = readUint32(fontBytes, offset + 4);
263
+ const startGlyphId = readUint32(fontBytes, offset + 8);
264
+ const length = Math.min(endCodePoint - startCodePoint + 1, MAX_CMAP_GLYPHS - startGlyphId + 1);
265
+ if (!Number.isSafeInteger(length) || length < 1) {
266
+ continue;
267
+ }
268
+ for (let innerIndex = 0; innerIndex < length; innerIndex += 1) {
269
+ if (visitedCodePoints >= MAX_CMAP_CODEPOINT_VISITS) {
270
+ return;
271
+ }
272
+ visitedCodePoints += 1;
273
+ setGlyphMapping(glyphToUnicode, startGlyphId + innerIndex, startCodePoint + innerIndex);
274
+ }
275
+ }
276
+ };
277
+ const parseFormat4Cmap = (fontBytes, table, glyphToUnicode) => {
278
+ const tableLength = readUint16(fontBytes, table.offset + 2);
279
+ const tableEnd = table.offset + tableLength;
280
+ const segmentCount = readUint16(fontBytes, table.offset + 6) / 2;
281
+ const endCodesOffset = table.offset + 14;
282
+ const startCodesOffset = endCodesOffset + segmentCount * 2 + 2;
283
+ const deltasOffset = startCodesOffset + segmentCount * 2;
284
+ const rangeOffsetsOffset = deltasOffset + segmentCount * 2;
285
+ if (!Number.isInteger(segmentCount) ||
286
+ segmentCount < 1 ||
287
+ tableLength < 16 ||
288
+ tableEnd > fontBytes.length ||
289
+ rangeOffsetsOffset + segmentCount * 2 > tableEnd) {
290
+ throw new Error('Invalid TrueType cmap format 4 segments.');
291
+ }
292
+ let visitedCodePoints = 0;
293
+ for (let index = 0; index < segmentCount; index += 1) {
294
+ const endCodePoint = readUint16(fontBytes, endCodesOffset + index * 2);
295
+ const startCodePoint = readUint16(fontBytes, startCodesOffset + index * 2);
296
+ const delta = readUint16(fontBytes, deltasOffset + index * 2);
297
+ const rangeOffset = readUint16(fontBytes, rangeOffsetsOffset + index * 2);
298
+ if (endCodePoint < startCodePoint) {
299
+ continue;
300
+ }
301
+ for (let codePoint = startCodePoint; codePoint <= endCodePoint && codePoint !== 0xffff; codePoint += 1) {
302
+ if (visitedCodePoints >= MAX_CMAP_CODEPOINT_VISITS) {
303
+ return;
304
+ }
305
+ visitedCodePoints += 1;
306
+ let glyphId = 0;
307
+ if (rangeOffset === 0) {
308
+ glyphId = (codePoint + delta) & 0xffff;
309
+ }
310
+ else {
311
+ const glyphOffset = rangeOffsetsOffset + index * 2 + rangeOffset +
312
+ (codePoint - startCodePoint) * 2;
313
+ if (glyphOffset + 2 <= tableEnd) {
314
+ glyphId = readUint16(fontBytes, glyphOffset);
315
+ if (glyphId) {
316
+ glyphId = (glyphId + delta) & 0xffff;
317
+ }
318
+ }
319
+ }
320
+ setGlyphMapping(glyphToUnicode, glyphId, codePoint);
321
+ }
322
+ }
323
+ };
324
+ const parseTrueTypeGlyphToUnicode = (fontBytes) => {
325
+ const selected = findCmapTable(fontBytes);
326
+ const glyphToUnicode = new Map();
327
+ if (selected.format === 12) {
328
+ parseFormat12Cmap(fontBytes, selected, glyphToUnicode);
329
+ }
330
+ else {
331
+ parseFormat4Cmap(fontBytes, selected, glyphToUnicode);
332
+ }
333
+ return glyphToUnicode;
334
+ };
335
+ const toHex = (value, minLength = 4) => value.toString(16).toUpperCase().padStart(minLength, '0');
336
+ const encodeUnicodeCodePoint = (codePoint) => {
337
+ if (codePoint <= 0xffff) {
338
+ return toHex(codePoint);
339
+ }
340
+ const offset = codePoint - 0x10000;
341
+ const highSurrogate = 0xd800 + (offset >> 10);
342
+ const lowSurrogate = 0xdc00 + (offset & 0x3ff);
343
+ return `${toHex(highSurrogate)}${toHex(lowSurrogate)}`;
344
+ };
345
+ const createToUnicodeCmap = (glyphToUnicode) => {
346
+ const entries = [...glyphToUnicode]
347
+ .filter(([glyphId, codePoint]) => glyphId > 0 && glyphId <= 0xffff &&
348
+ codePoint > 0 && codePoint <= 0x10ffff)
349
+ .sort(([left], [right]) => left - right);
350
+ const sections = [];
351
+ for (let index = 0; index < entries.length; index += CMAP_BFCHAR_CHUNK_SIZE) {
352
+ const chunk = entries.slice(index, index + CMAP_BFCHAR_CHUNK_SIZE);
353
+ sections.push([
354
+ `${chunk.length} beginbfchar`,
355
+ ...chunk.map(([glyphId, codePoint]) => `<${toHex(glyphId)}> <${encodeUnicodeCodePoint(codePoint)}>`),
356
+ 'endbfchar',
357
+ ].join('\n'));
358
+ }
359
+ return [
360
+ '/CIDInit /ProcSet findresource begin',
361
+ '12 dict begin',
362
+ 'begincmap',
363
+ '/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def',
364
+ '/CMapName /Adobe-Identity-UCS def',
365
+ '/CMapType 2 def',
366
+ '1 begincodespacerange',
367
+ '<0000> <FFFF>',
368
+ 'endcodespacerange',
369
+ ...sections,
370
+ 'endcmap',
371
+ 'CMapName currentdict /CMap defineresource pop',
372
+ 'end',
373
+ 'end',
374
+ ].join('\n');
375
+ };
376
+ const familiesOverlap = (left, right) => {
377
+ for (const key of left) {
378
+ if (right.has(key)) {
379
+ return true;
380
+ }
381
+ }
382
+ return false;
383
+ };
384
+ export const repairMalformedIdentityCjkFonts = async (sourceBytes, candidateFamilies = []) => {
385
+ if (!sourceBytes.byteLength || sourceBytes.byteLength > MAX_REPAIR_SOURCE_BYTES) {
386
+ return { bytes: sourceBytes, repairedFonts: 0, repairedFamilies: [] };
387
+ }
388
+ const candidateKeys = new Set(candidateFamilies.map(normalizeFontFamily).filter(Boolean));
389
+ const pdfDocument = await PDFDocument.load(sourceBytes, {
390
+ updateMetadata: false,
391
+ });
392
+ const records = collectFontRecords(pdfDocument);
393
+ const sourceMaps = new Map();
394
+ const getSourceMap = (record) => {
395
+ const cached = sourceMaps.get(record);
396
+ if (cached) {
397
+ return cached;
398
+ }
399
+ if (!record.embeddedFont) {
400
+ return undefined;
401
+ }
402
+ const decoded = decodePDFRawStream(record.embeddedFont).decode();
403
+ if (decoded.byteLength > MAX_DECODED_FONT_BYTES) {
404
+ throw new Error('Embedded TrueType font exceeds the Identity repair limit.');
405
+ }
406
+ const glyphMap = parseTrueTypeGlyphToUnicode(decoded);
407
+ sourceMaps.set(record, glyphMap);
408
+ return glyphMap;
409
+ };
410
+ let repairedFonts = 0;
411
+ const repairedFamilies = new Set();
412
+ for (const target of records) {
413
+ if (target.font.has(TO_UNICODE) ||
414
+ target.embeddedFont ||
415
+ !target.identityCidToGidMap ||
416
+ ![...target.familyKeys].some(key => isCjkFontFamily(key)) ||
417
+ (candidateKeys.size > 0 && ![...target.familyKeys].some(key => candidateKeys.has(key)))) {
418
+ continue;
419
+ }
420
+ let bestMap;
421
+ for (const source of records) {
422
+ if (!source.embeddedFont || !familiesOverlap(target.familyKeys, source.familyKeys)) {
423
+ continue;
424
+ }
425
+ try {
426
+ const sourceMap = getSourceMap(source);
427
+ if (sourceMap && (!bestMap || sourceMap.size > bestMap.size)) {
428
+ bestMap = sourceMap;
429
+ }
430
+ }
431
+ catch {
432
+ // A different same-family embedded font may still provide a valid cmap.
433
+ }
434
+ }
435
+ if (!bestMap || bestMap.size < 2) {
436
+ continue;
437
+ }
438
+ const cmap = createToUnicodeCmap(bestMap);
439
+ const cmapRef = pdfDocument.context.register(pdfDocument.context.flateStream(cmap));
440
+ target.font.set(TO_UNICODE, cmapRef);
441
+ repairedFonts += 1;
442
+ repairedFamilies.add(target.baseFont || [...target.familyKeys][0] || 'CJK font');
443
+ }
444
+ if (!repairedFonts) {
445
+ return { bytes: sourceBytes, repairedFonts: 0, repairedFamilies: [] };
446
+ }
447
+ return {
448
+ bytes: await pdfDocument.save({ useObjectStreams: true }),
449
+ repairedFonts,
450
+ repairedFamilies: [...repairedFamilies],
451
+ };
452
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-pdf",
3
- "version": "2.1.25",
3
+ "version": "2.1.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone PDF renderer plugin for Flyfish File Viewer powered by PDF.js.",
@@ -54,8 +54,10 @@
54
54
  "LICENSE"
55
55
  ],
56
56
  "dependencies": {
57
- "@file-viewer/core": "2.1.25",
58
- "pdfjs-dist": "4.10.38"
57
+ "@file-viewer/core": "2.1.27",
58
+ "@fontsource-variable/noto-sans-sc": "5.2.10",
59
+ "pdf-lib": "1.17.1",
60
+ "pdfjs-dist": "5.4.624"
59
61
  },
60
62
  "devDependencies": {
61
63
  "typescript": "^6.0.3"