ngx-pdf-export 0.0.1

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,2028 @@
1
+ import * as i0 from '@angular/core';
2
+ import { ElementRef, Injectable, HostListener, Input, Directive } from '@angular/core';
3
+ import { StandardFonts, PDFDocument, rgb, pushGraphicsState, moveTo, lineTo, closePath, clip, endPath, popGraphicsState } from 'pdf-lib';
4
+ import fontkit from '@pdf-lib/fontkit';
5
+
6
+ /**
7
+ * Unit conversions. See docs/architecture.md "Coordinate systems and units".
8
+ * Internal layout space is always PDF points (pt); DOM space is CSS px.
9
+ */
10
+ const PX_TO_PT = 72 / 96;
11
+ const PT_TO_PX = 96 / 72;
12
+ const MM_TO_PT = 72 / 25.4;
13
+ const PT_TO_MM = 25.4 / 72;
14
+ function pxToPt(px) {
15
+ return px * PX_TO_PT;
16
+ }
17
+ function ptToPx(pt) {
18
+ return pt * PT_TO_PX;
19
+ }
20
+ function mmToPt(mm) {
21
+ return mm * MM_TO_PT;
22
+ }
23
+ function ptToMm(pt) {
24
+ return pt * PT_TO_MM;
25
+ }
26
+ /** Standard page sizes in points, portrait orientation. */
27
+ const STANDARD_PAGE_SIZES_PT = {
28
+ A4: { widthPt: mmToPt(210), heightPt: mmToPt(297) },
29
+ A3: { widthPt: mmToPt(297), heightPt: mmToPt(420) },
30
+ Letter: { widthPt: 612, heightPt: 792 },
31
+ Legal: { widthPt: 612, heightPt: 1008 },
32
+ };
33
+
34
+ const RGBA_RE = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i;
35
+ /**
36
+ * Parses a getComputedStyle() color string. getComputedStyle always
37
+ * normalizes to rgb()/rgba() form in every browser we target, so we don't
38
+ * need to handle hex/hsl/named colors here.
39
+ */
40
+ function parseComputedColor(value) {
41
+ if (!value) {
42
+ return undefined;
43
+ }
44
+ if (value === 'transparent') {
45
+ return { r: 0, g: 0, b: 0, a: 0 };
46
+ }
47
+ const match = RGBA_RE.exec(value.trim());
48
+ if (!match) {
49
+ return undefined;
50
+ }
51
+ return {
52
+ r: clamp255(Number(match[1])),
53
+ g: clamp255(Number(match[2])),
54
+ b: clamp255(Number(match[3])),
55
+ a: match[4] !== undefined ? clamp01$1(Number(match[4])) : 1,
56
+ };
57
+ }
58
+ function isTransparent(color) {
59
+ return !color || color.a <= 0;
60
+ }
61
+ function clamp255(n) {
62
+ return Math.max(0, Math.min(255, Math.round(n)));
63
+ }
64
+ function clamp01$1(n) {
65
+ return Math.max(0, Math.min(1, n));
66
+ }
67
+
68
+ function readBoxPaint(style) {
69
+ const backgroundColor = parseComputedColor(style.backgroundColor);
70
+ const opacity = clamp01(parseFloat(style.opacity || '1'));
71
+ const paint = {
72
+ opacity: Number.isFinite(opacity) ? opacity : 1,
73
+ };
74
+ if (backgroundColor && !isTransparent(backgroundColor)) {
75
+ paint.backgroundColor = backgroundColor;
76
+ }
77
+ const borders = readBorders(style);
78
+ if (borders) {
79
+ paint.borders = borders;
80
+ }
81
+ const radius = readBorderRadius(style);
82
+ if (radius) {
83
+ paint.borderRadius = radius;
84
+ }
85
+ return paint;
86
+ }
87
+ function readBorders(style) {
88
+ const top = readEdge(style, 'Top');
89
+ const right = readEdge(style, 'Right');
90
+ const bottom = readEdge(style, 'Bottom');
91
+ const left = readEdge(style, 'Left');
92
+ const hasAny = [top, right, bottom, left].some((e) => e.style !== 'none' && e.widthPt > 0);
93
+ if (!hasAny) {
94
+ return undefined;
95
+ }
96
+ return { top, right, bottom, left };
97
+ }
98
+ function readEdge(style, side) {
99
+ const widthPx = parseFloat(style[`border${side}Width`] || '0');
100
+ const borderStyle = normalizeBorderStyle(style[`border${side}Style`]);
101
+ const color = parseComputedColor(style[`border${side}Color`]) ?? { r: 0, g: 0, b: 0, a: 1 };
102
+ return {
103
+ widthPt: borderStyle === 'none' ? 0 : pxToPt(widthPx || 0),
104
+ style: borderStyle,
105
+ color,
106
+ };
107
+ }
108
+ function normalizeBorderStyle(value) {
109
+ switch (value) {
110
+ case 'dashed':
111
+ return 'dashed';
112
+ case 'dotted':
113
+ return 'dotted';
114
+ case 'solid':
115
+ case 'double':
116
+ case 'groove':
117
+ case 'ridge':
118
+ case 'inset':
119
+ case 'outset':
120
+ return 'solid';
121
+ default:
122
+ return 'none';
123
+ }
124
+ }
125
+ function readBorderRadius(style) {
126
+ const topLeft = pxToPt(parseFloat(style.borderTopLeftRadius || '0'));
127
+ const topRight = pxToPt(parseFloat(style.borderTopRightRadius || '0'));
128
+ const bottomRight = pxToPt(parseFloat(style.borderBottomRightRadius || '0'));
129
+ const bottomLeft = pxToPt(parseFloat(style.borderBottomLeftRadius || '0'));
130
+ if (!topLeft && !topRight && !bottomRight && !bottomLeft) {
131
+ return undefined;
132
+ }
133
+ return { topLeft, topRight, bottomRight, bottomLeft };
134
+ }
135
+ function clamp01(n) {
136
+ return Math.max(0, Math.min(1, n));
137
+ }
138
+
139
+ /**
140
+ * Framework-independent internal document/layout tree.
141
+ * See docs/architecture.md and docs/layout-engine.md.
142
+ *
143
+ * All geometry on these nodes is in *layout space*: PDF points, y-down,
144
+ * origin at the top-left of the captured root element (a single tall
145
+ * "flow" -- pagination has not happened yet at this stage).
146
+ */
147
+ const DEFAULT_BREAK_RULES = {
148
+ breakInside: 'auto',
149
+ breakBefore: 'auto',
150
+ breakAfter: 'auto',
151
+ };
152
+ function isContainer(node) {
153
+ return node.type === 'block' || node.type === 'group';
154
+ }
155
+
156
+ /**
157
+ * Resolves break-inside/before/after from computed style, plus an optional
158
+ * caller-supplied `keepTogether` selector list matched against the live
159
+ * element (checked by the caller via `element.matches(selector)`).
160
+ * See docs/pagination.md "keepTogether / break controls".
161
+ */
162
+ function readBreakRules(style, forceKeepTogether) {
163
+ const breakInside = normalizeAvoid(style.breakInside) || normalizeAvoid(style.pageBreakInside);
164
+ const breakBefore = normalizePage(style.breakBefore) || normalizePage(style.pageBreakBefore);
165
+ const breakAfter = normalizePage(style.breakAfter) || normalizePage(style.pageBreakAfter);
166
+ return {
167
+ breakInside: forceKeepTogether || breakInside === 'avoid' ? 'avoid' : 'auto',
168
+ breakBefore: breakBefore === 'page' ? 'page' : 'auto',
169
+ breakAfter: breakAfter === 'page' ? 'page' : 'auto',
170
+ };
171
+ }
172
+ function normalizeAvoid(value) {
173
+ return value === 'avoid' ? 'avoid' : undefined;
174
+ }
175
+ function normalizePage(value) {
176
+ return value === 'page' || value === 'always' || value === 'left' || value === 'right' ? 'page' : undefined;
177
+ }
178
+
179
+ function extractTextRuns(textNode) {
180
+ const data = textNode.data;
181
+ if (!data || !/\S/.test(data)) {
182
+ return [];
183
+ }
184
+ const range = document.createRange();
185
+ range.selectNodeContents(textNode);
186
+ const lineRects = Array.from(range.getClientRects()).filter((r) => r.width > 0 && r.height > 0);
187
+ if (lineRects.length <= 1) {
188
+ const rect = lineRects[0] ?? textNode.parentElement?.getBoundingClientRect();
189
+ return rect ? [{ text: data, rect }] : [];
190
+ }
191
+ const boundaries = [0];
192
+ for (let lineIndex = 0; lineIndex < lineRects.length - 1; lineIndex++) {
193
+ const target = lineIndex + 2;
194
+ const prev = boundaries[boundaries.length - 1];
195
+ const k = findLineBoundary(range, textNode, data.length, target, prev);
196
+ boundaries.push(Math.max(prev, k - 1));
197
+ }
198
+ boundaries.push(data.length);
199
+ const runs = [];
200
+ for (let i = 0; i < lineRects.length; i++) {
201
+ const start = boundaries[i];
202
+ const end = boundaries[i + 1];
203
+ const text = data.slice(start, end);
204
+ if (text.trim().length === 0 && text.length === 0) {
205
+ continue;
206
+ }
207
+ runs.push({ text, rect: lineRects[i] });
208
+ }
209
+ return runs;
210
+ }
211
+ function countLinesForPrefix(range, node, k) {
212
+ range.setStart(node, 0);
213
+ range.setEnd(node, k);
214
+ return range.getClientRects().length;
215
+ }
216
+ /** Minimal k in (prev, maxLen] such that the prefix [0, k) spans >= target lines. */
217
+ function findLineBoundary(range, node, maxLen, target, prev) {
218
+ let low = prev + 1;
219
+ let high = maxLen;
220
+ while (low < high) {
221
+ const mid = Math.floor((low + high) / 2);
222
+ if (countLinesForPrefix(range, node, mid) >= target) {
223
+ high = mid;
224
+ }
225
+ else {
226
+ low = mid + 1;
227
+ }
228
+ }
229
+ return low;
230
+ }
231
+
232
+ const GRADIENT_RE = /(linear|radial|conic)-gradient\(/i;
233
+ function checkUnsupported(style) {
234
+ const backgroundImage = style.backgroundImage;
235
+ if (backgroundImage && backgroundImage !== 'none' && GRADIENT_RE.test(backgroundImage)) {
236
+ return { unsupported: true, reason: `background: ${backgroundImage} is a gradient and is not supported; rasterized.` };
237
+ }
238
+ const boxShadow = style.boxShadow;
239
+ if (boxShadow && boxShadow !== 'none') {
240
+ return { unsupported: true, reason: `box-shadow: ${boxShadow} is not supported; rasterized.` };
241
+ }
242
+ const filter = style.filter;
243
+ if (filter && filter !== 'none') {
244
+ return { unsupported: true, reason: `filter: ${filter} is not supported; rasterized.` };
245
+ }
246
+ const backdropFilter = style.backdropFilter;
247
+ if (backdropFilter && backdropFilter !== 'none') {
248
+ return { unsupported: true, reason: `backdrop-filter is not supported; rasterized.` };
249
+ }
250
+ const transform = style.transform;
251
+ if (transform && transform !== 'none' && !isTranslateOnly(transform)) {
252
+ return { unsupported: true, reason: `transform: ${transform} is not supported (only translate() is); rasterized.` };
253
+ }
254
+ const mixBlendMode = style.mixBlendMode;
255
+ if (mixBlendMode && mixBlendMode !== 'normal') {
256
+ return { unsupported: true, reason: `mix-blend-mode: ${mixBlendMode} is not supported; rasterized.` };
257
+ }
258
+ if (style.display === 'grid' || style.display === 'inline-grid') {
259
+ return { unsupported: true, reason: 'display: grid is not specially interpreted and is rasterized as a unit.' };
260
+ }
261
+ return { unsupported: false };
262
+ }
263
+ function isTranslateOnly(transform) {
264
+ // matrix(1, 0, 0, 1, tx, ty) is what getComputedStyle reports for a pure translate().
265
+ const m = /^matrix\(\s*1,\s*0,\s*0,\s*1,\s*[-\d.]+,\s*[-\d.]+\s*\)$/i;
266
+ return m.test(transform.trim());
267
+ }
268
+
269
+ /**
270
+ * Rewrites the numeric coordinates in an SVG path `d` string by a scale
271
+ * factor, so a path authored in the SVG's own viewBox units can be
272
+ * embedded directly in PDF point space without a separate transform.
273
+ *
274
+ * This is a generic tokenizer for well-formed path data (commands
275
+ * separated from numbers, numbers separated by whitespace/commas). It
276
+ * does not handle the rare "arc flags packed with no separator" shorthand
277
+ * (e.g. `A5 5 0 01 1 10 10`) some minifiers produce -- documented in
278
+ * docs/css-support.md.
279
+ */
280
+ const COMMAND_RE = /[MLHVCSQTAZmlhvcsqtaz]/;
281
+ const NUMBER_RE = /[-+]?(?:\d+\.\d+|\.\d+|\d+)(?:[eE][-+]?\d+)?/g;
282
+ // Number of coordinate values per command letter, and which of those are
283
+ // x-like vs y-like (for independent sx/sy scaling). 'f' = flag (unscaled).
284
+ const PARAM_ROLES = {
285
+ M: ['x', 'y'],
286
+ L: ['x', 'y'],
287
+ T: ['x', 'y'],
288
+ H: ['x'],
289
+ V: ['y'],
290
+ C: ['x', 'y', 'x', 'y', 'x', 'y'],
291
+ S: ['x', 'y', 'x', 'y'],
292
+ Q: ['x', 'y', 'x', 'y'],
293
+ A: ['x', 'y', 'f', 'f', 'f', 'x', 'y'],
294
+ Z: [],
295
+ };
296
+ function scalePathData(d, sx, sy) {
297
+ let out = '';
298
+ let i = 0;
299
+ let currentRoles = [];
300
+ let roleIndex = 0;
301
+ while (i < d.length) {
302
+ const ch = d[i];
303
+ if (COMMAND_RE.test(ch)) {
304
+ out += ch;
305
+ currentRoles = PARAM_ROLES[ch.toUpperCase()] ?? [];
306
+ roleIndex = 0;
307
+ i++;
308
+ continue;
309
+ }
310
+ if (/\s|,/.test(ch)) {
311
+ out += ch;
312
+ i++;
313
+ continue;
314
+ }
315
+ NUMBER_RE.lastIndex = i;
316
+ const match = NUMBER_RE.exec(d);
317
+ if (!match || match.index !== i) {
318
+ // Unrecognized character; copy through unchanged rather than throw.
319
+ out += ch;
320
+ i++;
321
+ continue;
322
+ }
323
+ const raw = match[0];
324
+ const value = Number(raw);
325
+ const role = currentRoles.length ? currentRoles[roleIndex % currentRoles.length] : 'x';
326
+ roleIndex++;
327
+ const scaled = role === 'y' ? value * sy : role === 'x' ? value * sx : value;
328
+ out += formatNumber(scaled);
329
+ i += raw.length;
330
+ }
331
+ return out;
332
+ }
333
+ function formatNumber(n) {
334
+ const rounded = Math.round(n * 1000) / 1000;
335
+ return String(rounded);
336
+ }
337
+
338
+ const SUPPORTED_TAGS = new Set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path', 'g', 'svg']);
339
+ /**
340
+ * Parses a supported subset of SVG into scale-normalized draw commands.
341
+ * Returns null if the subtree uses a feature we don't translate (gradient
342
+ * fill, <text>, <use>, <clipPath>, filters, patterns, images) -- the
343
+ * caller falls back to rasterizing the whole <svg> as one element. See
344
+ * docs/css-support.md.
345
+ */
346
+ function parseSvg(svg, renderedWidthPt, renderedHeightPt) {
347
+ const viewBox = svg.viewBox?.baseVal;
348
+ const vbWidth = viewBox && viewBox.width > 0 ? viewBox.width : svg.width.baseVal.value || renderedWidthPt;
349
+ const vbHeight = viewBox && viewBox.height > 0 ? viewBox.height : svg.height.baseVal.value || renderedHeightPt;
350
+ const vbX = viewBox?.x ?? 0;
351
+ const vbY = viewBox?.y ?? 0;
352
+ const sx = vbWidth > 0 ? renderedWidthPt / vbWidth : 1;
353
+ const sy = vbHeight > 0 ? renderedHeightPt / vbHeight : 1;
354
+ const commands = [];
355
+ const ok = walk(svg, sx, sy, vbX, vbY, commands);
356
+ return ok ? commands : null;
357
+ }
358
+ function walk(el, sx, sy, vbX, vbY, out) {
359
+ for (const child of Array.from(el.children)) {
360
+ const tag = child.tagName.toLowerCase();
361
+ if (!SUPPORTED_TAGS.has(tag)) {
362
+ return false;
363
+ }
364
+ if (tag === 'g' || tag === 'svg') {
365
+ if (!walk(child, sx, sy, vbX, vbY, out)) {
366
+ return false;
367
+ }
368
+ continue;
369
+ }
370
+ const style = getComputedStyle(child);
371
+ if (hasGradientOrPattern(style.fill) || hasGradientOrPattern(style.stroke)) {
372
+ return false;
373
+ }
374
+ const paint = readSvgPaint(style);
375
+ switch (tag) {
376
+ case 'rect': {
377
+ const r = child;
378
+ out.push({
379
+ op: 'rect',
380
+ x: (num(r, 'x') - vbX) * sx,
381
+ y: (num(r, 'y') - vbY) * sy,
382
+ width: num(r, 'width') * sx,
383
+ height: num(r, 'height') * sy,
384
+ rx: num(r, 'rx') * sx,
385
+ ry: num(r, 'ry') * sy,
386
+ paint,
387
+ });
388
+ break;
389
+ }
390
+ case 'circle': {
391
+ const c = child;
392
+ const rr = num(c, 'r');
393
+ out.push({
394
+ op: 'ellipse',
395
+ cx: (num(c, 'cx') - vbX) * sx,
396
+ cy: (num(c, 'cy') - vbY) * sy,
397
+ rx: rr * sx,
398
+ ry: rr * sy,
399
+ paint,
400
+ });
401
+ break;
402
+ }
403
+ case 'ellipse': {
404
+ const e = child;
405
+ out.push({
406
+ op: 'ellipse',
407
+ cx: (num(e, 'cx') - vbX) * sx,
408
+ cy: (num(e, 'cy') - vbY) * sy,
409
+ rx: num(e, 'rx') * sx,
410
+ ry: num(e, 'ry') * sy,
411
+ paint,
412
+ });
413
+ break;
414
+ }
415
+ case 'line': {
416
+ const l = child;
417
+ out.push({
418
+ op: 'line',
419
+ x1: (num(l, 'x1') - vbX) * sx,
420
+ y1: (num(l, 'y1') - vbY) * sy,
421
+ x2: (num(l, 'x2') - vbX) * sx,
422
+ y2: (num(l, 'y2') - vbY) * sy,
423
+ paint,
424
+ });
425
+ break;
426
+ }
427
+ case 'polyline':
428
+ case 'polygon': {
429
+ const points = (child.getAttribute('points') || '').trim();
430
+ if (!points)
431
+ break;
432
+ const d = pointsToPathData(points, tag === 'polygon');
433
+ out.push({ op: 'path', d: scalePathData(offsetPathOrigin(d, vbX, vbY), sx, sy), paint });
434
+ break;
435
+ }
436
+ case 'path': {
437
+ const d = child.getAttribute('d') || '';
438
+ if (!d)
439
+ break;
440
+ out.push({ op: 'path', d: scalePathData(d, sx, sy), paint });
441
+ break;
442
+ }
443
+ }
444
+ }
445
+ return true;
446
+ }
447
+ function hasGradientOrPattern(paintRef) {
448
+ return paintRef.startsWith('url(');
449
+ }
450
+ function readSvgPaint(style) {
451
+ const fillOpacity = parseFloat(style.fillOpacity || '1');
452
+ const strokeOpacity = parseFloat(style.strokeOpacity || '1');
453
+ const opacity = parseFloat(style.opacity || '1');
454
+ const fillColor = style.fill && style.fill !== 'none' ? withAlpha(parseComputedColor(style.fill), fillOpacity) : undefined;
455
+ const strokeColor = style.stroke && style.stroke !== 'none' ? withAlpha(parseComputedColor(style.stroke), strokeOpacity) : undefined;
456
+ return {
457
+ fill: fillColor,
458
+ stroke: strokeColor,
459
+ strokeWidthPt: pxToPt(parseFloat(style.strokeWidth || '0')),
460
+ opacity: Number.isFinite(opacity) ? opacity : 1,
461
+ };
462
+ }
463
+ function withAlpha(color, alphaMultiplier) {
464
+ if (!color)
465
+ return undefined;
466
+ return { ...color, a: color.a * (Number.isFinite(alphaMultiplier) ? alphaMultiplier : 1) };
467
+ }
468
+ function num(el, attr) {
469
+ const v = el[attr]?.baseVal?.value;
470
+ if (typeof v === 'number')
471
+ return v;
472
+ return parseFloat(el.getAttribute(attr) || '0') || 0;
473
+ }
474
+ function pointsToPathData(points, close) {
475
+ const coords = points.split(/[\s,]+/).filter(Boolean).map(Number);
476
+ let d = '';
477
+ for (let i = 0; i < coords.length - 1; i += 2) {
478
+ d += (i === 0 ? 'M' : 'L') + coords[i] + ' ' + coords[i + 1] + ' ';
479
+ }
480
+ return close ? d + 'Z' : d;
481
+ }
482
+ function offsetPathOrigin(d, vbX, vbY) {
483
+ if (!vbX && !vbY)
484
+ return d;
485
+ // Points already carry absolute coordinates; shift by viewBox origin using the same scaler with a translate pass.
486
+ return d.replace(/(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)/g, (_m, x, y) => `${Number(x) - vbX} ${Number(y) - vbY}`);
487
+ }
488
+
489
+ let idCounter = 0;
490
+ function nextId(prefix) {
491
+ idCounter += 1;
492
+ return `${prefix}-${idCounter}`;
493
+ }
494
+ function inspectElement(root, options) {
495
+ const rootRect = root.getBoundingClientRect();
496
+ const assetTasks = [];
497
+ const warnings = [];
498
+ const children = walkChildren(root, rootRect, options, assetTasks, warnings);
499
+ return {
500
+ document: {
501
+ flowWidthPt: pxToPt(rootRect.width),
502
+ flowHeightPt: pxToPt(rootRect.height),
503
+ children,
504
+ },
505
+ assetTasks,
506
+ warnings,
507
+ };
508
+ }
509
+ function relativeRect(elRect, rootRect) {
510
+ return {
511
+ x: pxToPt(elRect.left - rootRect.left),
512
+ y: pxToPt(elRect.top - rootRect.top),
513
+ width: pxToPt(elRect.width),
514
+ height: pxToPt(elRect.height),
515
+ };
516
+ }
517
+ function walkChildren(parent, rootRect, options, assetTasks, warnings) {
518
+ const out = [];
519
+ for (const child of Array.from(parent.childNodes)) {
520
+ if (child.nodeType === Node.TEXT_NODE) {
521
+ out.push(...buildTextNodes(child, parent, rootRect));
522
+ }
523
+ else if (child.nodeType === Node.ELEMENT_NODE) {
524
+ const node = visitElement(child, rootRect, options, assetTasks, warnings);
525
+ if (node) {
526
+ out.push(node);
527
+ }
528
+ }
529
+ }
530
+ return out;
531
+ }
532
+ const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT']);
533
+ function visitElement(el, rootRect, options, assetTasks, warnings) {
534
+ if (SKIP_TAGS.has(el.tagName)) {
535
+ return null;
536
+ }
537
+ const style = getComputedStyle(el);
538
+ if (style.display === 'none') {
539
+ return null;
540
+ }
541
+ const elRect = el.getBoundingClientRect();
542
+ if (elRect.width <= 0 || elRect.height <= 0) {
543
+ return null;
544
+ }
545
+ const rect = relativeRect(elRect, rootRect);
546
+ const visible = style.visibility !== 'hidden' && parseFloat(style.opacity || '1') > 0;
547
+ const forceKeepTogether = options.keepTogetherSelectors.some((sel) => {
548
+ try {
549
+ return el.matches(sel);
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ });
555
+ const breakRules = readBreakRules(style, forceKeepTogether);
556
+ const tag = el.tagName.toLowerCase();
557
+ if (tag === 'img') {
558
+ return buildImageNode(el, rect, visible, breakRules, assetTasks);
559
+ }
560
+ if (tag === 'canvas') {
561
+ return buildCanvasNode(el, rect, visible, breakRules, assetTasks);
562
+ }
563
+ if (tag === 'svg') {
564
+ return buildSvgNode(el, rect, visible, breakRules, assetTasks, warnings);
565
+ }
566
+ if (tag === 'table') {
567
+ return buildTableNode(el, rootRect, rect, visible, breakRules, options, assetTasks, warnings);
568
+ }
569
+ const unsupported = checkUnsupported(style);
570
+ if (unsupported.unsupported) {
571
+ warnings.push({ message: unsupported.reason, elementDescription: describeElement$1(el) });
572
+ return buildFallbackImageNode(el, rect, visible, breakRules, unsupported.reason, assetTasks, elRect);
573
+ }
574
+ const paint = readBoxPaint(style);
575
+ const children = walkChildren(el, rootRect, options, assetTasks, warnings);
576
+ if (children.length === 0 && !paint.backgroundColor && !paint.borders) {
577
+ return null;
578
+ }
579
+ const clips = style.overflow !== 'visible' && (style.overflowX !== 'visible' || style.overflowY !== 'visible');
580
+ if (clips) {
581
+ const group = {
582
+ id: nextId('group'),
583
+ type: 'group',
584
+ rect,
585
+ visible,
586
+ break: breakRules,
587
+ paint,
588
+ children,
589
+ clip: true,
590
+ };
591
+ return group;
592
+ }
593
+ const node = {
594
+ id: nextId('block'),
595
+ type: 'block',
596
+ rect,
597
+ visible,
598
+ break: breakRules,
599
+ paint,
600
+ children,
601
+ };
602
+ return node;
603
+ }
604
+ function buildTextNodes(textNode, parentEl, rootRect) {
605
+ const raw = extractTextRuns(textNode);
606
+ if (raw.length === 0) {
607
+ return [];
608
+ }
609
+ const style = getComputedStyle(parentEl);
610
+ const font = readFont(style);
611
+ const color = parseComputedColor(style.color) ?? { r: 0, g: 0, b: 0, a: 1 };
612
+ const letterSpacingPt = style.letterSpacing === 'normal' ? 0 : pxToPt(parseFloat(style.letterSpacing || '0') || 0);
613
+ const align = normalizeAlign(style.textAlign, style.direction);
614
+ const decorationLine = style.textDecorationLine || style.textDecoration || '';
615
+ const decoration = {
616
+ underline: decorationLine.includes('underline'),
617
+ lineThrough: decorationLine.includes('line-through'),
618
+ };
619
+ return raw.map((run) => {
620
+ const rect = relativeRect(run.rect, rootRect);
621
+ return {
622
+ id: nextId('text'),
623
+ type: 'text',
624
+ rect,
625
+ visible: style.visibility !== 'hidden',
626
+ break: { breakInside: 'avoid', breakBefore: 'auto', breakAfter: 'auto' },
627
+ text: run.text,
628
+ font,
629
+ color,
630
+ letterSpacingPt,
631
+ align,
632
+ decoration,
633
+ baselineOffsetPt: rect.height * 0.8,
634
+ };
635
+ });
636
+ }
637
+ function readFont(style) {
638
+ const weight = parseInt(style.fontWeight || '400', 10) || (style.fontWeight === 'bold' ? 700 : 400);
639
+ const fontStyle = style.fontStyle === 'italic' || style.fontStyle === 'oblique' ? 'italic' : 'normal';
640
+ return {
641
+ family: (style.fontFamily || 'sans-serif').split(',')[0].trim().replace(/^["']|["']$/g, ''),
642
+ weight,
643
+ style: fontStyle,
644
+ sizePt: pxToPt(parseFloat(style.fontSize || '16')),
645
+ };
646
+ }
647
+ function normalizeAlign(textAlign, direction) {
648
+ const isRtl = direction === 'rtl';
649
+ switch (textAlign) {
650
+ case 'center':
651
+ return 'center';
652
+ case 'right':
653
+ return 'right';
654
+ case 'left':
655
+ return 'left';
656
+ case 'justify':
657
+ return 'justify';
658
+ case 'start':
659
+ return isRtl ? 'right' : 'left';
660
+ case 'end':
661
+ return isRtl ? 'left' : 'right';
662
+ default:
663
+ return 'left';
664
+ }
665
+ }
666
+ function buildImageNode(img, rect, visible, breakRules, assetTasks) {
667
+ const style = getComputedStyle(img);
668
+ const node = {
669
+ id: nextId('img'),
670
+ type: 'image',
671
+ rect,
672
+ visible,
673
+ break: breakRules,
674
+ source: { kind: 'url', value: '' },
675
+ fit: style.objectFit || 'fill',
676
+ naturalWidth: img.naturalWidth || img.width,
677
+ naturalHeight: img.naturalHeight || img.height,
678
+ isFallbackRaster: false,
679
+ };
680
+ assetTasks.push({ kind: 'img', node, element: img });
681
+ return node;
682
+ }
683
+ function buildCanvasNode(canvas, rect, visible, breakRules, assetTasks) {
684
+ const node = {
685
+ id: nextId('canvas'),
686
+ type: 'image',
687
+ rect,
688
+ visible,
689
+ break: breakRules,
690
+ source: { kind: 'canvas', value: '' },
691
+ fit: 'fill',
692
+ naturalWidth: canvas.width,
693
+ naturalHeight: canvas.height,
694
+ isFallbackRaster: false,
695
+ };
696
+ assetTasks.push({ kind: 'canvas', node, element: canvas });
697
+ return node;
698
+ }
699
+ function buildSvgNode(svg, rect, visible, breakRules, assetTasks, warnings) {
700
+ try {
701
+ const commands = parseSvg(svg, rect.width, rect.height);
702
+ if (commands) {
703
+ return {
704
+ id: nextId('svg'),
705
+ type: 'svg',
706
+ rect,
707
+ visible,
708
+ break: breakRules,
709
+ commands,
710
+ viewBox: rect,
711
+ };
712
+ }
713
+ }
714
+ catch {
715
+ // fall through to rasterization
716
+ }
717
+ warnings.push({ message: 'This <svg> uses gradients/filters/text/use and is not supported; rasterized.', elementDescription: describeElement$1(svg) });
718
+ return buildFallbackImageNode(svg, rect, visible, breakRules, 'unsupported SVG features', assetTasks, svg.getBoundingClientRect());
719
+ }
720
+ function buildFallbackImageNode(el, rect, visible, breakRules, reason, assetTasks, elRect) {
721
+ const node = {
722
+ id: nextId('fallback'),
723
+ type: 'image',
724
+ rect,
725
+ visible,
726
+ break: breakRules,
727
+ fallback: { reason },
728
+ source: { kind: 'data-url', value: '' },
729
+ fit: 'fill',
730
+ naturalWidth: rect.width,
731
+ naturalHeight: rect.height,
732
+ isFallbackRaster: true,
733
+ };
734
+ // Captured at the element's true on-screen size (not `rect`, which may
735
+ // later be shrunk by fit-to-width scaling) so the clone -- which still
736
+ // carries its original, unscaled inline styles -- isn't clipped inside
737
+ // its own raster. See docs/architecture.md "Hybrid rendering".
738
+ assetTasks.push({ kind: 'fallback', node, element: el, captureWidthPx: elRect.width, captureHeightPx: elRect.height });
739
+ return node;
740
+ }
741
+ function buildTableNode(table, rootRect, rect, visible, breakRules, options, assetTasks, warnings) {
742
+ const rowEls = Array.from(table.querySelectorAll('tr'));
743
+ const headerRowEls = new Set(Array.from(table.querySelectorAll('thead tr')));
744
+ const rows = rowEls.map((tr) => {
745
+ const trRect = relativeRect(tr.getBoundingClientRect(), rootRect);
746
+ const cellEls = Array.from(tr.children).filter((c) => c.tagName === 'TD' || c.tagName === 'TH');
747
+ const cells = cellEls.map((cellEl) => {
748
+ const cellRect = relativeRect(cellEl.getBoundingClientRect(), rootRect);
749
+ const cellStyle = getComputedStyle(cellEl);
750
+ const paint = readBoxPaint(cellStyle);
751
+ const content = walkChildren(cellEl, rootRect, options, assetTasks, warnings);
752
+ return {
753
+ id: nextId('cell'),
754
+ rect: cellRect,
755
+ isHeader: cellEl.tagName === 'TH',
756
+ paint,
757
+ content,
758
+ };
759
+ });
760
+ return {
761
+ id: nextId('row'),
762
+ rect: trRect,
763
+ isHeader: headerRowEls.has(tr),
764
+ cells,
765
+ };
766
+ });
767
+ const firstBodyRow = rows.find((r) => !r.isHeader) ?? rows[0];
768
+ const columns = (firstBodyRow?.cells ?? []).map((c) => ({ x: c.rect.x, width: c.rect.width }));
769
+ return {
770
+ id: nextId('table'),
771
+ type: 'table',
772
+ rect,
773
+ visible,
774
+ break: breakRules,
775
+ columns,
776
+ rows,
777
+ headerRowCount: rows.filter((r) => r.isHeader).length,
778
+ repeatHeader: true,
779
+ };
780
+ }
781
+ function describeElement$1(el) {
782
+ const id = el.id ? `#${el.id}` : '';
783
+ const cls = el.className && typeof el.className === 'string' ? `.${el.className.trim().split(/\s+/).join('.')}` : '';
784
+ return `<${el.tagName.toLowerCase()}${id}${cls}>`;
785
+ }
786
+
787
+ /**
788
+ * `padPx` reserves extra canvas space around the element's own border box
789
+ * for effects that visually bleed outside it (box-shadow, outline) --
790
+ * without this, capturing exactly the element's own rect would clip a
791
+ * shadow's blur/spread at the box edge. Callers should grow the node's
792
+ * placement rect by the same amount on every side and shift it up/left by
793
+ * `padPx` so the padded raster lands in the right place. See
794
+ * docs/css-support.md.
795
+ */
796
+ async function rasterizeElement(el, widthPx, heightPx, scale, padPx = 0) {
797
+ if (widthPx <= 0 || heightPx <= 0) {
798
+ throw new Error('ngx-pdf-export: cannot rasterize an element with zero width/height.');
799
+ }
800
+ const totalWidthPx = widthPx + padPx * 2;
801
+ const totalHeightPx = heightPx + padPx * 2;
802
+ const clone = el.cloneNode(true);
803
+ inlineComputedStylesRecursive(el, clone);
804
+ copyCanvasBitmaps(el, clone);
805
+ resolveImageSources(clone);
806
+ clone.style.margin = '0';
807
+ clone.style.transform = 'none';
808
+ clone.style.position = 'static';
809
+ const svgNS = 'http://www.w3.org/2000/svg';
810
+ const xhtmlNS = 'http://www.w3.org/1999/xhtml';
811
+ const svg = document.createElementNS(svgNS, 'svg');
812
+ svg.setAttribute('xmlns', svgNS);
813
+ svg.setAttribute('width', String(totalWidthPx));
814
+ svg.setAttribute('height', String(totalHeightPx));
815
+ svg.setAttribute('viewBox', `0 0 ${totalWidthPx} ${totalHeightPx}`);
816
+ const foreignObject = document.createElementNS(svgNS, 'foreignObject');
817
+ foreignObject.setAttribute('width', '100%');
818
+ foreignObject.setAttribute('height', '100%');
819
+ const wrapper = document.createElementNS(xhtmlNS, 'div');
820
+ wrapper.setAttribute('xmlns', xhtmlNS);
821
+ wrapper.style.width = `${widthPx}px`;
822
+ wrapper.style.height = `${heightPx}px`;
823
+ wrapper.style.padding = `${padPx}px`;
824
+ wrapper.style.boxSizing = 'content-box';
825
+ wrapper.appendChild(clone);
826
+ foreignObject.appendChild(wrapper);
827
+ svg.appendChild(foreignObject);
828
+ const svgString = new XMLSerializer().serializeToString(svg);
829
+ const svgDataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgString);
830
+ const img = await loadImage(svgDataUrl);
831
+ const canvas = document.createElement('canvas');
832
+ canvas.width = Math.ceil(totalWidthPx * scale);
833
+ canvas.height = Math.ceil(totalHeightPx * scale);
834
+ const ctx = canvas.getContext('2d');
835
+ if (!ctx) {
836
+ throw new Error('ngx-pdf-export: 2D canvas context unavailable.');
837
+ }
838
+ ctx.scale(scale, scale);
839
+ ctx.drawImage(img, 0, 0, totalWidthPx, totalHeightPx);
840
+ const dataUrl = canvas.toDataURL('image/png');
841
+ canvas.width = 0;
842
+ canvas.height = 0;
843
+ return { dataUrl, width: totalWidthPx, height: totalHeightPx };
844
+ }
845
+ function inlineComputedStylesRecursive(source, target) {
846
+ const computed = getComputedStyle(source);
847
+ const styleText = [];
848
+ for (let i = 0; i < computed.length; i++) {
849
+ const prop = computed.item(i);
850
+ styleText.push(`${prop}:${computed.getPropertyValue(prop)}`);
851
+ }
852
+ target.setAttribute('style', styleText.join(';'));
853
+ const sourceChildren = Array.from(source.children);
854
+ const targetChildren = Array.from(target.children);
855
+ for (let i = 0; i < sourceChildren.length; i++) {
856
+ if (targetChildren[i]) {
857
+ inlineComputedStylesRecursive(sourceChildren[i], targetChildren[i]);
858
+ }
859
+ }
860
+ }
861
+ function copyCanvasBitmaps(source, target) {
862
+ const sourceCanvases = Array.from(source.querySelectorAll('canvas'));
863
+ const targetCanvases = Array.from(target.querySelectorAll('canvas'));
864
+ for (let i = 0; i < sourceCanvases.length; i++) {
865
+ const src = sourceCanvases[i];
866
+ const dst = targetCanvases[i];
867
+ if (!dst)
868
+ continue;
869
+ dst.width = src.width;
870
+ dst.height = src.height;
871
+ const ctx = dst.getContext('2d');
872
+ try {
873
+ ctx?.drawImage(src, 0, 0);
874
+ }
875
+ catch {
876
+ // Cross-origin-tainted canvas: leave blank rather than throw.
877
+ }
878
+ }
879
+ }
880
+ function resolveImageSources(target) {
881
+ target.querySelectorAll('img').forEach((img) => {
882
+ const resolved = img.src; // IDL property is already absolute-resolved.
883
+ img.setAttribute('src', resolved);
884
+ });
885
+ }
886
+ function loadImage(src) {
887
+ return new Promise((resolve, reject) => {
888
+ const img = new Image();
889
+ img.onload = () => resolve(img);
890
+ img.onerror = () => reject(new Error('ngx-pdf-export: failed to rasterize fallback element.'));
891
+ img.src = src;
892
+ });
893
+ }
894
+
895
+ /** Extra capture margin (CSS px) around a rasterized fallback element, for box-shadow/outline bleed. See docs/css-support.md. */
896
+ const FALLBACK_CAPTURE_PAD_PX = 24;
897
+ /**
898
+ * Resolves every deferred asset (images, canvases, fallback rasterizations)
899
+ * to a data URL, mutating each task's node in place. Runs after layout so
900
+ * we only pay encode/fetch cost for assets that actually made it into the
901
+ * export. See docs/architecture.md "Performance" and docs/layout-engine.md
902
+ * §5.
903
+ */
904
+ async function resolveAssets(tasks, scale, onWarning) {
905
+ await Promise.all(tasks.map(async (task) => {
906
+ try {
907
+ if (task.kind === 'img') {
908
+ task.node.source = { kind: 'data-url', value: await imageToDataUrl(task.element) };
909
+ }
910
+ else if (task.kind === 'canvas') {
911
+ task.node.source = { kind: 'data-url', value: task.element.toDataURL('image/png') };
912
+ }
913
+ else {
914
+ const result = await rasterizeElement(task.element, task.captureWidthPx, task.captureHeightPx, scale, FALLBACK_CAPTURE_PAD_PX);
915
+ task.node.source = { kind: 'data-url', value: result.dataUrl };
916
+ const padPt = pxToPt(FALLBACK_CAPTURE_PAD_PX);
917
+ task.node.rect = {
918
+ x: task.node.rect.x - padPt,
919
+ y: task.node.rect.y - padPt,
920
+ width: task.node.rect.width + padPt * 2,
921
+ height: task.node.rect.height + padPt * 2,
922
+ };
923
+ task.node.naturalWidth = result.width;
924
+ task.node.naturalHeight = result.height;
925
+ }
926
+ }
927
+ catch (err) {
928
+ task.node.visible = false;
929
+ onWarning({
930
+ message: task.kind === 'img' ? `Image could not be embedded (${err.message}). This is often a CORS restriction -- the image's server must send Access-Control-Allow-Origin, and the <img> needs crossorigin="anonymous".` : `Failed to render element: ${err.message}`,
931
+ elementDescription: describeElement(task.element),
932
+ });
933
+ }
934
+ }));
935
+ }
936
+ async function imageToDataUrl(img) {
937
+ const src = img.currentSrc || img.src;
938
+ if (src.startsWith('data:')) {
939
+ return src;
940
+ }
941
+ const response = await fetch(src, { mode: 'cors' });
942
+ if (!response.ok) {
943
+ throw new Error(`HTTP ${response.status}`);
944
+ }
945
+ const blob = await response.blob();
946
+ return await blobToDataUrl(blob);
947
+ }
948
+ function blobToDataUrl(blob) {
949
+ return new Promise((resolve, reject) => {
950
+ const reader = new FileReader();
951
+ reader.onload = () => resolve(reader.result);
952
+ reader.onerror = () => reject(new Error('failed to read image blob'));
953
+ reader.readAsDataURL(blob);
954
+ });
955
+ }
956
+ function describeElement(el) {
957
+ const id = el.id ? `#${el.id}` : '';
958
+ return `<${el.tagName.toLowerCase()}${id}>`;
959
+ }
960
+
961
+ /**
962
+ * Uniformly shrinks the whole captured flow (never enlarges) so its width
963
+ * fits the page's content width, the same "shrink to fit" behavior a
964
+ * browser's own print dialog applies to an over-wide page. Without this,
965
+ * any dashboard wider than the chosen page format (a very common case --
966
+ * most web layouts are wider than A4) would simply be clipped at the page
967
+ * edge. Every geometric quantity in the tree (rects, font sizes, border
968
+ * widths/radii, SVG coordinates) is scaled together so nothing distorts.
969
+ */
970
+ function fitDocumentToWidth(doc, contentWidthPt) {
971
+ if (doc.flowWidthPt <= contentWidthPt || doc.flowWidthPt <= 0) {
972
+ return 1;
973
+ }
974
+ const scale = contentWidthPt / doc.flowWidthPt;
975
+ doc.flowWidthPt *= scale;
976
+ doc.flowHeightPt *= scale;
977
+ doc.children.forEach((child) => scaleNode(child, scale));
978
+ return scale;
979
+ }
980
+ function scaleRect(rect, scale) {
981
+ return { x: rect.x * scale, y: rect.y * scale, width: rect.width * scale, height: rect.height * scale };
982
+ }
983
+ function scaleNode(node, scale) {
984
+ node.rect = scaleRect(node.rect, scale);
985
+ switch (node.type) {
986
+ case 'text':
987
+ node.font = { ...node.font, sizePt: node.font.sizePt * scale };
988
+ node.letterSpacingPt *= scale;
989
+ node.baselineOffsetPt *= scale;
990
+ return;
991
+ case 'image':
992
+ node.naturalWidth *= scale;
993
+ node.naturalHeight *= scale;
994
+ return;
995
+ case 'svg':
996
+ node.commands = node.commands.map((cmd) => scaleSvgCommand(cmd, scale));
997
+ node.viewBox = scaleRect(node.viewBox, scale);
998
+ return;
999
+ case 'block':
1000
+ case 'group':
1001
+ scalePaintBorders(node, scale);
1002
+ node.children.forEach((child) => scaleNode(child, scale));
1003
+ return;
1004
+ case 'table':
1005
+ node.columns = node.columns.map((c) => ({ x: c.x * scale, width: c.width * scale }));
1006
+ node.rows = node.rows.map((row) => scaleTableRow(row, scale));
1007
+ return;
1008
+ }
1009
+ }
1010
+ function scaleTableRow(row, scale) {
1011
+ return {
1012
+ ...row,
1013
+ rect: scaleRect(row.rect, scale),
1014
+ cells: row.cells.map((cell) => scaleTableCell(cell, scale)),
1015
+ };
1016
+ }
1017
+ function scaleTableCell(cell, scale) {
1018
+ scalePaintBorders(cell, scale);
1019
+ return {
1020
+ ...cell,
1021
+ rect: scaleRect(cell.rect, scale),
1022
+ content: cell.content.map((child) => {
1023
+ scaleNode(child, scale);
1024
+ return child;
1025
+ }),
1026
+ };
1027
+ }
1028
+ function scalePaintBorders(node, scale) {
1029
+ const paint = node.paint;
1030
+ if (paint.borders) {
1031
+ for (const edge of Object.values(paint.borders)) {
1032
+ edge.widthPt *= scale;
1033
+ }
1034
+ }
1035
+ if (paint.borderRadius) {
1036
+ for (const key of Object.keys(paint.borderRadius)) {
1037
+ paint.borderRadius[key] *= scale;
1038
+ }
1039
+ }
1040
+ }
1041
+ function scaleSvgCommand(cmd, scale) {
1042
+ const paint = { ...cmd.paint, strokeWidthPt: cmd.paint.strokeWidthPt * scale };
1043
+ switch (cmd.op) {
1044
+ case 'rect':
1045
+ return { ...cmd, x: cmd.x * scale, y: cmd.y * scale, width: cmd.width * scale, height: cmd.height * scale, rx: cmd.rx * scale, ry: cmd.ry * scale, paint };
1046
+ case 'ellipse':
1047
+ return { ...cmd, cx: cmd.cx * scale, cy: cmd.cy * scale, rx: cmd.rx * scale, ry: cmd.ry * scale, paint };
1048
+ case 'line':
1049
+ return { ...cmd, x1: cmd.x1 * scale, y1: cmd.y1 * scale, x2: cmd.x2 * scale, y2: cmd.y2 * scale, paint };
1050
+ case 'path':
1051
+ return { ...cmd, d: scalePathData(cmd.d, scale, scale), paint };
1052
+ }
1053
+ }
1054
+
1055
+ const EPS = 0.01;
1056
+ function paginate(doc, contentBox) {
1057
+ const CH = contentBox.contentHeightPt;
1058
+ const pages = [];
1059
+ const state = { shift: 0, hasPlaced: false, lastPageTouched: -1 };
1060
+ const ensurePage = (idx) => {
1061
+ while (pages.length <= idx)
1062
+ pages.push({ primitives: [] });
1063
+ return pages[idx];
1064
+ };
1065
+ const clipForPage = (clip, page) => (clip ? { ...clip, y: clip.y - page * CH } : undefined);
1066
+ /** Places an atomic node (never split): moves it whole to the next page if it straddles a boundary or has a forced break-before. */
1067
+ function pushAtomic(rect, breakBefore) {
1068
+ let adjustedY = rect.y + state.shift;
1069
+ let page = Math.floor(adjustedY / CH);
1070
+ if (breakBefore === 'page' && state.hasPlaced && page === state.lastPageTouched) {
1071
+ state.shift += (state.lastPageTouched + 1) * CH - adjustedY;
1072
+ adjustedY = rect.y + state.shift;
1073
+ page = Math.floor(adjustedY / CH);
1074
+ }
1075
+ const endPage = Math.floor((adjustedY + rect.height - EPS) / CH);
1076
+ if (endPage !== page) {
1077
+ state.shift += (page + 1) * CH - adjustedY;
1078
+ adjustedY = rect.y + state.shift;
1079
+ page = Math.floor(adjustedY / CH);
1080
+ }
1081
+ state.hasPlaced = true;
1082
+ state.lastPageTouched = page;
1083
+ ensurePage(page);
1084
+ return { page, localY: adjustedY - page * CH };
1085
+ }
1086
+ function forceBreakAfter(bottomAdjusted) {
1087
+ const bottomPage = Math.floor((bottomAdjusted - EPS) / CH);
1088
+ state.shift += (bottomPage + 1) * CH - bottomAdjusted;
1089
+ state.lastPageTouched = bottomPage + 1;
1090
+ }
1091
+ function placeContainerBackground(node, adjustedTop, adjustedBottom, clip) {
1092
+ const hasPaint = !!(node.paint.backgroundColor || node.paint.borders || node.paint.backgroundImage);
1093
+ if (!hasPaint)
1094
+ return;
1095
+ const firstPage = Math.max(0, Math.floor(adjustedTop / CH));
1096
+ const lastPage = Math.floor((adjustedBottom - EPS) / CH);
1097
+ for (let p = firstPage; p <= lastPage; p++) {
1098
+ const pageTop = p * CH;
1099
+ const pageBottom = pageTop + CH;
1100
+ const vTop = Math.max(adjustedTop, pageTop);
1101
+ const vBottom = Math.min(adjustedBottom, pageBottom);
1102
+ if (vBottom <= vTop)
1103
+ continue;
1104
+ const rect = { x: node.rect.x, y: vTop - pageTop, width: node.rect.width, height: vBottom - vTop };
1105
+ ensurePage(p).primitives.push({ kind: 'block', rect, node, clip: clipForPage(clip, p) });
1106
+ }
1107
+ }
1108
+ function placeMedia(node, clip) {
1109
+ if (node.rect.height <= CH) {
1110
+ const { page, localY } = pushAtomic(node.rect, node.break.breakBefore);
1111
+ ensurePage(page).primitives.push({ kind: node.type, rect: { ...node.rect, y: localY }, node, clip: clipForPage(clip, page) });
1112
+ if (node.break.breakAfter === 'page')
1113
+ forceBreakAfter(page * CH + localY + node.rect.height);
1114
+ return;
1115
+ }
1116
+ // Oversized media: clip a slice onto every page it intersects (docs/pagination.md step 3).
1117
+ const adjustedTop = node.rect.y + state.shift;
1118
+ const adjustedBottom = adjustedTop + node.rect.height;
1119
+ state.hasPlaced = true;
1120
+ const firstPage = Math.max(0, Math.floor(adjustedTop / CH));
1121
+ const lastPage = Math.floor((adjustedBottom - EPS) / CH);
1122
+ for (let p = firstPage; p <= lastPage; p++) {
1123
+ const pageTop = p * CH;
1124
+ const pageBottom = pageTop + CH;
1125
+ const vTop = Math.max(adjustedTop, pageTop);
1126
+ const vBottom = Math.min(adjustedBottom, pageBottom);
1127
+ if (vBottom <= vTop)
1128
+ continue;
1129
+ const rect = { x: node.rect.x, y: vTop - pageTop, width: node.rect.width, height: vBottom - vTop };
1130
+ ensurePage(p).primitives.push({ kind: node.type, rect, node, clip: clipForPage(clip, p) });
1131
+ }
1132
+ state.lastPageTouched = lastPage;
1133
+ }
1134
+ function placeNode(node, clip) {
1135
+ if (!node.visible)
1136
+ return;
1137
+ if (node.type === 'text') {
1138
+ const { page, localY } = pushAtomic(node.rect, node.break.breakBefore);
1139
+ ensurePage(page).primitives.push({ kind: 'text', rect: { ...node.rect, y: localY }, node, clip: clipForPage(clip, page) });
1140
+ if (node.break.breakAfter === 'page')
1141
+ forceBreakAfter(page * CH + localY + node.rect.height);
1142
+ return;
1143
+ }
1144
+ if (node.type === 'image' || node.type === 'svg') {
1145
+ placeMedia(node, clip);
1146
+ return;
1147
+ }
1148
+ if (node.type === 'table') {
1149
+ placeTable(node, clip);
1150
+ return;
1151
+ }
1152
+ // block | group
1153
+ const keepTogether = node.break.breakInside === 'avoid';
1154
+ const fitsOnePage = node.rect.height <= CH;
1155
+ if (keepTogether && fitsOnePage) {
1156
+ const { page, localY } = pushAtomic(node.rect, node.break.breakBefore);
1157
+ const adjustedTop = page * CH + localY;
1158
+ const adjustedBottom = adjustedTop + node.rect.height;
1159
+ placeContainerBackground(node, adjustedTop, adjustedBottom, clip);
1160
+ const childClip = node.type === 'group' && node.clip ? { x: node.rect.x, y: adjustedTop, width: node.rect.width, height: node.rect.height } : clip;
1161
+ for (const child of node.children)
1162
+ placeNode(child, childClip);
1163
+ if (node.break.breakAfter === 'page')
1164
+ forceBreakAfter(adjustedBottom);
1165
+ return;
1166
+ }
1167
+ // A keepTogether node taller than a full page can't actually be kept together;
1168
+ // it falls through to normal per-child splitting below.
1169
+ if (node.break.breakBefore === 'page') {
1170
+ const adjustedY = node.rect.y + state.shift;
1171
+ const page = Math.floor(adjustedY / CH);
1172
+ if (state.hasPlaced && page === state.lastPageTouched) {
1173
+ state.shift += (state.lastPageTouched + 1) * CH - adjustedY;
1174
+ }
1175
+ }
1176
+ const shiftAtStart = state.shift;
1177
+ const adjustedTop = node.rect.y + shiftAtStart;
1178
+ const childClip = node.type === 'group' && node.clip ? { x: node.rect.x, y: adjustedTop, width: node.rect.width, height: node.rect.height } : clip;
1179
+ for (const child of node.children)
1180
+ placeNode(child, childClip);
1181
+ const shiftAtEnd = state.shift;
1182
+ const adjustedBottom = node.rect.y + node.rect.height + shiftAtEnd;
1183
+ placeContainerBackground(node, adjustedTop, adjustedBottom, clip);
1184
+ state.hasPlaced = true;
1185
+ if (node.break.breakAfter === 'page') {
1186
+ forceBreakAfter(adjustedBottom);
1187
+ }
1188
+ else {
1189
+ state.lastPageTouched = Math.max(state.lastPageTouched, Math.floor((adjustedBottom - EPS) / CH));
1190
+ }
1191
+ }
1192
+ function emitFixed(node, page, dy, clip) {
1193
+ const rect = { ...node.rect, y: node.rect.y + dy };
1194
+ if (node.type === 'text' || node.type === 'image' || node.type === 'svg') {
1195
+ ensurePage(page).primitives.push({ kind: node.type, rect, node, clip: clipForPage(clip, page) });
1196
+ }
1197
+ else if (node.type === 'block' || node.type === 'group') {
1198
+ if (node.paint.backgroundColor || node.paint.borders || node.paint.backgroundImage) {
1199
+ ensurePage(page).primitives.push({ kind: 'block', rect, node, clip: clipForPage(clip, page) });
1200
+ }
1201
+ for (const child of node.children)
1202
+ emitFixed(child, page, dy, clip);
1203
+ }
1204
+ }
1205
+ function placeRowVisual(row, page, localY, clip) {
1206
+ ensurePage(page);
1207
+ const dy = localY - row.rect.y;
1208
+ for (const cell of row.cells) {
1209
+ if (cell.paint.backgroundColor || cell.paint.borders) {
1210
+ const rect = { x: cell.rect.x, y: localY, width: cell.rect.width, height: row.rect.height };
1211
+ const fakeCellBlock = { id: cell.id, type: 'block', rect, visible: true, break: { breakInside: 'avoid', breakBefore: 'auto', breakAfter: 'auto' }, paint: cell.paint, children: [] };
1212
+ ensurePage(page).primitives.push({ kind: 'block', rect, node: fakeCellBlock, clip: clipForPage(clip, page) });
1213
+ }
1214
+ for (const child of cell.content)
1215
+ emitFixed(child, page, dy, clip);
1216
+ }
1217
+ }
1218
+ function placeTable(table, clip) {
1219
+ const headerRows = table.rows.filter((r) => r.isHeader);
1220
+ const bodyRows = table.rows.filter((r) => !r.isHeader);
1221
+ let previousBodyPage = -1;
1222
+ for (const hRow of headerRows) {
1223
+ const { page, localY } = pushAtomic(hRow.rect, 'auto');
1224
+ placeRowVisual(hRow, page, localY, clip);
1225
+ previousBodyPage = page;
1226
+ }
1227
+ for (const row of bodyRows) {
1228
+ const { page, localY } = pushAtomic(row.rect, 'auto');
1229
+ let finalLocalY = localY;
1230
+ if (table.repeatHeader && headerRows.length > 0 && page !== previousBodyPage) {
1231
+ let offset = 0;
1232
+ for (const hRow of headerRows) {
1233
+ placeRowVisual(hRow, page, offset, clip);
1234
+ offset += hRow.rect.height;
1235
+ }
1236
+ if (localY < offset) {
1237
+ state.shift += offset - localY;
1238
+ finalLocalY = offset;
1239
+ state.lastPageTouched = page;
1240
+ }
1241
+ }
1242
+ placeRowVisual(row, page, finalLocalY, clip);
1243
+ previousBodyPage = page;
1244
+ }
1245
+ }
1246
+ for (const child of doc.children)
1247
+ placeNode(child, undefined);
1248
+ return { pages };
1249
+ }
1250
+ /**
1251
+ * Flattens a small, self-contained node list (header/footer content) into
1252
+ * primitives with no page-break logic -- used for content that is assumed
1253
+ * to fit within its own reserved header/footer box. See docs/pagination.md
1254
+ * "Headers / footers".
1255
+ */
1256
+ function flattenLayoutNodes(nodes) {
1257
+ const out = [];
1258
+ const visit = (node) => {
1259
+ if (!node.visible)
1260
+ return;
1261
+ if (node.type === 'text' || node.type === 'image' || node.type === 'svg') {
1262
+ out.push({ kind: node.type, rect: node.rect, node });
1263
+ }
1264
+ else if (node.type === 'table') {
1265
+ for (const row of node.rows) {
1266
+ for (const cell of row.cells) {
1267
+ cell.content.forEach(visit);
1268
+ }
1269
+ }
1270
+ }
1271
+ else {
1272
+ if (node.paint.backgroundColor || node.paint.borders || node.paint.backgroundImage) {
1273
+ out.push({ kind: 'block', rect: node.rect, node });
1274
+ }
1275
+ for (const child of node.children)
1276
+ visit(child);
1277
+ }
1278
+ };
1279
+ for (const node of nodes)
1280
+ visit(node);
1281
+ return out;
1282
+ }
1283
+
1284
+ function resolvePageGeometry(format, orientation, margin, extraTopPt, extraBottomPt) {
1285
+ let widthPt;
1286
+ let heightPt;
1287
+ if (!format || format === 'A4') {
1288
+ ({ widthPt, heightPt } = STANDARD_PAGE_SIZES_PT['A4']);
1289
+ }
1290
+ else if (typeof format === 'string') {
1291
+ const preset = STANDARD_PAGE_SIZES_PT[format];
1292
+ if (!preset)
1293
+ throw new Error(`ngx-pdf-export: unknown page format "${format}".`);
1294
+ ({ widthPt, heightPt } = preset);
1295
+ }
1296
+ else {
1297
+ const unit = format.unit ?? 'mm';
1298
+ const convert = unit === 'pt' ? (n) => n : unit === 'px' ? pxToPt : mmToPt;
1299
+ widthPt = convert(format.width);
1300
+ heightPt = convert(format.height);
1301
+ }
1302
+ if (orientation === 'landscape' && widthPt < heightPt) {
1303
+ [widthPt, heightPt] = [heightPt, widthPt];
1304
+ }
1305
+ else if (orientation === 'portrait' && widthPt > heightPt) {
1306
+ [widthPt, heightPt] = [heightPt, widthPt];
1307
+ }
1308
+ const resolvedMargin = resolveMargin(margin);
1309
+ const contentWidthPt = widthPt - resolvedMargin.left - resolvedMargin.right;
1310
+ const contentHeightPt = heightPt - resolvedMargin.top - resolvedMargin.bottom - extraTopPt - extraBottomPt;
1311
+ if (contentWidthPt <= 0 || contentHeightPt <= 0) {
1312
+ throw new Error('ngx-pdf-export: margins (and header/footer height) leave no room for content on the page.');
1313
+ }
1314
+ return { widthPt, heightPt, margin: resolvedMargin, contentWidthPt, contentHeightPt };
1315
+ }
1316
+ function resolveMargin(margin) {
1317
+ if (margin === undefined) {
1318
+ const mm10 = mmToPt(10);
1319
+ return { top: mm10, right: mm10, bottom: mm10, left: mm10 };
1320
+ }
1321
+ if (typeof margin === 'number') {
1322
+ const pt = mmToPt(margin);
1323
+ return { top: pt, right: pt, bottom: pt, left: pt };
1324
+ }
1325
+ const mm10 = mmToPt(10);
1326
+ return {
1327
+ top: margin.top !== undefined ? mmToPt(margin.top) : mm10,
1328
+ right: margin.right !== undefined ? mmToPt(margin.right) : mm10,
1329
+ bottom: margin.bottom !== undefined ? mmToPt(margin.bottom) : mm10,
1330
+ left: margin.left !== undefined ? mmToPt(margin.left) : mm10,
1331
+ };
1332
+ }
1333
+
1334
+ /**
1335
+ * Resolves a text run's CSS-derived font to an embedded PDFFont, preferring
1336
+ * a developer-registered custom font (full Unicode support, subset-embedded
1337
+ * via fontkit) and falling back to the nearest Standard-14 font (WinAnsi
1338
+ * only -- see docs/css-support.md "Unicode and non-Latin scripts").
1339
+ */
1340
+ class FontResolver {
1341
+ pdfDoc;
1342
+ registry;
1343
+ cache = new Map();
1344
+ constructor(pdfDoc, registry) {
1345
+ this.pdfDoc = pdfDoc;
1346
+ this.registry = registry;
1347
+ }
1348
+ async resolve(font) {
1349
+ const key = `${font.family}|${font.weight}|${font.style}`;
1350
+ const cached = this.cache.get(key);
1351
+ if (cached)
1352
+ return cached;
1353
+ const custom = this.registry.resolve(font.family, font.weight, font.style);
1354
+ let embedded;
1355
+ if (custom) {
1356
+ embedded = await this.pdfDoc.embedFont(custom.bytes, { subset: true });
1357
+ }
1358
+ else {
1359
+ embedded = await this.pdfDoc.embedFont(pickStandardFont(font));
1360
+ }
1361
+ this.cache.set(key, embedded);
1362
+ return embedded;
1363
+ }
1364
+ }
1365
+ function pickStandardFont(font) {
1366
+ const family = font.family.toLowerCase();
1367
+ const bold = font.weight >= 600;
1368
+ const italic = font.style === 'italic';
1369
+ if (/mono|courier|consolas|menlo/.test(family)) {
1370
+ if (bold && italic)
1371
+ return StandardFonts.CourierBoldOblique;
1372
+ if (bold)
1373
+ return StandardFonts.CourierBold;
1374
+ if (italic)
1375
+ return StandardFonts.CourierOblique;
1376
+ return StandardFonts.Courier;
1377
+ }
1378
+ if (/times|serif|georgia|garamond/.test(family)) {
1379
+ if (bold && italic)
1380
+ return StandardFonts.TimesRomanBoldItalic;
1381
+ if (bold)
1382
+ return StandardFonts.TimesRomanBold;
1383
+ if (italic)
1384
+ return StandardFonts.TimesRomanItalic;
1385
+ return StandardFonts.TimesRoman;
1386
+ }
1387
+ if (bold && italic)
1388
+ return StandardFonts.HelveticaBoldOblique;
1389
+ if (bold)
1390
+ return StandardFonts.HelveticaBold;
1391
+ if (italic)
1392
+ return StandardFonts.HelveticaOblique;
1393
+ return StandardFonts.Helvetica;
1394
+ }
1395
+
1396
+ async function renderPdf(pages, fontRegistry, options, headerFooter) {
1397
+ const pdfDoc = await PDFDocument.create();
1398
+ pdfDoc.registerFontkit(fontkit);
1399
+ const fontResolver = new FontResolver(pdfDoc, fontRegistry);
1400
+ const imageCache = new Map();
1401
+ const warnedGlyphs = new Set();
1402
+ for (let i = 0; i < pages.length; i++) {
1403
+ const page = pages[i];
1404
+ const pdfPage = pdfDoc.addPage([options.geometry.widthPt, options.geometry.heightPt]);
1405
+ for (const primitive of page.primitives) {
1406
+ await drawPrimitive(pdfDoc, pdfPage, primitive, options, fontResolver, imageCache, warnedGlyphs);
1407
+ }
1408
+ const headerPrimitives = headerFooter?.headerPrimitivesPerPage?.[i];
1409
+ if (headerPrimitives?.length && headerFooter?.headerGeometry) {
1410
+ const headerOptions = { ...options, geometry: headerFooter.headerGeometry };
1411
+ for (const primitive of headerPrimitives) {
1412
+ await drawPrimitive(pdfDoc, pdfPage, primitive, headerOptions, fontResolver, imageCache, warnedGlyphs);
1413
+ }
1414
+ }
1415
+ const footerPrimitives = headerFooter?.footerPrimitivesPerPage?.[i];
1416
+ if (footerPrimitives?.length && headerFooter?.footerGeometry) {
1417
+ const footerOptions = { ...options, geometry: headerFooter.footerGeometry };
1418
+ for (const primitive of footerPrimitives) {
1419
+ await drawPrimitive(pdfDoc, pdfPage, primitive, footerOptions, fontResolver, imageCache, warnedGlyphs);
1420
+ }
1421
+ }
1422
+ if (options.debug) {
1423
+ pdfPage.drawRectangle({
1424
+ x: 0,
1425
+ y: 0,
1426
+ width: options.geometry.widthPt,
1427
+ height: options.geometry.heightPt,
1428
+ borderColor: rgb(1, 0, 0),
1429
+ borderWidth: 1,
1430
+ borderDashArray: [4, 4],
1431
+ });
1432
+ }
1433
+ }
1434
+ return pdfDoc.save();
1435
+ }
1436
+ function rgbFrom(c) {
1437
+ return rgb(c.r / 255, c.g / 255, c.b / 255);
1438
+ }
1439
+ function toPdfSpace(rect, geometry) {
1440
+ const x = geometry.marginLeftPt + rect.x;
1441
+ const topY = geometry.marginTopPt + rect.y;
1442
+ const bottomYPdf = geometry.heightPt - (topY + rect.height);
1443
+ const topYPdf = geometry.heightPt - topY;
1444
+ return { x, topY: topYPdf, bottomY: bottomYPdf, width: rect.width, height: rect.height };
1445
+ }
1446
+ async function drawPrimitive(pdfDoc, pdfPage, primitive, options, fontResolver, imageCache, warnedGlyphs) {
1447
+ const clipRect = primitive.clip ? toPdfSpace(primitive.clip, options.geometry) : undefined;
1448
+ if (clipRect) {
1449
+ pushClip(pdfPage, clipRect);
1450
+ }
1451
+ try {
1452
+ switch (primitive.kind) {
1453
+ case 'block':
1454
+ drawBox(pdfPage, primitive.node, primitive.rect, options);
1455
+ if (options.debug && primitive.node.fallback) {
1456
+ drawDebugOutline(pdfPage, primitive.rect, options.geometry);
1457
+ }
1458
+ break;
1459
+ case 'text':
1460
+ await drawText(pdfPage, primitive.node, primitive.rect, options, fontResolver, warnedGlyphs);
1461
+ break;
1462
+ case 'image':
1463
+ await drawImage(pdfDoc, pdfPage, primitive.node, primitive.rect, options, imageCache);
1464
+ if (options.debug && primitive.node.isFallbackRaster) {
1465
+ drawDebugOutline(pdfPage, primitive.rect, options.geometry);
1466
+ }
1467
+ break;
1468
+ case 'svg':
1469
+ drawSvg(pdfPage, primitive.node, primitive.rect, options.geometry);
1470
+ break;
1471
+ }
1472
+ }
1473
+ finally {
1474
+ if (clipRect) {
1475
+ popClip(pdfPage);
1476
+ }
1477
+ }
1478
+ }
1479
+ function pushClip(page, r) {
1480
+ page.pushOperators(pushGraphicsState(), moveTo(r.x, r.bottomY), lineTo(r.x + r.width, r.bottomY), lineTo(r.x + r.width, r.bottomY + r.height), lineTo(r.x, r.bottomY + r.height), closePath(), clip(), endPath());
1481
+ }
1482
+ function popClip(page) {
1483
+ page.pushOperators(popGraphicsState());
1484
+ }
1485
+ function drawDebugOutline(page, rect, geometry) {
1486
+ const p = toPdfSpace(rect, geometry);
1487
+ page.drawRectangle({ x: p.x, y: p.bottomY, width: p.width, height: p.height, borderColor: rgb(1, 0, 0), borderWidth: 1, borderDashArray: [2, 2] });
1488
+ }
1489
+ // ---- box (background/border/border-radius) ----
1490
+ function drawBox(page, node, rect, options) {
1491
+ const p = toPdfSpace(rect, options.geometry);
1492
+ const paint = node.paint;
1493
+ const radius = paint.borderRadius;
1494
+ const hasRadius = !!radius && (radius.topLeft || radius.topRight || radius.bottomRight || radius.bottomLeft);
1495
+ if (hasRadius) {
1496
+ const d = roundedRectPath(p.width, p.height, radius);
1497
+ const drawOpts = { x: p.x, y: p.topY };
1498
+ if (paint.backgroundColor && paint.backgroundColor.a > 0) {
1499
+ drawOpts.color = rgbFrom(paint.backgroundColor);
1500
+ drawOpts.opacity = paint.backgroundColor.a * paint.opacity;
1501
+ }
1502
+ const edge = firstPaintedEdge(paint.borders);
1503
+ if (edge) {
1504
+ drawOpts.borderColor = rgbFrom(edge.color);
1505
+ drawOpts.borderWidth = edge.widthPt;
1506
+ drawOpts.borderOpacity = edge.color.a * paint.opacity;
1507
+ applyDash(drawOpts, edge);
1508
+ }
1509
+ if (drawOpts.color || drawOpts.borderColor) {
1510
+ page.drawSvgPath(d, drawOpts);
1511
+ }
1512
+ return;
1513
+ }
1514
+ if (paint.backgroundColor && paint.backgroundColor.a > 0) {
1515
+ page.drawRectangle({ x: p.x, y: p.bottomY, width: p.width, height: p.height, color: rgbFrom(paint.backgroundColor), opacity: paint.backgroundColor.a * paint.opacity });
1516
+ }
1517
+ if (paint.borders) {
1518
+ drawEdgeLine(page, paint.borders.top, p.x, p.topY, p.x + p.width, p.topY, paint.opacity);
1519
+ drawEdgeLine(page, paint.borders.bottom, p.x, p.bottomY, p.x + p.width, p.bottomY, paint.opacity);
1520
+ drawEdgeLine(page, paint.borders.left, p.x, p.bottomY, p.x, p.topY, paint.opacity);
1521
+ drawEdgeLine(page, paint.borders.right, p.x + p.width, p.bottomY, p.x + p.width, p.topY, paint.opacity);
1522
+ }
1523
+ }
1524
+ function firstPaintedEdge(borders) {
1525
+ if (!borders)
1526
+ return undefined;
1527
+ return [borders.top, borders.right, borders.bottom, borders.left].find((e) => e.style !== 'none' && e.widthPt > 0);
1528
+ }
1529
+ function applyDash(drawOpts, edge) {
1530
+ if (edge.style === 'dashed') {
1531
+ drawOpts.borderDashArray = [edge.widthPt * 2.5, edge.widthPt * 2];
1532
+ }
1533
+ else if (edge.style === 'dotted') {
1534
+ drawOpts.borderDashArray = [edge.widthPt * 0.6, edge.widthPt * 1.4];
1535
+ }
1536
+ }
1537
+ function drawEdgeLine(page, edge, x1, y1, x2, y2, groupOpacity) {
1538
+ if (edge.style === 'none' || edge.widthPt <= 0)
1539
+ return;
1540
+ const dashArray = edge.style === 'dashed' ? [edge.widthPt * 2.5, edge.widthPt * 2] : edge.style === 'dotted' ? [edge.widthPt * 0.6, edge.widthPt * 1.4] : undefined;
1541
+ page.drawLine({ start: { x: x1, y: y1 }, end: { x: x2, y: y2 }, thickness: edge.widthPt, color: rgbFrom(edge.color), opacity: edge.color.a * groupOpacity, dashArray });
1542
+ }
1543
+ function roundedRectPath(w, h, r) {
1544
+ const tl = Math.min(r.topLeft, w / 2, h / 2);
1545
+ const tr = Math.min(r.topRight, w / 2, h / 2);
1546
+ const br = Math.min(r.bottomRight, w / 2, h / 2);
1547
+ const bl = Math.min(r.bottomLeft, w / 2, h / 2);
1548
+ return [
1549
+ `M ${tl} 0`,
1550
+ `L ${w - tr} 0`,
1551
+ `Q ${w} 0 ${w} ${tr}`,
1552
+ `L ${w} ${h - br}`,
1553
+ `Q ${w} ${h} ${w - br} ${h}`,
1554
+ `L ${bl} ${h}`,
1555
+ `Q 0 ${h} 0 ${h - bl}`,
1556
+ `L 0 ${tl}`,
1557
+ `Q 0 0 ${tl} 0`,
1558
+ 'Z',
1559
+ ].join(' ');
1560
+ }
1561
+ // ---- text ----
1562
+ async function drawText(page, node, rect, options, fontResolver, warnedGlyphs) {
1563
+ const font = await fontResolver.resolve(node.font);
1564
+ const p = toPdfSpace(rect, options.geometry);
1565
+ const size = node.font.sizePt;
1566
+ const baselineY = options.geometry.heightPt - (options.geometry.marginTopPt + rect.y + node.baselineOffsetPt);
1567
+ const color = rgbFrom(node.color);
1568
+ const opacity = node.color.a;
1569
+ const textWidth = measureText(font, node.text, size, node.letterSpacingPt);
1570
+ let x = p.x;
1571
+ if (node.align === 'center')
1572
+ x = p.x + Math.max(0, (rect.width - textWidth) / 2);
1573
+ else if (node.align === 'right')
1574
+ x = p.x + Math.max(0, rect.width - textWidth);
1575
+ drawGlyphsSafely(page, font, node.text, x, baselineY, size, color, opacity, node.letterSpacingPt, warnedGlyphs, (msg) => options.onWarning({ message: msg, elementDescription: node.text.slice(0, 40) }));
1576
+ if (node.decoration.underline) {
1577
+ page.drawLine({ start: { x, y: baselineY - size * 0.08 }, end: { x: x + textWidth, y: baselineY - size * 0.08 }, thickness: Math.max(0.5, size * 0.05), color, opacity });
1578
+ }
1579
+ if (node.decoration.lineThrough) {
1580
+ page.drawLine({ start: { x, y: baselineY + size * 0.3 }, end: { x: x + textWidth, y: baselineY + size * 0.3 }, thickness: Math.max(0.5, size * 0.05), color, opacity });
1581
+ }
1582
+ }
1583
+ function measureText(font, text, size, letterSpacingPt) {
1584
+ let w = 0;
1585
+ for (const ch of text)
1586
+ w += safeWidth(font, ch, size);
1587
+ return w + letterSpacingPt * Math.max(0, [...text].length - 1);
1588
+ }
1589
+ function safeWidth(font, ch, size) {
1590
+ try {
1591
+ return font.widthOfTextAtSize(ch, size);
1592
+ }
1593
+ catch {
1594
+ return size * 0.5;
1595
+ }
1596
+ }
1597
+ function drawGlyphsSafely(page, font, text, x0, y, size, color, opacity, letterSpacingPt, warnedGlyphs, onWarning) {
1598
+ if (letterSpacingPt === 0 && canEncodeAll(font, text)) {
1599
+ page.drawText(text, { x: x0, y, size, font, color, opacity });
1600
+ return;
1601
+ }
1602
+ let x = x0;
1603
+ for (const ch of text) {
1604
+ if (canEncodeAll(font, ch)) {
1605
+ page.drawText(ch, { x, y, size, font, color, opacity });
1606
+ }
1607
+ else if (!warnedGlyphs.has(ch)) {
1608
+ warnedGlyphs.add(ch);
1609
+ onWarning(`Character "${ch}" has no glyph in the resolved font and was skipped. Register a Unicode-capable font via registerFont() for this text.`);
1610
+ }
1611
+ x += safeWidth(font, ch, size) + letterSpacingPt;
1612
+ }
1613
+ }
1614
+ function canEncodeAll(font, text) {
1615
+ try {
1616
+ font.widthOfTextAtSize(text, 10);
1617
+ return true;
1618
+ }
1619
+ catch {
1620
+ return false;
1621
+ }
1622
+ }
1623
+ // ---- images ----
1624
+ async function drawImage(pdfDoc, page, node, rect, options, cache) {
1625
+ if (!node.source.value)
1626
+ return;
1627
+ let embedded = cache.get(node.source.value);
1628
+ if (!embedded) {
1629
+ const decoded = decodeDataUrl(node.source.value);
1630
+ if (!decoded)
1631
+ return;
1632
+ embedded = decoded.isPng ? await pdfDoc.embedPng(decoded.bytes) : await pdfDoc.embedJpg(decoded.bytes);
1633
+ cache.set(node.source.value, embedded);
1634
+ }
1635
+ const box = toPdfSpace(rect, options.geometry);
1636
+ const natW = node.naturalWidth || embedded.width;
1637
+ const natH = node.naturalHeight || embedded.height;
1638
+ let drawW = box.width;
1639
+ let drawH = box.height;
1640
+ let drawX = box.x;
1641
+ let drawTopY = options.geometry.marginTopPt + rect.y;
1642
+ if ((node.fit === 'contain' || node.fit === 'cover') && natW > 0 && natH > 0) {
1643
+ const scale = node.fit === 'contain' ? Math.min(box.width / natW, box.height / natH) : Math.max(box.width / natW, box.height / natH);
1644
+ drawW = natW * scale;
1645
+ drawH = natH * scale;
1646
+ drawX = box.x + (box.width - drawW) / 2;
1647
+ drawTopY = options.geometry.marginTopPt + rect.y + (box.height - drawH) / 2;
1648
+ }
1649
+ else if (node.fit === 'none' && natW > 0 && natH > 0) {
1650
+ drawW = natW;
1651
+ drawH = natH;
1652
+ }
1653
+ const drawBottomYPdf = options.geometry.heightPt - (drawTopY + drawH);
1654
+ const needsClip = drawW > box.width + 0.5 || drawH > box.height + 0.5;
1655
+ if (needsClip) {
1656
+ pushClip(page, box);
1657
+ }
1658
+ page.drawImage(embedded, { x: drawX, y: drawBottomYPdf, width: drawW, height: drawH });
1659
+ if (needsClip) {
1660
+ popClip(page);
1661
+ }
1662
+ }
1663
+ function decodeDataUrl(dataUrl) {
1664
+ const match = /^data:image\/(png|jpe?g);base64,(.*)$/i.exec(dataUrl);
1665
+ if (!match)
1666
+ return null;
1667
+ const isPng = match[1].toLowerCase() === 'png';
1668
+ const binary = atob(match[2]);
1669
+ const bytes = new Uint8Array(binary.length);
1670
+ for (let i = 0; i < binary.length; i++)
1671
+ bytes[i] = binary.charCodeAt(i);
1672
+ return { bytes, isPng };
1673
+ }
1674
+ // ---- svg ----
1675
+ function drawSvg(page, node, rect, geometry) {
1676
+ const p = toPdfSpace(rect, geometry);
1677
+ for (const cmd of node.commands) {
1678
+ drawSvgCommand(page, cmd, p.x, p.topY);
1679
+ }
1680
+ }
1681
+ function drawSvgCommand(page, cmd, originX, originTopY) {
1682
+ const paint = cmd.paint;
1683
+ const drawOpts = {};
1684
+ if (paint.fill) {
1685
+ drawOpts.color = rgbFrom(paint.fill);
1686
+ drawOpts.opacity = paint.fill.a * paint.opacity;
1687
+ }
1688
+ if (paint.stroke && paint.strokeWidthPt > 0) {
1689
+ drawOpts.borderColor = rgbFrom(paint.stroke);
1690
+ drawOpts.borderWidth = paint.strokeWidthPt;
1691
+ drawOpts.borderOpacity = paint.stroke.a * paint.opacity;
1692
+ }
1693
+ if (!drawOpts.color && !drawOpts.borderColor)
1694
+ return;
1695
+ switch (cmd.op) {
1696
+ case 'rect': {
1697
+ const hasRadius = cmd.rx > 0 || cmd.ry > 0;
1698
+ if (hasRadius) {
1699
+ const d = roundedRectPath(cmd.width, cmd.height, { topLeft: cmd.rx, topRight: cmd.rx, bottomRight: cmd.rx, bottomLeft: cmd.rx });
1700
+ page.drawSvgPath(d, { ...drawOpts, x: originX + cmd.x, y: originTopY - cmd.y });
1701
+ }
1702
+ else {
1703
+ page.drawRectangle({ x: originX + cmd.x, y: originTopY - cmd.y - cmd.height, width: cmd.width, height: cmd.height, color: drawOpts.color, opacity: drawOpts.opacity, borderColor: drawOpts.borderColor, borderWidth: drawOpts.borderWidth, borderOpacity: drawOpts.borderOpacity });
1704
+ }
1705
+ break;
1706
+ }
1707
+ case 'ellipse':
1708
+ page.drawEllipse({ x: originX + cmd.cx, y: originTopY - cmd.cy, xScale: cmd.rx, yScale: cmd.ry, color: drawOpts.color, opacity: drawOpts.opacity, borderColor: drawOpts.borderColor, borderWidth: drawOpts.borderWidth, borderOpacity: drawOpts.borderOpacity });
1709
+ break;
1710
+ case 'line':
1711
+ if (drawOpts.borderColor) {
1712
+ page.drawLine({ start: { x: originX + cmd.x1, y: originTopY - cmd.y1 }, end: { x: originX + cmd.x2, y: originTopY - cmd.y2 }, thickness: drawOpts.borderWidth, color: drawOpts.borderColor, opacity: drawOpts.borderOpacity });
1713
+ }
1714
+ break;
1715
+ case 'path':
1716
+ page.drawSvgPath(cmd.d, { ...drawOpts, x: originX, y: originTopY });
1717
+ break;
1718
+ }
1719
+ }
1720
+
1721
+ /**
1722
+ * Builds the layout nodes for one page's header or footer callback result.
1723
+ * A returned string becomes a single default-styled text run; a returned
1724
+ * HTMLElement is run through the same DOM-inspection path as the body
1725
+ * (briefly attached off-screen so getBoundingClientRect/getComputedStyle
1726
+ * are meaningful), per docs/pagination.md "Headers / footers".
1727
+ */
1728
+ async function buildHeaderFooterNodes(result, boxWidthPt, boxHeightPt, scale, onWarning) {
1729
+ if (typeof result === 'string') {
1730
+ if (!result.trim())
1731
+ return [];
1732
+ const sizePt = 10;
1733
+ const node = {
1734
+ id: 'header-footer-text',
1735
+ type: 'text',
1736
+ rect: { x: 0, y: 0, width: boxWidthPt, height: boxHeightPt },
1737
+ visible: true,
1738
+ break: { breakInside: 'avoid', breakBefore: 'auto', breakAfter: 'auto' },
1739
+ text: result,
1740
+ font: { family: 'Helvetica', weight: 400, style: 'normal', sizePt },
1741
+ color: { r: 80, g: 80, b: 80, a: 1 },
1742
+ letterSpacingPt: 0,
1743
+ align: 'left',
1744
+ decoration: { underline: false, lineThrough: false },
1745
+ baselineOffsetPt: boxHeightPt / 2 + sizePt * 0.35,
1746
+ };
1747
+ return [node];
1748
+ }
1749
+ const host = document.createElement('div');
1750
+ host.style.position = 'fixed';
1751
+ host.style.top = '0';
1752
+ host.style.left = '-99999px';
1753
+ host.style.width = `${boxWidthPt / 0.75}px`;
1754
+ host.appendChild(result);
1755
+ document.body.appendChild(host);
1756
+ try {
1757
+ const { document: doc, assetTasks, warnings } = inspectElement(result, { keepTogetherSelectors: [] });
1758
+ warnings.forEach(onWarning);
1759
+ await resolveAssets(assetTasks, scale, onWarning);
1760
+ return doc.children;
1761
+ }
1762
+ finally {
1763
+ document.body.removeChild(host);
1764
+ }
1765
+ }
1766
+ function pageContext(pageNumber, pageCount) {
1767
+ return { pageNumber, pageCount };
1768
+ }
1769
+
1770
+ async function runExport(root, options, fontRegistry) {
1771
+ validateTarget(root);
1772
+ const warnings = [];
1773
+ const onWarning = (w) => {
1774
+ warnings.push(w);
1775
+ options.onWarning?.(w);
1776
+ // eslint-disable-next-line no-console
1777
+ console.warn(`[ngx-pdf-export] ${w.message} (${w.elementDescription})`);
1778
+ };
1779
+ const scale = options.scale ?? 2;
1780
+ const headerHeightPt = options.header ? options.headerHeight ?? 24 : 0;
1781
+ const footerHeightPt = options.footer ? options.footerHeight ?? 24 : 0;
1782
+ const geometry = resolvePageGeometry(options.format, options.orientation, options.margin, headerHeightPt, footerHeightPt);
1783
+ const { document: doc, assetTasks, warnings: inspectWarnings } = inspectElement(root, {
1784
+ keepTogetherSelectors: options.keepTogether ?? [],
1785
+ });
1786
+ inspectWarnings.forEach(onWarning);
1787
+ const fitScale = fitDocumentToWidth(doc, geometry.contentWidthPt);
1788
+ if (fitScale < 1) {
1789
+ onWarning({ message: `Content is wider than the page (by ${Math.round((1 / fitScale - 1) * 100)}%) and was scaled down to fit.`, elementDescription: `<${root.tagName.toLowerCase()}>` });
1790
+ }
1791
+ await resolveAssets(assetTasks, scale, onWarning);
1792
+ if (options.repeatTableHeaders === false) {
1793
+ disableTableHeaderRepeat(doc.children);
1794
+ }
1795
+ const paginated = paginate(doc, { contentHeightPt: geometry.contentHeightPt });
1796
+ const pageCount = paginated.pages.length || 1;
1797
+ const bodyGeometry = {
1798
+ widthPt: geometry.widthPt,
1799
+ heightPt: geometry.heightPt,
1800
+ marginTopPt: geometry.margin.top + headerHeightPt,
1801
+ marginLeftPt: geometry.margin.left,
1802
+ };
1803
+ let headerFooter;
1804
+ if (options.header || options.footer) {
1805
+ const headerGeometry = { widthPt: geometry.widthPt, heightPt: geometry.heightPt, marginTopPt: geometry.margin.top, marginLeftPt: geometry.margin.left };
1806
+ const footerGeometry = { widthPt: geometry.widthPt, heightPt: geometry.heightPt, marginTopPt: geometry.heightPt - geometry.margin.bottom - footerHeightPt, marginLeftPt: geometry.margin.left };
1807
+ const headerPrimitivesPerPage = options.header
1808
+ ? await Promise.all(Array.from({ length: pageCount }, (_, i) => i).map(async (i) => {
1809
+ const result = options.header(pageContext(i + 1, pageCount));
1810
+ const nodes = await buildHeaderFooterNodes(result, geometry.contentWidthPt, headerHeightPt, scale, onWarning);
1811
+ return flattenLayoutNodes(nodes);
1812
+ }))
1813
+ : undefined;
1814
+ const footerPrimitivesPerPage = options.footer
1815
+ ? await Promise.all(Array.from({ length: pageCount }, (_, i) => i).map(async (i) => {
1816
+ const result = options.footer(pageContext(i + 1, pageCount));
1817
+ const nodes = await buildHeaderFooterNodes(result, geometry.contentWidthPt, footerHeightPt, scale, onWarning);
1818
+ return flattenLayoutNodes(nodes);
1819
+ }))
1820
+ : undefined;
1821
+ headerFooter = { headerPrimitivesPerPage, footerPrimitivesPerPage, headerGeometry, footerGeometry };
1822
+ }
1823
+ const bytes = await renderPdf(paginated.pages, fontRegistry, { geometry: bodyGeometry, debug: !!options.debug, onWarning }, headerFooter);
1824
+ return new Blob([bytes], { type: 'application/pdf' });
1825
+ }
1826
+ function validateTarget(root) {
1827
+ if (!root) {
1828
+ throw new Error('ngx-pdf-export: export target not found.');
1829
+ }
1830
+ if (!root.isConnected) {
1831
+ throw new Error('ngx-pdf-export: export target is not attached to the document. The element must be rendered (not display:none, not in a detached view) at export time.');
1832
+ }
1833
+ const rect = root.getBoundingClientRect();
1834
+ if (rect.width <= 0 || rect.height <= 0) {
1835
+ throw new Error('ngx-pdf-export: export target has zero width or height.');
1836
+ }
1837
+ }
1838
+ function disableTableHeaderRepeat(nodes) {
1839
+ for (const node of nodes) {
1840
+ if (node.type === 'table') {
1841
+ node.repeatHeader = false;
1842
+ }
1843
+ else if (node.type === 'block' || node.type === 'group') {
1844
+ disableTableHeaderRepeat(node.children);
1845
+ }
1846
+ }
1847
+ }
1848
+
1849
+ /**
1850
+ * Holds developer-registered custom fonts (see docs/api-design.md "Font
1851
+ * registration"). Resolution is family match -> nearest registered weight
1852
+ * for that family+style, falling back to the opposite style if no exact
1853
+ * style match exists. Standard-14 fallback happens in render/fonts.ts,
1854
+ * outside this class, since it needs a live PDFDocument to embed against.
1855
+ */
1856
+ class FontRegistry {
1857
+ sources = [];
1858
+ async register(font) {
1859
+ const bytes = typeof font.src === 'string' ? await fetchBytes(font.src) : font.src;
1860
+ this.sources.push({
1861
+ family: font.family,
1862
+ weight: font.weight ?? 400,
1863
+ style: font.style ?? 'normal',
1864
+ bytes,
1865
+ });
1866
+ }
1867
+ resolve(family, weight, style) {
1868
+ const candidates = this.sources.filter((s) => s.family.toLowerCase() === family.toLowerCase());
1869
+ if (candidates.length === 0) {
1870
+ return undefined;
1871
+ }
1872
+ const styleMatches = candidates.filter((c) => c.style === style);
1873
+ const pool = styleMatches.length > 0 ? styleMatches : candidates;
1874
+ return pool.reduce((best, c) => (Math.abs(c.weight - weight) < Math.abs(best.weight - weight) ? c : best));
1875
+ }
1876
+ hasAnyFontFor(family) {
1877
+ return this.sources.some((s) => s.family.toLowerCase() === family.toLowerCase());
1878
+ }
1879
+ }
1880
+ async function fetchBytes(url) {
1881
+ const response = await fetch(url);
1882
+ if (!response.ok) {
1883
+ throw new Error(`ngx-pdf-export: failed to fetch font "${url}" (HTTP ${response.status}).`);
1884
+ }
1885
+ return response.arrayBuffer();
1886
+ }
1887
+
1888
+ /**
1889
+ * Accepts either `(target, options)` or a single `{ element, ...options }`
1890
+ * object -- see docs/api-design.md.
1891
+ */
1892
+ function normalizeArgs(target, options) {
1893
+ if (isOptionsWithElement(target)) {
1894
+ const { element, ...rest } = target;
1895
+ return { element, opts: rest };
1896
+ }
1897
+ return { element: target, opts: options ?? {} };
1898
+ }
1899
+ function isOptionsWithElement(value) {
1900
+ return typeof value === 'object' && value !== null && !(value instanceof ElementRef) && !(value instanceof HTMLElement) && 'element' in value;
1901
+ }
1902
+ function resolveElement(target) {
1903
+ if (typeof target === 'string') {
1904
+ const found = document.querySelector(target);
1905
+ if (!found) {
1906
+ throw new Error(`ngx-pdf-export: no element matches selector "${target}".`);
1907
+ }
1908
+ return found;
1909
+ }
1910
+ if (target instanceof ElementRef) {
1911
+ return target.nativeElement;
1912
+ }
1913
+ return target;
1914
+ }
1915
+
1916
+ /**
1917
+ * The library's public entry point. See docs/api-design.md "Public surface".
1918
+ * `export`/`download`/`toBlob` all accept either `(target, options)` or a
1919
+ * single `{ element, ...options }` object.
1920
+ */
1921
+ class PdfExportService {
1922
+ fontRegistry = new FontRegistry();
1923
+ /** Registers a custom TTF/OTF font for embedding -- see docs/api-design.md "Font registration". */
1924
+ async registerFont(font) {
1925
+ await this.fontRegistry.register(font);
1926
+ }
1927
+ /** Builds the PDF and returns it as a Blob, without triggering a download. */
1928
+ async toBlob(target, options) {
1929
+ const { element, opts } = normalizeArgs(target, options);
1930
+ const root = resolveElement(element);
1931
+ return runExport(root, opts, this.fontRegistry);
1932
+ }
1933
+ /** Builds the PDF and triggers a browser download. Alias for `download()` with the brief's example call shape. */
1934
+ async export(target, options) {
1935
+ const { element, opts } = normalizeArgs(target, options);
1936
+ const blob = await this.toBlob(element, opts);
1937
+ triggerDownload(blob, opts.filename ?? 'document.pdf');
1938
+ }
1939
+ /** Builds the PDF and triggers a browser download with an explicit filename. */
1940
+ async download(target, filename, options) {
1941
+ const { element, opts } = normalizeArgs(target, options);
1942
+ const finalFilename = filename ?? opts.filename ?? 'document.pdf';
1943
+ const blob = await this.toBlob(element, opts);
1944
+ triggerDownload(blob, finalFilename);
1945
+ }
1946
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PdfExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1947
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PdfExportService, providedIn: 'root' });
1948
+ }
1949
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PdfExportService, decorators: [{
1950
+ type: Injectable,
1951
+ args: [{ providedIn: 'root' }]
1952
+ }] });
1953
+ function triggerDownload(blob, filename) {
1954
+ const url = URL.createObjectURL(blob);
1955
+ const link = document.createElement('a');
1956
+ link.href = url;
1957
+ link.download = filename;
1958
+ link.style.display = 'none';
1959
+ document.body.appendChild(link);
1960
+ link.click();
1961
+ document.body.removeChild(link);
1962
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1963
+ }
1964
+
1965
+ /**
1966
+ * Thin wrapper around PdfExportService for template-driven use:
1967
+ *
1968
+ * ```html
1969
+ * <div id="dashboard" pdfExport pdfFileName="dashboard.pdf" #dash="pdfExport">
1970
+ * ...
1971
+ * </div>
1972
+ * <button (click)="dash.export()">Export</button>
1973
+ * ```
1974
+ *
1975
+ * By default this does not attach its own click handler -- the host element
1976
+ * shown in the brief's example is the content being exported, not
1977
+ * necessarily a button. Set `pdfTrigger="click"` to export when the host
1978
+ * element itself is clicked.
1979
+ */
1980
+ class PdfExportDirective {
1981
+ elementRef;
1982
+ pdf;
1983
+ pdfFileName;
1984
+ pdfOptions;
1985
+ pdfTrigger = 'none';
1986
+ constructor(elementRef, pdf) {
1987
+ this.elementRef = elementRef;
1988
+ this.pdf = pdf;
1989
+ }
1990
+ onHostClick() {
1991
+ if (this.pdfTrigger === 'click') {
1992
+ void this.export();
1993
+ }
1994
+ }
1995
+ async export() {
1996
+ await this.pdf.download(this.elementRef.nativeElement, this.pdfFileName, this.pdfOptions);
1997
+ }
1998
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PdfExportDirective, deps: [{ token: i0.ElementRef }, { token: PdfExportService }], target: i0.ɵɵFactoryTarget.Directive });
1999
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: PdfExportDirective, isStandalone: true, selector: "[pdfExport]", inputs: { pdfFileName: "pdfFileName", pdfOptions: "pdfOptions", pdfTrigger: "pdfTrigger" }, host: { listeners: { "click": "onHostClick()" } }, exportAs: ["pdfExport"], ngImport: i0 });
2000
+ }
2001
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: PdfExportDirective, decorators: [{
2002
+ type: Directive,
2003
+ args: [{
2004
+ selector: '[pdfExport]',
2005
+ exportAs: 'pdfExport',
2006
+ }]
2007
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: PdfExportService }], propDecorators: { pdfFileName: [{
2008
+ type: Input
2009
+ }], pdfOptions: [{
2010
+ type: Input
2011
+ }], pdfTrigger: [{
2012
+ type: Input
2013
+ }], onHostClick: [{
2014
+ type: HostListener,
2015
+ args: ['click']
2016
+ }] } });
2017
+
2018
+ /*
2019
+ * Public API surface of ngx-pdf-export.
2020
+ * Keep this small and curated -- see docs/api-design.md "Public surface".
2021
+ */
2022
+
2023
+ /**
2024
+ * Generated bundle index. Do not edit.
2025
+ */
2026
+
2027
+ export { PdfExportDirective, PdfExportService };
2028
+ //# sourceMappingURL=ngx-pdf-export.mjs.map