@cj-tech-master/excelts 9.5.5 → 9.5.6
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/excel/worksheet.d.ts +11 -0
- package/dist/browser/modules/excel/worksheet.js +13 -0
- package/dist/browser/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/browser/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +12 -0
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/browser/modules/formula/materialize/types.d.ts +19 -3
- package/dist/browser/modules/formula/materialize/types.js +13 -3
- package/dist/browser/modules/pdf/builder/document-builder.js +2 -2
- package/dist/browser/modules/pdf/font/system-fonts.d.ts +24 -4
- package/dist/browser/modules/pdf/font/system-fonts.js +76 -32
- package/dist/browser/modules/pdf/render/pdf-exporter.js +6 -3
- package/dist/browser/modules/word/advanced/field-engine.js +151 -23
- package/dist/browser/modules/word/advanced/math-convert.js +2 -1
- package/dist/browser/modules/word/advanced/style-map.js +44 -6
- package/dist/browser/modules/word/convert/html/html-import.js +434 -71
- package/dist/browser/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/browser/modules/word/layout/layout-full.js +4 -1
- package/dist/browser/modules/word/security/digital-signatures.js +160 -33
- package/dist/browser/modules/word/security/encryption.js +109 -9
- package/dist/cjs/modules/excel/worksheet.js +13 -0
- package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/cjs/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/cjs/modules/formula/materialize/types.js +13 -3
- package/dist/cjs/modules/pdf/builder/document-builder.js +1 -1
- package/dist/cjs/modules/pdf/font/system-fonts.js +77 -32
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +5 -2
- package/dist/cjs/modules/word/advanced/field-engine.js +151 -23
- package/dist/cjs/modules/word/advanced/math-convert.js +2 -1
- package/dist/cjs/modules/word/advanced/style-map.js +44 -6
- package/dist/cjs/modules/word/convert/html/html-import.js +434 -71
- package/dist/cjs/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/cjs/modules/word/layout/layout-full.js +4 -1
- package/dist/cjs/modules/word/security/digital-signatures.js +160 -33
- package/dist/cjs/modules/word/security/encryption.js +109 -9
- package/dist/esm/modules/excel/worksheet.js +13 -0
- package/dist/esm/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/esm/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/esm/modules/formula/materialize/types.js +13 -3
- package/dist/esm/modules/pdf/builder/document-builder.js +2 -2
- package/dist/esm/modules/pdf/font/system-fonts.js +76 -32
- package/dist/esm/modules/pdf/render/pdf-exporter.js +6 -3
- package/dist/esm/modules/word/advanced/field-engine.js +151 -23
- package/dist/esm/modules/word/advanced/math-convert.js +2 -1
- package/dist/esm/modules/word/advanced/style-map.js +44 -6
- package/dist/esm/modules/word/convert/html/html-import.js +434 -71
- package/dist/esm/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/esm/modules/word/layout/layout-full.js +4 -1
- package/dist/esm/modules/word/security/digital-signatures.js +160 -33
- package/dist/esm/modules/word/security/encryption.js +109 -9
- package/dist/iife/excelts.iife.js +40 -26
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +3 -3
- package/dist/types/modules/excel/worksheet.d.ts +11 -0
- package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +12 -0
- package/dist/types/modules/formula/materialize/types.d.ts +19 -3
- package/dist/types/modules/pdf/font/system-fonts.d.ts +24 -4
- package/package.json +1 -1
|
@@ -339,6 +339,17 @@ declare class Worksheet {
|
|
|
339
339
|
*/
|
|
340
340
|
private _spliceMerges;
|
|
341
341
|
get hasMerges(): boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Read-only enumeration of every merged region on this sheet
|
|
344
|
+
* (1-based, inclusive). Consumed by the formula engine's snapshot
|
|
345
|
+
* builder to detect `#SPILL!` conflicts. See issue #162 follow-up.
|
|
346
|
+
*/
|
|
347
|
+
get mergedRegions(): ReadonlyArray<{
|
|
348
|
+
readonly top: number;
|
|
349
|
+
readonly left: number;
|
|
350
|
+
readonly bottom: number;
|
|
351
|
+
readonly right: number;
|
|
352
|
+
}>;
|
|
342
353
|
/**
|
|
343
354
|
* Scan the range and if any cell is part of a merge, un-merge the group.
|
|
344
355
|
* Note this function can affect multiple merges and merge-blocks are
|
|
@@ -907,6 +907,19 @@ class Worksheet {
|
|
|
907
907
|
// return true if this._merges has a merge object
|
|
908
908
|
return Object.values(this._merges).some(Boolean);
|
|
909
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Read-only enumeration of every merged region on this sheet
|
|
912
|
+
* (1-based, inclusive). Consumed by the formula engine's snapshot
|
|
913
|
+
* builder to detect `#SPILL!` conflicts. See issue #162 follow-up.
|
|
914
|
+
*/
|
|
915
|
+
get mergedRegions() {
|
|
916
|
+
return Object.values(this._merges).map(merge => ({
|
|
917
|
+
top: merge.top,
|
|
918
|
+
left: merge.left,
|
|
919
|
+
bottom: merge.bottom,
|
|
920
|
+
right: merge.right
|
|
921
|
+
}));
|
|
922
|
+
}
|
|
910
923
|
/**
|
|
911
924
|
* Scan the range and if any cell is part of a merge, un-merge the group.
|
|
912
925
|
* Note this function can affect multiple merges and merge-blocks are
|
|
@@ -136,8 +136,14 @@ function applySpillWrite(workbook, op) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
|
-
// Ghost cell: set value (not result)
|
|
139
|
+
// Ghost cell: set value (not result). Defence in depth — the
|
|
140
|
+
// plan builder rejects spills onto merged regions, so a Merge
|
|
141
|
+
// type here is unreachable; guard anyway because writing
|
|
142
|
+
// through `MergeValue`'s setter would clobber the master.
|
|
140
143
|
const targetCell = ws.getCell(targetRow, targetCol);
|
|
144
|
+
if (targetCell.type === CellValueTypeLike.Merge) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
141
147
|
targetCell.value = snapshotValueToRaw(val);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
@@ -160,9 +166,17 @@ function applyCleanupWrite(workbook, op) {
|
|
|
160
166
|
}
|
|
161
167
|
for (const { row, col } of op.cells) {
|
|
162
168
|
const cell = ws.findCell(row, col);
|
|
163
|
-
if (cell) {
|
|
164
|
-
|
|
169
|
+
if (!cell) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Defence in depth: writing `null` to a merge slave would forward
|
|
173
|
+
// through `MergeValue`'s setter and wipe the master's value. The
|
|
174
|
+
// plan builder already skips merged regions in `collectStaleGhosts`,
|
|
175
|
+
// so this guard is belt-and-suspenders.
|
|
176
|
+
if (cell.type === CellValueTypeLike.Merge) {
|
|
177
|
+
continue;
|
|
165
178
|
}
|
|
179
|
+
cell.value = null;
|
|
166
180
|
}
|
|
167
181
|
}
|
|
168
182
|
// ============================================================================
|
|
@@ -95,13 +95,24 @@ function buildWorksheetSnapshot(ws, date1904) {
|
|
|
95
95
|
? { top: dims.top, left: dims.left, bottom: dims.bottom, right: dims.right }
|
|
96
96
|
: null;
|
|
97
97
|
const tables = buildTables(ws);
|
|
98
|
+
// Defensive copy — snapshot must not alias host-owned arrays.
|
|
99
|
+
const hostMergedRegions = ws.mergedRegions;
|
|
100
|
+
const mergedRegions = hostMergedRegions
|
|
101
|
+
? hostMergedRegions.map(r => ({
|
|
102
|
+
top: r.top,
|
|
103
|
+
left: r.left,
|
|
104
|
+
bottom: r.bottom,
|
|
105
|
+
right: r.right
|
|
106
|
+
}))
|
|
107
|
+
: [];
|
|
98
108
|
return {
|
|
99
109
|
id: ws.id,
|
|
100
110
|
name: ws.name,
|
|
101
111
|
dimensions,
|
|
102
112
|
cells,
|
|
103
113
|
hiddenRows,
|
|
104
|
-
tables
|
|
114
|
+
tables,
|
|
115
|
+
mergedRegions
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
// ============================================================================
|
|
@@ -113,6 +124,14 @@ function buildCellSnapshot(cell, row, col, date1904) {
|
|
|
113
124
|
if (cellType === CellValueTypeLike.Null) {
|
|
114
125
|
return null;
|
|
115
126
|
}
|
|
127
|
+
// Skip merge slaves — Excel treats them as blank for formula
|
|
128
|
+
// purposes, but the host's `MergeValue` proxy would forward
|
|
129
|
+
// `cell.value` from the master, so letting them into `cells` would
|
|
130
|
+
// double-count master values in range aggregates. See issue #162
|
|
131
|
+
// and the `Merge` case in `CellValueTypeLike`.
|
|
132
|
+
if (cellType === CellValueTypeLike.Merge) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
116
135
|
// ── Formula cells ──
|
|
117
136
|
if (cellType === CellValueTypeLike.Formula) {
|
|
118
137
|
return buildFormulaCellSnapshot(cell, row, col, date1904);
|
|
@@ -121,6 +121,18 @@ export interface WorksheetSnapshot {
|
|
|
121
121
|
readonly hiddenRows: ReadonlySet<number>;
|
|
122
122
|
/** Tables defined in this worksheet. */
|
|
123
123
|
readonly tables: readonly TableSnapshot[];
|
|
124
|
+
/**
|
|
125
|
+
* Merged regions on this worksheet, as 1-based inclusive rectangles.
|
|
126
|
+
* Consulted by the writeback planner to reject `#SPILL!` conflicts;
|
|
127
|
+
* the evaluator does not need this since merge slaves are already
|
|
128
|
+
* filtered out of `cells`.
|
|
129
|
+
*/
|
|
130
|
+
readonly mergedRegions: readonly {
|
|
131
|
+
readonly top: number;
|
|
132
|
+
readonly left: number;
|
|
133
|
+
readonly bottom: number;
|
|
134
|
+
readonly right: number;
|
|
135
|
+
}[];
|
|
124
136
|
}
|
|
125
137
|
/**
|
|
126
138
|
* Column definition within a table.
|
|
@@ -247,6 +247,14 @@ activeSpillTargets) {
|
|
|
247
247
|
if (inst.row + arr.height - 1 > 1048576 || inst.col + arr.width - 1 > 16384) {
|
|
248
248
|
return "error";
|
|
249
249
|
}
|
|
250
|
+
// Reject if the source cell itself sits inside a merged region.
|
|
251
|
+
// Excel reports #SPILL! whenever a dynamic-array formula is placed
|
|
252
|
+
// in a merged cell, even when the ghosts land outside the merge.
|
|
253
|
+
// The ghost loop below skips `(r=0, c=0)` and the master's value is
|
|
254
|
+
// already in `cells`, so it would not catch this case.
|
|
255
|
+
if (isInMergedRegion(ws, inst.row, inst.col)) {
|
|
256
|
+
return "error";
|
|
257
|
+
}
|
|
250
258
|
// Check spill availability: verify all target ghost cells are unoccupied
|
|
251
259
|
for (let r = 0; r < arr.height; r++) {
|
|
252
260
|
for (let c = 0; c < arr.width; c++) {
|
|
@@ -261,6 +269,16 @@ activeSpillTargets) {
|
|
|
261
269
|
if (activeSpillTargets.has(targetKey)) {
|
|
262
270
|
return "error";
|
|
263
271
|
}
|
|
272
|
+
// Refuse to spill onto any cell that belongs to a merged region.
|
|
273
|
+
// The cell itself may be a merge slave — which the snapshot
|
|
274
|
+
// builder filters out of `ws.cells`, so the value/formula checks
|
|
275
|
+
// below would treat it as empty — but writing there would mutate
|
|
276
|
+
// the master via `MergeValue`'s setter in `@excel/cell` and
|
|
277
|
+
// silently corrupt the merge. Excel reports `#SPILL!` whenever a
|
|
278
|
+
// dynamic-array result tries to land in a merge.
|
|
279
|
+
if (isInMergedRegion(ws, targetRow, targetCol)) {
|
|
280
|
+
return "error";
|
|
281
|
+
}
|
|
264
282
|
// Check if the cell is a ghost from ANY previous spill.
|
|
265
283
|
// If the user hasn't modified it, it's safe to overwrite — the
|
|
266
284
|
// originating formula will clean it up (or we'll overwrite it).
|
|
@@ -349,6 +367,19 @@ function collectStaleGhosts(region, previousGhosts, snapshot) {
|
|
|
349
367
|
}
|
|
350
368
|
const targetRow = region.sourceRow + r;
|
|
351
369
|
const targetCol = region.sourceCol + c;
|
|
370
|
+
// If the user (or a previous edit) has placed this former ghost
|
|
371
|
+
// inside a merged region, skip it. The cell is now either a merge
|
|
372
|
+
// master (carrying the user's intentional value) or a merge slave
|
|
373
|
+
// (whose `cell.value = null` writeback would forward through
|
|
374
|
+
// `MergeValue`'s setter and clobber the master). Either way,
|
|
375
|
+
// cleanup must not touch it. The snapshot builder filters merge
|
|
376
|
+
// slaves out of `ws.cells` (see issue #162), so the
|
|
377
|
+
// `isGhostUnmodified` check below would otherwise miss this case
|
|
378
|
+
// — `cell` would be `undefined`, which currently means
|
|
379
|
+
// "unmodified, safe to wipe".
|
|
380
|
+
if (isInMergedRegion(ws, targetRow, targetCol)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
352
383
|
const targetKey = spillCellKeyFromId(region.worksheetId, targetRow, targetCol);
|
|
353
384
|
const cell = ws.cells.get(snapshotCellKey(targetRow, targetCol));
|
|
354
385
|
if (isGhostUnmodified(cell, targetKey, previousGhosts)) {
|
|
@@ -380,6 +411,22 @@ function emitPreviousSpillCleanup(previousRegion, previousGhosts, snapshot, oper
|
|
|
380
411
|
cells: cleanupCells
|
|
381
412
|
});
|
|
382
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Test whether `(row, col)` falls inside any merged region of `ws`.
|
|
416
|
+
*
|
|
417
|
+
* Linear scan — merge counts per sheet are small in practice. The
|
|
418
|
+
* snapshot builder filters merge slaves out of `ws.cells`, so callers
|
|
419
|
+
* use this helper to recover the "is this cell part of a merge?"
|
|
420
|
+
* signal that the cell map alone no longer carries.
|
|
421
|
+
*/
|
|
422
|
+
function isInMergedRegion(ws, row, col) {
|
|
423
|
+
for (const region of ws.mergedRegions) {
|
|
424
|
+
if (row >= region.top && row <= region.bottom && col >= region.left && col <= region.right) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
383
430
|
function isGhostUnmodified(cell, ghostKey, previousGhosts) {
|
|
384
431
|
if (!cell) {
|
|
385
432
|
return true;
|
|
@@ -23,18 +23,28 @@ export interface CellErrorValueLike {
|
|
|
23
23
|
*/
|
|
24
24
|
export type FormulaResultLike = number | string | boolean | Date | CellErrorValueLike | undefined;
|
|
25
25
|
/**
|
|
26
|
-
* Numeric cell-type tag exposed by host cells. The engine
|
|
27
|
-
* against `Null` and `Formula`; any other value is treated as
|
|
28
|
-
* literal.
|
|
26
|
+
* Numeric cell-type tag exposed by host cells. The engine compares
|
|
27
|
+
* against `Null`, `Merge`, and `Formula`; any other value is treated as
|
|
28
|
+
* a scalar literal.
|
|
29
|
+
*
|
|
30
|
+
* `Merge` identifies a non-master cell inside a merged region. The
|
|
31
|
+
* host's in-memory model may proxy `cell.value` from slaves to the
|
|
32
|
+
* master (see `MergeValue` in `@excel/cell`), so the snapshot builder
|
|
33
|
+
* must filter merge slaves out — otherwise range aggregates count the
|
|
34
|
+
* master's value once per slave. See issue #162.
|
|
29
35
|
*
|
|
30
36
|
* Kept as inline numeric literals (not an enum) so this file stays free
|
|
31
37
|
* of runtime dependencies. The `const` object and `type` alias share a
|
|
32
38
|
* name via TypeScript's declaration merging — the value form
|
|
33
39
|
* (`CellValueTypeLike.Null`, `CellValueTypeLike.Formula`) is used at
|
|
34
40
|
* comparison sites, the type form annotates `CellLike.type`.
|
|
41
|
+
*
|
|
42
|
+
* The numeric values must stay in sync with `ValueType` in
|
|
43
|
+
* `@excel/enums`, which is what `@excel/cell` writes into `cell.type`.
|
|
35
44
|
*/
|
|
36
45
|
export declare const CellValueTypeLike: {
|
|
37
46
|
readonly Null: 0;
|
|
47
|
+
readonly Merge: 1;
|
|
38
48
|
readonly Formula: 6;
|
|
39
49
|
};
|
|
40
50
|
export type CellValueTypeLike = number;
|
|
@@ -118,6 +128,12 @@ export interface WorksheetLike {
|
|
|
118
128
|
findCell(row: number, col: number): CellLike | undefined;
|
|
119
129
|
getCell(row: number, col: number): CellLike;
|
|
120
130
|
getTables?(): TableRefLike[];
|
|
131
|
+
/**
|
|
132
|
+
* Read-only enumeration of the worksheet's merged regions (1-based,
|
|
133
|
+
* inclusive). Optional for hosts that don't model merge state — the
|
|
134
|
+
* snapshot builder treats absence as "no merges".
|
|
135
|
+
*/
|
|
136
|
+
readonly mergedRegions?: readonly DimensionsLike[];
|
|
121
137
|
}
|
|
122
138
|
/**
|
|
123
139
|
* A complete defined name entry with all details.
|
|
@@ -13,17 +13,27 @@
|
|
|
13
13
|
// ValueType — numeric mirror of `@excel/enums` ValueType
|
|
14
14
|
// ============================================================================
|
|
15
15
|
/**
|
|
16
|
-
* Numeric cell-type tag exposed by host cells. The engine
|
|
17
|
-
* against `Null` and `Formula`; any other value is treated as
|
|
18
|
-
* literal.
|
|
16
|
+
* Numeric cell-type tag exposed by host cells. The engine compares
|
|
17
|
+
* against `Null`, `Merge`, and `Formula`; any other value is treated as
|
|
18
|
+
* a scalar literal.
|
|
19
|
+
*
|
|
20
|
+
* `Merge` identifies a non-master cell inside a merged region. The
|
|
21
|
+
* host's in-memory model may proxy `cell.value` from slaves to the
|
|
22
|
+
* master (see `MergeValue` in `@excel/cell`), so the snapshot builder
|
|
23
|
+
* must filter merge slaves out — otherwise range aggregates count the
|
|
24
|
+
* master's value once per slave. See issue #162.
|
|
19
25
|
*
|
|
20
26
|
* Kept as inline numeric literals (not an enum) so this file stays free
|
|
21
27
|
* of runtime dependencies. The `const` object and `type` alias share a
|
|
22
28
|
* name via TypeScript's declaration merging — the value form
|
|
23
29
|
* (`CellValueTypeLike.Null`, `CellValueTypeLike.Formula`) is used at
|
|
24
30
|
* comparison sites, the type form annotates `CellLike.type`.
|
|
31
|
+
*
|
|
32
|
+
* The numeric values must stay in sync with `ValueType` in
|
|
33
|
+
* `@excel/enums`, which is what `@excel/cell` writes into `cell.type`.
|
|
25
34
|
*/
|
|
26
35
|
export const CellValueTypeLike = {
|
|
27
36
|
Null: 0,
|
|
37
|
+
Merge: 1,
|
|
28
38
|
Formula: 6
|
|
29
39
|
};
|
|
@@ -24,7 +24,7 @@ import { PdfContentStream } from "../core/pdf-stream.js";
|
|
|
24
24
|
import { PdfWriter } from "../core/pdf-writer.js";
|
|
25
25
|
import { writePdfAMetadata, writePdfAOutputIntent } from "../core/pdfa.js";
|
|
26
26
|
import { FontManager } from "../font/font-manager.js";
|
|
27
|
-
import {
|
|
27
|
+
import { iterateSystemFontCandidates } from "../font/system-fonts.js";
|
|
28
28
|
import { parseTtf } from "../font/ttf-parser.js";
|
|
29
29
|
import { wrapTextLines, emitTextWithMatrix, alphaGsName } from "../render/page-renderer.js";
|
|
30
30
|
import { writeImageXObject } from "./image-utils.js";
|
|
@@ -839,7 +839,7 @@ export class PdfDocumentBuilder {
|
|
|
839
839
|
if (nonWinAnsi.size > 0) {
|
|
840
840
|
// Try auto-discovery unless the caller opted out.
|
|
841
841
|
if (!this._disableFontAutoDiscovery) {
|
|
842
|
-
for (const candidate of
|
|
842
|
+
for (const candidate of iterateSystemFontCandidates()) {
|
|
843
843
|
try {
|
|
844
844
|
const testTtf = parseTtf(candidate);
|
|
845
845
|
const allCovered = [...nonWinAnsi].every(cp => testTtf.cmap.has(cp));
|
|
@@ -11,23 +11,43 @@
|
|
|
11
11
|
* .ttc (TrueType Collection) files are supported — parseTtf() extracts
|
|
12
12
|
* the first font from the collection automatically.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Discovery is exposed both as a generator
|
|
15
|
+
* ({@link iterateSystemFontCandidates}) for early-exit callers and as
|
|
16
|
+
* an array snapshot ({@link discoverSystemFontCandidates}) for tests
|
|
17
|
+
* and full enumeration. Once the full snapshot has been produced it is
|
|
18
|
+
* cached and replayed on subsequent calls; partial iterations rely on
|
|
19
|
+
* the OS page cache to make repeat reads of the same font files cheap.
|
|
15
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* Lazily yield discoverable system font candidates, in preference order.
|
|
23
|
+
*
|
|
24
|
+
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
25
|
+
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
26
|
+
*
|
|
27
|
+
* Iterating one candidate at a time lets callers `break` as soon as
|
|
28
|
+
* they find a match, avoiding the cost of recursively reading every
|
|
29
|
+
* font in every system font directory just to discard them.
|
|
30
|
+
*/
|
|
31
|
+
export declare function iterateSystemFontCandidates(): Generator<Uint8Array, void, void>;
|
|
16
32
|
/**
|
|
17
33
|
* Return all discoverable system font candidates, ordered by preference.
|
|
18
34
|
*
|
|
19
35
|
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
20
36
|
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
21
37
|
*
|
|
22
|
-
*
|
|
38
|
+
* The full snapshot is cached: once produced, repeated calls return the
|
|
39
|
+
* same array without touching the filesystem.
|
|
40
|
+
*
|
|
41
|
+
* Prefer {@link iterateSystemFontCandidates} when you only need the
|
|
42
|
+
* first candidate that satisfies a predicate: it avoids reading every
|
|
43
|
+
* font in every system directory just to discard them.
|
|
23
44
|
*/
|
|
24
45
|
export declare function discoverSystemFontCandidates(): Uint8Array[];
|
|
25
46
|
/**
|
|
26
47
|
* Search for a system font suitable for Unicode rendering.
|
|
27
48
|
*
|
|
28
49
|
* Returns the raw font file bytes of the highest-priority candidate,
|
|
29
|
-
* or `null` if no font was found.
|
|
30
|
-
* {@link discoverSystemFontCandidates}.
|
|
50
|
+
* or `null` if no font was found.
|
|
31
51
|
*/
|
|
32
52
|
export declare function discoverSystemFont(): Uint8Array | null;
|
|
33
53
|
/**
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* .ttc (TrueType Collection) files are supported — parseTtf() extracts
|
|
12
12
|
* the first font from the collection automatically.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Discovery is exposed both as a generator
|
|
15
|
+
* ({@link iterateSystemFontCandidates}) for early-exit callers and as
|
|
16
|
+
* an array snapshot ({@link discoverSystemFontCandidates}) for tests
|
|
17
|
+
* and full enumeration. Once the full snapshot has been produced it is
|
|
18
|
+
* cached and replayed on subsequent calls; partial iterations rely on
|
|
19
|
+
* the OS page cache to make repeat reads of the same font files cheap.
|
|
15
20
|
*/
|
|
16
21
|
import { fileExistsSync, readFileBytesSync, traverseDirectorySync } from "../../../utils/fs.browser.js";
|
|
17
22
|
// =============================================================================
|
|
@@ -88,75 +93,114 @@ const PREFERRED_FONTS = [
|
|
|
88
93
|
// =============================================================================
|
|
89
94
|
// Font Discovery
|
|
90
95
|
// =============================================================================
|
|
96
|
+
// Cached, fully-populated candidate list. Set when the generator runs
|
|
97
|
+
// to completion via `discoverSystemFontCandidates()`, or when a test
|
|
98
|
+
// injects candidates via `_setCandidatesForTest`. Partial iterations
|
|
99
|
+
// (where the caller `break`s after a match) intentionally do not
|
|
100
|
+
// populate this cache — they rely on the OS page cache to keep repeat
|
|
101
|
+
// reads cheap.
|
|
91
102
|
let _cachedCandidates;
|
|
92
103
|
/**
|
|
93
|
-
*
|
|
104
|
+
* Lazily yield discoverable system font candidates, in preference order.
|
|
94
105
|
*
|
|
95
106
|
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
96
107
|
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
97
108
|
*
|
|
98
|
-
*
|
|
109
|
+
* Iterating one candidate at a time lets callers `break` as soon as
|
|
110
|
+
* they find a match, avoiding the cost of recursively reading every
|
|
111
|
+
* font in every system font directory just to discard them.
|
|
99
112
|
*/
|
|
100
|
-
export function
|
|
113
|
+
export function* iterateSystemFontCandidates() {
|
|
114
|
+
// Fast path: a previous call already produced the full snapshot.
|
|
101
115
|
if (_cachedCandidates !== undefined) {
|
|
102
|
-
|
|
116
|
+
for (const c of _cachedCandidates) {
|
|
117
|
+
yield c;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
103
120
|
}
|
|
104
121
|
if (typeof process === "undefined" || !process.platform) {
|
|
105
|
-
|
|
106
|
-
return _cachedCandidates;
|
|
122
|
+
return;
|
|
107
123
|
}
|
|
108
|
-
const
|
|
109
|
-
const seen = new Set(); // dedupe by path
|
|
124
|
+
const seen = new Set(); // dedupe by path within this iteration
|
|
110
125
|
const dirs = getSystemFontDirs();
|
|
111
|
-
// Strategy 1: Check preferred font filenames (in order)
|
|
126
|
+
// Strategy 1: Check preferred font filenames (in order). These stat
|
|
127
|
+
// calls are cheap and run regardless — they are the only path most
|
|
128
|
+
// callers need to traverse.
|
|
112
129
|
for (const fontName of PREFERRED_FONTS) {
|
|
113
130
|
for (const dir of dirs) {
|
|
114
131
|
const fontPath = `${dir}/${fontName}`;
|
|
115
132
|
if (seen.has(fontPath)) {
|
|
116
133
|
continue;
|
|
117
134
|
}
|
|
135
|
+
seen.add(fontPath);
|
|
118
136
|
if (fileExistsSync(fontPath)) {
|
|
119
137
|
const data = tryReadFont(fontPath);
|
|
120
138
|
if (data) {
|
|
121
|
-
|
|
122
|
-
seen.add(fontPath);
|
|
139
|
+
yield data;
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
|
-
// Strategy 2:
|
|
144
|
+
// Strategy 2: Recursively scan each directory for any other .ttf/.ttc.
|
|
145
|
+
// This is the expensive step (hundreds of MB on macOS / Windows), so
|
|
146
|
+
// it walks one directory at a time and yields candidates as it goes —
|
|
147
|
+
// a caller that found a match in Strategy 1 (or in an earlier
|
|
148
|
+
// directory here) never reaches further directories.
|
|
128
149
|
const broadRe = /noto|unicode|cjk|yahei|heiti|gothic|sans|serif|ming|song|dejavu|liberation|droid|wqy/i;
|
|
129
150
|
for (const dir of dirs) {
|
|
151
|
+
let entries;
|
|
130
152
|
try {
|
|
131
|
-
|
|
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
|
-
}
|
|
153
|
+
entries = traverseDirectorySync(dir, { recursive: true, filter: e => !e.isDirectory });
|
|
146
154
|
}
|
|
147
155
|
catch {
|
|
148
156
|
// Directory doesn't exist or not readable
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const fonts = entries.filter(e => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
|
|
160
|
+
// Broad-coverage names first, then large files
|
|
161
|
+
const broad = fonts.filter(e => broadRe.test(e.absolutePath));
|
|
162
|
+
const rest = fonts.filter(e => !broadRe.test(e.absolutePath) && e.size > 50000);
|
|
163
|
+
for (const entry of [...broad, ...rest]) {
|
|
164
|
+
if (seen.has(entry.absolutePath)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
seen.add(entry.absolutePath);
|
|
168
|
+
const data = tryReadFont(entry.absolutePath);
|
|
169
|
+
if (data) {
|
|
170
|
+
yield data;
|
|
171
|
+
}
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Return all discoverable system font candidates, ordered by preference.
|
|
177
|
+
*
|
|
178
|
+
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
179
|
+
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
180
|
+
*
|
|
181
|
+
* The full snapshot is cached: once produced, repeated calls return the
|
|
182
|
+
* same array without touching the filesystem.
|
|
183
|
+
*
|
|
184
|
+
* Prefer {@link iterateSystemFontCandidates} when you only need the
|
|
185
|
+
* first candidate that satisfies a predicate: it avoids reading every
|
|
186
|
+
* font in every system directory just to discard them.
|
|
187
|
+
*/
|
|
188
|
+
export function discoverSystemFontCandidates() {
|
|
189
|
+
if (_cachedCandidates !== undefined) {
|
|
190
|
+
return _cachedCandidates;
|
|
191
|
+
}
|
|
192
|
+
const all = [];
|
|
193
|
+
for (const candidate of iterateSystemFontCandidates()) {
|
|
194
|
+
all.push(candidate);
|
|
195
|
+
}
|
|
196
|
+
_cachedCandidates = all;
|
|
197
|
+
return all;
|
|
153
198
|
}
|
|
154
199
|
/**
|
|
155
200
|
* Search for a system font suitable for Unicode rendering.
|
|
156
201
|
*
|
|
157
202
|
* Returns the raw font file bytes of the highest-priority candidate,
|
|
158
|
-
* or `null` if no font was found.
|
|
159
|
-
* {@link discoverSystemFontCandidates}.
|
|
203
|
+
* or `null` if no font was found.
|
|
160
204
|
*/
|
|
161
205
|
export function discoverSystemFont() {
|
|
162
206
|
const candidates = discoverSystemFontCandidates();
|
|
@@ -15,7 +15,7 @@ import { PdfContentStream, isWinAnsiCodePoint } from "../core/pdf-stream.js";
|
|
|
15
15
|
import { PdfWriter } from "../core/pdf-writer.js";
|
|
16
16
|
import { PdfError, PdfRenderError } from "../errors.js";
|
|
17
17
|
import { FontManager, resolvePdfFontName } from "../font/font-manager.js";
|
|
18
|
-
import {
|
|
18
|
+
import { iterateSystemFontCandidates } from "../font/system-fonts.js";
|
|
19
19
|
import { parseTtf } from "../font/ttf-parser.js";
|
|
20
20
|
import { PageSizes, PdfCellType, isPdfChartsheet } from "../types.js";
|
|
21
21
|
import { createChartSurface } from "./chart-surface.js";
|
|
@@ -58,8 +58,11 @@ function prepareExport(workbook, options) {
|
|
|
58
58
|
// Collect non-WinAnsi code points from the document (single pass)
|
|
59
59
|
const nonWinAnsi = collectNonWinAnsiCodePoints(sheets);
|
|
60
60
|
if (nonWinAnsi.size > 0) {
|
|
61
|
-
// Try system font candidates in preference order until one
|
|
62
|
-
|
|
61
|
+
// Try system font candidates lazily in preference order until one
|
|
62
|
+
// covers all chars. Iterating instead of materializing the full
|
|
63
|
+
// candidate list lets us stop the moment a match is found, which
|
|
64
|
+
// avoids the cost of recursively scanning every system font dir.
|
|
65
|
+
for (const candidate of iterateSystemFontCandidates()) {
|
|
63
66
|
try {
|
|
64
67
|
const testTtf = parseTtf(candidate);
|
|
65
68
|
const allCovered = [...nonWinAnsi].every(cp => testTtf.cmap.has(cp));
|