@cj-tech-master/excelts 9.4.1 → 9.4.2
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/dist/browser/modules/pdf/builder/document-builder.js +4 -23
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +15 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +47 -3
- package/dist/browser/modules/pdf/font/font-manager.d.ts +37 -6
- package/dist/browser/modules/pdf/font/font-manager.js +129 -17
- package/dist/browser/modules/pdf/font/system-fonts.d.ts +41 -0
- package/dist/browser/modules/pdf/font/system-fonts.js +188 -0
- package/dist/browser/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/browser/modules/pdf/font/type3-font.d.ts +35 -0
- package/dist/browser/modules/pdf/font/type3-font.js +228 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended.js +4164 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-fill.js +5438 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
- package/dist/browser/modules/pdf/font/type3-glyphs-quality.js +5345 -0
- package/dist/browser/modules/pdf/font/type3-glyphs.d.ts +79 -0
- package/dist/browser/modules/pdf/font/type3-glyphs.js +2567 -0
- package/dist/browser/modules/pdf/render/layout-engine.js +36 -23
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +9 -0
- package/dist/browser/modules/pdf/render/page-renderer.js +110 -78
- package/dist/browser/modules/pdf/render/pdf-exporter.js +73 -5
- package/dist/cjs/modules/pdf/builder/document-builder.js +3 -22
- package/dist/cjs/modules/pdf/core/pdf-stream.js +49 -3
- package/dist/cjs/modules/pdf/font/font-manager.js +129 -17
- package/dist/cjs/modules/pdf/font/system-fonts.js +194 -0
- package/dist/cjs/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/cjs/modules/pdf/font/type3-font.js +231 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-extended.js +4167 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-extended2.js +9652 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-fill.js +5441 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs-quality.js +5348 -0
- package/dist/cjs/modules/pdf/font/type3-glyphs.js +2573 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +36 -23
- package/dist/cjs/modules/pdf/render/page-renderer.js +111 -78
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +71 -3
- package/dist/esm/modules/pdf/builder/document-builder.js +4 -23
- package/dist/esm/modules/pdf/core/pdf-stream.js +47 -3
- package/dist/esm/modules/pdf/font/font-manager.js +129 -17
- package/dist/esm/modules/pdf/font/system-fonts.js +188 -0
- package/dist/esm/modules/pdf/font/ttf-parser.js +29 -1
- package/dist/esm/modules/pdf/font/type3-font.js +228 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-extended.js +4164 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-extended2.js +9649 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-fill.js +5438 -0
- package/dist/esm/modules/pdf/font/type3-glyphs-quality.js +5345 -0
- package/dist/esm/modules/pdf/font/type3-glyphs.js +2567 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +36 -23
- package/dist/esm/modules/pdf/render/page-renderer.js +110 -78
- package/dist/esm/modules/pdf/render/pdf-exporter.js +73 -5
- package/dist/iife/excelts.iife.js +25445 -344
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +48 -46
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +15 -0
- package/dist/types/modules/pdf/font/font-manager.d.ts +37 -6
- package/dist/types/modules/pdf/font/system-fonts.d.ts +41 -0
- package/dist/types/modules/pdf/font/type3-font.d.ts +35 -0
- package/dist/types/modules/pdf/font/type3-glyphs-extended.d.ts +33 -0
- package/dist/types/modules/pdf/font/type3-glyphs-extended2.d.ts +16 -0
- package/dist/types/modules/pdf/font/type3-glyphs-fill.d.ts +17 -0
- package/dist/types/modules/pdf/font/type3-glyphs-quality.d.ts +28 -0
- package/dist/types/modules/pdf/font/type3-glyphs.d.ts +79 -0
- package/dist/types/modules/pdf/render/page-renderer.d.ts +9 -0
- package/package.json +1 -1
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Font manager for PDF generation.
|
|
3
3
|
*
|
|
4
|
-
* Manages
|
|
4
|
+
* Manages three kinds of fonts:
|
|
5
5
|
* 1. **Standard Type1 fonts** (Helvetica, Times, Courier) — always available,
|
|
6
|
-
* used
|
|
6
|
+
* used for Latin text (WinAnsi repertoire) when no embedded font is provided.
|
|
7
7
|
* 2. **Embedded TrueType fonts** — user-provided .ttf files for full
|
|
8
8
|
* Unicode support (CJK, Arabic, Hindi, etc.)
|
|
9
|
+
* 3. **Type3 fallback fonts** — auto-generated vector-drawn glyphs for
|
|
10
|
+
* Unicode characters outside WinAnsi when no embedded font is provided.
|
|
9
11
|
*
|
|
10
12
|
* When an embedded font is registered, ALL text uses the embedded font.
|
|
11
|
-
* When no embedded font is provided, the system
|
|
12
|
-
*
|
|
13
|
+
* When no embedded font is provided, the system uses Type1 for WinAnsi
|
|
14
|
+
* characters and Type3 for everything else.
|
|
13
15
|
*
|
|
14
16
|
* The manager tracks which Unicode code points are used so the font embedder
|
|
15
|
-
* can create
|
|
17
|
+
* and Type3 builder can create minimal subsets when writing the PDF.
|
|
16
18
|
*/
|
|
17
19
|
import { PdfDict, pdfName, pdfRef } from "../core/pdf-object.js";
|
|
20
|
+
import { hasNonWinAnsiChars, isWinAnsiCodePoint } from "../core/pdf-stream.js";
|
|
18
21
|
import { PdfFontError } from "../errors.js";
|
|
19
22
|
import { embedTtfFont } from "./font-embedder.js";
|
|
20
23
|
import { measureText as measureType1Text, getFontAscent as getType1Ascent, getFontDescent as getType1Descent, getLineHeight as getType1LineHeight } from "./metrics.js";
|
|
24
|
+
import { writeType3Fonts } from "./type3-font.js";
|
|
21
25
|
// =============================================================================
|
|
22
26
|
// Font Name Mapping (Type1 fallback)
|
|
23
27
|
// =============================================================================
|
|
@@ -104,7 +108,8 @@ export function resolvePdfFontName(fontFamily, bold, italic) {
|
|
|
104
108
|
// =============================================================================
|
|
105
109
|
/**
|
|
106
110
|
* Manages PDF font resources for a document.
|
|
107
|
-
* Supports
|
|
111
|
+
* Supports standard Type1 fonts, embedded TrueType fonts, and auto-generated
|
|
112
|
+
* Type3 fallback fonts for non-WinAnsi Unicode characters.
|
|
108
113
|
*/
|
|
109
114
|
export class FontManager {
|
|
110
115
|
constructor() {
|
|
@@ -117,6 +122,9 @@ export class FontManager {
|
|
|
117
122
|
this.embeddedResourceName = "";
|
|
118
123
|
this.usedCodePoints = new Set();
|
|
119
124
|
this.nextEmbeddedId = 1;
|
|
125
|
+
// --- Type3 fallback font tracking ---
|
|
126
|
+
this.type3CodePoints = new Set();
|
|
127
|
+
this._type3Result = null;
|
|
120
128
|
/** Stored after writeFontResources is called */
|
|
121
129
|
this._embeddedResult = null;
|
|
122
130
|
}
|
|
@@ -149,14 +157,25 @@ export class FontManager {
|
|
|
149
157
|
* Must be called for every text string before writing the PDF.
|
|
150
158
|
*/
|
|
151
159
|
trackText(text) {
|
|
152
|
-
if (
|
|
153
|
-
|
|
160
|
+
if (this.embeddedFont) {
|
|
161
|
+
for (let i = 0; i < text.length; i++) {
|
|
162
|
+
const cp = text.codePointAt(i);
|
|
163
|
+
this.usedCodePoints.add(cp);
|
|
164
|
+
if (cp > 0xffff) {
|
|
165
|
+
i++; // skip low surrogate
|
|
166
|
+
}
|
|
167
|
+
}
|
|
154
168
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
else {
|
|
170
|
+
// No embedded font — track non-WinAnsi chars for Type3 fallback
|
|
171
|
+
for (let i = 0; i < text.length; i++) {
|
|
172
|
+
const cp = text.codePointAt(i);
|
|
173
|
+
if (cp > 0xffff) {
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
if (!isWinAnsiCodePoint(cp)) {
|
|
177
|
+
this.type3CodePoints.add(cp);
|
|
178
|
+
}
|
|
160
179
|
}
|
|
161
180
|
}
|
|
162
181
|
}
|
|
@@ -194,17 +213,72 @@ export class FontManager {
|
|
|
194
213
|
return this.resourceToType1.get(resourceName) ?? "Helvetica";
|
|
195
214
|
}
|
|
196
215
|
// ==========================================================================
|
|
216
|
+
// Type3 Fallback Font
|
|
217
|
+
// ==========================================================================
|
|
218
|
+
/**
|
|
219
|
+
* Check if Type3 fallback fonts are available (after writeFontResources).
|
|
220
|
+
*/
|
|
221
|
+
hasType3Fonts() {
|
|
222
|
+
return this._type3Result !== null && this._type3Result.fontObjects.size > 0;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the Type3 font resource name and char code for a code point.
|
|
226
|
+
* Returns null if the code point is not in the Type3 encoding.
|
|
227
|
+
*/
|
|
228
|
+
resolveType3(codePoint) {
|
|
229
|
+
if (!this._type3Result) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return this._type3Result.encoding.get(codePoint) ?? null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if a code point needs Type3 rendering (non-WinAnsi, no embedded font).
|
|
236
|
+
*/
|
|
237
|
+
needsType3(codePoint) {
|
|
238
|
+
return !this.embeddedFont && !isWinAnsiCodePoint(codePoint);
|
|
239
|
+
}
|
|
240
|
+
// ==========================================================================
|
|
197
241
|
// Text Measurement
|
|
198
242
|
// ==========================================================================
|
|
199
243
|
/**
|
|
200
244
|
* Measure text width using the correct font metrics.
|
|
245
|
+
* For mixed Type1/Type3 text, measures each character with the right font.
|
|
201
246
|
*/
|
|
202
247
|
measureText(text, resourceName, fontSize) {
|
|
203
248
|
if (this.embeddedFont && resourceName === this.embeddedResourceName) {
|
|
204
249
|
return measureEmbeddedText(text, this.embeddedFont, fontSize);
|
|
205
250
|
}
|
|
251
|
+
// If no Type3 fonts or text has no non-WinAnsi chars, use Type1 directly
|
|
252
|
+
if (!this._type3Result || !hasNonWinAnsiChars(text)) {
|
|
253
|
+
const pdfFontName = this.getPdfFontName(resourceName);
|
|
254
|
+
return measureType1Text(text, pdfFontName, fontSize);
|
|
255
|
+
}
|
|
256
|
+
// Mixed text: measure char by char
|
|
257
|
+
let totalWidth = 0;
|
|
206
258
|
const pdfFontName = this.getPdfFontName(resourceName);
|
|
207
|
-
|
|
259
|
+
for (let i = 0; i < text.length; i++) {
|
|
260
|
+
const cp = text.codePointAt(i);
|
|
261
|
+
if (cp > 0xffff) {
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
if (isWinAnsiCodePoint(cp)) {
|
|
265
|
+
totalWidth += measureType1Text(String.fromCodePoint(cp), pdfFontName, fontSize);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Type3 character width
|
|
269
|
+
const t3 = this._type3Result.encoding.get(cp);
|
|
270
|
+
if (t3) {
|
|
271
|
+
const widthMap = this._type3Result.widths.get(t3.resourceName);
|
|
272
|
+
const glyphWidth = widthMap?.get(t3.charCode) ?? 600;
|
|
273
|
+
totalWidth += (glyphWidth / 1000) * fontSize;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Notdef width
|
|
277
|
+
totalWidth += (600 / 1000) * fontSize;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return totalWidth;
|
|
208
282
|
}
|
|
209
283
|
/**
|
|
210
284
|
* Get the font ascent in points.
|
|
@@ -213,7 +287,11 @@ export class FontManager {
|
|
|
213
287
|
if (this.embeddedFont && resourceName === this.embeddedResourceName) {
|
|
214
288
|
return (this.embeddedFont.ascent / this.embeddedFont.unitsPerEm) * fontSize;
|
|
215
289
|
}
|
|
216
|
-
|
|
290
|
+
// Type3 fonts use the same metrics as the base Type1 font
|
|
291
|
+
const base = this.isType3Resource(resourceName)
|
|
292
|
+
? "Helvetica"
|
|
293
|
+
: this.getPdfFontName(resourceName);
|
|
294
|
+
return getType1Ascent(base, fontSize);
|
|
217
295
|
}
|
|
218
296
|
/**
|
|
219
297
|
* Get the font descent in points (negative value).
|
|
@@ -222,7 +300,10 @@ export class FontManager {
|
|
|
222
300
|
if (this.embeddedFont && resourceName === this.embeddedResourceName) {
|
|
223
301
|
return (this.embeddedFont.descent / this.embeddedFont.unitsPerEm) * fontSize;
|
|
224
302
|
}
|
|
225
|
-
|
|
303
|
+
const base = this.isType3Resource(resourceName)
|
|
304
|
+
? "Helvetica"
|
|
305
|
+
: this.getPdfFontName(resourceName);
|
|
306
|
+
return getType1Descent(base, fontSize);
|
|
226
307
|
}
|
|
227
308
|
/**
|
|
228
309
|
* Get the line height in points.
|
|
@@ -232,7 +313,10 @@ export class FontManager {
|
|
|
232
313
|
const f = this.embeddedFont;
|
|
233
314
|
return ((f.ascent - f.descent) / f.unitsPerEm) * fontSize;
|
|
234
315
|
}
|
|
235
|
-
|
|
316
|
+
const base = this.isType3Resource(resourceName)
|
|
317
|
+
? "Helvetica"
|
|
318
|
+
: this.getPdfFontName(resourceName);
|
|
319
|
+
return getType1LineHeight(base, fontSize);
|
|
236
320
|
}
|
|
237
321
|
// ==========================================================================
|
|
238
322
|
// Text Encoding
|
|
@@ -243,6 +327,12 @@ export class FontManager {
|
|
|
243
327
|
isEmbeddedFont(resourceName) {
|
|
244
328
|
return this.embeddedFont !== null && resourceName === this.embeddedResourceName;
|
|
245
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Check if a resource name refers to a Type3 fallback font.
|
|
332
|
+
*/
|
|
333
|
+
isType3Resource(resourceName) {
|
|
334
|
+
return this._type3Result?.fontObjects.has(resourceName) ?? false;
|
|
335
|
+
}
|
|
246
336
|
/**
|
|
247
337
|
* Encode text for the given font resource.
|
|
248
338
|
* For embedded fonts, returns a hex string `<0012003A...>`.
|
|
@@ -263,6 +353,21 @@ export class FontManager {
|
|
|
263
353
|
// writeFontResources not called yet — this is a programming error
|
|
264
354
|
throw new PdfFontError("encodeText called before writeFontResources — subset mapping not available");
|
|
265
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Encode a single character for a Type3 font.
|
|
358
|
+
* Returns a hex string `<XX>` suitable for the Tj operator.
|
|
359
|
+
*/
|
|
360
|
+
encodeType3Char(codePoint) {
|
|
361
|
+
if (!this._type3Result) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const entry = this._type3Result.encoding.get(codePoint);
|
|
365
|
+
if (!entry) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const hex = entry.charCode.toString(16).toUpperCase().padStart(2, "0");
|
|
369
|
+
return `<${hex}>`;
|
|
370
|
+
}
|
|
266
371
|
// ==========================================================================
|
|
267
372
|
// PDF Object Writing
|
|
268
373
|
// ==========================================================================
|
|
@@ -290,6 +395,13 @@ export class FontManager {
|
|
|
290
395
|
// Store the embedding result for text re-encoding
|
|
291
396
|
this._embeddedResult = embedded;
|
|
292
397
|
}
|
|
398
|
+
// Write Type3 fallback fonts (only when no embedded font)
|
|
399
|
+
if (!this.embeddedFont && this.type3CodePoints.size > 0) {
|
|
400
|
+
this._type3Result = writeType3Fonts(writer, this.type3CodePoints);
|
|
401
|
+
for (const [resourceName, objNum] of this._type3Result.fontObjects) {
|
|
402
|
+
fontObjectMap.set(resourceName, objNum);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
293
405
|
return fontObjectMap;
|
|
294
406
|
}
|
|
295
407
|
/**
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System font discovery for PDF generation.
|
|
3
|
+
*
|
|
4
|
+
* When no embedded font is provided and the document contains non-WinAnsi
|
|
5
|
+
* characters, this module searches standard system font directories for a
|
|
6
|
+
* TrueType font (.ttf or .ttc) with broad Unicode coverage.
|
|
7
|
+
*
|
|
8
|
+
* This is a Node.js-only feature — browser environments do not have
|
|
9
|
+
* file system access and must always provide fonts explicitly.
|
|
10
|
+
*
|
|
11
|
+
* .ttc (TrueType Collection) files are supported — parseTtf() extracts
|
|
12
|
+
* the first font from the collection automatically.
|
|
13
|
+
*
|
|
14
|
+
* Results are cached: the filesystem search runs only once per process.
|
|
15
|
+
*/
|
|
16
|
+
import { fileExistsSync, readFileBytesSync, traverseDirectorySync } from "../../../utils/fs.js";
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Platform Font Directories
|
|
19
|
+
// =============================================================================
|
|
20
|
+
function getSystemFontDirs() {
|
|
21
|
+
const platform = typeof process !== "undefined" ? process.platform : "";
|
|
22
|
+
const home = typeof process !== "undefined" ? (process.env.HOME ?? process.env.USERPROFILE ?? "") : "";
|
|
23
|
+
const dirs = [];
|
|
24
|
+
switch (platform) {
|
|
25
|
+
case "darwin":
|
|
26
|
+
dirs.push("/System/Library/Fonts", "/System/Library/Fonts/Supplemental", "/Library/Fonts", `${home}/Library/Fonts`);
|
|
27
|
+
break;
|
|
28
|
+
case "win32": {
|
|
29
|
+
const winDir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
|
|
30
|
+
dirs.push(`${winDir}\\Fonts`, `${process.env.LOCALAPPDATA ?? ""}\\Microsoft\\Windows\\Fonts`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "linux":
|
|
34
|
+
default:
|
|
35
|
+
dirs.push("/usr/share/fonts", "/usr/local/share/fonts", "/usr/share/fonts/truetype", "/usr/share/fonts/opentype", "/usr/share/fonts/TTF", "/usr/share/fonts/noto", "/usr/share/fonts/noto-cjk", "/usr/share/fonts/google-noto", "/usr/share/fonts/google-noto-cjk", "/usr/share/fonts/truetype/noto", "/usr/share/fonts/truetype/dejavu", "/usr/share/fonts/truetype/liberation", "/usr/share/fonts/truetype/droid", "/usr/share/fonts/wqy", `${home}/.local/share/fonts`, `${home}/.fonts`);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return dirs;
|
|
39
|
+
}
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Preferred Font Names (ordered by preference — first match wins)
|
|
42
|
+
// =============================================================================
|
|
43
|
+
const PREFERRED_FONTS = [
|
|
44
|
+
// Noto Sans CJK — broadest open-source coverage
|
|
45
|
+
"NotoSansCJKsc-Regular.ttf",
|
|
46
|
+
"NotoSansCJK-Regular.ttc",
|
|
47
|
+
"NotoSansCJKSC-Regular.otf",
|
|
48
|
+
"NotoSansSC-Regular.ttf",
|
|
49
|
+
"NotoSansTC-Regular.ttf",
|
|
50
|
+
"NotoSansJP-Regular.ttf",
|
|
51
|
+
"NotoSansKR-Regular.ttf",
|
|
52
|
+
"NotoSans-Regular.ttf",
|
|
53
|
+
// Arial Unicode MS
|
|
54
|
+
"Arial Unicode.ttf",
|
|
55
|
+
"Arial Unicode MS.ttf",
|
|
56
|
+
"ArialUnicode.ttf",
|
|
57
|
+
"arialuni.ttf",
|
|
58
|
+
// macOS
|
|
59
|
+
"PingFang.ttc",
|
|
60
|
+
"Hiragino Sans GB.ttc",
|
|
61
|
+
"STHeiti Light.ttc",
|
|
62
|
+
"STHeiti Medium.ttc",
|
|
63
|
+
"Songti.ttc",
|
|
64
|
+
"AppleSDGothicNeo.ttc",
|
|
65
|
+
// Windows
|
|
66
|
+
"msyh.ttc",
|
|
67
|
+
"msyhbd.ttc",
|
|
68
|
+
"msjh.ttc",
|
|
69
|
+
"simsun.ttc",
|
|
70
|
+
"simhei.ttf",
|
|
71
|
+
"malgun.ttf",
|
|
72
|
+
"meiryo.ttc",
|
|
73
|
+
"yugothic.ttf",
|
|
74
|
+
"segoeui.ttf",
|
|
75
|
+
"arial.ttf",
|
|
76
|
+
// Linux
|
|
77
|
+
"DejaVuSans.ttf",
|
|
78
|
+
"LiberationSans-Regular.ttf",
|
|
79
|
+
"FreeSans.ttf",
|
|
80
|
+
"DroidSansFallbackFull.ttf",
|
|
81
|
+
"DroidSansFallback.ttf",
|
|
82
|
+
"wqy-microhei.ttc",
|
|
83
|
+
"wqy-zenhei.ttc",
|
|
84
|
+
"uming.ttc",
|
|
85
|
+
"NanumGothic.ttf",
|
|
86
|
+
"IPAexGothic.ttf"
|
|
87
|
+
];
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Font Discovery
|
|
90
|
+
// =============================================================================
|
|
91
|
+
let _cachedCandidates;
|
|
92
|
+
/**
|
|
93
|
+
* Return all discoverable system font candidates, ordered by preference.
|
|
94
|
+
*
|
|
95
|
+
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
96
|
+
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
97
|
+
*
|
|
98
|
+
* Results are cached — the filesystem scan runs only once per process.
|
|
99
|
+
*/
|
|
100
|
+
export function discoverSystemFontCandidates() {
|
|
101
|
+
if (_cachedCandidates !== undefined) {
|
|
102
|
+
return _cachedCandidates;
|
|
103
|
+
}
|
|
104
|
+
if (typeof process === "undefined" || !process.platform) {
|
|
105
|
+
_cachedCandidates = [];
|
|
106
|
+
return _cachedCandidates;
|
|
107
|
+
}
|
|
108
|
+
const candidates = [];
|
|
109
|
+
const seen = new Set(); // dedupe by path
|
|
110
|
+
const dirs = getSystemFontDirs();
|
|
111
|
+
// Strategy 1: Check preferred font filenames (in order)
|
|
112
|
+
for (const fontName of PREFERRED_FONTS) {
|
|
113
|
+
for (const dir of dirs) {
|
|
114
|
+
const fontPath = `${dir}/${fontName}`;
|
|
115
|
+
if (seen.has(fontPath)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (fileExistsSync(fontPath)) {
|
|
119
|
+
const data = tryReadFont(fontPath);
|
|
120
|
+
if (data) {
|
|
121
|
+
candidates.push(data);
|
|
122
|
+
seen.add(fontPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Strategy 2: Scan directories for any .ttf/.ttc not already found
|
|
128
|
+
const broadRe = /noto|unicode|cjk|yahei|heiti|gothic|sans|serif|ming|song|dejavu|liberation|droid|wqy/i;
|
|
129
|
+
for (const dir of dirs) {
|
|
130
|
+
try {
|
|
131
|
+
const entries = traverseDirectorySync(dir, { recursive: true, filter: e => !e.isDirectory });
|
|
132
|
+
const fonts = entries.filter(e => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
|
|
133
|
+
// Broad-coverage names first, then large files
|
|
134
|
+
const broad = fonts.filter(e => broadRe.test(e.absolutePath));
|
|
135
|
+
const rest = fonts.filter(e => !broadRe.test(e.absolutePath) && e.size > 50000);
|
|
136
|
+
for (const entry of [...broad, ...rest]) {
|
|
137
|
+
if (seen.has(entry.absolutePath)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const data = tryReadFont(entry.absolutePath);
|
|
141
|
+
if (data) {
|
|
142
|
+
candidates.push(data);
|
|
143
|
+
seen.add(entry.absolutePath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Directory doesn't exist or not readable
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
_cachedCandidates = candidates;
|
|
152
|
+
return candidates;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Search for a system font suitable for Unicode rendering.
|
|
156
|
+
*
|
|
157
|
+
* Returns the raw font file bytes of the highest-priority candidate,
|
|
158
|
+
* or `null` if no font was found. This is a convenience wrapper around
|
|
159
|
+
* {@link discoverSystemFontCandidates}.
|
|
160
|
+
*/
|
|
161
|
+
export function discoverSystemFont() {
|
|
162
|
+
const candidates = discoverSystemFontCandidates();
|
|
163
|
+
return candidates.length > 0 ? candidates[0] : null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Reset the cached font discovery result (for testing).
|
|
167
|
+
*/
|
|
168
|
+
export function resetFontDiscoveryCache() {
|
|
169
|
+
_cachedCandidates = undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Override the cached candidates with a custom list (for testing).
|
|
173
|
+
* Call {@link resetFontDiscoveryCache} to clear the override.
|
|
174
|
+
*/
|
|
175
|
+
export function _setCandidatesForTest(candidates) {
|
|
176
|
+
_cachedCandidates = candidates;
|
|
177
|
+
}
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Internal
|
|
180
|
+
// =============================================================================
|
|
181
|
+
function tryReadFont(fontPath) {
|
|
182
|
+
try {
|
|
183
|
+
return readFileBytesSync(fontPath);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -111,8 +111,36 @@ class BEReader {
|
|
|
111
111
|
*/
|
|
112
112
|
export function parseTtf(data) {
|
|
113
113
|
const r = new BEReader(data);
|
|
114
|
+
// --- Check for TTC (TrueType Collection) ---
|
|
115
|
+
const magic = r.u32();
|
|
116
|
+
if (magic === 0x74746366) {
|
|
117
|
+
// 'ttcf' — TrueType Collection
|
|
118
|
+
// Read the offset of the first font and restart parsing from there
|
|
119
|
+
r.skip(4); // majorVersion, minorVersion
|
|
120
|
+
const numFonts = r.u32();
|
|
121
|
+
if (numFonts === 0) {
|
|
122
|
+
throw new PdfFontError("TrueType Collection is empty (0 fonts)");
|
|
123
|
+
}
|
|
124
|
+
const firstFontOffset = r.u32();
|
|
125
|
+
return parseTtfAtOffset(data, firstFontOffset);
|
|
126
|
+
}
|
|
127
|
+
// Not TTC — parse as regular TTF starting from the magic we already read
|
|
128
|
+
return parseTtfFromMagic(data, r, magic);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse a TTF font starting at a given byte offset within the data.
|
|
132
|
+
* Used for TTC collections where each font starts at a different offset.
|
|
133
|
+
*/
|
|
134
|
+
function parseTtfAtOffset(data, offset) {
|
|
135
|
+
const r = new BEReader(data, offset);
|
|
136
|
+
const magic = r.u32();
|
|
137
|
+
return parseTtfFromMagic(data, r, magic);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Core TTF parsing after the first 4 bytes (sfVersion) have been read.
|
|
141
|
+
*/
|
|
142
|
+
function parseTtfFromMagic(data, r, sfVersion) {
|
|
114
143
|
// --- Offset table ---
|
|
115
|
-
const sfVersion = r.u32();
|
|
116
144
|
// TrueType: 0x00010000 or 'true' (0x74727565)
|
|
117
145
|
// OpenType with CFF: 'OTTO' (0x4F54544F) — not supported for subsetting
|
|
118
146
|
if (sfVersion !== 0x00010000 && sfVersion !== 0x74727565) {
|