@file-viewer/renderer-pdf 2.1.24 → 2.1.26
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.en.md +9 -2
- package/README.md +9 -2
- package/dist/pdf.js +272 -21
- package/dist/pdfFontFallback.d.ts +33 -0
- package/dist/pdfFontFallback.js +282 -0
- package/dist/pdfIdentityFontRepair.d.ts +6 -0
- package/dist/pdfIdentityFontRepair.js +452 -0
- package/package.json +5 -3
package/README.en.md
CHANGED
|
@@ -25,21 +25,28 @@ const options = {
|
|
|
25
25
|
|
|
26
26
|
## Offline Assets
|
|
27
27
|
|
|
28
|
-
PDF preview depends on the PDF.js worker, cMaps, WASM helpers, and
|
|
28
|
+
PDF preview depends on the PDF.js worker, cMaps, WASM helpers, standard fonts, and an offline fallback for unembedded CJK fonts. Asset paths use the same unified options as `@file-viewer/core`:
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
const options = {
|
|
32
32
|
renderers: pdfRenderer,
|
|
33
33
|
pdf: {
|
|
34
|
+
assetBaseUrl: '/workspace/',
|
|
34
35
|
workerUrl: '/vendor/pdf/pdf.worker.mjs',
|
|
35
36
|
cMapUrl: '/vendor/pdf/cmaps/',
|
|
36
37
|
wasmUrl: '/vendor/pdf/wasm/',
|
|
37
38
|
standardFontDataUrl: '/vendor/pdf/standard_fonts/',
|
|
39
|
+
cjkFontFallbackPath: '/vendor/pdf/fonts/',
|
|
40
|
+
identityFontRepair: true,
|
|
38
41
|
},
|
|
39
42
|
}
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
When no explicit URL is
|
|
45
|
+
When no explicit PDF asset URL is configured, the renderer first uses the public base exposed by build tools such as Vite and can fall back to the page entry script. An app deployed under `/workspace/` therefore still requests `/workspace/vendor/pdf/pdf.worker.mjs` while its current SPA route is `/workspace/c/`. UMI, custom proxies, and builds whose public path cannot be detected can set `pdf.assetBaseUrl: '/workspace/'` to pin the base for every PDF offline asset. If the host app did not run `file-viewer-copy-assets`, does not use `@file-viewer/vite-plugin`, or a local dev server falls back to HTML for that path, the renderer lazy-loads the packaged PDF.js worker handler as a compatibility fallback so preview does not fail with `Setting up fake worker failed`. For best performance, complete cMap/standard-font/WASM decoding, or strict offline deployments, still copy viewer assets and point these URLs at real static files. The pinned PDF.js 5.4 stable line loads local JBIG2, JPEG 2000, color-management, and related helpers through `wasmUrl`; viewer assets and the Vite plugin copy them without relying on the public internet.
|
|
46
|
+
|
|
47
|
+
`cjkFontFallback` is enabled by default. When a PDF references `MicrosoftYaHei-Bold`, SimSun, SimHei, or another CJK font without embedding the font program, the renderer prefers an installed system font and otherwise aliases the original PDF font name to the bundled Noto Sans SC variable font. Only WOFF2 shards needed by the current page text are loaded. The first page waits for the font before rendering, and later pages are redrawn when they introduce new glyphs. Set the option to `false` to disable this behavior, or point `cjkFontFallbackPath` at a self-hosted directory with the same structure.
|
|
48
|
+
|
|
49
|
+
`identityFontRepair` is also enabled by default for malformed PDFs that write TrueType glyph IDs through `Identity-H`/`Identity-V` but omit `ToUnicode`. The repair module is loaded only after CJK font substitution and multiple control-character glyphs are detected. If the same PDF embeds a usable same-family TrueType font, its cmap is used to rebuild the Unicode mapping for the in-memory preview. The repair is applied only to a preview copy and never overwrites the original file or download source; an unsafe or unsupported repair falls back to the original preview. Set the option to `false` to disable this compatibility path.
|
|
43
50
|
|
|
44
51
|
## Migration Note
|
|
45
52
|
|
package/README.md
CHANGED
|
@@ -25,21 +25,28 @@ const options = {
|
|
|
25
25
|
|
|
26
26
|
## 离线资源
|
|
27
27
|
|
|
28
|
-
PDF 预览依赖 PDF.js worker、cMaps、WASM
|
|
28
|
+
PDF 预览依赖 PDF.js worker、cMaps、WASM、standard fonts,以及未嵌入中文字体的离线回退资源。资源路径沿用 `@file-viewer/core` 的统一 options:
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
31
|
const options = {
|
|
32
32
|
renderers: pdfRenderer,
|
|
33
33
|
pdf: {
|
|
34
|
+
assetBaseUrl: '/workspace/',
|
|
34
35
|
workerUrl: '/vendor/pdf/pdf.worker.mjs',
|
|
35
36
|
cMapUrl: '/vendor/pdf/cmaps/',
|
|
36
37
|
wasmUrl: '/vendor/pdf/wasm/',
|
|
37
38
|
standardFontDataUrl: '/vendor/pdf/standard_fonts/',
|
|
39
|
+
cjkFontFallbackPath: '/vendor/pdf/fonts/',
|
|
40
|
+
identityFontRepair: true,
|
|
38
41
|
},
|
|
39
42
|
}
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
默认未显式配置时,渲染器优先使用 Vite 等构建器提供的公开基址,并可从页面入口脚本回退识别部署子路径。因此应用部署在 `/workspace/`、当前 SPA 路由为 `/workspace/c/` 时,仍会请求 `/workspace/vendor/pdf/pdf.worker.mjs`。UMI、自定义代理或无法自动识别的构建环境可以设置 `pdf.assetBaseUrl: '/workspace/'` 固定全部 PDF 离线资源的基址。如果项目没有执行 `file-viewer-copy-assets`、没有使用 `@file-viewer/vite-plugin`,或者本地临时服务器把该路径回退成 HTML,PDF renderer 会自动懒加载包内 PDF.js worker handler 作为兼容兜底,避免 `Setting up fake worker failed` 直接中断预览。需要最佳性能、完整 cMap/standard fonts/WASM 解码或严格离线部署时,仍建议复制 viewer assets 并配置真实静态地址。当前锁定的 PDF.js 5.4 稳定线会通过 `wasmUrl` 本地加载 JBIG2、JPEG 2000、颜色管理等辅助资源;这些资源由 viewer assets 和 Vite 插件统一复制,不依赖公网。
|
|
46
|
+
|
|
47
|
+
`cjkFontFallback` 默认开启。PDF 引用了 `MicrosoftYaHei-Bold`、宋体、黑体或其他中文字体但没有嵌入字体数据时,渲染器优先使用本机字体;本机缺失时,会把原 PDF 字体名映射到本地 Noto Sans SC 可变字体,并根据当前页实际文字只加载需要的 WOFF2 分片。首屏会在字体就绪后再渲染,后续页发现新字形时会自动重绘。可设为 `false` 完全关闭,或用 `cjkFontFallbackPath` 指向同结构的自托管字体目录。
|
|
48
|
+
|
|
49
|
+
`identityFontRepair` 也默认开启,用于少数把 TrueType 字形 ID 写入 `Identity-H`/`Identity-V`、却遗漏 `ToUnicode` 的异常 PDF。只有检测到中文字体替换和多个控制字符乱码时,渲染器才会懒加载修复模块;若文件内同时嵌入了可用的同族 TrueType 字体,就从其 cmap 重建内存预览所需的 Unicode 映射。修复只作用于预览副本,不覆盖原始文件和下载源;无法安全修复时继续使用原预览。可设为 `false` 关闭。
|
|
43
50
|
|
|
44
51
|
## 迁移说明
|
|
45
52
|
|
package/dist/pdf.js
CHANGED
|
@@ -3,6 +3,7 @@ import { EventBus, GenericL10n, PDFFindController, PDFLinkService, PDFViewer, }
|
|
|
3
3
|
import { registerFileViewerSearchProvider, registerFileViewerZoomProvider, unregisterFileViewerSearchProvider, unregisterFileViewerZoomProvider, registerFileViewerViewStateProvider, unregisterFileViewerViewStateProvider, createFileViewerZoomChangeEmitter, createFileViewerViewStateChange, createFileViewerViewStateChangeEmitter, createFileViewerTranslator, buildPrintPageStyle, formatCssPixels, DEFAULT_PDF_RANGE_CHUNK_SIZE, resolveFileViewerLocale, resolveFileViewerFitScale, } from '@file-viewer/core';
|
|
4
4
|
import { DEFAULT_FILE_VIEWER_PDF_WORKER_PATH, resolveFileViewerPdfAssetUrls, } from '@file-viewer/core/assets';
|
|
5
5
|
import { pdfViewerStyle } from './pdfStyles.js';
|
|
6
|
+
import { collectMalformedIdentityFontNames, createPdfCjkFontFallbackManager, detectMalformedIdentityCjkFontFamilies, } from './pdfFontFallback.js';
|
|
6
7
|
export const DEFAULT_FILE_VIEWER_PDF_WORKER_URL = DEFAULT_FILE_VIEWER_PDF_WORKER_PATH;
|
|
7
8
|
const MIN_SCALE = 0.2;
|
|
8
9
|
const MAX_SCALE = 3;
|
|
@@ -19,6 +20,7 @@ const normalizedPdfViewerStyle = pdfViewerStyle
|
|
|
19
20
|
.replace(/--page-border-image:\s*url\(images\/shadow\.png\)\s*9 9 repeat;/g, '--page-border-image:none;')
|
|
20
21
|
.replace(/background:\s*url\("\.\/images\/loading-icon\.gif"\)\s*center no-repeat;/g, 'background:none;');
|
|
21
22
|
const pdfJsConsoleErrorSuppressions = new WeakMap();
|
|
23
|
+
const pdfJsConsoleWarningSuppressions = new WeakMap();
|
|
22
24
|
const createStyle = (documentRef) => {
|
|
23
25
|
const style = documentRef.createElement('style');
|
|
24
26
|
style.textContent = `${normalizedPdfViewerStyle}
|
|
@@ -164,6 +166,63 @@ const suppressPdfJsDestroyedTransportPageInitErrors = (view) => {
|
|
|
164
166
|
}, PDF_JS_DESTROY_CONSOLE_SUPPRESSION_MS);
|
|
165
167
|
};
|
|
166
168
|
};
|
|
169
|
+
const isPdfJsMissingSystemFontWarning = (args) => {
|
|
170
|
+
const [message] = args;
|
|
171
|
+
return typeof message === 'string' &&
|
|
172
|
+
/^(?:Warning:\s*)?Cannot load system font: .+installing it could help to improve PDF rendering\.$/.test(message);
|
|
173
|
+
};
|
|
174
|
+
const suppressPdfJsMissingSystemFontWarnings = (view) => {
|
|
175
|
+
const consoleRef = (view.console ||
|
|
176
|
+
globalThis.console);
|
|
177
|
+
if (!consoleRef || typeof consoleRef.warn !== 'function') {
|
|
178
|
+
return () => { };
|
|
179
|
+
}
|
|
180
|
+
let suppression = pdfJsConsoleWarningSuppressions.get(consoleRef);
|
|
181
|
+
if (!suppression) {
|
|
182
|
+
const originalWarn = consoleRef.warn;
|
|
183
|
+
suppression = {
|
|
184
|
+
originalWarn,
|
|
185
|
+
patchedWarn: (...args) => {
|
|
186
|
+
if (isPdfJsMissingSystemFontWarning(args)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
return originalWarn.apply(consoleRef, args);
|
|
190
|
+
},
|
|
191
|
+
depth: 0,
|
|
192
|
+
restoreTimer: undefined,
|
|
193
|
+
};
|
|
194
|
+
pdfJsConsoleWarningSuppressions.set(consoleRef, suppression);
|
|
195
|
+
consoleRef.warn = suppression.patchedWarn;
|
|
196
|
+
}
|
|
197
|
+
else if (suppression.restoreTimer !== undefined) {
|
|
198
|
+
view.clearTimeout(suppression.restoreTimer);
|
|
199
|
+
suppression.restoreTimer = undefined;
|
|
200
|
+
}
|
|
201
|
+
suppression.depth += 1;
|
|
202
|
+
let released = false;
|
|
203
|
+
return () => {
|
|
204
|
+
if (released) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
released = true;
|
|
208
|
+
const current = pdfJsConsoleWarningSuppressions.get(consoleRef);
|
|
209
|
+
if (!current) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
current.depth = Math.max(0, current.depth - 1);
|
|
213
|
+
if (current.depth || current.restoreTimer !== undefined) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
current.restoreTimer = view.setTimeout(() => {
|
|
217
|
+
current.restoreTimer = undefined;
|
|
218
|
+
if (current.depth || consoleRef.warn !== current.patchedWarn) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
consoleRef.warn = current.originalWarn;
|
|
222
|
+
pdfJsConsoleWarningSuppressions.delete(consoleRef);
|
|
223
|
+
}, PDF_JS_DESTROY_CONSOLE_SUPPRESSION_MS);
|
|
224
|
+
};
|
|
225
|
+
};
|
|
167
226
|
const isConfiguredUrl = (value) => {
|
|
168
227
|
return value !== undefined && value !== null && String(value).trim().length > 0;
|
|
169
228
|
};
|
|
@@ -192,8 +251,52 @@ const installBundledPdfFakeWorker = async () => {
|
|
|
192
251
|
WorkerMessageHandler: workerModule.WorkerMessageHandler,
|
|
193
252
|
};
|
|
194
253
|
};
|
|
195
|
-
const resolvePdfWorkerUrl = (options,
|
|
196
|
-
return resolveFileViewerPdfAssetUrls(options,
|
|
254
|
+
const resolvePdfWorkerUrl = (options, documentBaseUrl) => {
|
|
255
|
+
return resolveFileViewerPdfAssetUrls(options, documentBaseUrl).workerUrl;
|
|
256
|
+
};
|
|
257
|
+
const readBundlerPublicBaseUrl = () => {
|
|
258
|
+
var _a;
|
|
259
|
+
try {
|
|
260
|
+
const value = (_a = import.meta.env) === null || _a === void 0 ? void 0 : _a.BASE_URL;
|
|
261
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const resolvePdfRuntimeAssetBaseUrl = (documentRef) => {
|
|
268
|
+
const documentBaseUrl = documentRef.baseURI || documentRef.URL || 'file:///';
|
|
269
|
+
const bundlerBaseUrl = readBundlerPublicBaseUrl();
|
|
270
|
+
if (bundlerBaseUrl && bundlerBaseUrl !== '.' && bundlerBaseUrl !== './') {
|
|
271
|
+
try {
|
|
272
|
+
return new URL(bundlerBaseUrl.endsWith('/') ? bundlerBaseUrl : `${bundlerBaseUrl}/`, documentBaseUrl).href;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Continue with DOM-based public-path detection.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// An explicit <base> is authoritative and already reflected by baseURI.
|
|
279
|
+
if (documentRef.querySelector('base[href]')) {
|
|
280
|
+
return documentBaseUrl;
|
|
281
|
+
}
|
|
282
|
+
// Entry scripts remain under the public deployment base when an SPA route
|
|
283
|
+
// changes document.baseURI. This covers Vite and common UMI/Webpack layouts.
|
|
284
|
+
for (const script of Array.from(documentRef.querySelectorAll('script[src]'))) {
|
|
285
|
+
try {
|
|
286
|
+
const scriptUrl = new URL(script.src || script.getAttribute('src') || '', documentBaseUrl);
|
|
287
|
+
const assetDirectory = scriptUrl.pathname.match(/^(.*\/)(?:assets|static)\/[^/]+$/i);
|
|
288
|
+
if (assetDirectory) {
|
|
289
|
+
return new URL(assetDirectory[1], scriptUrl.origin).href;
|
|
290
|
+
}
|
|
291
|
+
if (/\/(?:umi|main|index)(?:[.-][^/]*)?\.(?:m?js)$/i.test(scriptUrl.pathname)) {
|
|
292
|
+
return new URL('./', scriptUrl).href;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Ignore unrelated or malformed script URLs.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return documentBaseUrl;
|
|
197
300
|
};
|
|
198
301
|
const buildOutlineItems = (items, prefix = 'outline', getFallbackTitle = index => `Outline ${index + 1}`) => items.map((item, index) => {
|
|
199
302
|
const id = `${prefix}-${index}`;
|
|
@@ -218,6 +321,10 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
218
321
|
throw new Error(t('pdf.error.browserWindow'));
|
|
219
322
|
}
|
|
220
323
|
const options = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.pdf;
|
|
324
|
+
const pdfRuntimeAssetBaseUrl = resolvePdfRuntimeAssetBaseUrl(documentRef);
|
|
325
|
+
const cjkFontFallbackEnabled = (options === null || options === void 0 ? void 0 : options.cjkFontFallback) !== false;
|
|
326
|
+
const identityFontRepairEnabled = (options === null || options === void 0 ? void 0 : options.identityFontRepair) !== false;
|
|
327
|
+
const fontInspectionEnabled = cjkFontFallbackEnabled || identityFontRepairEnabled;
|
|
221
328
|
const initialViewState = (options === null || options === void 0 ? void 0 : options.initialViewState) || ((_b = context === null || context === void 0 ? void 0 : context.options) === null || _b === void 0 ? void 0 : _b.initialViewState) || null;
|
|
222
329
|
const navigationEnabled = (options === null || options === void 0 ? void 0 : options.navigation) !== false;
|
|
223
330
|
const toolbarVisible = (options === null || options === void 0 ? void 0 : options.toolbar) !== false;
|
|
@@ -260,6 +367,10 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
260
367
|
let pdfSearchWaiters = [];
|
|
261
368
|
const pdfThumbnails = new Map();
|
|
262
369
|
const pendingPdfThumbnails = new Set();
|
|
370
|
+
const pdfCjkFontFallbackPageLoads = new Map();
|
|
371
|
+
const pdfCjkFontFallbackRenderHandledPages = new Set();
|
|
372
|
+
let pdfCjkFontFallbackManager = null;
|
|
373
|
+
let restorePdfJsMissingSystemFontWarnings = () => { };
|
|
263
374
|
const pdfContext = {
|
|
264
375
|
viewer: null,
|
|
265
376
|
linkService: null,
|
|
@@ -269,6 +380,17 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
269
380
|
document: null,
|
|
270
381
|
search: '',
|
|
271
382
|
};
|
|
383
|
+
const ensurePdfPageCjkFontFallback = (pageNumber, page) => {
|
|
384
|
+
if (!pdfCjkFontFallbackManager) {
|
|
385
|
+
return Promise.resolve(false);
|
|
386
|
+
}
|
|
387
|
+
let pending = pdfCjkFontFallbackPageLoads.get(pageNumber);
|
|
388
|
+
if (!pending) {
|
|
389
|
+
pending = pdfCjkFontFallbackManager.ensurePage(page);
|
|
390
|
+
pdfCjkFontFallbackPageLoads.set(pageNumber, pending);
|
|
391
|
+
}
|
|
392
|
+
return pending;
|
|
393
|
+
};
|
|
272
394
|
const root = createElement(documentRef, 'div', 'pdf-shell');
|
|
273
395
|
root.dataset.viewerSearchProvider = 'pdf';
|
|
274
396
|
root.dataset.viewerZoomProvider = 'pdf';
|
|
@@ -426,6 +548,7 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
426
548
|
if (destroyed || pdfContext.document !== pdfDocument) {
|
|
427
549
|
return;
|
|
428
550
|
}
|
|
551
|
+
await ensurePdfPageCjkFontFallback(pageNumber, page);
|
|
429
552
|
const baseViewport = page.getViewport({
|
|
430
553
|
scale: PixelsPerInch.PDF_TO_CSS_UNITS,
|
|
431
554
|
rotation: currentRotation,
|
|
@@ -597,14 +720,16 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
597
720
|
}
|
|
598
721
|
};
|
|
599
722
|
const createPdfWorker = async () => {
|
|
600
|
-
const workerUrl = resolvePdfWorkerUrl(options,
|
|
723
|
+
const workerUrl = resolvePdfWorkerUrl(options, pdfRuntimeAssetBaseUrl);
|
|
601
724
|
const hasExplicitWorkerUrl = isConfiguredUrl(options === null || options === void 0 ? void 0 : options.workerUrl);
|
|
602
725
|
const shouldUseRealWorker = !!(targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.Worker) &&
|
|
603
726
|
(hasExplicitWorkerUrl || await canUseResolvedPdfWorkerUrl(workerUrl));
|
|
604
727
|
if (shouldUseRealWorker) {
|
|
605
728
|
GlobalWorkerOptions.workerSrc = workerUrl;
|
|
606
729
|
try {
|
|
607
|
-
const worker = PdfJsWorker
|
|
730
|
+
const worker = new PdfJsWorker({
|
|
731
|
+
name: 'file-viewer-pdf-worker',
|
|
732
|
+
});
|
|
608
733
|
await worker.promise;
|
|
609
734
|
return worker;
|
|
610
735
|
}
|
|
@@ -1193,6 +1318,7 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1193
1318
|
throw new Error(t('pdf.error.unloaded'));
|
|
1194
1319
|
}
|
|
1195
1320
|
const page = await pdfDocument.getPage(pageNumber);
|
|
1321
|
+
await ensurePdfPageCjkFontFallback(pageNumber, page);
|
|
1196
1322
|
const baseViewport = page.getViewport({
|
|
1197
1323
|
scale: PixelsPerInch.PDF_TO_CSS_UNITS,
|
|
1198
1324
|
rotation: currentRotation,
|
|
@@ -1227,8 +1353,13 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1227
1353
|
return `<div class="pdf-export-document">${pagesHtml.join('')}</div>`;
|
|
1228
1354
|
};
|
|
1229
1355
|
const loadFile = async () => {
|
|
1230
|
-
var _a, _b;
|
|
1356
|
+
var _a, _b, _c;
|
|
1231
1357
|
const requestVersion = ++loadVersion;
|
|
1358
|
+
restorePdfJsMissingSystemFontWarnings();
|
|
1359
|
+
restorePdfJsMissingSystemFontWarnings = () => { };
|
|
1360
|
+
pdfCjkFontFallbackManager = null;
|
|
1361
|
+
pdfCjkFontFallbackPageLoads.clear();
|
|
1362
|
+
pdfCjkFontFallbackRenderHandledPages.clear();
|
|
1232
1363
|
loadStatus = 'loading';
|
|
1233
1364
|
errorMessage = '';
|
|
1234
1365
|
pdfContext.document = null;
|
|
@@ -1313,12 +1444,46 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1313
1444
|
emitViewStateChange('zoom-change', 'viewer');
|
|
1314
1445
|
}
|
|
1315
1446
|
});
|
|
1316
|
-
eventBus.on('pagerendered',
|
|
1447
|
+
eventBus.on('pagerendered', ({ pageNumber }) => {
|
|
1448
|
+
scheduleLegacyPageDimensionPatch();
|
|
1449
|
+
if (!pdfCjkFontFallbackManager ||
|
|
1450
|
+
pdfCjkFontFallbackRenderHandledPages.has(pageNumber)) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
pdfCjkFontFallbackRenderHandledPages.add(pageNumber);
|
|
1454
|
+
const pdfDocument = pdfContext.document;
|
|
1455
|
+
const alreadyPrepared = pdfCjkFontFallbackPageLoads.has(pageNumber);
|
|
1456
|
+
if (!pdfDocument || alreadyPrepared) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
void pdfDocument.getPage(pageNumber)
|
|
1460
|
+
.then(page => ensurePdfPageCjkFontFallback(pageNumber, page))
|
|
1461
|
+
.then(fontLoaded => {
|
|
1462
|
+
var _a;
|
|
1463
|
+
if (fontLoaded &&
|
|
1464
|
+
!destroyed &&
|
|
1465
|
+
pdfContext.document === pdfDocument) {
|
|
1466
|
+
(_a = pdfContext.viewer) === null || _a === void 0 ? void 0 : _a.refresh();
|
|
1467
|
+
}
|
|
1468
|
+
})
|
|
1469
|
+
.catch(error => {
|
|
1470
|
+
console.warn('[file-viewer] Unable to inspect a PDF page for CJK font fallback.', error);
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1317
1473
|
if (!(context === null || context === void 0 ? void 0 : context.streamUrl) && !buffer.byteLength) {
|
|
1318
1474
|
throw new Error(t('pdf.error.missingSource'));
|
|
1319
1475
|
}
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1476
|
+
const pdfAssets = resolveFileViewerPdfAssetUrls(options, pdfRuntimeAssetBaseUrl);
|
|
1477
|
+
if (cjkFontFallbackEnabled) {
|
|
1478
|
+
pdfCjkFontFallbackManager = createPdfCjkFontFallbackManager({
|
|
1479
|
+
documentRef,
|
|
1480
|
+
fontAssetPath: pdfAssets.cjkFontFallbackPath,
|
|
1481
|
+
onWarning: (message, error) => {
|
|
1482
|
+
console.warn(`[file-viewer] ${message}`, error || '');
|
|
1483
|
+
},
|
|
1484
|
+
});
|
|
1485
|
+
restorePdfJsMissingSystemFontWarnings = suppressPdfJsMissingSystemFontWarnings(targetWindow);
|
|
1486
|
+
}
|
|
1322
1487
|
const source = (context === null || context === void 0 ? void 0 : context.streamUrl)
|
|
1323
1488
|
? {
|
|
1324
1489
|
url: context.streamUrl,
|
|
@@ -1328,19 +1493,24 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1328
1493
|
: {
|
|
1329
1494
|
data: buffer,
|
|
1330
1495
|
};
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1496
|
+
const createLoadingResource = async (loadingSource) => {
|
|
1497
|
+
const worker = await createPdfWorker();
|
|
1498
|
+
const loadingTask = getDocument({
|
|
1499
|
+
...loadingSource,
|
|
1500
|
+
worker: worker || undefined,
|
|
1501
|
+
cMapUrl: pdfAssets.cMapUrl,
|
|
1502
|
+
wasmUrl: pdfAssets.wasmUrl,
|
|
1503
|
+
standardFontDataUrl: pdfAssets.standardFontDataUrl,
|
|
1504
|
+
useWorkerFetch: true,
|
|
1505
|
+
cMapPacked: true,
|
|
1506
|
+
enableXfa: true,
|
|
1507
|
+
fontExtraProperties: fontInspectionEnabled,
|
|
1508
|
+
});
|
|
1509
|
+
return { loadingTask, worker };
|
|
1510
|
+
};
|
|
1511
|
+
resource = await createLoadingResource(source);
|
|
1342
1512
|
pdfContext.resource = resource;
|
|
1343
|
-
|
|
1513
|
+
let pdfDocument = await resource.loadingTask.promise;
|
|
1344
1514
|
if (destroyed || requestVersion !== loadVersion || pdfContext.resource !== resource) {
|
|
1345
1515
|
if (pdfContext.resource === resource) {
|
|
1346
1516
|
pdfContext.resource = null;
|
|
@@ -1348,10 +1518,86 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1348
1518
|
}
|
|
1349
1519
|
return;
|
|
1350
1520
|
}
|
|
1521
|
+
let firstPageTextContent = null;
|
|
1522
|
+
let firstPageForInspection = null;
|
|
1523
|
+
if (fontInspectionEnabled && pdfDocument.numPages > 0) {
|
|
1524
|
+
const firstPage = await pdfDocument.getPage(1);
|
|
1525
|
+
firstPageForInspection = firstPage;
|
|
1526
|
+
firstPageTextContent = await firstPageForInspection.getTextContent();
|
|
1527
|
+
}
|
|
1528
|
+
if (identityFontRepairEnabled && firstPageTextContent) {
|
|
1529
|
+
const malformedFontNames = collectMalformedIdentityFontNames(firstPageTextContent);
|
|
1530
|
+
if (malformedFontNames.length && firstPageForInspection) {
|
|
1531
|
+
await firstPageForInspection.getOperatorList();
|
|
1532
|
+
}
|
|
1533
|
+
const malformedFontFamilies = new Map();
|
|
1534
|
+
for (const fontName of malformedFontNames) {
|
|
1535
|
+
try {
|
|
1536
|
+
const family = (_b = firstPageForInspection === null || firstPageForInspection === void 0 ? void 0 : firstPageForInspection.commonObjs.get(fontName)) === null || _b === void 0 ? void 0 : _b.name;
|
|
1537
|
+
if (family) {
|
|
1538
|
+
malformedFontFamilies.set(fontName, family);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
// A font object that is still unresolved cannot be repaired safely.
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const candidateFamilies = detectMalformedIdentityCjkFontFamilies(firstPageTextContent, fontName => malformedFontFamilies.get(fontName) || '');
|
|
1546
|
+
if (candidateFamilies.length) {
|
|
1547
|
+
let replacementResource = null;
|
|
1548
|
+
try {
|
|
1549
|
+
const sourceBytes = await pdfDocument.getData();
|
|
1550
|
+
const { repairMalformedIdentityCjkFonts } = await import('./pdfIdentityFontRepair.js');
|
|
1551
|
+
const repaired = await repairMalformedIdentityCjkFonts(sourceBytes, candidateFamilies);
|
|
1552
|
+
if (repaired.repairedFonts > 0) {
|
|
1553
|
+
const previousResource = resource;
|
|
1554
|
+
replacementResource = await createLoadingResource({ data: repaired.bytes });
|
|
1555
|
+
const replacementDocument = await replacementResource.loadingTask.promise;
|
|
1556
|
+
const repairedFirstPage = await replacementDocument.getPage(1);
|
|
1557
|
+
const replacementTextContent = await repairedFirstPage.getTextContent();
|
|
1558
|
+
if (destroyed ||
|
|
1559
|
+
requestVersion !== loadVersion ||
|
|
1560
|
+
pdfContext.resource !== previousResource) {
|
|
1561
|
+
await destroyPdfResource(replacementResource);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
resource = replacementResource;
|
|
1565
|
+
replacementResource = null;
|
|
1566
|
+
pdfContext.resource = resource;
|
|
1567
|
+
pdfDocument = replacementDocument;
|
|
1568
|
+
firstPageTextContent = replacementTextContent;
|
|
1569
|
+
await destroyPdfResource(previousResource);
|
|
1570
|
+
console.info(`[file-viewer] Repaired ${repaired.repairedFonts} malformed PDF Identity CJK font mapping(s).`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
if (replacementResource) {
|
|
1575
|
+
await destroyPdfResource(replacementResource);
|
|
1576
|
+
}
|
|
1577
|
+
console.warn('[file-viewer] Unable to repair a malformed PDF Identity CJK font; continuing with the original preview.', error);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (destroyed || requestVersion !== loadVersion || pdfContext.resource !== resource) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1351
1584
|
pageCount = pdfDocument.numPages;
|
|
1352
1585
|
currentPage = 1;
|
|
1353
1586
|
pdfContext.document = pdfDocument;
|
|
1354
|
-
(
|
|
1587
|
+
if (pdfCjkFontFallbackManager && pageCount > 0) {
|
|
1588
|
+
if (firstPageTextContent) {
|
|
1589
|
+
pdfCjkFontFallbackPageLoads.set(1, pdfCjkFontFallbackManager.ensureTextContent(firstPageTextContent));
|
|
1590
|
+
await pdfCjkFontFallbackPageLoads.get(1);
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
const firstPage = await pdfDocument.getPage(1);
|
|
1594
|
+
await ensurePdfPageCjkFontFallback(1, firstPage);
|
|
1595
|
+
}
|
|
1596
|
+
if (destroyed || requestVersion !== loadVersion || pdfContext.document !== pdfDocument) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
(_c = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _c === void 0 ? void 0 : _c.call(context, {
|
|
1355
1601
|
includeDocumentStyles: false,
|
|
1356
1602
|
printStyle: buildPdfPrintStyle,
|
|
1357
1603
|
toHtml: renderPdfPagesForExport,
|
|
@@ -1436,6 +1682,11 @@ export default async function renderPdf(buffer, target, context) {
|
|
|
1436
1682
|
var _a;
|
|
1437
1683
|
destroyed = true;
|
|
1438
1684
|
loadVersion += 1;
|
|
1685
|
+
restorePdfJsMissingSystemFontWarnings();
|
|
1686
|
+
restorePdfJsMissingSystemFontWarnings = () => { };
|
|
1687
|
+
pdfCjkFontFallbackManager = null;
|
|
1688
|
+
pdfCjkFontFallbackPageLoads.clear();
|
|
1689
|
+
pdfCjkFontFallbackRenderHandledPages.clear();
|
|
1439
1690
|
targetWindow.cancelAnimationFrame(fitFrame);
|
|
1440
1691
|
targetWindow.cancelAnimationFrame(pageDimensionFrame);
|
|
1441
1692
|
targetWindow.cancelAnimationFrame(scrollStateFrame);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type PdfTextContentItem = {
|
|
2
|
+
str?: string;
|
|
3
|
+
fontName?: string;
|
|
4
|
+
};
|
|
5
|
+
type PdfTextContentStyle = {
|
|
6
|
+
fontSubstitution?: string;
|
|
7
|
+
};
|
|
8
|
+
export type PdfTextContent = {
|
|
9
|
+
items?: PdfTextContentItem[];
|
|
10
|
+
styles?: Record<string, PdfTextContentStyle>;
|
|
11
|
+
};
|
|
12
|
+
export type PdfTextContentPage = {
|
|
13
|
+
getTextContent: () => Promise<PdfTextContent>;
|
|
14
|
+
};
|
|
15
|
+
export interface PdfCjkFontFallbackManager {
|
|
16
|
+
prepare: () => Promise<boolean>;
|
|
17
|
+
ensureTextContent: (textContent: PdfTextContent) => Promise<boolean>;
|
|
18
|
+
ensurePage: (page: PdfTextContentPage) => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export interface CreatePdfCjkFontFallbackManagerOptions {
|
|
21
|
+
documentRef: Document;
|
|
22
|
+
fontAssetPath: string;
|
|
23
|
+
onWarning?: (message: string, error?: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Some PDF generators write TrueType glyph IDs through Identity-H but omit
|
|
27
|
+
* ToUnicode. PDF.js then exposes those glyph IDs as control characters, so a
|
|
28
|
+
* replacement font alone cannot recover the intended text.
|
|
29
|
+
*/
|
|
30
|
+
export declare const detectMalformedIdentityCjkFontFamilies: (textContent: PdfTextContent, resolveFontFamily?: (fontName: string) => string) => string[];
|
|
31
|
+
export declare const collectMalformedIdentityFontNames: (textContent: PdfTextContent) => string[];
|
|
32
|
+
export declare const createPdfCjkFontFallbackManager: ({ documentRef, fontAssetPath, onWarning, }: CreatePdfCjkFontFallbackManagerOptions) => PdfCjkFontFallbackManager;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const PDF_CJK_FONT_CSS_FILE = 'noto-sans-sc.css';
|
|
2
|
+
const PDF_CJK_FONT_TEMPLATE_FAMILY = 'Noto Sans SC Variable';
|
|
3
|
+
const PDF_CJK_TEXT_RE = /[\u2e80-\u2fff\u3000-\u303f\u3040-\u30ff\u3100-\u312f\u31a0-\u31bf\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff00-\uffef]/;
|
|
4
|
+
const PDF_CONTROL_TEXT_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
5
|
+
const PDF_CJK_FONT_MAX_PROBE_CHARS = 4096;
|
|
6
|
+
const PDF_IDENTITY_FONT_MIN_CONTROL_CHARS = 2;
|
|
7
|
+
const fontTemplatePromises = new Map();
|
|
8
|
+
const fontDocumentStates = new WeakMap();
|
|
9
|
+
const escapeCssString = (value) => value
|
|
10
|
+
.replace(/\\/g, '\\\\')
|
|
11
|
+
.replace(/"/g, '\\"')
|
|
12
|
+
.replace(/[\r\n\f]/g, ' ');
|
|
13
|
+
const unescapeCssString = (value) => value
|
|
14
|
+
.replace(/\\([\\"'])/g, '$1')
|
|
15
|
+
.trim();
|
|
16
|
+
const normalizeFontFamilyKey = (value) => value
|
|
17
|
+
.normalize('NFKC')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[\s_,-]+/g, '');
|
|
20
|
+
const PDF_CJK_LOCAL_FONT_CANDIDATES = {
|
|
21
|
+
microsoftyahei: ['Microsoft YaHei', 'Microsoft YaHei UI', '微软雅黑'],
|
|
22
|
+
microsoftyaheiui: ['Microsoft YaHei UI', 'Microsoft YaHei', '微软雅黑'],
|
|
23
|
+
simhei: ['SimHei', 'Heiti SC', '黑体', '黑体-简'],
|
|
24
|
+
simsun: ['SimSun', 'NSimSun', 'Songti SC', '宋体', '宋体-简'],
|
|
25
|
+
kaiti: ['KaiTi', 'STKaiti', '楷体'],
|
|
26
|
+
fangsong: ['FangSong', 'STFangsong', '仿宋'],
|
|
27
|
+
pingfangsc: ['PingFang SC', 'Hiragino Sans GB', 'Heiti SC'],
|
|
28
|
+
notosanscjksc: ['Noto Sans CJK SC', 'Source Han Sans SC'],
|
|
29
|
+
sourcehansanssc: ['Source Han Sans SC', 'Noto Sans CJK SC'],
|
|
30
|
+
arialunicodems: ['Arial Unicode MS'],
|
|
31
|
+
};
|
|
32
|
+
const getLocalFontCandidates = (family) => {
|
|
33
|
+
const candidates = [family, ...(PDF_CJK_LOCAL_FONT_CANDIDATES[normalizeFontFamilyKey(family)] || [])];
|
|
34
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
35
|
+
};
|
|
36
|
+
const resolveFontCssUrl = (fontAssetPath) => {
|
|
37
|
+
const normalizedPath = fontAssetPath.endsWith('/') ? fontAssetPath : `${fontAssetPath}/`;
|
|
38
|
+
return new URL(PDF_CJK_FONT_CSS_FILE, normalizedPath).href;
|
|
39
|
+
};
|
|
40
|
+
const resolveFontTemplateUrls = (css, cssUrl) => css.replace(/url\(\s*(['"]?)(\.\/files\/[^'"\s)]+)\1\s*\)/g, (_match, _quote, relativeUrl) => {
|
|
41
|
+
const absoluteUrl = escapeCssString(new URL(relativeUrl, cssUrl).href);
|
|
42
|
+
return `url("${absoluteUrl}")`;
|
|
43
|
+
});
|
|
44
|
+
const loadFontTemplate = (documentRef, cssUrl) => {
|
|
45
|
+
var _a;
|
|
46
|
+
const cached = fontTemplatePromises.get(cssUrl);
|
|
47
|
+
if (cached) {
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
const view = documentRef.defaultView;
|
|
51
|
+
const fetcher = ((_a = view === null || view === void 0 ? void 0 : view.fetch) === null || _a === void 0 ? void 0 : _a.bind(view)) || globalThis.fetch;
|
|
52
|
+
const promise = fetcher(cssUrl, { credentials: 'same-origin' })
|
|
53
|
+
.then(async (response) => {
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`HTTP ${response.status} while loading ${cssUrl}`);
|
|
56
|
+
}
|
|
57
|
+
const css = await response.text();
|
|
58
|
+
if (!css.includes(PDF_CJK_FONT_TEMPLATE_FAMILY) || !css.includes('./files/')) {
|
|
59
|
+
throw new Error(`Invalid PDF CJK font fallback stylesheet: ${cssUrl}`);
|
|
60
|
+
}
|
|
61
|
+
return resolveFontTemplateUrls(css, cssUrl);
|
|
62
|
+
})
|
|
63
|
+
.catch(error => {
|
|
64
|
+
fontTemplatePromises.delete(cssUrl);
|
|
65
|
+
throw error;
|
|
66
|
+
});
|
|
67
|
+
fontTemplatePromises.set(cssUrl, promise);
|
|
68
|
+
return promise;
|
|
69
|
+
};
|
|
70
|
+
const getDocumentState = (documentRef, cssUrl) => {
|
|
71
|
+
let states = fontDocumentStates.get(documentRef);
|
|
72
|
+
if (!states) {
|
|
73
|
+
states = new Map();
|
|
74
|
+
fontDocumentStates.set(documentRef, states);
|
|
75
|
+
}
|
|
76
|
+
let state = states.get(cssUrl);
|
|
77
|
+
if (!state) {
|
|
78
|
+
state = { families: new Map() };
|
|
79
|
+
states.set(cssUrl, state);
|
|
80
|
+
}
|
|
81
|
+
return state;
|
|
82
|
+
};
|
|
83
|
+
const extractSubstitutionFamily = (value) => {
|
|
84
|
+
if (!value) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
const match = value.match(/^\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+))/);
|
|
88
|
+
const family = unescapeCssString((match === null || match === void 0 ? void 0 : match[1]) || (match === null || match === void 0 ? void 0 : match[2]) || (match === null || match === void 0 ? void 0 : match[3]) || '');
|
|
89
|
+
if (!family ||
|
|
90
|
+
family.length > 160 ||
|
|
91
|
+
/[\u0000-\u001f\u007f]/.test(family) ||
|
|
92
|
+
/^(?:serif|sans-serif|monospace|cursive|fantasy|system-ui)$/i.test(family)) {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
return family;
|
|
96
|
+
};
|
|
97
|
+
const isKnownCjkFontFamily = (family) => {
|
|
98
|
+
const key = normalizeFontFamilyKey(family);
|
|
99
|
+
return Boolean(PDF_CJK_LOCAL_FONT_CANDIDATES[key]) ||
|
|
100
|
+
/(?:cjk|han|song|hei|kai|fang|yahei|ming|gothic|mincho)/i.test(key) ||
|
|
101
|
+
/[\u3400-\u9fff]/.test(family);
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Some PDF generators write TrueType glyph IDs through Identity-H but omit
|
|
105
|
+
* ToUnicode. PDF.js then exposes those glyph IDs as control characters, so a
|
|
106
|
+
* replacement font alone cannot recover the intended text.
|
|
107
|
+
*/
|
|
108
|
+
export const detectMalformedIdentityCjkFontFamilies = (textContent, resolveFontFamily = () => '') => {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
const styles = textContent.styles || {};
|
|
111
|
+
const families = new Set();
|
|
112
|
+
for (const item of textContent.items || []) {
|
|
113
|
+
const text = item.str || '';
|
|
114
|
+
const controlChars = ((_a = text.match(PDF_CONTROL_TEXT_RE)) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
115
|
+
if (controlChars < PDF_IDENTITY_FONT_MIN_CONTROL_CHARS) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const fontName = item.fontName || '';
|
|
119
|
+
const resolvedFamily = resolveFontFamily(fontName);
|
|
120
|
+
const safeResolvedFamily = resolvedFamily.length <= 160 &&
|
|
121
|
+
!/[\u0000-\u001f\u007f]/.test(resolvedFamily)
|
|
122
|
+
? resolvedFamily
|
|
123
|
+
: '';
|
|
124
|
+
const family = extractSubstitutionFamily((_b = styles[fontName]) === null || _b === void 0 ? void 0 : _b.fontSubstitution) ||
|
|
125
|
+
safeResolvedFamily;
|
|
126
|
+
if (family && isKnownCjkFontFamily(family)) {
|
|
127
|
+
families.add(family);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...families];
|
|
131
|
+
};
|
|
132
|
+
export const collectMalformedIdentityFontNames = (textContent) => {
|
|
133
|
+
var _a;
|
|
134
|
+
const fontNames = new Set();
|
|
135
|
+
for (const item of textContent.items || []) {
|
|
136
|
+
const controlChars = ((_a = (item.str || '').match(PDF_CONTROL_TEXT_RE)) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
137
|
+
if (controlChars >= PDF_IDENTITY_FONT_MIN_CONTROL_CHARS && item.fontName) {
|
|
138
|
+
fontNames.add(item.fontName);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...fontNames];
|
|
142
|
+
};
|
|
143
|
+
const collectPageFontText = (textContent) => {
|
|
144
|
+
var _a;
|
|
145
|
+
const styles = textContent.styles || {};
|
|
146
|
+
const familyChars = new Map();
|
|
147
|
+
let totalChars = 0;
|
|
148
|
+
for (const item of textContent.items || []) {
|
|
149
|
+
const text = item.str || '';
|
|
150
|
+
if (!PDF_CJK_TEXT_RE.test(text)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const family = extractSubstitutionFamily((_a = styles[item.fontName || '']) === null || _a === void 0 ? void 0 : _a.fontSubstitution);
|
|
154
|
+
if (!family) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let chars = familyChars.get(family);
|
|
158
|
+
if (!chars) {
|
|
159
|
+
chars = new Set();
|
|
160
|
+
familyChars.set(family, chars);
|
|
161
|
+
}
|
|
162
|
+
for (const char of text) {
|
|
163
|
+
if (!/[\u0000-\u001f\u007f]/.test(char) && !chars.has(char)) {
|
|
164
|
+
chars.add(char);
|
|
165
|
+
totalChars += 1;
|
|
166
|
+
if (totalChars >= PDF_CJK_FONT_MAX_PROBE_CHARS) {
|
|
167
|
+
return familyChars;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return familyChars;
|
|
173
|
+
};
|
|
174
|
+
const createAliasStylesheet = (template, family) => {
|
|
175
|
+
const escapedFamily = escapeCssString(family);
|
|
176
|
+
const localSources = getLocalFontCandidates(family)
|
|
177
|
+
.map(candidate => `local("${escapeCssString(candidate)}")`)
|
|
178
|
+
.join(', ');
|
|
179
|
+
return template
|
|
180
|
+
.replace(/font-family:\s*'Noto Sans SC Variable';/g, `font-family: "${escapedFamily}";`)
|
|
181
|
+
.replace(/font-display:\s*swap;/g, 'font-display: block;')
|
|
182
|
+
.replace(/src:\s*url\(/g, `src: ${localSources}, url(`);
|
|
183
|
+
};
|
|
184
|
+
const ensureAliasStyle = (documentRef, state, template, family) => {
|
|
185
|
+
if (state.styleInjected) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const style = documentRef.createElement('style');
|
|
189
|
+
style.dataset.fileViewerPdfCjkFallbackFamily = family;
|
|
190
|
+
style.textContent = createAliasStylesheet(template, family);
|
|
191
|
+
(documentRef.head || documentRef.documentElement).append(style);
|
|
192
|
+
state.styleInjected = true;
|
|
193
|
+
};
|
|
194
|
+
const loadFamilyText = async (documentRef, documentState, template, family, chars) => {
|
|
195
|
+
let state = documentState.families.get(family);
|
|
196
|
+
if (!state) {
|
|
197
|
+
state = {
|
|
198
|
+
loadedChars: new Set(),
|
|
199
|
+
tail: Promise.resolve(),
|
|
200
|
+
styleInjected: false,
|
|
201
|
+
};
|
|
202
|
+
documentState.families.set(family, state);
|
|
203
|
+
}
|
|
204
|
+
ensureAliasStyle(documentRef, state, template, family);
|
|
205
|
+
const pendingChars = [...chars].filter(char => !state.loadedChars.has(char));
|
|
206
|
+
if (!pendingChars.length) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
pendingChars.forEach(char => state === null || state === void 0 ? void 0 : state.loadedChars.add(char));
|
|
210
|
+
const probeText = pendingChars.join('');
|
|
211
|
+
const escapedFamily = escapeCssString(family);
|
|
212
|
+
const fontSet = documentRef.fonts;
|
|
213
|
+
if (!(fontSet === null || fontSet === void 0 ? void 0 : fontSet.load)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
const operation = state.tail.then(async () => {
|
|
217
|
+
const loadedFaces = await Promise.all([
|
|
218
|
+
fontSet.load(`normal 400 16px "${escapedFamily}"`, probeText),
|
|
219
|
+
fontSet.load(`normal 700 16px "${escapedFamily}"`, probeText),
|
|
220
|
+
]);
|
|
221
|
+
if (!loadedFaces.some(faces => faces.length > 0)) {
|
|
222
|
+
throw new Error(`No matching CJK fallback font face loaded for ${family}`);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
state.tail = operation.catch(() => { });
|
|
226
|
+
try {
|
|
227
|
+
await operation;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
pendingChars.forEach(char => state === null || state === void 0 ? void 0 : state.loadedChars.delete(char));
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
export const createPdfCjkFontFallbackManager = ({ documentRef, fontAssetPath, onWarning, }) => {
|
|
236
|
+
const cssUrl = resolveFontCssUrl(fontAssetPath);
|
|
237
|
+
const documentState = getDocumentState(documentRef, cssUrl);
|
|
238
|
+
let templatePromise = null;
|
|
239
|
+
let warningReported = false;
|
|
240
|
+
const warnOnce = (message, error) => {
|
|
241
|
+
if (warningReported) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
warningReported = true;
|
|
245
|
+
onWarning === null || onWarning === void 0 ? void 0 : onWarning(message, error);
|
|
246
|
+
};
|
|
247
|
+
const getTemplate = () => {
|
|
248
|
+
templatePromise || (templatePromise = loadFontTemplate(documentRef, cssUrl));
|
|
249
|
+
return templatePromise;
|
|
250
|
+
};
|
|
251
|
+
const ensureTextContent = async (textContent) => {
|
|
252
|
+
try {
|
|
253
|
+
const familyChars = collectPageFontText(textContent);
|
|
254
|
+
if (!familyChars.size) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const template = await getTemplate();
|
|
258
|
+
const results = await Promise.all([...familyChars].map(([family, chars]) => (loadFamilyText(documentRef, documentState, template, family, chars))));
|
|
259
|
+
return results.some(Boolean);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
warnOnce('Unable to load an offline fallback for an unembedded PDF CJK font.', error);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
async prepare() {
|
|
268
|
+
try {
|
|
269
|
+
await getTemplate();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
warnOnce(`Unable to load the offline PDF CJK font fallback from ${cssUrl}.`, error);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
ensureTextContent,
|
|
278
|
+
async ensurePage(page) {
|
|
279
|
+
return ensureTextContent(await page.getTextContent());
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type PdfIdentityFontRepairResult = {
|
|
2
|
+
bytes: Uint8Array;
|
|
3
|
+
repairedFonts: number;
|
|
4
|
+
repairedFamilies: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare const repairMalformedIdentityCjkFonts: (sourceBytes: Uint8Array, candidateFamilies?: readonly string[]) => Promise<PdfIdentityFontRepairResult>;
|
|
@@ -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.
|
|
3
|
+
"version": "2.1.26",
|
|
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.
|
|
58
|
-
"
|
|
57
|
+
"@file-viewer/core": "2.1.26",
|
|
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"
|