@lupinum/nuxt-pdf 0.3.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.
Files changed (75) hide show
  1. package/API_REPORT.md +262 -0
  2. package/CHANGELOG.md +168 -0
  3. package/CONFORMANCE.md +496 -0
  4. package/LICENSE +21 -0
  5. package/README.md +203 -0
  6. package/THIRD_PARTY_NOTICES.md +75 -0
  7. package/dist/module.d.mts +24 -0
  8. package/dist/module.json +12 -0
  9. package/dist/module.mjs +368 -0
  10. package/dist/runtime/authoring.d.ts +141 -0
  11. package/dist/runtime/authoring.js +25 -0
  12. package/dist/runtime/components/_props.d.ts +196 -0
  13. package/dist/runtime/components/_props.js +63 -0
  14. package/dist/runtime/components/document.d.ts +9 -0
  15. package/dist/runtime/components/document.js +26 -0
  16. package/dist/runtime/components/index.d.ts +4 -0
  17. package/dist/runtime/components/index.js +27 -0
  18. package/dist/runtime/components/svg.d.ts +17 -0
  19. package/dist/runtime/components/svg.js +50 -0
  20. package/dist/runtime/composables/index.d.ts +2 -0
  21. package/dist/runtime/composables/index.js +3 -0
  22. package/dist/runtime/composables/use-pdf-page-numbers.d.ts +39 -0
  23. package/dist/runtime/composables/use-pdf-page-numbers.js +17 -0
  24. package/dist/runtime/define-pdf.d.ts +6 -0
  25. package/dist/runtime/define-pdf.js +5 -0
  26. package/dist/runtime/fonts.d.ts +16 -0
  27. package/dist/runtime/fonts.js +0 -0
  28. package/dist/runtime/renderer/index.d.ts +3 -0
  29. package/dist/runtime/renderer/index.js +1 -0
  30. package/dist/runtime/renderer/node-ops.d.ts +5 -0
  31. package/dist/runtime/renderer/node-ops.js +281 -0
  32. package/dist/runtime/renderer/patch-prop.d.ts +3 -0
  33. package/dist/runtime/renderer/patch-prop.js +223 -0
  34. package/dist/runtime/renderer/render-component.d.ts +14 -0
  35. package/dist/runtime/renderer/render-component.js +201 -0
  36. package/dist/runtime/renderer/types.d.ts +33 -0
  37. package/dist/runtime/renderer/types.js +56 -0
  38. package/dist/runtime/renderer/validate-tree.d.ts +3 -0
  39. package/dist/runtime/renderer/validate-tree.js +428 -0
  40. package/dist/runtime/server/assets/errors.d.ts +12 -0
  41. package/dist/runtime/server/assets/errors.js +15 -0
  42. package/dist/runtime/server/assets/remote.d.ts +48 -0
  43. package/dist/runtime/server/assets/remote.js +234 -0
  44. package/dist/runtime/server/assets/resolve-asset.d.ts +53 -0
  45. package/dist/runtime/server/assets/resolve-asset.js +482 -0
  46. package/dist/runtime/server/engine/CONTRACTS.md +386 -0
  47. package/dist/runtime/server/engine/fonts.d.ts +8 -0
  48. package/dist/runtime/server/engine/fonts.js +19 -0
  49. package/dist/runtime/server/engine/layout-passes.d.ts +42 -0
  50. package/dist/runtime/server/engine/layout-passes.js +58 -0
  51. package/dist/runtime/server/engine/react-pdf-pdfkit.d.ts +7 -0
  52. package/dist/runtime/server/engine/render-document.d.ts +31 -0
  53. package/dist/runtime/server/engine/render-document.js +236 -0
  54. package/dist/runtime/server/index.d.ts +5 -0
  55. package/dist/runtime/server/index.js +9 -0
  56. package/dist/runtime/server/preview.d.ts +17 -0
  57. package/dist/runtime/server/preview.js +332 -0
  58. package/dist/runtime/server/registry.d.ts +41 -0
  59. package/dist/runtime/server/registry.js +270 -0
  60. package/dist/runtime/server/render-limits.d.ts +51 -0
  61. package/dist/runtime/server/render-limits.js +143 -0
  62. package/dist/runtime/server/result.d.ts +6 -0
  63. package/dist/runtime/server/result.js +81 -0
  64. package/dist/runtime/server/tsconfig.json +3 -0
  65. package/dist/runtime/shared/errors.d.ts +22 -0
  66. package/dist/runtime/shared/errors.js +22 -0
  67. package/dist/runtime/shared/index.d.ts +4 -0
  68. package/dist/runtime/shared/index.js +7 -0
  69. package/dist/runtime/shared/template.d.ts +63 -0
  70. package/dist/runtime/shared/template.js +1 -0
  71. package/dist/shared/nuxt-pdf.D2ZziYn4.mjs +1132 -0
  72. package/dist/test.d.mts +198 -0
  73. package/dist/test.mjs +696 -0
  74. package/dist/types.d.mts +13 -0
  75. package/package.json +135 -0
package/dist/test.mjs ADDED
@@ -0,0 +1,696 @@
1
+ import { createRequire } from 'node:module';
2
+ import { defineComponent, h } from 'vue';
3
+ import { normalizeRemoteAssetPolicy } from '../dist/runtime/server/assets/remote.js';
4
+ import { createPdfTemplate } from '../dist/runtime/server/registry.js';
5
+ import { normalizePdfLimits, resolvePdfRenderLimits } from '../dist/runtime/server/render-limits.js';
6
+ import { PDF_DEFINITION_PROPERTY } from '../dist/runtime/shared/template.js';
7
+ import { Buffer } from 'node:buffer';
8
+ import { existsSync } from 'node:fs';
9
+ import { mkdir, writeFile, rm, readFile, readdir } from 'node:fs/promises';
10
+ import { resolve, dirname, join, relative, basename } from 'node:path';
11
+ import { pathToFileURL, fileURLToPath } from 'node:url';
12
+ import { build } from 'esbuild';
13
+ import { t as templateKeyFromRelativePath, b as discoverPdfImageFiles, c as bundlePdfFonts, g as compilePdfSfc } from './shared/nuxt-pdf.D2ZziYn4.mjs';
14
+ import { loadPdfImageAsset } from '../dist/runtime/server/assets/resolve-asset.js';
15
+ import '@vue/compiler-sfc';
16
+ import '@jridgewell/remapping';
17
+ import 'unimport';
18
+
19
+ const DEFAULT_CHANNEL_THRESHOLD = 25;
20
+ const DEFAULT_MAX_CHANGED_PIXEL_RATIO = 5e-3;
21
+ const require_ = createRequire(import.meta.url);
22
+ let pdfJsPromise;
23
+ let canvasModule;
24
+ const missingPeerMessage = (name) => `@lupinum/nuxt-pdf/test needs the optional peer dependency "${name}", which is not installed. Install it in the project under test, e.g. \`pnpm add -D ${name}\`.`;
25
+ const isModuleNotFound = (error) => typeof error === "object" && error !== null && "code" in error && (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND");
26
+ function loadCanvas() {
27
+ if (canvasModule) return canvasModule;
28
+ try {
29
+ canvasModule = require_("@napi-rs/canvas");
30
+ } catch (error) {
31
+ if (isModuleNotFound(error)) {
32
+ throw new Error(missingPeerMessage("@napi-rs/canvas"), { cause: error });
33
+ }
34
+ throw error;
35
+ }
36
+ return canvasModule;
37
+ }
38
+ class NapiCanvasFactory {
39
+ create(width, height) {
40
+ const canvas = loadCanvas().createCanvas(width, height);
41
+ return {
42
+ canvas,
43
+ context: canvas.getContext("2d")
44
+ };
45
+ }
46
+ reset(canvasAndContext, width, height) {
47
+ canvasAndContext.canvas.width = width;
48
+ canvasAndContext.canvas.height = height;
49
+ }
50
+ destroy(canvasAndContext) {
51
+ canvasAndContext.canvas.width = 0;
52
+ canvasAndContext.canvas.height = 0;
53
+ }
54
+ }
55
+ function installPdfCanvasGlobals() {
56
+ const { DOMMatrix, ImageData, Path2D } = loadCanvas();
57
+ const globals = globalThis;
58
+ globals.DOMMatrix ??= DOMMatrix;
59
+ globals.ImageData ??= ImageData;
60
+ globals.Path2D ??= Path2D;
61
+ }
62
+ function normalizePdfText(text) {
63
+ return text.replace(/\s+/g, " ").trim();
64
+ }
65
+ async function toPdfBytes(input) {
66
+ if (input instanceof Uint8Array) return input;
67
+ if (input instanceof ArrayBuffer) return new Uint8Array(input);
68
+ if (typeof input === "object" && input !== null && "toUint8Array" in input && typeof input.toUint8Array === "function") {
69
+ return input.toUint8Array();
70
+ }
71
+ throw new TypeError(
72
+ "parsePdf expected PDF bytes (Uint8Array/ArrayBuffer) or a PdfRenderResult."
73
+ );
74
+ }
75
+ const simplifyOutline = (items) => items.map((item) => {
76
+ const children = Array.isArray(item.items) ? simplifyOutline(item.items) : [];
77
+ return {
78
+ title: typeof item.title === "string" ? item.title : "",
79
+ ...children.length === 0 ? {} : { expanded: typeof item.count === "number" && item.count > 0 },
80
+ children
81
+ };
82
+ });
83
+ const flattenLinks = (pages) => pages.flatMap(
84
+ (page) => page.annotations.filter((annotation) => annotation.subtype === "Link").map((annotation) => {
85
+ const destination = typeof annotation.destination === "string" ? annotation.destination : void 0;
86
+ return {
87
+ page: page.number,
88
+ ...destination === void 0 ? {} : { destination },
89
+ ...annotation.url === void 0 ? {} : { url: annotation.url }
90
+ };
91
+ })
92
+ );
93
+ async function parsePdf(input) {
94
+ return withPdfDocument(await toPdfBytes(input), async (document) => {
95
+ const pages = [];
96
+ for (let pageNumber = 1; pageNumber <= document.numPages; pageNumber += 1) {
97
+ const page = await document.getPage(pageNumber);
98
+ try {
99
+ const textContent = await page.getTextContent();
100
+ const textItems = textContent.items.flatMap((item) => "str" in item ? [item.str] : []);
101
+ const textRuns = textContent.items.flatMap((item) => {
102
+ if (!("str" in item)) return [];
103
+ const [scaleX = 0, scaleY = 0, , , x = 0, y = 0] = item.transform;
104
+ return [{
105
+ direction: item.dir,
106
+ fontName: item.fontName,
107
+ fontSize: roundCoordinate(Math.hypot(scaleX, scaleY)),
108
+ height: roundCoordinate(item.height),
109
+ text: item.str,
110
+ width: roundCoordinate(item.width),
111
+ x: roundCoordinate(x),
112
+ y: roundCoordinate(y)
113
+ }];
114
+ });
115
+ const rawText = textContent.items.flatMap((item) => "str" in item ? [item.str, item.hasEOL ? "\n" : ""] : []).join("");
116
+ const annotations = (await page.getAnnotations({ intent: "display" })).map(normalizeAnnotation);
117
+ const viewport = page.getViewport({ scale: 1 });
118
+ pages.push({
119
+ annotations,
120
+ height: roundCoordinate(viewport.height),
121
+ number: pageNumber,
122
+ rawText,
123
+ text: normalizePdfText(rawText),
124
+ textItems,
125
+ textRuns,
126
+ width: roundCoordinate(viewport.width)
127
+ });
128
+ } finally {
129
+ page.cleanup();
130
+ }
131
+ }
132
+ const outline = await document.getOutline();
133
+ return {
134
+ pageCount: document.numPages,
135
+ pages,
136
+ links: flattenLinks(pages),
137
+ outline: outline ? simplifyOutline(outline) : []
138
+ };
139
+ });
140
+ }
141
+ async function rasterizePdf(input, options = {}) {
142
+ const scale = options.scale ?? 1;
143
+ if (!Number.isFinite(scale) || scale <= 0) {
144
+ throw new RangeError("PDF raster scale must be a finite number greater than zero");
145
+ }
146
+ const { createCanvas } = loadCanvas();
147
+ return withPdfDocument(await toPdfBytes(input), async (document) => {
148
+ const images = [];
149
+ for (let pageNumber = 1; pageNumber <= document.numPages; pageNumber += 1) {
150
+ const page = await document.getPage(pageNumber);
151
+ try {
152
+ const viewport = page.getViewport({ scale });
153
+ const width = Math.ceil(viewport.width);
154
+ const height = Math.ceil(viewport.height);
155
+ const canvas = createCanvas(width, height);
156
+ const context = canvas.getContext("2d");
157
+ await page.render({
158
+ background: options.background ?? "#ffffff",
159
+ canvas: null,
160
+ canvasContext: context,
161
+ viewport
162
+ }).promise;
163
+ images.push({
164
+ height,
165
+ number: pageNumber,
166
+ pixels: new Uint8ClampedArray(context.getImageData(0, 0, width, height).data),
167
+ png: new Uint8Array(canvas.encodeSync("png")),
168
+ width
169
+ });
170
+ } finally {
171
+ page.cleanup();
172
+ }
173
+ }
174
+ return images;
175
+ });
176
+ }
177
+ async function decodePngPage(data, number) {
178
+ if (!Number.isInteger(number) || number < 1) {
179
+ throw new RangeError("PNG page number must be a positive integer");
180
+ }
181
+ const { createCanvas, loadImage } = loadCanvas();
182
+ const png = copyPdfData(data);
183
+ const image = await loadImage(png);
184
+ const canvas = createCanvas(image.width, image.height);
185
+ const context = canvas.getContext("2d");
186
+ context.drawImage(image, 0, 0);
187
+ return {
188
+ height: image.height,
189
+ number,
190
+ pixels: new Uint8ClampedArray(
191
+ context.getImageData(0, 0, image.width, image.height).data
192
+ ),
193
+ png,
194
+ width: image.width
195
+ };
196
+ }
197
+ function encodePngPage(image) {
198
+ assertPageImage(image, "encoded");
199
+ const { createCanvas } = loadCanvas();
200
+ const canvas = createCanvas(image.width, image.height);
201
+ const context = canvas.getContext("2d");
202
+ const data = context.createImageData(image.width, image.height);
203
+ data.data.set(image.pixels);
204
+ context.putImageData(data, 0, 0);
205
+ return new Uint8Array(canvas.encodeSync("png"));
206
+ }
207
+ function comparePageImages(actual, expected, options = {}) {
208
+ const channelThreshold = options.channelThreshold ?? DEFAULT_CHANNEL_THRESHOLD;
209
+ const maxChangedPixelRatio = options.maxChangedPixelRatio ?? DEFAULT_MAX_CHANGED_PIXEL_RATIO;
210
+ assertThreshold(channelThreshold, 255, "channelThreshold");
211
+ assertThreshold(maxChangedPixelRatio, 1, "maxChangedPixelRatio");
212
+ assertPageImage(actual, "actual");
213
+ assertPageImage(expected, "expected");
214
+ const pageNumbersMatch = actual.number === expected.number;
215
+ const dimensionsMatch = actual.width === expected.width && actual.height === expected.height;
216
+ if (!dimensionsMatch) {
217
+ const totalPixels2 = Math.max(actual.width * actual.height, expected.width * expected.height);
218
+ return {
219
+ changedPixelRatio: totalPixels2 === 0 ? 0 : 1,
220
+ changedPixels: totalPixels2,
221
+ dimensionsMatch: false,
222
+ matches: false,
223
+ maxChannelDifference: 255,
224
+ pageNumbersMatch,
225
+ totalPixels: totalPixels2
226
+ };
227
+ }
228
+ const totalPixels = actual.width * actual.height;
229
+ let changedPixels = 0;
230
+ let maxChannelDifference = 0;
231
+ for (let offset = 0; offset < actual.pixels.length; offset += 4) {
232
+ let pixelChanged = false;
233
+ for (let channel = 0; channel < 4; channel += 1) {
234
+ const difference = Math.abs(
235
+ actual.pixels[offset + channel] - expected.pixels[offset + channel]
236
+ );
237
+ maxChannelDifference = Math.max(maxChannelDifference, difference);
238
+ pixelChanged ||= difference > channelThreshold;
239
+ }
240
+ if (pixelChanged) changedPixels += 1;
241
+ }
242
+ const changedPixelRatio = totalPixels === 0 ? 0 : changedPixels / totalPixels;
243
+ return {
244
+ changedPixelRatio,
245
+ changedPixels,
246
+ dimensionsMatch,
247
+ matches: pageNumbersMatch && changedPixelRatio <= maxChangedPixelRatio,
248
+ maxChannelDifference,
249
+ pageNumbersMatch,
250
+ totalPixels
251
+ };
252
+ }
253
+ async function getPdfJs() {
254
+ installPdfCanvasGlobals();
255
+ if (!pdfJsPromise) {
256
+ pdfJsPromise = import('pdfjs-dist/legacy/build/pdf.mjs').catch((error) => {
257
+ pdfJsPromise = void 0;
258
+ if (isModuleNotFound(error)) {
259
+ throw new Error(missingPeerMessage("pdfjs-dist"), { cause: error });
260
+ }
261
+ throw error;
262
+ });
263
+ }
264
+ return pdfJsPromise;
265
+ }
266
+ async function withPdfDocument(data, read) {
267
+ const pdfJs = await getPdfJs();
268
+ const loadingTask = pdfJs.getDocument({
269
+ CanvasFactory: NapiCanvasFactory,
270
+ data: copyPdfData(data),
271
+ isEvalSupported: false,
272
+ stopAtErrors: true,
273
+ useWorkerFetch: false,
274
+ verbosity: 0
275
+ });
276
+ try {
277
+ return await read(await loadingTask.promise);
278
+ } finally {
279
+ await loadingTask.destroy();
280
+ }
281
+ }
282
+ function copyPdfData(data) {
283
+ return data instanceof Uint8Array ? Uint8Array.from(data) : new Uint8Array(data.slice(0));
284
+ }
285
+ function normalizeAnnotation(value) {
286
+ const annotation = isRecord(value) ? value : {};
287
+ const annotationType = typeof annotation.annotationType === "number" ? annotation.annotationType : void 0;
288
+ const destination = normalizeDestination(annotation.dest);
289
+ const rect = Array.isArray(annotation.rect) ? annotation.rect.filter((item) => typeof item === "number").map(roundCoordinate) : void 0;
290
+ const unsafeUrl = asUrl(annotation.unsafeUrl);
291
+ const url = asUrl(annotation.url);
292
+ return {
293
+ ...annotationType === void 0 ? {} : { annotationType },
294
+ ...destination === void 0 ? {} : { destination },
295
+ ...rect?.length === 4 ? { rect } : {},
296
+ subtype: typeof annotation.subtype === "string" ? annotation.subtype : "Unknown",
297
+ ...unsafeUrl === void 0 ? {} : { unsafeUrl },
298
+ ...url === void 0 ? {} : { url }
299
+ };
300
+ }
301
+ function normalizeDestination(value) {
302
+ if (typeof value === "string") return value;
303
+ if (!Array.isArray(value)) return void 0;
304
+ const destination = [];
305
+ for (const part of value) {
306
+ if (part === null || typeof part === "string" || typeof part === "number") {
307
+ destination.push(part);
308
+ continue;
309
+ }
310
+ if (isRecord(part) && typeof part.num === "number" && typeof part.gen === "number") {
311
+ destination.push({ generation: part.gen, number: part.num });
312
+ }
313
+ }
314
+ return destination;
315
+ }
316
+ function asUrl(value) {
317
+ if (typeof value === "string") return value;
318
+ if (value instanceof URL) return value.href;
319
+ return void 0;
320
+ }
321
+ function isRecord(value) {
322
+ return typeof value === "object" && value !== null;
323
+ }
324
+ function roundCoordinate(value) {
325
+ return Math.round(value * 1e3) / 1e3;
326
+ }
327
+ function assertThreshold(value, maximum, name) {
328
+ if (!Number.isFinite(value) || value < 0 || value > maximum) {
329
+ throw new RangeError(`${name} must be between 0 and ${maximum}`);
330
+ }
331
+ }
332
+ function assertPageImage(image, name) {
333
+ if (!Number.isInteger(image.number) || image.number < 1) {
334
+ throw new RangeError(`${name} page number must be a positive integer`);
335
+ }
336
+ if (!Number.isInteger(image.width) || image.width < 1 || !Number.isInteger(image.height) || image.height < 1) {
337
+ throw new RangeError(`${name} page dimensions must be positive integers`);
338
+ }
339
+ const expectedLength = image.width * image.height * 4;
340
+ if (image.pixels.length !== expectedLength) {
341
+ throw new RangeError(`${name} page pixels must contain ${expectedLength} RGBA channels`);
342
+ }
343
+ }
344
+
345
+ class PdfAssertionError extends Error {
346
+ constructor(message) {
347
+ super(message);
348
+ this.name = "PdfAssertionError";
349
+ }
350
+ }
351
+ const truncate = (value, max = 200) => value.length > max ? `${value.slice(0, max)}\u2026` : value;
352
+ const pageText = (parsed, page) => parsed.pages.find((candidate) => candidate.number === page)?.text;
353
+ const linkMatches = (link, query) => {
354
+ if ("destination" in query && link.destination !== query.destination) return false;
355
+ if ("url" in query && link.url !== query.url) return false;
356
+ if (query.page !== void 0 && link.page !== query.page) return false;
357
+ return true;
358
+ };
359
+ const describeQuery = (query) => {
360
+ const parts = [];
361
+ if ("destination" in query) parts.push(`destination=${JSON.stringify(query.destination)}`);
362
+ if ("url" in query) parts.push(`url=${JSON.stringify(query.url)}`);
363
+ if (query.page !== void 0) parts.push(`page=${query.page}`);
364
+ return parts.join(", ");
365
+ };
366
+ const describeLinks = (links) => links.length === 0 ? "(no links found)" : links.map((link) => `{ page: ${link.page}${link.destination ? `, destination: ${JSON.stringify(link.destination)}` : ""}${link.url ? `, url: ${JSON.stringify(link.url)}` : ""} }`).join(", ");
367
+ const outlineMismatch = (actual, shape, path) => {
368
+ if (actual.length !== shape.length) {
369
+ return `expected ${shape.length} item(s) at ${path}, found ${actual.length} (${actual.map((item) => JSON.stringify(item.title)).join(", ") || "none"})`;
370
+ }
371
+ for (let index = 0; index < shape.length; index += 1) {
372
+ const expected = shape[index];
373
+ const found = actual[index];
374
+ const here = `${path}[${index}]`;
375
+ if (found.title !== expected.title) {
376
+ return `expected outline title ${JSON.stringify(expected.title)} at ${here}, found ${JSON.stringify(found.title)}`;
377
+ }
378
+ if (expected.expanded !== void 0 && found.expanded !== expected.expanded) {
379
+ return `expected outline expanded=${expected.expanded} at ${here}, found ${String(found.expanded)}`;
380
+ }
381
+ if (expected.children !== void 0) {
382
+ const nested = outlineMismatch(found.children, expected.children, `${here}.children`);
383
+ if (nested) return nested;
384
+ }
385
+ }
386
+ return void 0;
387
+ };
388
+ function expectPdf(parsed) {
389
+ const expectation = {
390
+ toHavePageCount(count) {
391
+ if (parsed.pageCount !== count) {
392
+ throw new PdfAssertionError(
393
+ `Expected the PDF to have ${count} page(s), but it has ${parsed.pageCount}.`
394
+ );
395
+ }
396
+ return expectation;
397
+ },
398
+ toContainText(text, options = {}) {
399
+ if (options.page !== void 0) {
400
+ const target = pageText(parsed, options.page);
401
+ if (target === void 0) {
402
+ throw new PdfAssertionError(
403
+ `Expected text ${JSON.stringify(text)} on page ${options.page}, but the PDF has no page ${options.page} (page count: ${parsed.pageCount}).`
404
+ );
405
+ }
406
+ if (!target.includes(text)) {
407
+ throw new PdfAssertionError(
408
+ `Expected page ${options.page} to contain ${JSON.stringify(text)}, but its text was ${JSON.stringify(truncate(target))}.`
409
+ );
410
+ }
411
+ return expectation;
412
+ }
413
+ const found = parsed.pages.some((page) => page.text.includes(text));
414
+ if (!found) {
415
+ const all = parsed.pages.map((page) => `p${page.number}: ${truncate(page.text, 120)}`).join(" | ");
416
+ throw new PdfAssertionError(
417
+ `Expected some page to contain ${JSON.stringify(text)}, but none did. Pages: ${all}`
418
+ );
419
+ }
420
+ return expectation;
421
+ },
422
+ toHaveLink(query) {
423
+ if (!parsed.links.some((link) => linkMatches(link, query))) {
424
+ throw new PdfAssertionError(
425
+ `Expected a link matching { ${describeQuery(query)} }, but found: ${describeLinks(parsed.links)}.`
426
+ );
427
+ }
428
+ return expectation;
429
+ },
430
+ toHaveOutline(shape) {
431
+ const mismatch = outlineMismatch(parsed.outline, shape, "outline");
432
+ if (mismatch) {
433
+ throw new PdfAssertionError(`Outline did not match: ${mismatch}.`);
434
+ }
435
+ return expectation;
436
+ }
437
+ };
438
+ return expectation;
439
+ }
440
+
441
+ const RENDER_PDF_TEMPLATE_OPTION_KEYS = ["limits", "remote"];
442
+ const hasDefinition = (component) => typeof component === "object" && component !== null && PDF_DEFINITION_PROPERTY in component;
443
+ const ensurePdfComponent = (component) => {
444
+ if (hasDefinition(component)) return component;
445
+ const wrapper = defineComponent({
446
+ name: "RenderPdfTemplateHost",
447
+ inheritAttrs: false,
448
+ setup(_props, { attrs }) {
449
+ return () => h(component, attrs);
450
+ }
451
+ });
452
+ const definition = {};
453
+ return Object.assign(wrapper, { [PDF_DEFINITION_PROPERTY]: definition });
454
+ };
455
+ const componentName = (component) => {
456
+ const name = component.name;
457
+ return typeof name === "string" && name !== "" ? name : "template";
458
+ };
459
+ function assertRenderOptionKeys(helper, options, allowedKeys) {
460
+ const unsupportedKey = Object.keys(options).find((key) => !allowedKeys.includes(key));
461
+ if (unsupportedKey === void 0) return;
462
+ throw new TypeError(
463
+ `${helper} received unsupported option ${JSON.stringify(unsupportedKey)}. Supported options: ${allowedKeys.join(", ")}.`
464
+ );
465
+ }
466
+ async function renderPreparedPdfTemplate(component, props, options = {}) {
467
+ const { key = componentName(component), ...runtimeOptions } = options;
468
+ const template = createPdfTemplate(
469
+ key,
470
+ ensurePdfComponent(component),
471
+ runtimeOptions
472
+ );
473
+ const result = await template.render(props);
474
+ const bytes = await result.toUint8Array();
475
+ const parsed = await parsePdf(bytes);
476
+ return { bytes, parsed, result };
477
+ }
478
+ async function renderPdfTemplate(component, props, options = {}) {
479
+ assertRenderOptionKeys(
480
+ "renderPdfTemplate",
481
+ options,
482
+ RENDER_PDF_TEMPLATE_OPTION_KEYS
483
+ );
484
+ return renderPreparedPdfTemplate(component, props, {
485
+ limits: normalizePdfLimits(options.limits),
486
+ remote: normalizeRemoteAssetPolicy(options.remote)
487
+ });
488
+ }
489
+
490
+ const RENDER_PDF_SFC_OPTION_KEYS = ["fonts", "limits", "remote"];
491
+ const findPdfRoot = (filename) => {
492
+ let directory = dirname(filename);
493
+ while (dirname(directory) !== directory) {
494
+ if (basename(directory) === "pdfs") return directory;
495
+ directory = dirname(directory);
496
+ }
497
+ throw new Error(`PDF SFC ${JSON.stringify(filename)} must be inside a pdfs directory.`);
498
+ };
499
+ const resolveComposablesImport = () => {
500
+ const directory = dirname(fileURLToPath(import.meta.url));
501
+ const built = join(directory, "runtime", "composables", "index.js");
502
+ if (existsSync(built)) return built;
503
+ const source = resolve(directory, "..", "runtime", "composables", "index.ts");
504
+ if (existsSync(source)) return source;
505
+ throw new Error("Unable to locate the Nuxt PDF composables runtime.");
506
+ };
507
+ const sfcCompilerPlugin = (entry, composablesImport) => ({
508
+ name: "nuxt-pdf:test-sfc",
509
+ setup(build2) {
510
+ build2.onResolve({ filter: /^(?:@[^/]+\/)?[^./][^:]*/ }, ({ path }) => ({
511
+ external: true,
512
+ path
513
+ }));
514
+ build2.onLoad({ filter: /\.vue$/ }, async ({ path }) => {
515
+ const result = await compilePdfSfc(
516
+ await readFile(path, "utf8"),
517
+ path,
518
+ path === entry ? "template" : "component",
519
+ true,
520
+ composablesImport
521
+ );
522
+ const sourceMap = result.map ? `
523
+ //# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(result.map)).toString("base64")}` : "";
524
+ return {
525
+ contents: result.code + sourceMap,
526
+ loader: "js",
527
+ resolveDir: dirname(path)
528
+ };
529
+ });
530
+ }
531
+ });
532
+ async function loadPdfSfc(filename) {
533
+ const entry = resolve(filename);
534
+ const composablesImport = resolveComposablesImport();
535
+ const result = await build({
536
+ absWorkingDir: dirname(entry),
537
+ bundle: true,
538
+ entryPoints: [entry],
539
+ format: "esm",
540
+ packages: "external",
541
+ platform: "node",
542
+ plugins: [sfcCompilerPlugin(entry, composablesImport)],
543
+ sourcemap: "inline",
544
+ target: "node22",
545
+ write: false
546
+ });
547
+ const output = result.outputFiles?.[0];
548
+ if (!output) throw new Error(`PDF SFC ${JSON.stringify(entry)} produced no JavaScript output.`);
549
+ const appRoot = dirname(findPdfRoot(entry));
550
+ const cacheDirectory = join(appRoot, "node_modules", ".cache");
551
+ const compiledFile = join(cacheDirectory, `nuxt-pdf-sfc-${process.pid}-${Date.now()}.mjs`);
552
+ await mkdir(cacheDirectory, { recursive: true });
553
+ await writeFile(compiledFile, output.contents);
554
+ try {
555
+ const loaded = await import(`${pathToFileURL(compiledFile).href}?v=${Date.now()}`);
556
+ if (!loaded.default || typeof loaded.default !== "object" && typeof loaded.default !== "function") {
557
+ throw new Error(`PDF SFC ${JSON.stringify(entry)} has no component default export.`);
558
+ }
559
+ return loaded.default;
560
+ } finally {
561
+ await rm(compiledFile, { force: true });
562
+ }
563
+ }
564
+ async function renderPdfSfc(filename, props, options = {}) {
565
+ assertRenderOptionKeys("renderPdfSfc", options, RENDER_PDF_SFC_OPTION_KEYS);
566
+ const entry = resolve(filename);
567
+ const pdfRoot = findPdfRoot(entry);
568
+ const rootDir = dirname(pdfRoot);
569
+ const relativePath = relative(pdfRoot, entry).replaceAll("\\", "/");
570
+ const key = templateKeyFromRelativePath(relativePath);
571
+ if (key === null) {
572
+ throw new Error(`PDF SFC ${JSON.stringify(entry)} must be a template directly inside pdfs/ or one of its feature directories.`);
573
+ }
574
+ const normalizedLimits = normalizePdfLimits(options.limits);
575
+ const limits = resolvePdfRenderLimits(normalizedLimits);
576
+ const [component, imageFiles, fonts] = await Promise.all([
577
+ loadPdfSfc(entry),
578
+ discoverPdfImageFiles([{ name: "test", rootDir }]),
579
+ bundlePdfFonts(options.fonts ?? [], { fontRoots: [join(rootDir, "pdfs", "fonts")] })
580
+ ]);
581
+ const loadedAssets = await Promise.all(imageFiles.map((image) => loadPdfImageAsset(image.key, {
582
+ roots: [image.rootDir],
583
+ maxBytes: limits.maxImageBytes,
584
+ maxPixels: limits.maxImagePixels
585
+ })));
586
+ const assets = Object.fromEntries(loadedAssets.map((asset) => [asset.key, asset]));
587
+ return renderPreparedPdfTemplate(component, props, {
588
+ assets,
589
+ file: `pdfs/${relativePath}`,
590
+ fonts,
591
+ key,
592
+ limits: normalizedLimits,
593
+ remote: normalizeRemoteAssetPolicy(options.remote)
594
+ });
595
+ }
596
+
597
+ const pageFileName = (pageNumber) => `page-${pageNumber}.png`;
598
+ const isPageBaseline = (name) => /^page-\d+\.png$/.test(name);
599
+ const differenceImage = (actual, expected) => {
600
+ const width = Math.max(actual.width, expected.width);
601
+ const height = Math.max(actual.height, expected.height);
602
+ const pixels = new Uint8ClampedArray(width * height * 4);
603
+ for (let y = 0; y < height; y += 1) {
604
+ for (let x = 0; x < width; x += 1) {
605
+ const output = (y * width + x) * 4;
606
+ const actualOffset = (y * actual.width + x) * 4;
607
+ const expectedOffset = (y * expected.width + x) * 4;
608
+ const inActual = x < actual.width && y < actual.height;
609
+ const inExpected = x < expected.width && y < expected.height;
610
+ if (!inActual || !inExpected) {
611
+ pixels.set([255, 0, 0, 255], output);
612
+ continue;
613
+ }
614
+ const difference = Math.max(
615
+ ...[0, 1, 2, 3].map((channel) => Math.abs(
616
+ actual.pixels[actualOffset + channel] - expected.pixels[expectedOffset + channel]
617
+ ))
618
+ );
619
+ const intensity = Math.min(255, difference * 4);
620
+ pixels.set([255, 255 - intensity, 255 - intensity, 255], output);
621
+ }
622
+ }
623
+ return { height, number: actual.number, pixels, png: new Uint8Array(), width };
624
+ };
625
+ const writeFailureArtifacts = async (artifactDir, failures, thresholds) => {
626
+ await rm(artifactDir, { force: true, recursive: true });
627
+ await mkdir(artifactDir, { recursive: true });
628
+ await Promise.all(failures.flatMap(({ actual, expected }) => {
629
+ const page = `page-${actual.number}`;
630
+ return [
631
+ writeFile(join(artifactDir, `${page}-actual.png`), actual.png),
632
+ writeFile(join(artifactDir, `${page}-expected.png`), expected.png),
633
+ writeFile(
634
+ join(artifactDir, `${page}-diff.png`),
635
+ encodePngPage(differenceImage(actual, expected))
636
+ )
637
+ ];
638
+ }));
639
+ await writeFile(join(artifactDir, "metrics.json"), `${JSON.stringify({
640
+ pages: failures.map(({ actual, comparison }) => ({
641
+ page: actual.number,
642
+ ...comparison
643
+ })),
644
+ thresholds
645
+ }, null, 2)}
646
+ `);
647
+ };
648
+ async function comparePdfSnapshot(input, baselineDir, options = {}) {
649
+ const update = options.update ?? process.env.UPDATE_PDF_BASELINES === "1";
650
+ const thresholds = {
651
+ channelThreshold: options.channelThreshold ?? 25,
652
+ maxChangedPixelRatio: options.threshold ?? 5e-3
653
+ };
654
+ const artifactDir = options.artifactDir ?? join(process.cwd(), "reports", "pdf-snapshots", basename(baselineDir));
655
+ const pages = await rasterizePdf(input, { scale: options.scale ?? 1 });
656
+ if (update) {
657
+ await mkdir(baselineDir, { recursive: true });
658
+ const stale = (await readdir(baselineDir).catch(() => [])).filter((name) => isPageBaseline(name));
659
+ const kept = new Set(pages.map((page) => pageFileName(page.number)));
660
+ await Promise.all([
661
+ ...pages.map((page) => writeFile(join(baselineDir, pageFileName(page.number)), page.png)),
662
+ ...stale.filter((name) => !kept.has(name)).map((name) => rm(join(baselineDir, name)))
663
+ ]);
664
+ return { matches: true, updated: true, pages: [] };
665
+ }
666
+ const baselineNames = (await readdir(baselineDir).catch(() => {
667
+ throw new PdfAssertionError(
668
+ `No reviewed PDF baseline found at ${JSON.stringify(baselineDir)}. Create it by running with UPDATE_PDF_BASELINES=1.`
669
+ );
670
+ })).filter(isPageBaseline).sort();
671
+ if (baselineNames.length !== pages.length) {
672
+ throw new PdfAssertionError(
673
+ `Rendered ${pages.length} page(s) but the baseline in ${JSON.stringify(baselineDir)} has ${baselineNames.length}. Re-run with UPDATE_PDF_BASELINES=1 if this change is intended.`
674
+ );
675
+ }
676
+ const comparisons = [];
677
+ const failures = [];
678
+ for (const page of pages) {
679
+ const baselinePath = join(baselineDir, pageFileName(page.number));
680
+ const baseline = await decodePngPage(await readFile(baselinePath), page.number);
681
+ const comparison = comparePageImages(page, baseline, thresholds);
682
+ comparisons.push(comparison);
683
+ if (!comparison.matches) {
684
+ failures.push({ actual: page, comparison, expected: baseline });
685
+ }
686
+ }
687
+ if (failures.length > 0) {
688
+ await writeFailureArtifacts(artifactDir, failures, thresholds);
689
+ throw new PdfAssertionError(
690
+ `${failures.length} page(s) do not match their reviewed baselines. Expected, actual, diff, and metrics artifacts were written to ${JSON.stringify(artifactDir)}. Re-run with UPDATE_PDF_BASELINES=1 if this change is intended.`
691
+ );
692
+ }
693
+ return { matches: true, updated: false, pages: comparisons };
694
+ }
695
+
696
+ export { PdfAssertionError, comparePdfSnapshot, expectPdf, loadPdfSfc, parsePdf, rasterizePdf, renderPdfSfc, renderPdfTemplate, toPdfBytes };