@ganttloom/gantt-export 0.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.
package/dist/pdf.cjs ADDED
@@ -0,0 +1,671 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/pdf.ts
21
+ var pdf_exports = {};
22
+ __export(pdf_exports, {
23
+ computePagination: () => computePagination,
24
+ renderToPDF: () => renderToPDF
25
+ });
26
+ module.exports = __toCommonJS(pdf_exports);
27
+
28
+ // src/paginate.ts
29
+ var PAGE_SIZES_PT = {
30
+ // Values are the portrait (short-edge width x long-edge height) dimensions
31
+ // in PDF/typographic points (72pt = 1in).
32
+ A4: { width: 595.28, height: 841.89 },
33
+ Letter: { width: 612, height: 792 },
34
+ A3: { width: 841.89, height: 1190.55 }
35
+ };
36
+ var DEFAULT_MARGIN_PT = 36;
37
+ var DEFAULT_SCALE = 0.75;
38
+ var MIN_TIMELINE_WIDTH_PT = 50;
39
+ var MIN_ROWS_HEIGHT_PT = 20;
40
+ function resolvePageSize(opt) {
41
+ if (opt?.custom) {
42
+ return { width: opt.custom.widthPt, height: opt.custom.heightPt };
43
+ }
44
+ const name = opt?.name ?? "A4";
45
+ const base = PAGE_SIZES_PT[name];
46
+ const orientation = opt?.orientation ?? "landscape";
47
+ const short = Math.min(base.width, base.height);
48
+ const long = Math.max(base.width, base.height);
49
+ return orientation === "landscape" ? { width: long, height: short } : { width: short, height: long };
50
+ }
51
+ function defaultGridPanelWidthPx(model) {
52
+ const cols = model.columns ?? [];
53
+ if (cols.length === 0) return 160;
54
+ return cols.reduce((sum, c) => sum + (c.width ?? 120), 0);
55
+ }
56
+ function computePagination(model, options = {}) {
57
+ const { width: pageWidthPt, height: pageHeightPt } = resolvePageSize(options.pageSize);
58
+ const margins = {
59
+ top: options.margins?.top ?? DEFAULT_MARGIN_PT,
60
+ right: options.margins?.right ?? DEFAULT_MARGIN_PT,
61
+ bottom: options.margins?.bottom ?? DEFAULT_MARGIN_PT,
62
+ left: options.margins?.left ?? DEFAULT_MARGIN_PT
63
+ };
64
+ const scale = options.scale ?? DEFAULT_SCALE;
65
+ const gridPanelWidthPx = options.gridPanelWidthPx ?? defaultGridPanelWidthPx(model);
66
+ const gridPanelWidthPt = gridPanelWidthPx * scale;
67
+ const headerHeightPx = model.headerHeight;
68
+ const headerHeightPt = headerHeightPx * scale;
69
+ const contentWidthPt = pageWidthPt - margins.left - margins.right;
70
+ const contentHeightPt = pageHeightPt - margins.top - margins.bottom;
71
+ const timelineWidthPtPerPage = Math.max(contentWidthPt - gridPanelWidthPt, MIN_TIMELINE_WIDTH_PT);
72
+ const rowsHeightPtPerPage = Math.max(contentHeightPt - headerHeightPt, MIN_ROWS_HEIGHT_PT);
73
+ const timelineWidthPxPerPage = timelineWidthPtPerPage / scale;
74
+ const rowsHeightPxPerPage = rowsHeightPtPerPage / scale;
75
+ const totalHeightPx = Math.max(model.height, 1);
76
+ const totalWidthPx = Math.max(model.width, 1);
77
+ const rowCount = Math.max(1, Math.ceil(totalHeightPx / rowsHeightPxPerPage));
78
+ const colCount = Math.max(1, Math.ceil(totalWidthPx / timelineWidthPxPerPage));
79
+ const rowBands = [];
80
+ for (let i = 0; i < rowCount; i++) {
81
+ rowBands.push({
82
+ start: i * rowsHeightPxPerPage,
83
+ end: Math.min(totalHeightPx, (i + 1) * rowsHeightPxPerPage)
84
+ });
85
+ }
86
+ const colBands = [];
87
+ for (let i = 0; i < colCount; i++) {
88
+ colBands.push({
89
+ start: i * timelineWidthPxPerPage,
90
+ end: Math.min(totalWidthPx, (i + 1) * timelineWidthPxPerPage)
91
+ });
92
+ }
93
+ const pages = [];
94
+ for (let r = 0; r < rowCount; r++) {
95
+ const rowBand = rowBands[r];
96
+ for (let c = 0; c < colCount; c++) {
97
+ const colBand = colBands[c];
98
+ pages.push({
99
+ rowBand: r,
100
+ colBand: c,
101
+ rowStartY: rowBand.start,
102
+ rowEndY: rowBand.end,
103
+ colStartX: colBand.start,
104
+ colEndX: colBand.end
105
+ });
106
+ }
107
+ }
108
+ return {
109
+ pageWidthPt,
110
+ pageHeightPt,
111
+ margins,
112
+ scale,
113
+ gridPanelWidthPx,
114
+ gridPanelWidthPt,
115
+ headerHeightPx,
116
+ headerHeightPt,
117
+ timelineWidthPxPerPage,
118
+ rowsHeightPxPerPage,
119
+ rowBands,
120
+ colBands,
121
+ pages,
122
+ rowCount,
123
+ colCount
124
+ };
125
+ }
126
+ function clipSegment(x0, y0, x1, y1, rect) {
127
+ let t0 = 0;
128
+ let t1 = 1;
129
+ const dx = x1 - x0;
130
+ const dy = y1 - y0;
131
+ const checks = [
132
+ [-dx, x0 - rect.x0],
133
+ [dx, rect.x1 - x0],
134
+ [-dy, y0 - rect.y0],
135
+ [dy, rect.y1 - y0]
136
+ ];
137
+ for (const [p, q] of checks) {
138
+ if (p === 0) {
139
+ if (q < 0) return null;
140
+ } else {
141
+ const r = q / p;
142
+ if (p < 0) {
143
+ if (r > t1) return null;
144
+ if (r > t0) t0 = r;
145
+ } else {
146
+ if (r < t0) return null;
147
+ if (r < t1) t1 = r;
148
+ }
149
+ }
150
+ }
151
+ return { x0: x0 + t0 * dx, y0: y0 + t0 * dy, x1: x0 + t1 * dx, y1: y0 + t1 * dy };
152
+ }
153
+ function clipRect(x, y, w, h, rect) {
154
+ const x0 = Math.max(x, rect.x0);
155
+ const y0 = Math.max(y, rect.y0);
156
+ const x1 = Math.min(x + w, rect.x1);
157
+ const y1 = Math.min(y + h, rect.y1);
158
+ if (x1 <= x0 || y1 <= y0) return null;
159
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
160
+ }
161
+
162
+ // src/geom.ts
163
+ function computeLinkPolyline(fromBar, toBar) {
164
+ const x0 = fromBar.x + fromBar.width;
165
+ const y0 = fromBar.y + fromBar.height / 2;
166
+ const x1 = toBar.x;
167
+ const y1 = toBar.y + toBar.height / 2;
168
+ if (Math.abs(y1 - y0) < 0.01) {
169
+ return [{ x: x0, y: y0 }, { x: x1, y: y1 }];
170
+ }
171
+ const midX = x0 + (x1 - x0) / 2;
172
+ return [
173
+ { x: x0, y: y0 },
174
+ { x: midX, y: y0 },
175
+ { x: midX, y: y1 },
176
+ { x: x1, y: y1 }
177
+ ];
178
+ }
179
+ function clipPolylineToRect(points, rect) {
180
+ const segments = [];
181
+ let current = [];
182
+ for (let i = 0; i < points.length - 1; i++) {
183
+ const a = points[i];
184
+ const b = points[i + 1];
185
+ const clipped = clipSegment(a.x, a.y, b.x, b.y, rect);
186
+ if (!clipped) {
187
+ if (current.length > 1) segments.push(current);
188
+ current = [];
189
+ continue;
190
+ }
191
+ if (current.length === 0) {
192
+ current.push({ x: clipped.x0, y: clipped.y0 });
193
+ } else {
194
+ const last = current[current.length - 1];
195
+ if (Math.abs(last.x - clipped.x0) > 0.01 || Math.abs(last.y - clipped.y0) > 0.01) {
196
+ if (current.length > 1) segments.push(current);
197
+ current = [{ x: clipped.x0, y: clipped.y0 }];
198
+ }
199
+ }
200
+ current.push({ x: clipped.x1, y: clipped.y1 });
201
+ }
202
+ if (current.length > 1) segments.push(current);
203
+ return segments;
204
+ }
205
+
206
+ // src/layout.ts
207
+ function columnWidthsPt(model, layout) {
208
+ const columns = model.columns.length > 0 ? model.columns : [{ id: "name", title: "Task" }];
209
+ const naturalTotal = columns.reduce((sum, c) => sum + (c.width ?? 120), 0) || 1;
210
+ return columns.map((column) => ({
211
+ column,
212
+ widthPt: (column.width ?? 120) / naturalTotal * layout.gridPanelWidthPt
213
+ }));
214
+ }
215
+ function cellText(column, task, depth) {
216
+ if (!task) return "";
217
+ if (column.accessor) {
218
+ const v = column.accessor(task);
219
+ return v === null || v === void 0 ? "" : String(v);
220
+ }
221
+ const indent = " ".repeat(Math.max(0, depth));
222
+ return `${indent}${task.name}`;
223
+ }
224
+ function buildPageContent(model, tasks, layout, page) {
225
+ const { scale, gridPanelWidthPt, headerHeightPt, pageWidthPt, pageHeightPt, margins } = layout;
226
+ const theme = model.theme;
227
+ const tasksById = new Map(tasks.map((t) => [t.id, t]));
228
+ const barsByTaskId = new Map(model.bars.map((b) => [b.taskId, b]));
229
+ const rowsByTaskId = new Map(model.rows.map((r) => [r.taskId, r]));
230
+ const commands = [];
231
+ const contentX0 = margins.left;
232
+ const contentY0 = margins.top;
233
+ const timelineRect = {
234
+ x0: page.colStartX,
235
+ y0: page.rowStartY,
236
+ x1: page.colEndX,
237
+ y1: page.rowEndY
238
+ };
239
+ commands.push({ kind: "rect", x: 0, y: 0, w: pageWidthPt, h: pageHeightPt, fill: theme.backgroundColor });
240
+ const cols = columnWidthsPt(model, layout);
241
+ let colCursor = contentX0;
242
+ for (const { column, widthPt } of cols) {
243
+ commands.push({
244
+ kind: "text",
245
+ x: colCursor + 4,
246
+ y: contentY0 + headerHeightPt / 2 - 5,
247
+ w: widthPt - 8,
248
+ text: column.title,
249
+ size: 9,
250
+ color: theme.textColor,
251
+ bold: true,
252
+ align: column.align ?? "left"
253
+ });
254
+ colCursor += widthPt;
255
+ }
256
+ commands.push({
257
+ kind: "rect",
258
+ x: contentX0,
259
+ y: contentY0,
260
+ w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
261
+ h: headerHeightPt,
262
+ stroke: theme.gridColor
263
+ });
264
+ const timelineOriginX = contentX0 + gridPanelWidthPt;
265
+ for (const tick of model.ticks) {
266
+ if (tick.x < page.colStartX || tick.x > page.colEndX) continue;
267
+ const px = timelineOriginX + (tick.x - page.colStartX) * scale;
268
+ if (tick.isWeekend) {
269
+ }
270
+ commands.push({
271
+ kind: "line",
272
+ points: [
273
+ { x: px, y: contentY0 },
274
+ { x: px, y: pageHeightPt - margins.bottom }
275
+ ],
276
+ stroke: tick.isToday ? theme.todayColor : theme.gridColor
277
+ });
278
+ commands.push({
279
+ kind: "text",
280
+ x: px + 2,
281
+ y: contentY0 + headerHeightPt / 2 - 5,
282
+ text: tick.label,
283
+ size: 7,
284
+ color: theme.textColor,
285
+ align: "left"
286
+ });
287
+ }
288
+ const rowTop = (y) => contentY0 + headerHeightPt + (y - page.rowStartY) * scale;
289
+ for (const row of model.rows) {
290
+ if (row.y + row.height <= page.rowStartY || row.y >= page.rowEndY) continue;
291
+ const clippedRowY0 = Math.max(row.y, page.rowStartY);
292
+ const clippedRowY1 = Math.min(row.y + row.height, page.rowEndY);
293
+ const rowYTop = rowTop(clippedRowY0);
294
+ const rowHPt = (clippedRowY1 - clippedRowY0) * scale;
295
+ const task = tasksById.get(row.taskId);
296
+ let cx = contentX0;
297
+ for (const { column, widthPt } of cols) {
298
+ commands.push({
299
+ kind: "text",
300
+ x: cx + 4,
301
+ y: rowYTop + rowHPt / 2 - 4,
302
+ w: widthPt - 8,
303
+ text: cellText(column, task, row.depth),
304
+ size: 8,
305
+ color: theme.textColor,
306
+ align: column.align ?? "left"
307
+ });
308
+ cx += widthPt;
309
+ }
310
+ commands.push({
311
+ kind: "rect",
312
+ x: contentX0,
313
+ y: rowYTop,
314
+ w: gridPanelWidthPt + (page.colEndX - page.colStartX) * scale,
315
+ h: rowHPt,
316
+ stroke: theme.gridColor
317
+ });
318
+ const bar = barsByTaskId.get(row.taskId);
319
+ if (bar) {
320
+ const clippedBar = clipRect(bar.x, bar.y, bar.width, bar.height, timelineRect);
321
+ if (clippedBar) {
322
+ const bx = timelineOriginX + (clippedBar.x - page.colStartX) * scale;
323
+ const by = rowTop(clippedBar.y);
324
+ const bw = clippedBar.w * scale;
325
+ const bh = clippedBar.h * scale;
326
+ if (bar.baseline) {
327
+ const clippedBaseline = clipRect(bar.baseline.x, bar.y, bar.baseline.width, bar.height, timelineRect);
328
+ if (clippedBaseline) {
329
+ commands.push({
330
+ kind: "rect",
331
+ x: timelineOriginX + (clippedBaseline.x - page.colStartX) * scale,
332
+ y: rowTop(clippedBaseline.y),
333
+ w: clippedBaseline.w * scale,
334
+ h: clippedBaseline.h * scale,
335
+ fill: theme.baselineColor
336
+ });
337
+ }
338
+ }
339
+ if (bar.isMilestone) {
340
+ const r = bh / 2;
341
+ commands.push({
342
+ kind: "diamond",
343
+ cx: bx + r,
344
+ cy: by + r,
345
+ r,
346
+ fill: bar.isCritical ? theme.criticalColor : bar.color,
347
+ stroke: theme.textColor
348
+ });
349
+ if (bar.label) {
350
+ commands.push({
351
+ kind: "text",
352
+ x: bx + bh + 4,
353
+ y: by + bh / 2 - 4,
354
+ text: bar.label,
355
+ size: 8,
356
+ color: theme.textColor
357
+ });
358
+ }
359
+ } else {
360
+ commands.push({
361
+ kind: "rect",
362
+ x: bx,
363
+ y: by,
364
+ w: bw,
365
+ h: bh,
366
+ fill: bar.isCritical ? theme.criticalColor : bar.color
367
+ });
368
+ if (bar.progressWidth > 0) {
369
+ const clippedProgress = clipRect(bar.x, bar.y, bar.progressWidth, bar.height, timelineRect);
370
+ if (clippedProgress) {
371
+ commands.push({
372
+ kind: "rect",
373
+ x: timelineOriginX + (clippedProgress.x - page.colStartX) * scale,
374
+ y: rowTop(clippedProgress.y),
375
+ w: clippedProgress.w * scale,
376
+ h: clippedProgress.h * scale,
377
+ fill: bar.progressColor
378
+ });
379
+ }
380
+ }
381
+ commands.push({
382
+ kind: "text",
383
+ x: bx + 3,
384
+ y: by + bh / 2 - 4,
385
+ text: bar.label,
386
+ size: 8,
387
+ color: theme.textColor
388
+ });
389
+ }
390
+ }
391
+ }
392
+ }
393
+ for (const link of model.links) {
394
+ const fromBar = barsByTaskId.get(link.fromId);
395
+ const toBar = barsByTaskId.get(link.toId);
396
+ if (!fromBar || !toBar) continue;
397
+ const fromRow = rowsByTaskId.get(link.fromId);
398
+ const toRow = rowsByTaskId.get(link.toId);
399
+ if (!fromRow || !toRow) continue;
400
+ const polyline = computeLinkPolyline(fromBar, toBar);
401
+ const pieces = clipPolylineToRect(polyline, timelineRect);
402
+ for (const piece of pieces) {
403
+ commands.push({
404
+ kind: "line",
405
+ points: piece.map((p) => ({
406
+ x: timelineOriginX + (p.x - page.colStartX) * scale,
407
+ y: rowTop(p.y)
408
+ })),
409
+ stroke: link.isCritical ? theme.criticalColor : theme.linkColor
410
+ });
411
+ }
412
+ }
413
+ return { widthPt: pageWidthPt, heightPt: pageHeightPt, commands };
414
+ }
415
+
416
+ // src/pdf/writer.ts
417
+ var encoder = new TextEncoder();
418
+ function encodeLatin1(str) {
419
+ const out = new Uint8Array(str.length);
420
+ for (let i = 0; i < str.length; i++) {
421
+ const code = str.charCodeAt(i);
422
+ out[i] = code <= 255 ? code : 63;
423
+ }
424
+ return out;
425
+ }
426
+ function escapePdfText(str) {
427
+ return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
428
+ }
429
+ function xrefLine(offset, generation, type) {
430
+ const line = `${String(offset).padStart(10, "0")} ${String(generation).padStart(5, "0")} ${type}
431
+ `;
432
+ return line;
433
+ }
434
+ var PdfBuilder = class {
435
+ constructor() {
436
+ this.parts = [];
437
+ this.length = 0;
438
+ this.offsetsById = /* @__PURE__ */ new Map();
439
+ this.nextId = 1;
440
+ }
441
+ reserveId() {
442
+ return this.nextId++;
443
+ }
444
+ raw(bytes) {
445
+ this.parts.push(bytes);
446
+ this.length += bytes.length;
447
+ }
448
+ text(s) {
449
+ this.raw(encoder.encode(s));
450
+ }
451
+ writeHeader() {
452
+ this.text("%PDF-1.4\n");
453
+ this.raw(new Uint8Array([37, 226, 227, 207, 211, 10]));
454
+ }
455
+ /** Append a simple (non-stream) indirect object, e.g. a dictionary or array. */
456
+ addObject(id, body) {
457
+ this.offsetsById.set(id, this.length);
458
+ this.text(`${id} 0 obj
459
+ ${body}
460
+ endobj
461
+ `);
462
+ }
463
+ /** Append a stream object; `dictExtra` is the dictionary content besides `/Length`. */
464
+ addStreamObject(id, dictExtra, data) {
465
+ this.offsetsById.set(id, this.length);
466
+ this.text(`${id} 0 obj
467
+ << ${dictExtra} /Length ${data.length} >>
468
+ stream
469
+ `);
470
+ this.raw(data);
471
+ this.text("\nendstream\nendobj\n");
472
+ }
473
+ /** Finalize the file: write the xref table, trailer, and startxref/%%EOF footer. */
474
+ build(rootId) {
475
+ const xrefOffset = this.length;
476
+ const maxId = this.nextId - 1;
477
+ let xref = `xref
478
+ 0 ${maxId + 1}
479
+ `;
480
+ xref += xrefLine(0, 65535, "f");
481
+ for (let id = 1; id <= maxId; id++) {
482
+ const off = this.offsetsById.get(id);
483
+ if (off === void 0) {
484
+ throw new Error(`PdfBuilder: object ${id} was reserved but never written`);
485
+ }
486
+ xref += xrefLine(off, 0, "n");
487
+ }
488
+ this.text(xref);
489
+ this.text(`trailer
490
+ << /Size ${maxId + 1} /Root ${rootId} 0 R >>
491
+ startxref
492
+ ${xrefOffset}
493
+ %%EOF`);
494
+ const total = new Uint8Array(this.length);
495
+ let o = 0;
496
+ for (const p of this.parts) {
497
+ total.set(p, o);
498
+ o += p.length;
499
+ }
500
+ return total;
501
+ }
502
+ /** Byte offset an object was (or will be) written at — exposed for tests. */
503
+ getOffset(id) {
504
+ return this.offsetsById.get(id);
505
+ }
506
+ };
507
+
508
+ // src/color.ts
509
+ function parseColor(input) {
510
+ if (!input) return [0, 0, 0];
511
+ const s = input.trim();
512
+ const hex = s.match(/^#?([0-9a-fA-F]{6})$/) ?? s.match(/^#?([0-9a-fA-F]{3})$/);
513
+ if (hex) {
514
+ let h = hex[1];
515
+ if (h.length === 3) {
516
+ h = h.split("").map((c) => c + c).join("");
517
+ }
518
+ const r = parseInt(h.slice(0, 2), 16) / 255;
519
+ const g = parseInt(h.slice(2, 4), 16) / 255;
520
+ const b = parseInt(h.slice(4, 6), 16) / 255;
521
+ return [r, g, b];
522
+ }
523
+ const rgb = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i);
524
+ if (rgb) {
525
+ return [Number(rgb[1]) / 255, Number(rgb[2]) / 255, Number(rgb[3]) / 255];
526
+ }
527
+ const named = {
528
+ black: [0, 0, 0],
529
+ white: [1, 1, 1],
530
+ red: [1, 0, 0],
531
+ green: [0, 0.5, 0],
532
+ blue: [0, 0, 1],
533
+ gray: [0.5, 0.5, 0.5],
534
+ grey: [0.5, 0.5, 0.5],
535
+ transparent: [1, 1, 1]
536
+ };
537
+ return named[s.toLowerCase()] ?? [0, 0, 0];
538
+ }
539
+
540
+ // src/pdf/content.ts
541
+ function num(n) {
542
+ const r = Math.round(n * 1e3) / 1e3;
543
+ return Object.is(r, -0) ? "0" : String(r);
544
+ }
545
+ function colorOp(color, op) {
546
+ const [r, g, b] = parseColor(color);
547
+ return `${num(r)} ${num(g)} ${num(b)} ${op}
548
+ `;
549
+ }
550
+ function buildContentStream(commands, pageHeightPt) {
551
+ let s = "q\n";
552
+ for (const cmd of commands) {
553
+ switch (cmd.kind) {
554
+ case "rect": {
555
+ const x = cmd.x;
556
+ const yTop = pageHeightPt - (cmd.y + cmd.h);
557
+ if (cmd.fill) s += colorOp(cmd.fill, "rg");
558
+ if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
559
+ s += `${num(x)} ${num(yTop)} ${num(cmd.w)} ${num(cmd.h)} re
560
+ `;
561
+ if (cmd.fill && cmd.stroke) s += "B\n";
562
+ else if (cmd.fill) s += "f\n";
563
+ else if (cmd.stroke) s += "S\n";
564
+ break;
565
+ }
566
+ case "diamond": {
567
+ const pts = [
568
+ { x: cmd.cx, y: cmd.cy - cmd.r },
569
+ { x: cmd.cx + cmd.r, y: cmd.cy },
570
+ { x: cmd.cx, y: cmd.cy + cmd.r },
571
+ { x: cmd.cx - cmd.r, y: cmd.cy }
572
+ ].map((p) => ({ x: p.x, y: pageHeightPt - p.y }));
573
+ if (cmd.fill) s += colorOp(cmd.fill, "rg");
574
+ if (cmd.stroke) s += colorOp(cmd.stroke, "RG");
575
+ s += `${num(pts[0].x)} ${num(pts[0].y)} m
576
+ `;
577
+ for (let i = 1; i < pts.length; i++) s += `${num(pts[i].x)} ${num(pts[i].y)} l
578
+ `;
579
+ s += "h\n";
580
+ if (cmd.fill && cmd.stroke) s += "B\n";
581
+ else if (cmd.fill) s += "f\n";
582
+ else if (cmd.stroke) s += "S\n";
583
+ break;
584
+ }
585
+ case "line": {
586
+ if (cmd.points.length < 2) break;
587
+ s += colorOp(cmd.stroke, "RG");
588
+ const p0 = cmd.points[0];
589
+ s += `${num(p0.x)} ${num(pageHeightPt - p0.y)} m
590
+ `;
591
+ for (let i = 1; i < cmd.points.length; i++) {
592
+ const p = cmd.points[i];
593
+ s += `${num(p.x)} ${num(pageHeightPt - p.y)} l
594
+ `;
595
+ }
596
+ s += "S\n";
597
+ break;
598
+ }
599
+ case "text": {
600
+ if (!cmd.text) break;
601
+ const font = cmd.bold ? "/F2" : "/F1";
602
+ const baselineY = pageHeightPt - (cmd.y + cmd.size * 0.8);
603
+ s += colorOp(cmd.color, "rg");
604
+ s += "BT\n";
605
+ s += `${font} ${num(cmd.size)} Tf
606
+ `;
607
+ s += `${num(cmd.x)} ${num(baselineY)} Td
608
+ `;
609
+ s += `(${escapePdfText(latin1Safe(cmd.text))}) Tj
610
+ `;
611
+ s += "ET\n";
612
+ break;
613
+ }
614
+ }
615
+ }
616
+ s += "Q\n";
617
+ return encodeLatin1(s);
618
+ }
619
+ function latin1Safe(s) {
620
+ let out = "";
621
+ for (const ch of s) {
622
+ const code = ch.codePointAt(0);
623
+ out += code <= 255 ? ch : "?";
624
+ }
625
+ return out;
626
+ }
627
+
628
+ // src/pdf/index.ts
629
+ function renderToPDF(model, tasks, options = {}) {
630
+ const layout = computePagination(model, options);
631
+ const pdf = new PdfBuilder();
632
+ pdf.writeHeader();
633
+ const catalogId = pdf.reserveId();
634
+ const pagesId = pdf.reserveId();
635
+ const helveticaId = pdf.reserveId();
636
+ const helveticaBoldId = pdf.reserveId();
637
+ pdf.addObject(helveticaId, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
638
+ pdf.addObject(
639
+ helveticaBoldId,
640
+ "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>"
641
+ );
642
+ const pageIds = [];
643
+ for (const page of layout.pages) {
644
+ const content = buildPageContent(model, tasks, layout, page);
645
+ const streamBytes = buildContentStream(content.commands, layout.pageHeightPt);
646
+ const pageId = pdf.reserveId();
647
+ const contentId = pdf.reserveId();
648
+ pdf.addStreamObject(contentId, "", streamBytes);
649
+ pdf.addObject(
650
+ pageId,
651
+ `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${num2(layout.pageWidthPt)} ${num2(layout.pageHeightPt)}] /Resources << /Font << /F1 ${helveticaId} 0 R /F2 ${helveticaBoldId} 0 R >> >> /Contents ${contentId} 0 R >>`
652
+ );
653
+ pageIds.push(pageId);
654
+ }
655
+ pdf.addObject(
656
+ pagesId,
657
+ `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageIds.length} >>`
658
+ );
659
+ pdf.addObject(catalogId, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
660
+ return pdf.build(catalogId);
661
+ }
662
+ function num2(n) {
663
+ const r = Math.round(n * 1e3) / 1e3;
664
+ return Object.is(r, -0) ? "0" : String(r);
665
+ }
666
+ // Annotate the CommonJS export names for ESM import in node:
667
+ 0 && (module.exports = {
668
+ computePagination,
669
+ renderToPDF
670
+ });
671
+ //# sourceMappingURL=pdf.cjs.map