@node-projects/excelforge 3.0.0 → 3.2.0

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,1567 @@
1
+ import { colIndexToLetter, colLetterToIndex, base64ToBytes } from '../utils/helpers.js';
2
+ import { deflateRaw } from '../utils/zip.js';
3
+ import { FormulaEngine } from './FormulaEngine.js';
4
+ const PAPER_SIZES = {
5
+ letter: [612, 792],
6
+ legal: [612, 1008],
7
+ a4: [595.28, 841.89],
8
+ a3: [841.89, 1190.55],
9
+ tabloid: [792, 1224],
10
+ };
11
+ const THEME_COLORS = [
12
+ '#000000', '#FFFFFF', '#44546A', '#E7E6E6', '#4472C4', '#ED7D31',
13
+ '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47',
14
+ ];
15
+ const HELV = [
16
+ 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
17
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
18
+ 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
19
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
20
+ 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
21
+ 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
22
+ ];
23
+ const HELV_B = [
24
+ 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
25
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
26
+ 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
27
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
28
+ 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
29
+ 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
30
+ ];
31
+ function textWidthPt(text, fontSize, bold) {
32
+ const tbl = bold ? HELV_B : HELV;
33
+ let w = 0;
34
+ for (let i = 0; i < text.length; i++) {
35
+ const code = text.charCodeAt(i);
36
+ w += (code >= 32 && code <= 126) ? tbl[code - 32] : 500;
37
+ }
38
+ return (w / 1000) * fontSize;
39
+ }
40
+ function colorToHex(c) {
41
+ if (!c)
42
+ return '';
43
+ if (c.startsWith('#'))
44
+ return c;
45
+ if (c.startsWith('theme:')) {
46
+ const idx = parseInt(c.slice(6), 10);
47
+ return THEME_COLORS[idx] ?? '#000000';
48
+ }
49
+ if (c.length === 8 && !c.startsWith('#'))
50
+ return '#' + c.slice(2);
51
+ return '#' + c;
52
+ }
53
+ function hexToRgb(hex) {
54
+ const h = hex.replace('#', '');
55
+ return [
56
+ parseInt(h.slice(0, 2), 16) / 255,
57
+ parseInt(h.slice(2, 4), 16) / 255,
58
+ parseInt(h.slice(4, 6), 16) / 255,
59
+ ];
60
+ }
61
+ function colorRgb(c) {
62
+ const h = colorToHex(c);
63
+ return h ? hexToRgb(h) : null;
64
+ }
65
+ function formatNumber(value, fmt) {
66
+ if (value == null)
67
+ return '';
68
+ if (!fmt || fmt === 'General')
69
+ return String(value);
70
+ const num = typeof value === 'number' ? value : parseFloat(String(value));
71
+ if (isNaN(num))
72
+ return String(value);
73
+ if (fmt.includes('%')) {
74
+ const decimals = (fmt.match(/0\.(0+)%/) ?? [])[1]?.length ?? 0;
75
+ return (num * 100).toFixed(decimals) + '%';
76
+ }
77
+ const currMatch = fmt.match(/[$€£¥]|"CHF"/);
78
+ if (currMatch) {
79
+ const sym = currMatch[0].replace(/"/g, '');
80
+ const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 2;
81
+ const formatted = Math.abs(num).toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
82
+ if (fmt.indexOf(currMatch[0]) < fmt.indexOf('0'))
83
+ return (num < 0 ? '-' : '') + sym + formatted;
84
+ return (num < 0 ? '-' : '') + formatted + ' ' + sym;
85
+ }
86
+ if (fmt.includes('#,##0') || fmt.includes('#,###')) {
87
+ const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 0;
88
+ return num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
89
+ }
90
+ const fixedMatch = fmt.match(/^0\.(0+)$/);
91
+ if (fixedMatch)
92
+ return num.toFixed(fixedMatch[1].length);
93
+ if (/[ymdh]/i.test(fmt))
94
+ return formatDate(num, fmt);
95
+ if (/0\.0+E\+0+/i.test(fmt)) {
96
+ const decimals = (fmt.match(/0\.(0+)/) ?? [])[1]?.length ?? 2;
97
+ return num.toExponential(decimals).toUpperCase();
98
+ }
99
+ return String(value);
100
+ }
101
+ function formatDate(serial, fmt) {
102
+ const epoch = new Date(1899, 11, 30);
103
+ const d = new Date(epoch.getTime() + serial * 86400000);
104
+ const Y = d.getFullYear(), M = d.getMonth() + 1, D = d.getDate();
105
+ const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
106
+ return fmt
107
+ .replace(/yyyy/gi, String(Y))
108
+ .replace(/yy/gi, String(Y).slice(-2))
109
+ .replace(/mmmm/gi, d.toLocaleDateString('en', { month: 'long' }))
110
+ .replace(/mmm/gi, d.toLocaleDateString('en', { month: 'short' }))
111
+ .replace(/mm/gi, String(M).padStart(2, '0'))
112
+ .replace(/m/gi, String(M))
113
+ .replace(/dd/gi, String(D).padStart(2, '0'))
114
+ .replace(/d/gi, String(D))
115
+ .replace(/hh/gi, String(h).padStart(2, '0'))
116
+ .replace(/h/gi, String(h))
117
+ .replace(/ss/gi, String(s).padStart(2, '0'))
118
+ .replace(/nn|MM/g, String(m).padStart(2, '0'));
119
+ }
120
+ function pdfStr(s) {
121
+ return s.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
122
+ }
123
+ function encodeWinAnsi(s) {
124
+ const out = new Uint8Array(s.length);
125
+ for (let i = 0; i < s.length; i++) {
126
+ const code = s.charCodeAt(i);
127
+ out[i] = code < 256 ? code : 63;
128
+ }
129
+ return out;
130
+ }
131
+ function n(v) {
132
+ return +v.toFixed(4) + '';
133
+ }
134
+ const _enc = new TextEncoder();
135
+ class PdfDoc {
136
+ constructor() {
137
+ this.objects = [null];
138
+ this.streams = [null];
139
+ }
140
+ alloc() {
141
+ this.objects.push(null);
142
+ this.streams.push(null);
143
+ return this.objects.length - 1;
144
+ }
145
+ set(id, dict, stream) {
146
+ while (this.objects.length <= id) {
147
+ this.objects.push(null);
148
+ this.streams.push(null);
149
+ }
150
+ this.objects[id] = dict;
151
+ this.streams[id] = stream ?? null;
152
+ }
153
+ add(dict, stream) {
154
+ const id = this.alloc();
155
+ this.set(id, dict, stream);
156
+ return id;
157
+ }
158
+ addDeflated(dict, data) {
159
+ const raw = deflateRaw(data, 6);
160
+ const zlib = new Uint8Array(2 + raw.length + 4);
161
+ zlib[0] = 0x78;
162
+ zlib[1] = 0x9C;
163
+ zlib.set(raw, 2);
164
+ let a = 1, b = 0;
165
+ for (let i = 0; i < data.length; i++) {
166
+ a = (a + data[i]) % 65521;
167
+ b = (b + a) % 65521;
168
+ }
169
+ const adler = ((b << 16) | a) >>> 0;
170
+ const off = 2 + raw.length;
171
+ zlib[off] = (adler >>> 24) & 0xFF;
172
+ zlib[off + 1] = (adler >>> 16) & 0xFF;
173
+ zlib[off + 2] = (adler >>> 8) & 0xFF;
174
+ zlib[off + 3] = adler & 0xFF;
175
+ const full = `${dict}/Filter/FlateDecode/Length ${zlib.length}>>`;
176
+ return this.add(full, zlib);
177
+ }
178
+ build(rootId, infoId) {
179
+ const parts = [];
180
+ const push = (s) => parts.push(_enc.encode(s));
181
+ const pushRaw = (b) => parts.push(b);
182
+ push('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n');
183
+ let offset = 0;
184
+ const offsets = new Array(this.objects.length).fill(0);
185
+ for (const p of parts)
186
+ offset += p.length;
187
+ const objParts = [];
188
+ for (let i = 1; i < this.objects.length; i++) {
189
+ const dict = this.objects[i];
190
+ if (!dict)
191
+ continue;
192
+ const subParts = [];
193
+ const stream = this.streams[i];
194
+ offsets[i] = offset;
195
+ const header = `${i} 0 obj\n`;
196
+ subParts.push(_enc.encode(header));
197
+ offset += header.length;
198
+ if (stream) {
199
+ const dictLine = dict + '\nstream\n';
200
+ subParts.push(_enc.encode(dictLine));
201
+ offset += dictLine.length;
202
+ subParts.push(stream);
203
+ offset += stream.length;
204
+ const tail = '\nendstream\nendobj\n';
205
+ subParts.push(_enc.encode(tail));
206
+ offset += tail.length;
207
+ }
208
+ else {
209
+ const body = dict + '\nendobj\n';
210
+ subParts.push(_enc.encode(body));
211
+ offset += body.length;
212
+ }
213
+ objParts.push(subParts);
214
+ }
215
+ for (const sp of objParts)
216
+ for (const p of sp)
217
+ pushRaw(p);
218
+ const xrefOffset = offset;
219
+ push(`xref\n0 ${this.objects.length}\n`);
220
+ push('0000000000 65535 f \n');
221
+ for (let i = 1; i < this.objects.length; i++) {
222
+ push(`${String(offsets[i]).padStart(10, '0')} 00000 n \n`);
223
+ }
224
+ push(`trailer\n<</Size ${this.objects.length}/Root ${rootId} 0 R`);
225
+ if (infoId)
226
+ push(`/Info ${infoId} 0 R`);
227
+ push(`>>\nstartxref\n${xrefOffset}\n%%EOF\n`);
228
+ let total = 0;
229
+ for (const p of parts)
230
+ total += p.length;
231
+ const result = new Uint8Array(total);
232
+ let pos = 0;
233
+ for (const p of parts) {
234
+ result.set(p, pos);
235
+ pos += p.length;
236
+ }
237
+ return result;
238
+ }
239
+ }
240
+ class StreamBuilder {
241
+ constructor() {
242
+ this._parts = [];
243
+ }
244
+ raw(s) { this._parts.push(s); return this; }
245
+ gsave() { this._parts.push('q'); return this; }
246
+ grestore() { this._parts.push('Q'); return this; }
247
+ fillColor(r, g, b) {
248
+ this._parts.push(`${n(r)} ${n(g)} ${n(b)} rg`);
249
+ return this;
250
+ }
251
+ strokeColor(r, g, b) {
252
+ this._parts.push(`${n(r)} ${n(g)} ${n(b)} RG`);
253
+ return this;
254
+ }
255
+ lineWidth(w) { this._parts.push(`${n(w)} w`); return this; }
256
+ dash(pattern, phase) {
257
+ this._parts.push(`[${pattern.map(n).join(' ')}] ${n(phase)} d`);
258
+ return this;
259
+ }
260
+ noDash() { this._parts.push('[] 0 d'); return this; }
261
+ fillRect(x, y, w, h) {
262
+ this._parts.push(`${n(x)} ${n(y)} ${n(w)} ${n(h)} re f`);
263
+ return this;
264
+ }
265
+ strokeRect(x, y, w, h) {
266
+ this._parts.push(`${n(x)} ${n(y)} ${n(w)} ${n(h)} re S`);
267
+ return this;
268
+ }
269
+ line(x1, y1, x2, y2) {
270
+ this._parts.push(`${n(x1)} ${n(y1)} m ${n(x2)} ${n(y2)} l S`);
271
+ return this;
272
+ }
273
+ beginText() { this._parts.push('BT'); return this; }
274
+ endText() { this._parts.push('ET'); return this; }
275
+ font(name, size) {
276
+ this._parts.push(`/${name} ${n(size)} Tf`);
277
+ return this;
278
+ }
279
+ textPos(x, y) {
280
+ this._parts.push(`${n(x)} ${n(y)} Td`);
281
+ return this;
282
+ }
283
+ showText(s) {
284
+ this._parts.push(`(${pdfStr(s)}) Tj`);
285
+ return this;
286
+ }
287
+ drawImage(name, x, y, w, h) {
288
+ this.gsave();
289
+ this._parts.push(`${n(w)} 0 0 ${n(h)} ${n(x)} ${n(y)} cm`);
290
+ this._parts.push(`/${name} Do`);
291
+ this.grestore();
292
+ return this;
293
+ }
294
+ clipRect(x, y, w, h) {
295
+ this._parts.push(`${n(x)} ${n(y)} ${n(w)} ${n(h)} re W n`);
296
+ return this;
297
+ }
298
+ toBytes() {
299
+ return _enc.encode(this._parts.join('\n') + '\n');
300
+ }
301
+ }
302
+ function borderWidth(style) {
303
+ if (!style)
304
+ return 0;
305
+ const map = {
306
+ thin: 0.5, medium: 1, thick: 1.5, dashed: 0.5, dotted: 0.5,
307
+ double: 1.5, hair: 0.25, mediumDashed: 1, dashDot: 0.5,
308
+ mediumDashDot: 1, dashDotDot: 0.5, mediumDashDotDot: 1, slantDashDot: 1,
309
+ };
310
+ return map[style] ?? 0.5;
311
+ }
312
+ function borderDash(style) {
313
+ if (!style)
314
+ return null;
315
+ const map = {
316
+ dashed: [3, 2], dotted: [1, 1], mediumDashed: [4, 2],
317
+ dashDot: [3, 1, 1, 1], mediumDashDot: [4, 1, 1, 1],
318
+ dashDotDot: [3, 1, 1, 1, 1, 1], mediumDashDotDot: [4, 1, 1, 1, 1, 1],
319
+ slantDashDot: [4, 1, 2, 1],
320
+ };
321
+ return map[style] ?? null;
322
+ }
323
+ function parseJpeg(data) {
324
+ if (data[0] !== 0xFF || data[1] !== 0xD8)
325
+ return null;
326
+ let pos = 2;
327
+ while (pos < data.length - 1) {
328
+ if (data[pos] !== 0xFF)
329
+ break;
330
+ const marker = data[pos + 1];
331
+ if (marker === 0xD9)
332
+ break;
333
+ if (marker === 0xDA)
334
+ break;
335
+ const len = (data[pos + 2] << 8) | data[pos + 3];
336
+ if (marker === 0xC0 || marker === 0xC1 || marker === 0xC2) {
337
+ const height = (data[pos + 5] << 8) | data[pos + 6];
338
+ const width = (data[pos + 7] << 8) | data[pos + 8];
339
+ return { width, height };
340
+ }
341
+ pos += 2 + len;
342
+ }
343
+ return null;
344
+ }
345
+ export function worksheetToPdf(ws, options = {}) {
346
+ const range = ws.getUsedRange();
347
+ if (!range)
348
+ return emptyPdf(options);
349
+ let { startRow, startCol, endRow, endCol } = range;
350
+ if (options.printAreaOnly && ws.printArea) {
351
+ const m = ws.printArea.match(/^'?[^']*'?!?\$?([A-Z]+)\$?(\d+):\$?([A-Z]+)\$?(\d+)$/);
352
+ if (m) {
353
+ startCol = colLetterToIndex(m[1]);
354
+ startRow = parseInt(m[2], 10);
355
+ endCol = colLetterToIndex(m[3]);
356
+ endRow = parseInt(m[4], 10);
357
+ }
358
+ }
359
+ const wsSetup = ws.pageSetup;
360
+ const wsMargins = ws.pageMargins;
361
+ const paperKey = options.paperSize ?? (wsSetup?.paperSize ? paperSizeToKey(wsSetup.paperSize) : 'a4');
362
+ const orientation = options.orientation ?? wsSetup?.orientation ?? 'portrait';
363
+ const [pw, ph] = PAPER_SIZES[paperKey] ?? PAPER_SIZES.a4;
364
+ const pageW = orientation === 'landscape' ? ph : pw;
365
+ const pageH = orientation === 'landscape' ? pw : ph;
366
+ const mar = {
367
+ top: (options.margins?.top ?? wsMargins?.top ?? 0.75) * 72,
368
+ bottom: (options.margins?.bottom ?? wsMargins?.bottom ?? 0.75) * 72,
369
+ left: (options.margins?.left ?? wsMargins?.left ?? 0.7) * 72,
370
+ right: (options.margins?.right ?? wsMargins?.right ?? 0.7) * 72,
371
+ };
372
+ const contentW = pageW - mar.left - mar.right;
373
+ const contentH = pageH - mar.top - mar.bottom;
374
+ const defaultFontSize = options.defaultFontSize ?? 10;
375
+ const drawGridLines = options.gridLines !== false;
376
+ const drawHeadings = options.headings === true;
377
+ const repeatRows = options.repeatRows ?? 0;
378
+ const headerText = options.headerText ?? ws.headerFooter?.oddHeader;
379
+ const footerText = options.footerText ?? ws.headerFooter?.oddFooter;
380
+ const headingWidth = drawHeadings ? 30 : 0;
381
+ const cols = [];
382
+ const colWidthsPt = [];
383
+ for (let c = startCol; c <= endCol; c++) {
384
+ const def = ws.getColumn(c);
385
+ if (options.skipHidden && def?.hidden)
386
+ continue;
387
+ cols.push(c);
388
+ const w = def?.width ? def.width * 6 : 48;
389
+ colWidthsPt.push(w);
390
+ }
391
+ const totalGridW = colWidthsPt.reduce((a, b) => a + b, 0) + headingWidth;
392
+ let scale = options.scale ?? 1;
393
+ if (options.fitToWidth !== false && totalGridW * scale > contentW) {
394
+ scale = contentW / totalGridW;
395
+ }
396
+ scale = Math.max(0.1, Math.min(2, scale));
397
+ const rows = [];
398
+ const rowHeightsPt = [];
399
+ for (let r = startRow; r <= endRow; r++) {
400
+ const def = ws.getRow(r);
401
+ if (options.skipHidden && def?.hidden)
402
+ continue;
403
+ rows.push(r);
404
+ const h = def?.height ?? (defaultFontSize + 6);
405
+ rowHeightsPt.push(h);
406
+ }
407
+ const merges = ws.getMerges();
408
+ const mergeMap = new Map();
409
+ for (const m of merges) {
410
+ const rs = m.endRow - m.startRow + 1;
411
+ const cs = m.endCol - m.startCol + 1;
412
+ mergeMap.set(`${m.startRow},${m.startCol}`, { rowSpan: rs, colSpan: cs });
413
+ for (let r = m.startRow; r <= m.endRow; r++) {
414
+ for (let c = m.startCol; c <= m.endCol; c++) {
415
+ if (r !== m.startRow || c !== m.startCol)
416
+ mergeMap.set(`${r},${c}`, 'skip');
417
+ }
418
+ }
419
+ }
420
+ const sparklineMap = new Map();
421
+ const sparklines = ws.getSparklines?.() ?? [];
422
+ for (const sp of sparklines) {
423
+ const lm = sp.location.match(/^([A-Z]+)(\d+)$/);
424
+ if (lm)
425
+ sparklineMap.set(`${parseInt(lm[2], 10)},${colLetterToIndex(lm[1])}`, sp);
426
+ }
427
+ const cellImageMap = new Map();
428
+ const cellImages = ws.getCellImages?.() ?? [];
429
+ for (const ci of cellImages)
430
+ cellImageMap.set(ci.cell, ci);
431
+ if (options.evaluateFormulas) {
432
+ new FormulaEngine().calculateSheet(ws);
433
+ }
434
+ const rowBreakSet = new Set();
435
+ for (const brk of ws.getRowBreaks())
436
+ rowBreakSet.add(brk.id);
437
+ const pages = [];
438
+ let pageRowStart = 0;
439
+ let usedH = 0;
440
+ const repeatH = rows.slice(0, repeatRows).reduce((s, _, i) => s + rowHeightsPt[i] * scale, 0);
441
+ for (let ri = 0; ri < rows.length; ri++) {
442
+ const rh = rowHeightsPt[ri] * scale;
443
+ const effectiveContentH = contentH - (pages.length > 0 ? repeatH : 0);
444
+ const explicitBreak = ri > pageRowStart && rowBreakSet.has(rows[ri]);
445
+ if (usedH + rh > effectiveContentH && ri > pageRowStart || explicitBreak) {
446
+ pages.push({ rowStart: pageRowStart, rowEnd: ri - 1 });
447
+ pageRowStart = ri;
448
+ usedH = repeatH + rh;
449
+ }
450
+ else {
451
+ usedH += rh;
452
+ }
453
+ }
454
+ if (pageRowStart < rows.length) {
455
+ pages.push({ rowStart: pageRowStart, rowEnd: rows.length - 1 });
456
+ }
457
+ const doc = new PdfDoc();
458
+ const catalogId = doc.alloc();
459
+ const pagesId = doc.alloc();
460
+ const fontReg = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>');
461
+ const fontBold = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>');
462
+ const fontItalic = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Oblique/Encoding/WinAnsiEncoding>>');
463
+ const fontBI = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-BoldOblique/Encoding/WinAnsiEncoding>>');
464
+ const images = ws.getImages?.() ?? [];
465
+ const pdfImages = [];
466
+ let imgCounter = 0;
467
+ for (const img of images) {
468
+ const data = typeof img.data === 'string' ? base64Decode(img.data) : img.data;
469
+ if (img.format === 'jpeg' || img.format === 'png') {
470
+ if (img.format === 'jpeg') {
471
+ const info = parseJpeg(data);
472
+ if (!info)
473
+ continue;
474
+ const name = `Im${++imgCounter}`;
475
+ const objId = doc.add(`<</Type/XObject/Subtype/Image/Width ${info.width}/Height ${info.height}` +
476
+ `/ColorSpace/DeviceRGB/BitsPerComponent 8/Filter/DCTDecode/Length ${data.length}>>`, data);
477
+ pdfImages.push({ id: objId, name, width: info.width, height: info.height });
478
+ }
479
+ if (img.format === 'png') {
480
+ const pngImg = parsePngForPdf(data, doc);
481
+ if (pngImg) {
482
+ pngImg.name = `Im${++imgCounter}`;
483
+ pdfImages.push(pngImg);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ const cellImgResources = [];
489
+ function buildImgResources() {
490
+ const all = [...pdfImages.map(im => ({ name: im.name, id: im.id })), ...cellImgResources];
491
+ if (!all.length)
492
+ return '';
493
+ return '/XObject<<' + all.map(im => `/${im.name} ${im.id} 0 R`).join('') + '>>';
494
+ }
495
+ let infoId;
496
+ if (options.title || options.author) {
497
+ const infoParts = ['/Producer(ExcelForge)'];
498
+ if (options.title)
499
+ infoParts.push(`/Title(${pdfStr(options.title)})`);
500
+ if (options.author)
501
+ infoParts.push(`/Author(${pdfStr(options.author)})`);
502
+ infoId = doc.add(`<<${infoParts.join('')}>>`);
503
+ }
504
+ const pageIds = [];
505
+ for (let pi = 0; pi < pages.length; pi++) {
506
+ const page = pages[pi];
507
+ const sb = new StreamBuilder();
508
+ if (scale !== 1) {
509
+ sb.gsave();
510
+ sb.raw(`${n(scale)} 0 0 ${n(scale)} ${n(mar.left * (1 - scale))} ${n(mar.bottom * (1 - scale))} cm`);
511
+ }
512
+ const pageRows = [];
513
+ if (pi > 0 && repeatRows > 0) {
514
+ for (let ri = 0; ri < Math.min(repeatRows, rows.length); ri++) {
515
+ pageRows.push({ sheetRow: rows[ri], height: rowHeightsPt[ri], ri });
516
+ }
517
+ }
518
+ for (let ri = page.rowStart; ri <= page.rowEnd; ri++) {
519
+ pageRows.push({ sheetRow: rows[ri], height: rowHeightsPt[ri], ri });
520
+ }
521
+ let curY = pageH - mar.top;
522
+ for (const pr of pageRows) {
523
+ const rh = pr.height * scale;
524
+ let curX = mar.left;
525
+ if (drawHeadings) {
526
+ sb.gsave();
527
+ sb.fillColor(0.9, 0.9, 0.9);
528
+ sb.fillRect(curX, curY - rh, headingWidth * scale, rh);
529
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.25);
530
+ sb.strokeRect(curX, curY - rh, headingWidth * scale, rh);
531
+ sb.fillColor(0.3, 0.3, 0.3);
532
+ sb.beginText().font('F1', 7 * scale);
533
+ const label = String(pr.sheetRow);
534
+ const lw = textWidthPt(label, 7, false) * scale;
535
+ sb.textPos(curX + (headingWidth * scale - lw) / 2, curY - rh + (rh - 7 * scale) / 2 + 1);
536
+ sb.showText(label).endText();
537
+ sb.grestore();
538
+ curX += headingWidth * scale;
539
+ }
540
+ for (let ci = 0; ci < cols.length; ci++) {
541
+ const sheetCol = cols[ci];
542
+ const cw = colWidthsPt[ci] * scale;
543
+ const key = `${pr.sheetRow},${sheetCol}`;
544
+ const merge = mergeMap.get(key);
545
+ if (merge === 'skip') {
546
+ curX += cw;
547
+ continue;
548
+ }
549
+ let cellW = cw;
550
+ let cellH = rh;
551
+ if (merge && typeof merge !== 'string') {
552
+ cellW = 0;
553
+ for (let mc = 0; mc < merge.colSpan; mc++) {
554
+ const idx = cols.indexOf(sheetCol + mc);
555
+ if (idx >= 0)
556
+ cellW += colWidthsPt[idx] * scale;
557
+ }
558
+ cellH = 0;
559
+ for (let mr = 0; mr < merge.rowSpan; mr++) {
560
+ const idx = rows.indexOf(pr.sheetRow + mr);
561
+ if (idx >= 0)
562
+ cellH += rowHeightsPt[idx] * scale;
563
+ }
564
+ }
565
+ const cell = ws.getCell(pr.sheetRow, sheetCol);
566
+ const style = cell.style;
567
+ if (style?.fill && style.fill.type === 'pattern') {
568
+ const pf = style.fill;
569
+ if (pf.pattern === 'solid' && pf.fgColor) {
570
+ const rgb = colorRgb(pf.fgColor);
571
+ if (rgb) {
572
+ sb.gsave();
573
+ sb.fillColor(rgb[0], rgb[1], rgb[2]);
574
+ sb.fillRect(curX, curY - cellH, cellW, cellH);
575
+ sb.grestore();
576
+ }
577
+ }
578
+ }
579
+ if (drawGridLines && !style?.border) {
580
+ sb.gsave();
581
+ sb.strokeColor(0.82, 0.82, 0.82).lineWidth(0.25);
582
+ sb.strokeRect(curX, curY - cellH, cellW, cellH);
583
+ sb.grestore();
584
+ }
585
+ if (style?.border) {
586
+ sb.gsave();
587
+ drawBorder(sb, style.border, curX, curY - cellH, cellW, cellH);
588
+ sb.grestore();
589
+ }
590
+ const textVal = getCellText(cell, style);
591
+ if (textVal) {
592
+ sb.gsave();
593
+ sb.clipRect(curX + 1, curY - cellH, cellW - 2, cellH);
594
+ const fontSize = (style?.font?.size ?? defaultFontSize) * scale;
595
+ const bold = style?.font?.bold ?? false;
596
+ const italic = style?.font?.italic ?? false;
597
+ const fontName = bold && italic ? 'F4' : bold ? 'F2' : italic ? 'F3' : 'F1';
598
+ const rgb = colorRgb(style?.font?.color) ?? [0, 0, 0];
599
+ sb.fillColor(rgb[0], rgb[1], rgb[2]);
600
+ const tw = textWidthPt(textVal, fontSize / scale, bold) * scale;
601
+ const hAlign = style?.alignment?.horizontal ?? (typeof cell.value === 'number' ? 'right' : 'left');
602
+ const vAlign = style?.alignment?.vertical ?? 'bottom';
603
+ let tx;
604
+ const pad = 2 * scale;
605
+ switch (hAlign) {
606
+ case 'center':
607
+ case 'fill':
608
+ case 'justify':
609
+ case 'distributed':
610
+ tx = curX + (cellW - tw) / 2;
611
+ break;
612
+ case 'right':
613
+ tx = curX + cellW - tw - pad;
614
+ break;
615
+ default:
616
+ tx = curX + pad + (style?.alignment?.indent ?? 0) * 6 * scale;
617
+ break;
618
+ }
619
+ let ty;
620
+ switch (vAlign) {
621
+ case 'top':
622
+ ty = curY - fontSize - pad;
623
+ break;
624
+ case 'center':
625
+ case 'distributed':
626
+ ty = curY - cellH / 2 - fontSize * 0.35;
627
+ break;
628
+ default:
629
+ ty = curY - cellH + pad;
630
+ break;
631
+ }
632
+ sb.beginText().font(fontName, fontSize).textPos(tx, ty).showText(textVal).endText();
633
+ if (style?.font?.underline && style.font.underline !== 'none') {
634
+ sb.strokeColor(rgb[0], rgb[1], rgb[2]);
635
+ sb.lineWidth(fontSize * 0.05);
636
+ sb.line(tx, ty - fontSize * 0.15, tx + tw, ty - fontSize * 0.15);
637
+ }
638
+ if (style?.font?.strike) {
639
+ sb.strokeColor(rgb[0], rgb[1], rgb[2]);
640
+ sb.lineWidth(fontSize * 0.05);
641
+ const sy = ty + fontSize * 0.3;
642
+ sb.line(tx, sy, tx + tw, sy);
643
+ }
644
+ sb.grestore();
645
+ }
646
+ const spKey = `${pr.sheetRow},${sheetCol}`;
647
+ const sp = sparklineMap.get(spKey);
648
+ if (sp) {
649
+ const spValues = resolveSparklineValues(ws, sp.dataRange);
650
+ if (spValues.length)
651
+ drawSparkline(sb, sp, spValues, curX, curY - cellH, cellW, cellH);
652
+ }
653
+ const cellRef = `${colIndexToLetter(sheetCol)}${pr.sheetRow}`;
654
+ const ciImg = cellImageMap.get(cellRef);
655
+ if (ciImg) {
656
+ const ciData = typeof ciImg.data === 'string' ? base64Decode(ciImg.data) : ciImg.data;
657
+ if (ciImg.format === 'jpeg') {
658
+ const info = parseJpeg(ciData);
659
+ if (info) {
660
+ const ciName = `Ci${pr.sheetRow}_${sheetCol}`;
661
+ const ciObjId = doc.add(`<</Type/XObject/Subtype/Image/Width ${info.width}/Height ${info.height}` +
662
+ `/ColorSpace/DeviceRGB/BitsPerComponent 8/Filter/DCTDecode/Length ${ciData.length}>>`, ciData);
663
+ cellImgResources.push({ name: ciName, id: ciObjId });
664
+ const aspect = info.width / info.height;
665
+ let iw = cellW - 2, ih = cellH - 2;
666
+ if (iw / ih > aspect)
667
+ iw = ih * aspect;
668
+ else
669
+ ih = iw / aspect;
670
+ sb.drawImage(ciName, curX + 1, curY - cellH + 1, iw, ih);
671
+ }
672
+ }
673
+ else if (ciImg.format === 'png') {
674
+ const pngImg = parsePngForPdf(ciData, doc);
675
+ if (pngImg) {
676
+ const ciName = `Ci${pr.sheetRow}_${sheetCol}`;
677
+ pngImg.name = ciName;
678
+ cellImgResources.push({ name: ciName, id: pngImg.id });
679
+ const aspect = pngImg.width / pngImg.height;
680
+ let iw = cellW - 2, ih = cellH - 2;
681
+ if (iw / ih > aspect)
682
+ iw = ih * aspect;
683
+ else
684
+ ih = iw / aspect;
685
+ sb.drawImage(ciName, curX + 1, curY - cellH + 1, iw, ih);
686
+ }
687
+ }
688
+ }
689
+ curX += cw;
690
+ }
691
+ curY -= rh;
692
+ }
693
+ if (drawHeadings) {
694
+ const headY = pageH - mar.top;
695
+ let headX = mar.left + headingWidth * scale;
696
+ sb.gsave();
697
+ for (let ci = 0; ci < cols.length; ci++) {
698
+ const cw = colWidthsPt[ci] * scale;
699
+ sb.fillColor(0.9, 0.9, 0.9);
700
+ sb.fillRect(headX, headY, cw, 14 * scale);
701
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.25);
702
+ sb.strokeRect(headX, headY, cw, 14 * scale);
703
+ sb.fillColor(0.3, 0.3, 0.3);
704
+ const label = colIndexToLetter(cols[ci]);
705
+ const lw = textWidthPt(label, 7, false) * scale;
706
+ sb.beginText().font('F1', 7 * scale);
707
+ sb.textPos(headX + (cw - lw) / 2, headY + 3 * scale);
708
+ sb.showText(label).endText();
709
+ headX += cw;
710
+ }
711
+ sb.grestore();
712
+ }
713
+ for (let ii = 0; ii < pdfImages.length; ii++) {
714
+ const img = images[ii];
715
+ const pImg = pdfImages[ii];
716
+ if (!img || !pImg)
717
+ continue;
718
+ const pos = resolveImagePos(img, cols, rows, colWidthsPt, rowHeightsPt, scale, mar, pageH, startCol, startRow);
719
+ if (pos) {
720
+ sb.drawImage(pImg.name, pos.x, pos.y, pos.w, pos.h);
721
+ }
722
+ }
723
+ const fcs = ws.getFormControls?.() ?? [];
724
+ for (const fc of fcs) {
725
+ drawFormControl(sb, fc, cols, rows, colWidthsPt, rowHeightsPt, scale, mar, pageH);
726
+ }
727
+ if (headerText) {
728
+ const text = replaceHFTokens(headerText, pi + 1, pages.length);
729
+ sb.gsave();
730
+ sb.fillColor(0.3, 0.3, 0.3);
731
+ sb.beginText().font('F1', 8);
732
+ const tw = textWidthPt(text, 8, false);
733
+ sb.textPos(pageW / 2 - tw / 2, pageH - mar.top / 2);
734
+ sb.showText(text).endText();
735
+ sb.grestore();
736
+ }
737
+ if (footerText) {
738
+ const text = replaceHFTokens(footerText, pi + 1, pages.length);
739
+ sb.gsave();
740
+ sb.fillColor(0.3, 0.3, 0.3);
741
+ sb.beginText().font('F1', 8);
742
+ const tw = textWidthPt(text, 8, false);
743
+ sb.textPos(pageW / 2 - tw / 2, mar.bottom / 2);
744
+ sb.showText(text).endText();
745
+ sb.grestore();
746
+ }
747
+ if (scale !== 1)
748
+ sb.grestore();
749
+ const streamData = sb.toBytes();
750
+ const contentId = doc.addDeflated('<<', streamData);
751
+ const pageId = doc.add(`<</Type/Page/Parent ${pagesId} 0 R/MediaBox[0 0 ${n(pageW)} ${n(pageH)}]` +
752
+ `/Contents ${contentId} 0 R` +
753
+ `/Resources<</Font<</F1 ${fontReg} 0 R/F2 ${fontBold} 0 R/F3 ${fontItalic} 0 R/F4 ${fontBI} 0 R>>` +
754
+ buildImgResources() +
755
+ `>>>>`);
756
+ pageIds.push(pageId);
757
+ }
758
+ doc.set(pagesId, `<</Type/Pages/Kids[${pageIds.map(id => `${id} 0 R`).join(' ')}]/Count ${pageIds.length}>>`);
759
+ doc.set(catalogId, `<</Type/Catalog/Pages ${pagesId} 0 R>>`);
760
+ return doc.build(catalogId, infoId);
761
+ }
762
+ export function workbookToPdf(wb, options = {}) {
763
+ const sheets = wb.getSheets();
764
+ const names = wb.getSheetNames();
765
+ const selected = options.sheets ?? names;
766
+ if (options.evaluateFormulas) {
767
+ new FormulaEngine().calculateWorkbook(wb);
768
+ }
769
+ const filtered = sheets.filter((_, i) => selected.includes(names[i]));
770
+ if (filtered.length === 0)
771
+ return emptyPdf(options);
772
+ if (filtered.length === 1 && !filtered[0]._isChartSheet && !filtered[0]._isDialogSheet)
773
+ return worksheetToPdf(filtered[0], options);
774
+ const doc = new PdfDoc();
775
+ const catalogId = doc.alloc();
776
+ const pagesId = doc.alloc();
777
+ const fontReg = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>');
778
+ const fontBold = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>');
779
+ const fontItalic = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Oblique/Encoding/WinAnsiEncoding>>');
780
+ const fontBI = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica-BoldOblique/Encoding/WinAnsiEncoding>>');
781
+ let infoId;
782
+ if (options.title || options.author) {
783
+ const infoParts = ['/Producer(ExcelForge)'];
784
+ if (options.title)
785
+ infoParts.push(`/Title(${pdfStr(options.title)})`);
786
+ if (options.author)
787
+ infoParts.push(`/Author(${pdfStr(options.author)})`);
788
+ infoId = doc.add(`<<${infoParts.join('')}>>`);
789
+ }
790
+ const allPageIds = [];
791
+ const paperKey = options.paperSize ?? 'a4';
792
+ const orientation = options.orientation ?? 'portrait';
793
+ const [pw, ph] = PAPER_SIZES[paperKey] ?? PAPER_SIZES.a4;
794
+ const pageW = orientation === 'landscape' ? ph : pw;
795
+ const pageH = orientation === 'landscape' ? pw : ph;
796
+ for (const ws of filtered) {
797
+ if (ws._isChartSheet) {
798
+ const charts = ws.getCharts();
799
+ if (charts.length) {
800
+ const sb = new StreamBuilder();
801
+ drawChartOnPage(sb, charts[0], ws, 40, 40, pageW - 80, pageH - 80);
802
+ const streamData = sb.toBytes();
803
+ const contentId = doc.addDeflated('<<', streamData);
804
+ const pageId = doc.add(`<</Type/Page/Parent ${pagesId} 0 R/MediaBox[0 0 ${n(pageW)} ${n(pageH)}]` +
805
+ `/Contents ${contentId} 0 R` +
806
+ `/Resources<</Font<</F1 ${fontReg} 0 R/F2 ${fontBold} 0 R>>>>>>`);
807
+ allPageIds.push(pageId);
808
+ }
809
+ }
810
+ else if (ws._isDialogSheet) {
811
+ const fcs = ws.getFormControls?.() ?? [];
812
+ if (fcs.length) {
813
+ const sb = new StreamBuilder();
814
+ const mar = { left: 50, top: 50 };
815
+ const dummyCols = [1], dummyRows = [1], dummyColW = [48], dummyRowH = [16];
816
+ for (const fc of fcs) {
817
+ drawFormControl(sb, fc, dummyCols, dummyRows, dummyColW, dummyRowH, 1, mar, pageH);
818
+ }
819
+ const streamData = sb.toBytes();
820
+ const contentId = doc.addDeflated('<<', streamData);
821
+ const pageId = doc.add(`<</Type/Page/Parent ${pagesId} 0 R/MediaBox[0 0 ${n(pageW)} ${n(pageH)}]` +
822
+ `/Contents ${contentId} 0 R` +
823
+ `/Resources<</Font<</F1 ${fontReg} 0 R/F2 ${fontBold} 0 R>>>>>>`);
824
+ allPageIds.push(pageId);
825
+ }
826
+ }
827
+ else {
828
+ const sheetPages = renderSheetPages(ws, options, doc, fontReg, fontBold, fontItalic, fontBI, pagesId);
829
+ allPageIds.push(...sheetPages);
830
+ }
831
+ }
832
+ doc.set(pagesId, `<</Type/Pages/Kids[${allPageIds.map(id => `${id} 0 R`).join(' ')}]/Count ${allPageIds.length}>>`);
833
+ doc.set(catalogId, `<</Type/Catalog/Pages ${pagesId} 0 R>>`);
834
+ return doc.build(catalogId, infoId);
835
+ }
836
+ function renderSheetPages(ws, options, doc, fontReg, fontBold, fontItalic, fontBI, pagesId) {
837
+ const range = ws.getUsedRange();
838
+ if (!range)
839
+ return [];
840
+ let { startRow, startCol, endRow, endCol } = range;
841
+ if (options.printAreaOnly && ws.printArea) {
842
+ const m = ws.printArea.match(/^'?[^']*'?!?\$?([A-Z]+)\$?(\d+):\$?([A-Z]+)\$?(\d+)$/);
843
+ if (m) {
844
+ startCol = colLetterToIndex(m[1]);
845
+ startRow = parseInt(m[2], 10);
846
+ endCol = colLetterToIndex(m[3]);
847
+ endRow = parseInt(m[4], 10);
848
+ }
849
+ }
850
+ const wsSetup = ws.pageSetup;
851
+ const wsMargins = ws.pageMargins;
852
+ const paperKey = options.paperSize ?? (wsSetup?.paperSize ? paperSizeToKey(wsSetup.paperSize) : 'a4');
853
+ const orientation = options.orientation ?? wsSetup?.orientation ?? 'portrait';
854
+ const [pw, ph] = PAPER_SIZES[paperKey] ?? PAPER_SIZES.a4;
855
+ const pageW = orientation === 'landscape' ? ph : pw;
856
+ const pageH = orientation === 'landscape' ? pw : ph;
857
+ const mar = {
858
+ top: (options.margins?.top ?? wsMargins?.top ?? 0.75) * 72,
859
+ bottom: (options.margins?.bottom ?? wsMargins?.bottom ?? 0.75) * 72,
860
+ left: (options.margins?.left ?? wsMargins?.left ?? 0.7) * 72,
861
+ right: (options.margins?.right ?? wsMargins?.right ?? 0.7) * 72,
862
+ };
863
+ const contentW = pageW - mar.left - mar.right;
864
+ const contentH = pageH - mar.top - mar.bottom;
865
+ const defaultFontSize = options.defaultFontSize ?? 10;
866
+ const drawGridLines = options.gridLines !== false;
867
+ const repeatRows = options.repeatRows ?? 0;
868
+ const headerText = options.headerText ?? ws.headerFooter?.oddHeader;
869
+ const footerText = options.footerText ?? ws.headerFooter?.oddFooter;
870
+ const cols = [];
871
+ const colWidthsPt = [];
872
+ for (let c = startCol; c <= endCol; c++) {
873
+ const def = ws.getColumn(c);
874
+ if (options.skipHidden && def?.hidden)
875
+ continue;
876
+ cols.push(c);
877
+ colWidthsPt.push(def?.width ? def.width * 6 : 48);
878
+ }
879
+ const totalGridW = colWidthsPt.reduce((a, b) => a + b, 0);
880
+ let scale = options.scale ?? 1;
881
+ if (options.fitToWidth !== false && totalGridW * scale > contentW) {
882
+ scale = contentW / totalGridW;
883
+ }
884
+ scale = Math.max(0.1, Math.min(2, scale));
885
+ const rows = [];
886
+ const rowHeightsPt = [];
887
+ for (let r = startRow; r <= endRow; r++) {
888
+ const def = ws.getRow(r);
889
+ if (options.skipHidden && def?.hidden)
890
+ continue;
891
+ rows.push(r);
892
+ rowHeightsPt.push(def?.height ?? (defaultFontSize + 6));
893
+ }
894
+ const merges = ws.getMerges();
895
+ const mergeMap = new Map();
896
+ for (const m of merges) {
897
+ mergeMap.set(`${m.startRow},${m.startCol}`, { rowSpan: m.endRow - m.startRow + 1, colSpan: m.endCol - m.startCol + 1 });
898
+ for (let r = m.startRow; r <= m.endRow; r++)
899
+ for (let c = m.startCol; c <= m.endCol; c++)
900
+ if (r !== m.startRow || c !== m.startCol)
901
+ mergeMap.set(`${r},${c}`, 'skip');
902
+ }
903
+ const sparklineMap = new Map();
904
+ for (const sp of (ws.getSparklines?.() ?? [])) {
905
+ const lm = sp.location.match(/^([A-Z]+)(\d+)$/);
906
+ if (lm)
907
+ sparklineMap.set(`${parseInt(lm[2], 10)},${colLetterToIndex(lm[1])}`, sp);
908
+ }
909
+ const cellImageMap = new Map();
910
+ for (const ci of (ws.getCellImages?.() ?? []))
911
+ cellImageMap.set(ci.cell, ci);
912
+ const rowBreakSet = new Set();
913
+ for (const brk of ws.getRowBreaks())
914
+ rowBreakSet.add(brk.id);
915
+ const pages = [];
916
+ let pageRowStart = 0;
917
+ let usedH = 0;
918
+ const repeatH = rows.slice(0, repeatRows).reduce((s, _, i) => s + rowHeightsPt[i] * scale, 0);
919
+ for (let ri = 0; ri < rows.length; ri++) {
920
+ const rh = rowHeightsPt[ri] * scale;
921
+ const effectiveContentH = contentH - (pages.length > 0 ? repeatH : 0);
922
+ const explicitBreak = ri > pageRowStart && rowBreakSet.has(rows[ri]);
923
+ if ((usedH + rh > effectiveContentH && ri > pageRowStart) || explicitBreak) {
924
+ pages.push({ rowStart: pageRowStart, rowEnd: ri - 1 });
925
+ pageRowStart = ri;
926
+ usedH = repeatH + rh;
927
+ }
928
+ else {
929
+ usedH += rh;
930
+ }
931
+ }
932
+ if (pageRowStart < rows.length)
933
+ pages.push({ rowStart: pageRowStart, rowEnd: rows.length - 1 });
934
+ const pageIds = [];
935
+ for (let pi = 0; pi < pages.length; pi++) {
936
+ const page = pages[pi];
937
+ const sb = new StreamBuilder();
938
+ const pageRows = [];
939
+ if (pi > 0 && repeatRows > 0) {
940
+ for (let ri = 0; ri < Math.min(repeatRows, rows.length); ri++)
941
+ pageRows.push({ sheetRow: rows[ri], height: rowHeightsPt[ri] });
942
+ }
943
+ for (let ri = page.rowStart; ri <= page.rowEnd; ri++)
944
+ pageRows.push({ sheetRow: rows[ri], height: rowHeightsPt[ri] });
945
+ let curY = pageH - mar.top;
946
+ for (const pr of pageRows) {
947
+ const rh = pr.height * scale;
948
+ let curX = mar.left;
949
+ for (let ci = 0; ci < cols.length; ci++) {
950
+ const sheetCol = cols[ci];
951
+ const cw = colWidthsPt[ci] * scale;
952
+ const key = `${pr.sheetRow},${sheetCol}`;
953
+ const merge = mergeMap.get(key);
954
+ if (merge === 'skip') {
955
+ curX += cw;
956
+ continue;
957
+ }
958
+ let cellW = cw, cellH = rh;
959
+ if (merge && typeof merge !== 'string') {
960
+ cellW = 0;
961
+ for (let mc = 0; mc < merge.colSpan; mc++) {
962
+ const idx = cols.indexOf(sheetCol + mc);
963
+ if (idx >= 0)
964
+ cellW += colWidthsPt[idx] * scale;
965
+ }
966
+ cellH = 0;
967
+ for (let mr = 0; mr < merge.rowSpan; mr++) {
968
+ const idx = rows.indexOf(pr.sheetRow + mr);
969
+ if (idx >= 0)
970
+ cellH += rowHeightsPt[idx] * scale;
971
+ }
972
+ }
973
+ const cell = ws.getCell(pr.sheetRow, sheetCol);
974
+ const style = cell.style;
975
+ if (style?.fill && style.fill.type === 'pattern') {
976
+ const pf = style.fill;
977
+ if (pf.pattern === 'solid' && pf.fgColor) {
978
+ const rgb = colorRgb(pf.fgColor);
979
+ if (rgb) {
980
+ sb.gsave();
981
+ sb.fillColor(rgb[0], rgb[1], rgb[2]);
982
+ sb.fillRect(curX, curY - cellH, cellW, cellH);
983
+ sb.grestore();
984
+ }
985
+ }
986
+ }
987
+ if (drawGridLines && !style?.border) {
988
+ sb.gsave();
989
+ sb.strokeColor(0.82, 0.82, 0.82).lineWidth(0.25);
990
+ sb.strokeRect(curX, curY - cellH, cellW, cellH);
991
+ sb.grestore();
992
+ }
993
+ if (style?.border) {
994
+ sb.gsave();
995
+ drawBorder(sb, style.border, curX, curY - cellH, cellW, cellH);
996
+ sb.grestore();
997
+ }
998
+ const textVal = getCellText(cell, style);
999
+ if (textVal) {
1000
+ sb.gsave();
1001
+ sb.clipRect(curX + 1, curY - cellH, cellW - 2, cellH);
1002
+ const fontSize = (style?.font?.size ?? defaultFontSize) * scale;
1003
+ const bold = style?.font?.bold ?? false;
1004
+ const italic = style?.font?.italic ?? false;
1005
+ const fontName = bold && italic ? 'F4' : bold ? 'F2' : italic ? 'F3' : 'F1';
1006
+ const rgb = colorRgb(style?.font?.color) ?? [0, 0, 0];
1007
+ sb.fillColor(rgb[0], rgb[1], rgb[2]);
1008
+ const tw = textWidthPt(textVal, fontSize / scale, bold) * scale;
1009
+ const hAlign = style?.alignment?.horizontal ?? (typeof cell.value === 'number' ? 'right' : 'left');
1010
+ const vAlign = style?.alignment?.vertical ?? 'bottom';
1011
+ const pad = 2 * scale;
1012
+ let tx;
1013
+ switch (hAlign) {
1014
+ case 'center':
1015
+ case 'fill':
1016
+ case 'justify':
1017
+ case 'distributed':
1018
+ tx = curX + (cellW - tw) / 2;
1019
+ break;
1020
+ case 'right':
1021
+ tx = curX + cellW - tw - pad;
1022
+ break;
1023
+ default:
1024
+ tx = curX + pad + (style?.alignment?.indent ?? 0) * 6 * scale;
1025
+ break;
1026
+ }
1027
+ let ty;
1028
+ switch (vAlign) {
1029
+ case 'top':
1030
+ ty = curY - fontSize - pad;
1031
+ break;
1032
+ case 'center':
1033
+ case 'distributed':
1034
+ ty = curY - cellH / 2 - fontSize * 0.35;
1035
+ break;
1036
+ default:
1037
+ ty = curY - cellH + pad;
1038
+ break;
1039
+ }
1040
+ sb.beginText().font(fontName, fontSize).textPos(tx, ty).showText(textVal).endText();
1041
+ sb.grestore();
1042
+ }
1043
+ const spKey = `${pr.sheetRow},${sheetCol}`;
1044
+ const sp = sparklineMap.get(spKey);
1045
+ if (sp) {
1046
+ const spVals = resolveSparklineValues(ws, sp.dataRange);
1047
+ if (spVals.length)
1048
+ drawSparkline(sb, sp, spVals, curX, curY - cellH, cellW, cellH);
1049
+ }
1050
+ curX += cw;
1051
+ }
1052
+ curY -= rh;
1053
+ }
1054
+ const fcs = ws.getFormControls?.() ?? [];
1055
+ for (const fc of fcs) {
1056
+ drawFormControl(sb, fc, cols, rows, colWidthsPt, rowHeightsPt, scale, mar, pageH);
1057
+ }
1058
+ if (headerText) {
1059
+ const text = replaceHFTokens(headerText, pi + 1, pages.length);
1060
+ sb.gsave().fillColor(0.3, 0.3, 0.3);
1061
+ const tw = textWidthPt(text, 8, false);
1062
+ sb.beginText().font('F1', 8).textPos(pageW / 2 - tw / 2, pageH - mar.top / 2).showText(text).endText();
1063
+ sb.grestore();
1064
+ }
1065
+ if (footerText) {
1066
+ const text = replaceHFTokens(footerText, pi + 1, pages.length);
1067
+ sb.gsave().fillColor(0.3, 0.3, 0.3);
1068
+ const tw = textWidthPt(text, 8, false);
1069
+ sb.beginText().font('F1', 8).textPos(pageW / 2 - tw / 2, mar.bottom / 2).showText(text).endText();
1070
+ sb.grestore();
1071
+ }
1072
+ const streamData = sb.toBytes();
1073
+ const contentId = doc.addDeflated('<<', streamData);
1074
+ const pageId = doc.add(`<</Type/Page/Parent ${pagesId} 0 R/MediaBox[0 0 ${n(pageW)} ${n(pageH)}]` +
1075
+ `/Contents ${contentId} 0 R` +
1076
+ `/Resources<</Font<</F1 ${fontReg} 0 R/F2 ${fontBold} 0 R/F3 ${fontItalic} 0 R/F4 ${fontBI} 0 R>>>>>>`);
1077
+ pageIds.push(pageId);
1078
+ }
1079
+ return pageIds;
1080
+ }
1081
+ function getCellText(cell, style) {
1082
+ if (cell.richText)
1083
+ return cell.richText.map(r => r.text).join('');
1084
+ if (cell.value == null)
1085
+ return '';
1086
+ if (style?.numberFormat)
1087
+ return formatNumber(cell.value, style.numberFormat.formatCode);
1088
+ return String(cell.value);
1089
+ }
1090
+ function drawBorder(sb, border, x, y, w, h) {
1091
+ const sides = [
1092
+ [border.bottom, x, y, x + w, y],
1093
+ [border.top, x, y + h, x + w, y + h],
1094
+ [border.left, x, y, x, y + h],
1095
+ [border.right, x + w, y, x + w, y + h],
1096
+ ];
1097
+ for (const [side, x1, y1, x2, y2] of sides) {
1098
+ if (!side?.style)
1099
+ continue;
1100
+ const bw = borderWidth(side.style);
1101
+ const rgb = colorRgb(side.color) ?? [0, 0, 0];
1102
+ sb.strokeColor(rgb[0], rgb[1], rgb[2]);
1103
+ sb.lineWidth(bw);
1104
+ const dp = borderDash(side.style);
1105
+ if (dp)
1106
+ sb.dash(dp, 0);
1107
+ else
1108
+ sb.noDash();
1109
+ sb.line(x1, y1, x2, y2);
1110
+ }
1111
+ }
1112
+ function paperSizeToKey(ps) {
1113
+ const map = {
1114
+ 1: 'letter', 5: 'legal', 9: 'a4', 8: 'a3', 3: 'tabloid',
1115
+ };
1116
+ return map[ps] ?? 'a4';
1117
+ }
1118
+ function replaceHFTokens(text, pageNum, totalPages) {
1119
+ let clean = text
1120
+ .replace(/&[LCR]/g, '')
1121
+ .replace(/&"[^"]*"/g, '')
1122
+ .replace(/&\d+/g, '');
1123
+ clean = clean
1124
+ .replace(/&P/gi, String(pageNum))
1125
+ .replace(/&N/gi, String(totalPages))
1126
+ .replace(/&D/gi, new Date().toLocaleDateString())
1127
+ .replace(/&T/gi, new Date().toLocaleTimeString())
1128
+ .replace(/&F/gi, '')
1129
+ .replace(/&A/gi, '');
1130
+ return clean.trim();
1131
+ }
1132
+ function resolveImagePos(img, cols, rows, colWidthsPt, rowHeightsPt, scale, mar, pageH, startCol, startRow) {
1133
+ if (img.position) {
1134
+ const x = mar.left + img.position.x * 0.75 * scale;
1135
+ const y = pageH - mar.top - img.position.y * 0.75 * scale;
1136
+ const w = (img.width ?? 100) * 0.75 * scale;
1137
+ const h = (img.height ?? 100) * 0.75 * scale;
1138
+ return { x, y: y - h, w, h };
1139
+ }
1140
+ if (img.from) {
1141
+ let x = mar.left;
1142
+ for (let ci = 0; ci < cols.length; ci++) {
1143
+ if (cols[ci] >= img.from.col)
1144
+ break;
1145
+ x += colWidthsPt[ci] * scale;
1146
+ }
1147
+ let y = 0;
1148
+ for (let ri = 0; ri < rows.length; ri++) {
1149
+ if (rows[ri] >= img.from.row)
1150
+ break;
1151
+ y += rowHeightsPt[ri] * scale;
1152
+ }
1153
+ const w = (img.width ?? 100) * 0.75 * scale;
1154
+ const h = (img.height ?? 100) * 0.75 * scale;
1155
+ const pdfY = pageH - mar.top - y;
1156
+ return { x, y: pdfY - h, w, h };
1157
+ }
1158
+ return null;
1159
+ }
1160
+ function base64Decode(b64) {
1161
+ return base64ToBytes(b64);
1162
+ }
1163
+ function parsePngForPdf(data, doc) {
1164
+ if (data[0] !== 0x89 || data[1] !== 0x50 || data[2] !== 0x4E || data[3] !== 0x47)
1165
+ return null;
1166
+ let pos = 8;
1167
+ const readU32 = (p) => (data[p] << 24 | data[p + 1] << 16 | data[p + 2] << 8 | data[p + 3]) >>> 0;
1168
+ const ihdrLen = readU32(pos);
1169
+ pos += 4;
1170
+ const ihdrType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
1171
+ pos += 4;
1172
+ if (ihdrType !== 'IHDR' || ihdrLen !== 13)
1173
+ return null;
1174
+ const width = readU32(pos);
1175
+ pos += 4;
1176
+ const height = readU32(pos);
1177
+ pos += 4;
1178
+ const bitDepth = data[pos++];
1179
+ const colorType = data[pos++];
1180
+ pos += 2 + 1 + 4;
1181
+ if (bitDepth !== 8)
1182
+ return null;
1183
+ if (colorType !== 2 && colorType !== 6)
1184
+ return null;
1185
+ const idatChunks = [];
1186
+ while (pos < data.length - 4) {
1187
+ const chunkLen = readU32(pos);
1188
+ pos += 4;
1189
+ const chunkType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
1190
+ pos += 4;
1191
+ if (chunkType === 'IDAT') {
1192
+ idatChunks.push(data.subarray(pos, pos + chunkLen));
1193
+ }
1194
+ if (chunkType === 'IEND')
1195
+ break;
1196
+ pos += chunkLen + 4;
1197
+ }
1198
+ if (!idatChunks.length)
1199
+ return null;
1200
+ let totalLen = 0;
1201
+ for (const c of idatChunks)
1202
+ totalLen += c.length;
1203
+ const zlibData = new Uint8Array(totalLen);
1204
+ let off = 0;
1205
+ for (const c of idatChunks) {
1206
+ zlibData.set(c, off);
1207
+ off += c.length;
1208
+ }
1209
+ const colors = colorType === 6 ? 4 : 3;
1210
+ if (colorType === 2) {
1211
+ const objId = doc.add(`<</Type/XObject/Subtype/Image/Width ${width}/Height ${height}` +
1212
+ `/ColorSpace/DeviceRGB/BitsPerComponent 8/Filter/FlateDecode` +
1213
+ `/DecodeParms<</Predictor 15/Colors 3/BitsPerComponent 8/Columns ${width}>>` +
1214
+ `/Length ${zlibData.length}>>`, zlibData);
1215
+ return { id: objId, name: '', width, height };
1216
+ }
1217
+ if (colorType === 6) {
1218
+ return null;
1219
+ }
1220
+ return null;
1221
+ }
1222
+ function resolveSparklineValues(ws, dataRange) {
1223
+ const vals = [];
1224
+ const ref = dataRange.includes('!') ? dataRange.split('!')[1] : dataRange;
1225
+ const m = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
1226
+ if (m) {
1227
+ const c1 = colLetterToIndex(m[1]), r1 = parseInt(m[2], 10);
1228
+ const c2 = colLetterToIndex(m[3]), r2 = parseInt(m[4], 10);
1229
+ for (let r = r1; r <= r2; r++) {
1230
+ for (let c = c1; c <= c2; c++) {
1231
+ const cell = ws.getCell(r, c);
1232
+ if (typeof cell.value === 'number')
1233
+ vals.push(cell.value);
1234
+ }
1235
+ }
1236
+ }
1237
+ return vals;
1238
+ }
1239
+ function drawSparkline(sb, sp, values, x, y, w, h) {
1240
+ if (!values.length)
1241
+ return;
1242
+ const pad = 2;
1243
+ const sx = x + pad, sy = y + pad, sw = w - pad * 2, sh = h - pad * 2;
1244
+ const min = Math.min(...values), max = Math.max(...values);
1245
+ const range = max - min || 1;
1246
+ const rgb = colorRgb(sp.color) ?? [0.267, 0.447, 0.769];
1247
+ sb.gsave();
1248
+ sb.clipRect(x, y, w, h);
1249
+ if (sp.type === 'bar' || sp.type === 'stacked') {
1250
+ const bw = sw / values.length;
1251
+ for (let i = 0; i < values.length; i++) {
1252
+ const v = values[i];
1253
+ const barH = Math.max(1, ((v - min) / range) * sh);
1254
+ const bx = sx + i * bw + bw * 0.1;
1255
+ const by = sy + (sh - barH);
1256
+ let fill = rgb;
1257
+ if (v < 0 && sp.negativeColor)
1258
+ fill = colorRgb(sp.negativeColor) ?? rgb;
1259
+ sb.fillColor(fill[0], fill[1], fill[2]);
1260
+ sb.fillRect(bx, by, bw * 0.8, barH);
1261
+ }
1262
+ }
1263
+ else {
1264
+ sb.strokeColor(rgb[0], rgb[1], rgb[2]);
1265
+ sb.lineWidth(sp.lineWidth ?? 1);
1266
+ const pts = values.map((v, i) => [
1267
+ sx + (i / (values.length - 1 || 1)) * sw,
1268
+ sy + ((v - min) / range) * sh,
1269
+ ]);
1270
+ if (pts.length >= 2) {
1271
+ let path = `${n(pts[0][0])} ${n(pts[0][1])} m`;
1272
+ for (let i = 1; i < pts.length; i++)
1273
+ path += ` ${n(pts[i][0])} ${n(pts[i][1])} l`;
1274
+ sb.raw(path + ' S');
1275
+ }
1276
+ if (sp.showMarkers && sp.markersColor) {
1277
+ const mc = colorRgb(sp.markersColor) ?? rgb;
1278
+ sb.fillColor(mc[0], mc[1], mc[2]);
1279
+ for (const [px, py] of pts)
1280
+ sb.fillRect(px - 1.5, py - 1.5, 3, 3);
1281
+ }
1282
+ }
1283
+ sb.grestore();
1284
+ }
1285
+ function drawFormControl(sb, fc, cols, rows, colWidthsPt, rowHeightsPt, scale, mar, pageH) {
1286
+ let fx = mar.left, fy = 0;
1287
+ for (let ci = 0; ci < cols.length; ci++) {
1288
+ if (cols[ci] >= fc.from.col + 1)
1289
+ break;
1290
+ fx += colWidthsPt[ci] * scale;
1291
+ }
1292
+ for (let ri = 0; ri < rows.length; ri++) {
1293
+ if (rows[ri] >= fc.from.row + 1)
1294
+ break;
1295
+ fy += rowHeightsPt[ri] * scale;
1296
+ }
1297
+ const fw = (fc.width ?? 80) * 0.75 * scale;
1298
+ const fh = (fc.height ?? 24) * 0.75 * scale;
1299
+ const pyTop = pageH - mar.top - fy;
1300
+ const pyBot = pyTop - fh;
1301
+ const text = fc.text ?? '';
1302
+ sb.gsave();
1303
+ switch (fc.type) {
1304
+ case 'button':
1305
+ case 'dialog':
1306
+ sb.fillColor(0.93, 0.93, 0.93);
1307
+ sb.fillRect(fx, pyBot, fw, fh);
1308
+ sb.strokeColor(0.6, 0.6, 0.6).lineWidth(0.5);
1309
+ sb.strokeRect(fx, pyBot, fw, fh);
1310
+ if (text) {
1311
+ sb.fillColor(0, 0, 0);
1312
+ const fs = Math.min(9 * scale, fh * 0.6);
1313
+ const tw = textWidthPt(text, fs / scale, false) * scale;
1314
+ sb.beginText().font('F1', fs)
1315
+ .textPos(fx + (fw - tw) / 2, pyBot + (fh - fs) / 2)
1316
+ .showText(text).endText();
1317
+ }
1318
+ break;
1319
+ case 'checkBox': {
1320
+ sb.strokeColor(0.4, 0.4, 0.4).lineWidth(0.5);
1321
+ const boxS = Math.min(fh - 2, 10 * scale);
1322
+ sb.strokeRect(fx + 2, pyBot + (fh - boxS) / 2, boxS, boxS);
1323
+ if (fc.checked === 'checked') {
1324
+ sb.strokeColor(0, 0, 0).lineWidth(1);
1325
+ const bx = fx + 2, by = pyBot + (fh - boxS) / 2;
1326
+ sb.line(bx + 2, by + boxS / 2, bx + boxS / 2, by + 2);
1327
+ sb.line(bx + boxS / 2, by + 2, bx + boxS - 2, by + boxS - 2);
1328
+ }
1329
+ if (text) {
1330
+ sb.fillColor(0, 0, 0);
1331
+ const fs = Math.min(8 * scale, fh * 0.7);
1332
+ sb.beginText().font('F1', fs)
1333
+ .textPos(fx + boxS + 6, pyBot + (fh - fs) / 2)
1334
+ .showText(text).endText();
1335
+ }
1336
+ break;
1337
+ }
1338
+ case 'optionButton': {
1339
+ const rad = Math.min(fh / 2 - 1, 5 * scale);
1340
+ const cx = fx + 2 + rad, cy = pyBot + fh / 2;
1341
+ const k = 0.5523;
1342
+ sb.strokeColor(0.4, 0.4, 0.4).lineWidth(0.5);
1343
+ sb.raw(`${n(cx + rad)} ${n(cy)} m ${n(cx + rad)} ${n(cy + rad * k)} ${n(cx + rad * k)} ${n(cy + rad)} ${n(cx)} ${n(cy + rad)} c`);
1344
+ sb.raw(`${n(cx - rad * k)} ${n(cy + rad)} ${n(cx - rad)} ${n(cy + rad * k)} ${n(cx - rad)} ${n(cy)} c`);
1345
+ sb.raw(`${n(cx - rad)} ${n(cy - rad * k)} ${n(cx - rad * k)} ${n(cy - rad)} ${n(cx)} ${n(cy - rad)} c`);
1346
+ sb.raw(`${n(cx + rad * k)} ${n(cy - rad)} ${n(cx + rad)} ${n(cy - rad * k)} ${n(cx + rad)} ${n(cy)} c S`);
1347
+ if (fc.checked === 'checked') {
1348
+ const ir = rad * 0.5;
1349
+ sb.fillColor(0, 0, 0);
1350
+ sb.raw(`${n(cx + ir)} ${n(cy)} m ${n(cx + ir)} ${n(cy + ir * k)} ${n(cx + ir * k)} ${n(cy + ir)} ${n(cx)} ${n(cy + ir)} c`);
1351
+ sb.raw(`${n(cx - ir * k)} ${n(cy + ir)} ${n(cx - ir)} ${n(cy + ir * k)} ${n(cx - ir)} ${n(cy)} c`);
1352
+ sb.raw(`${n(cx - ir)} ${n(cy - ir * k)} ${n(cx - ir * k)} ${n(cy - ir)} ${n(cx)} ${n(cy - ir)} c`);
1353
+ sb.raw(`${n(cx + ir * k)} ${n(cy - ir)} ${n(cx + ir)} ${n(cy - ir * k)} ${n(cx + ir)} ${n(cy)} c f`);
1354
+ }
1355
+ if (text) {
1356
+ sb.fillColor(0, 0, 0);
1357
+ const fs = Math.min(8 * scale, fh * 0.7);
1358
+ sb.beginText().font('F1', fs)
1359
+ .textPos(fx + rad * 2 + 6, pyBot + (fh - fs) / 2)
1360
+ .showText(text).endText();
1361
+ }
1362
+ break;
1363
+ }
1364
+ case 'label':
1365
+ case 'groupBox':
1366
+ if (text) {
1367
+ sb.fillColor(0, 0, 0);
1368
+ const fs = Math.min(9 * scale, fh * 0.7);
1369
+ sb.beginText().font('F1', fs)
1370
+ .textPos(fx + 2, pyBot + (fh - fs) / 2)
1371
+ .showText(text).endText();
1372
+ }
1373
+ if (fc.type === 'groupBox') {
1374
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.5);
1375
+ sb.strokeRect(fx, pyBot, fw, fh);
1376
+ }
1377
+ break;
1378
+ case 'comboBox':
1379
+ case 'listBox':
1380
+ sb.fillColor(1, 1, 1);
1381
+ sb.fillRect(fx, pyBot, fw, fh);
1382
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.5);
1383
+ sb.strokeRect(fx, pyBot, fw, fh);
1384
+ if (fc.type === 'comboBox') {
1385
+ const aw = Math.min(16 * scale, fw * 0.2);
1386
+ sb.strokeRect(fx + fw - aw, pyBot, aw, fh);
1387
+ const ax = fx + fw - aw / 2, ay = pyBot + fh / 2;
1388
+ sb.fillColor(0.3, 0.3, 0.3);
1389
+ sb.raw(`${n(ax - 3)} ${n(ay + 2)} m ${n(ax + 3)} ${n(ay + 2)} l ${n(ax)} ${n(ay - 2)} l f`);
1390
+ }
1391
+ break;
1392
+ case 'scrollBar':
1393
+ case 'spinner':
1394
+ sb.fillColor(0.92, 0.92, 0.92);
1395
+ sb.fillRect(fx, pyBot, fw, fh);
1396
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.5);
1397
+ sb.strokeRect(fx, pyBot, fw, fh);
1398
+ break;
1399
+ default:
1400
+ sb.strokeColor(0.7, 0.7, 0.7).lineWidth(0.5);
1401
+ sb.strokeRect(fx, pyBot, fw, fh);
1402
+ if (text) {
1403
+ sb.fillColor(0, 0, 0);
1404
+ const fs = Math.min(8 * scale, fh * 0.7);
1405
+ sb.beginText().font('F1', fs)
1406
+ .textPos(fx + 2, pyBot + (fh - fs) / 2)
1407
+ .showText(text).endText();
1408
+ }
1409
+ }
1410
+ sb.grestore();
1411
+ }
1412
+ const PDF_CHART_PALETTE = [
1413
+ [0.267, 0.447, 0.769], [0.929, 0.490, 0.192], [0.647, 0.647, 0.647],
1414
+ [1, 0.753, 0], [0.357, 0.608, 0.835], [0.439, 0.678, 0.278],
1415
+ ];
1416
+ function drawChartOnPage(sb, chart, ws, x, y, w, h) {
1417
+ const PAD = 10;
1418
+ const plotX = x + PAD + 30, plotY = y + PAD;
1419
+ const plotW = w - PAD * 2 - 40, plotH = h - PAD * 2 - 20;
1420
+ const allSeries = [];
1421
+ for (let si = 0; si < chart.series.length; si++) {
1422
+ const s = chart.series[si];
1423
+ const vals = resolveChartDataValues(ws, s.values);
1424
+ allSeries.push({
1425
+ name: s.name ?? `Series ${si + 1}`,
1426
+ values: vals,
1427
+ color: PDF_CHART_PALETTE[si % PDF_CHART_PALETTE.length],
1428
+ });
1429
+ }
1430
+ if (!allSeries.length || !allSeries[0].values.length)
1431
+ return;
1432
+ sb.gsave();
1433
+ if (chart.title) {
1434
+ sb.fillColor(0, 0, 0);
1435
+ const titleFs = 10;
1436
+ const tw = textWidthPt(chart.title, titleFs, true);
1437
+ sb.beginText().font('F2', titleFs)
1438
+ .textPos(x + (w - tw) / 2, y + h - 8)
1439
+ .showText(chart.title).endText();
1440
+ }
1441
+ const type = chart.type;
1442
+ const allVals = allSeries.flatMap(s => s.values);
1443
+ const dataMin = Math.min(0, ...allVals);
1444
+ const dataMax = Math.max(1, ...allVals);
1445
+ const dataRange = dataMax - dataMin || 1;
1446
+ sb.strokeColor(0.5, 0.5, 0.5).lineWidth(0.5);
1447
+ sb.line(plotX, plotY, plotX, plotY + plotH);
1448
+ sb.line(plotX, plotY, plotX + plotW, plotY);
1449
+ if (type === 'pie' || type === 'doughnut') {
1450
+ const cx = x + w / 2, cy = y + h / 2;
1451
+ const radius = Math.min(plotW, plotH) / 2 - 5;
1452
+ const vals = allSeries[0].values.filter(v => v > 0);
1453
+ const total = vals.reduce((s, v) => s + v, 0) || 1;
1454
+ let startAngle = 0;
1455
+ for (let i = 0; i < vals.length; i++) {
1456
+ const sweep = (vals[i] / total) * 2 * Math.PI;
1457
+ const c = PDF_CHART_PALETTE[i % PDF_CHART_PALETTE.length];
1458
+ sb.fillColor(c[0], c[1], c[2]);
1459
+ sb.raw(`${n(cx)} ${n(cy)} m`);
1460
+ const steps = Math.max(8, Math.ceil(sweep / 0.2));
1461
+ for (let s = 0; s <= steps; s++) {
1462
+ const a = startAngle + (sweep * s) / steps;
1463
+ sb.raw(`${n(cx + radius * Math.cos(a))} ${n(cy + radius * Math.sin(a))} l`);
1464
+ }
1465
+ sb.raw('f');
1466
+ startAngle += sweep;
1467
+ }
1468
+ }
1469
+ else if (type.startsWith('bar')) {
1470
+ const numCats = allSeries[0].values.length;
1471
+ const catH = plotH / numCats;
1472
+ for (let i = 0; i < numCats; i++) {
1473
+ for (let si = 0; si < allSeries.length; si++) {
1474
+ const v = allSeries[si].values[i] ?? 0;
1475
+ const barW = ((v - dataMin) / dataRange) * plotW;
1476
+ const by = plotY + i * catH + (catH * 0.1);
1477
+ const bh = (catH * 0.8) / allSeries.length;
1478
+ const c = allSeries[si].color;
1479
+ sb.fillColor(c[0], c[1], c[2]);
1480
+ sb.fillRect(plotX, by + si * bh, Math.max(0, barW), bh);
1481
+ }
1482
+ }
1483
+ }
1484
+ else if (type.startsWith('column') || type === 'stock') {
1485
+ const numCats = allSeries[0].values.length;
1486
+ const catW = plotW / numCats;
1487
+ for (let i = 0; i < numCats; i++) {
1488
+ for (let si = 0; si < allSeries.length; si++) {
1489
+ const v = allSeries[si].values[i] ?? 0;
1490
+ const barH = ((v - dataMin) / dataRange) * plotH;
1491
+ const bx = plotX + i * catW + (catW * 0.1);
1492
+ const bw = (catW * 0.8) / allSeries.length;
1493
+ const c = allSeries[si].color;
1494
+ sb.fillColor(c[0], c[1], c[2]);
1495
+ sb.fillRect(bx + si * bw, plotY, bw, Math.max(0, barH));
1496
+ }
1497
+ }
1498
+ }
1499
+ else {
1500
+ for (const series of allSeries) {
1501
+ const pts = series.values.map((v, i) => [
1502
+ plotX + (i / (series.values.length - 1 || 1)) * plotW,
1503
+ plotY + ((v - dataMin) / dataRange) * plotH,
1504
+ ]);
1505
+ sb.strokeColor(series.color[0], series.color[1], series.color[2]).lineWidth(1.5);
1506
+ if (pts.length >= 2) {
1507
+ let path = `${n(pts[0][0])} ${n(pts[0][1])} m`;
1508
+ for (let i = 1; i < pts.length; i++)
1509
+ path += ` ${n(pts[i][0])} ${n(pts[i][1])} l`;
1510
+ if (type.startsWith('area')) {
1511
+ path += ` ${n(pts[pts.length - 1][0])} ${n(plotY)} l ${n(pts[0][0])} ${n(plotY)} l`;
1512
+ sb.fillColor(series.color[0], series.color[1], series.color[2]);
1513
+ sb.raw(path + ' f');
1514
+ }
1515
+ else {
1516
+ sb.raw(path + ' S');
1517
+ }
1518
+ }
1519
+ if (type.startsWith('scatter') || type === 'bubble') {
1520
+ sb.fillColor(series.color[0], series.color[1], series.color[2]);
1521
+ for (const [px, py] of pts)
1522
+ sb.fillRect(px - 2, py - 2, 4, 4);
1523
+ }
1524
+ }
1525
+ }
1526
+ sb.grestore();
1527
+ }
1528
+ function resolveChartDataValues(ws, ref) {
1529
+ const vals = [];
1530
+ const part = ref.includes('!') ? ref.split('!')[1] : ref;
1531
+ const clean = part.replace(/\$/g, '');
1532
+ const m = clean.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
1533
+ if (!m)
1534
+ return vals;
1535
+ const c1 = colLetterToIndex(m[1]), r1 = parseInt(m[2], 10);
1536
+ const c2 = colLetterToIndex(m[3]), r2 = parseInt(m[4], 10);
1537
+ for (let r = r1; r <= r2; r++) {
1538
+ for (let c = c1; c <= c2; c++) {
1539
+ const cell = ws.getCell(r, c);
1540
+ vals.push(typeof cell.value === 'number' ? cell.value : 0);
1541
+ }
1542
+ }
1543
+ return vals;
1544
+ }
1545
+ function emptyPdf(options) {
1546
+ const doc = new PdfDoc();
1547
+ const catalogId = doc.alloc();
1548
+ const pagesId = doc.alloc();
1549
+ const paperKey = options.paperSize ?? 'a4';
1550
+ const orientation = options.orientation ?? 'portrait';
1551
+ const [pw, ph] = PAPER_SIZES[paperKey] ?? PAPER_SIZES.a4;
1552
+ const pageW = orientation === 'landscape' ? ph : pw;
1553
+ const pageH = orientation === 'landscape' ? pw : ph;
1554
+ const fontId = doc.add('<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>');
1555
+ const sb = new StreamBuilder();
1556
+ sb.beginText().font('F1', 12).fillColor(0.5, 0.5, 0.5);
1557
+ sb.textPos(pageW / 2 - 40, pageH / 2);
1558
+ sb.showText('Empty worksheet').endText();
1559
+ const streamData = sb.toBytes();
1560
+ const contentId = doc.addDeflated('<<', streamData);
1561
+ const pageId = doc.add(`<</Type/Page/Parent ${pagesId} 0 R/MediaBox[0 0 ${n(pageW)} ${n(pageH)}]` +
1562
+ `/Contents ${contentId} 0 R/Resources<</Font<</F1 ${fontId} 0 R>>>>>>`);
1563
+ doc.set(pagesId, `<</Type/Pages/Kids[${pageId} 0 R]/Count 1>>`);
1564
+ doc.set(catalogId, `<</Type/Catalog/Pages ${pagesId} 0 R>>`);
1565
+ return doc.build(catalogId);
1566
+ }
1567
+ //# sourceMappingURL=PdfModule.js.map