@file-viewer/renderer-pdf 2.1.25 → 2.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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>;