@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 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 standard fonts. Asset paths use the same unified options as `@file-viewer/core`:
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 provided, the renderer first probes `/vendor/pdf/pdf.worker.mjs` from the site root. 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, full cMap/WASM/standard-font support, or strict offline deployments, still copy viewer assets and point these URLs at real static files.
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、WASMstandard fonts。资源路径沿用 `@file-viewer/core` 的统一 options:
28
+ PDF 预览依赖 PDF.js worker、cMaps、WASMstandard 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
- 默认未显式配置时,渲染器会先探测站点根路径的 `/vendor/pdf/pdf.worker.mjs`。如果客户项目没有执行 `file-viewer-copy-assets`、没有使用 `@file-viewer/vite-plugin`,或者本地临时服务器把该路径回退成 HTML,PDF renderer 会自动懒加载包内 PDF.js worker handler 作为兼容兜底,避免 `Setting up fake worker failed` 直接中断预览。需要最佳性能、完整 cMap/WASM/standard fonts 或严格离线部署时,仍建议复制 viewer assets 并配置真实静态地址。
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, documentUrl) => {
196
- return resolveFileViewerPdfAssetUrls(options, documentUrl).workerUrl;
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, documentRef.URL || undefined);
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.create({ name: 'file-viewer-pdf-worker' });
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', scheduleLegacyPageDimensionPatch);
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 worker = await createPdfWorker();
1321
- const pdfAssets = resolveFileViewerPdfAssetUrls(options, documentRef.URL || documentRef.baseURI);
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 loadingTask = getDocument({
1332
- ...source,
1333
- worker: worker || undefined,
1334
- cMapUrl: pdfAssets.cMapUrl,
1335
- wasmUrl: pdfAssets.wasmUrl,
1336
- standardFontDataUrl: pdfAssets.standardFontDataUrl,
1337
- useWorkerFetch: true,
1338
- cMapPacked: true,
1339
- enableXfa: true,
1340
- });
1341
- resource = { loadingTask, worker };
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
- const pdfDocument = await loadingTask.promise;
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
- (_b = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _b === void 0 ? void 0 : _b.call(context, {
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.24",
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.24",
58
- "pdfjs-dist": "^6.0.227"
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"