pdf-visual-compare 3.5.0 → 4.0.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 (58) hide show
  1. package/README.md +326 -72
  2. package/out/comparePdf.d.ts +48 -6
  3. package/out/comparePdf.js +44 -86
  4. package/out/const.d.ts +2 -2
  5. package/out/const.js +5 -3
  6. package/out/errors/ComparePdfComparisonError.d.ts +6 -0
  7. package/out/errors/ComparePdfComparisonError.js +10 -0
  8. package/out/errors/ComparePdfConfigurationError.d.ts +6 -0
  9. package/out/errors/ComparePdfConfigurationError.js +10 -0
  10. package/out/errors/ComparePdfError.d.ts +6 -0
  11. package/out/errors/ComparePdfError.js +13 -0
  12. package/out/errors/ComparePdfInputError.d.ts +6 -0
  13. package/out/errors/ComparePdfInputError.js +10 -0
  14. package/out/errors/ComparePdfRenderingError.d.ts +6 -0
  15. package/out/errors/ComparePdfRenderingError.js +10 -0
  16. package/out/index.d.ts +16 -2
  17. package/out/index.js +14 -2
  18. package/out/internal/adapters/comparePngOptions.d.ts +3 -0
  19. package/out/internal/adapters/comparePngOptions.js +41 -0
  20. package/out/internal/adapters/pdfRenderOptions.d.ts +3 -0
  21. package/out/internal/adapters/pdfRenderOptions.js +19 -0
  22. package/out/internal/comparePlannedPage.d.ts +3 -0
  23. package/out/internal/comparePlannedPage.js +72 -0
  24. package/out/internal/diffOutputGuards.d.ts +56 -0
  25. package/out/internal/diffOutputGuards.js +176 -0
  26. package/out/internal/normalizeComparisonOptions.d.ts +2 -0
  27. package/out/internal/normalizeComparisonOptions.js +104 -0
  28. package/out/internal/normalizePdfInput.d.ts +13 -0
  29. package/out/internal/normalizePdfInput.js +110 -0
  30. package/out/internal/pageComparisonPlan.d.ts +4 -0
  31. package/out/internal/pageComparisonPlan.js +35 -0
  32. package/out/internal/renderOutputFolderGuards.d.ts +22 -0
  33. package/out/internal/renderOutputFolderGuards.js +70 -0
  34. package/out/internal/renderPdfPages.d.ts +10 -0
  35. package/out/internal/renderPdfPages.js +105 -0
  36. package/out/internal/securePath.d.ts +34 -0
  37. package/out/internal/securePath.js +75 -0
  38. package/out/internal/types.d.ts +26 -0
  39. package/out/internal/types.js +2 -0
  40. package/out/types/ComparePdfDetailedResult.d.ts +35 -0
  41. package/out/types/ComparePdfDetailedResult.js +2 -0
  42. package/out/types/ComparePdfOptions.d.ts +33 -11
  43. package/out/types/ComparePdfPageResult.d.ts +42 -0
  44. package/out/types/ComparePdfPageResult.js +2 -0
  45. package/out/types/ComparePdfPageStatus.d.ts +4 -0
  46. package/out/types/ComparePdfPageStatus.js +2 -0
  47. package/out/types/ExcludedPageArea.d.ts +3 -39
  48. package/out/types/PageArea.d.ts +13 -0
  49. package/out/types/PageArea.js +2 -0
  50. package/out/types/PageExclusion.d.ts +46 -0
  51. package/out/types/PageExclusion.js +2 -0
  52. package/out/types/PdfInput.d.ts +11 -0
  53. package/out/types/PdfInput.js +2 -0
  54. package/out/types/PdfRenderOptions.d.ts +51 -0
  55. package/out/types/PdfRenderOptions.js +2 -0
  56. package/out/types/RgbColor.d.ts +11 -0
  57. package/out/types/RgbColor.js +2 -0
  58. package/package.json +86 -75
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # pdf-visual-compare
2
2
 
3
3
  <p align="center">
4
- <strong>Visual regression testing library for PDFs in JavaScript/TypeScript without binary or OS dependencies</strong>
4
+ <strong>Visual regression testing library for PDFs in JavaScript/TypeScript without external system package dependencies</strong>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -26,20 +26,35 @@
26
26
  - [Requirements](#requirements)
27
27
  - [Installation](#installation)
28
28
  - [Usage](#usage)
29
- - [Basic comparison](#basic-comparison)
30
- - [Comparison with options](#comparison-with-options)
31
- - [Comparing PDF buffers](#comparing-pdf-buffers)
29
+ - [Basic comparison](#basic-comparison)
30
+ - [Detailed comparison results](#detailed-comparison-results)
31
+ - [Comparison with options](#comparison-with-options)
32
+ - [Comparing PDF buffers](#comparing-pdf-buffers)
32
33
  - [API](#api)
33
- - [comparePdf](#comparepdfactualpdf-expectedpdf-options)
34
- - [ComparePdfOptions](#comparepdfoptions)
35
- - [ExcludedPageArea](#excludedpagearea)
34
+ - [comparePdf](#comparepdfactualpdf-expectedpdf-options)
35
+ - [comparePdfDetailed](#comparepdfdetailedactualpdf-expectedpdf-options)
36
+ - [ComparePdfOptions](#comparepdfoptions)
37
+ - [ComparePdfDetailedResult](#comparepdfdetailedresult)
38
+ - [ComparePdfPageResult](#comparepdfpageresult)
39
+ - [ComparePdfPageStatus](#comparepdfpagestatus)
40
+ - [PdfRenderOptions](#pdfrenderoptions)
41
+ - [PageExclusion](#pageexclusion)
42
+ - [PageArea](#pagearea)
43
+ - [RgbColor](#rgbcolor)
36
44
  - [Support](#support)
37
45
 
38
46
  ---
39
47
 
40
48
  ## Requirements
41
49
 
42
- - Node.js >= 20
50
+ - Node.js >= 24
51
+ - Supported and CI-validated runtimes: Linux and macOS
52
+ - Windows is not supported
53
+
54
+ ## Docker
55
+
56
+ The Docker image is for local/containerized test execution only. It builds a slim test runner that
57
+ installs just the dependencies needed to execute Vitest against the checked-in source and fixtures.
43
58
 
44
59
  ---
45
60
 
@@ -49,6 +64,31 @@
49
64
  npm install -D pdf-visual-compare
50
65
  ```
51
66
 
67
+ `pdf-visual-compare` now depends on `pdf-to-png-converter` 4.x, which ships prebuilt native canvas
68
+ bindings through `@napi-rs/canvas`. No external system packages are required, but consumers must use
69
+ Node 24 or newer.
70
+
71
+ ## Validation
72
+
73
+ ```sh
74
+ npm run test:types # Type-check all repository TypeScript, including the published-surface fixture
75
+ npm run test:artifacts # Verify the built package entry points and npm pack contents
76
+ npm test # Full pipeline: clean → lint → license check → build → type/artifact checks → vitest --coverage
77
+ ```
78
+
79
+ `npm run test:types` includes the published-surface fixture under `__tests__/published-artifacts/`,
80
+ so it still expects `./out/` to be up to date. `npm test` handles that automatically.
81
+
82
+ ## Repository merge policy
83
+
84
+ For pull requests targeting `main`, the required GitHub status checks are:
85
+
86
+ - `test (ubuntu-24.04)`
87
+ - `test (macos-15)`
88
+
89
+ Those check names come from the matrix jobs in the `CI` workflow (`.github/workflows/test.yml`).
90
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the contributor workflow and merge expectations.
91
+
52
92
  ---
53
93
 
54
94
  ## Usage
@@ -63,54 +103,87 @@ const isEqual = await comparePdf('./actual.pdf', './expected.pdf');
63
103
  // false → PDFs differ
64
104
  ```
65
105
 
106
+ ### Detailed comparison results
107
+
108
+ ```typescript
109
+ import { comparePdfDetailed } from 'pdf-visual-compare';
110
+
111
+ const result = await comparePdfDetailed('./actual.pdf', './expected.pdf', {
112
+ compareThreshold: 25,
113
+ });
114
+
115
+ console.log(result.isEqual);
116
+ console.log(result.actualPageCount, result.expectedPageCount);
117
+ console.log(result.pages[0]);
118
+ ```
119
+
66
120
  ### Comparison with options
67
121
 
68
122
  ```typescript
123
+ import type { ComparePdfOptions } from 'pdf-visual-compare';
69
124
  import { comparePdf } from 'pdf-visual-compare';
70
125
 
71
- const isEqual = await comparePdf('./actual.pdf', './expected.pdf', {
72
- // Folder for diff PNG images written when differences are found. Default: ./comparePdfOutput
73
- diffsOutputFolder: 'test-results/diffs',
74
-
75
- // Maximum number of differing pixels allowed before the comparison fails.
76
- // 0 = pixel-perfect match required (default). Must be >= 0.
77
- compareThreshold: 200,
78
-
79
- // Per-page exclusion zones, matched by the `pageNumber` field (1-based).
80
- // `pageNumber: 1` → first page, `pageNumber: 2` → second page, etc.
81
- // Pixel coordinates are relative to the rendered PNG at the configured viewportScale.
82
- excludedAreas: [
83
- {
84
- pageNumber: 1,
85
- excludedAreas: [
86
- { x1: 700, y1: 375, x2: 790, y2: 400 }, // dynamic timestamp on page 1
87
- ],
88
- },
89
- {
90
- pageNumber: 2,
91
- excludedAreas: [
92
- { x1: 680, y1: 240, x2: 955, y2: 465 }, // chart on page 2
93
- ],
126
+ const options: ComparePdfOptions = {
127
+ // Enable diff PNG output explicitly. Default: false
128
+ writeDiffs: true,
129
+
130
+ // Trusted root folder for diff PNG images written when differences are found.
131
+ // Generated diff paths and per-page diffFilePath overrides must stay inside this folder.
132
+ // If the path already exists, it must be a directory.
133
+ // Default: ./comparePdfOutput
134
+ diffsOutputFolder: 'test-results/diffs',
135
+
136
+ // Optional trust boundary for string PDF paths. When set, both actualPdf and expectedPdf
137
+ // must resolve inside this directory or comparePdf throws ComparePdfConfigurationError.
138
+ allowedInputRoot: '.',
139
+
140
+ // Maximum number of differing pixels allowed before the comparison fails.
141
+ // 0 = pixel-perfect match required (default). Must be a finite non-negative integer.
142
+ compareThreshold: 200,
143
+
144
+ // Per-page exclusion zones, matched by the `pageNumber` field (1-based).
145
+ // `pageNumber: 1` → first page, `pageNumber: 2` → second page, etc.
146
+ // Pixel coordinates are relative to the rendered PNG at the configured viewportScale.
147
+ excludedAreas: [
148
+ {
149
+ pageNumber: 1,
150
+ excludedAreas: [{ x1: 700, y1: 375, x2: 790, y2: 400 }],
151
+ },
152
+ {
153
+ pageNumber: 2,
154
+ excludedAreas: [{ x1: 680, y1: 240, x2: 955, y2: 465 }],
155
+ },
156
+ ],
157
+
158
+ pdfToPngConvertOptions: {
159
+ viewportScale: 2.0,
160
+ disableFontFace: true,
161
+ useSystemFonts: false,
162
+ enableXfa: false,
163
+ pdfFilePassword: 'pa$$word',
164
+ // Renderer intermediate files are written under output/pngs/actual and output/pngs/expected.
165
+ outputFolder: 'output/pngs',
166
+ outputFileMaskFunc: (pageNumber) => `page_${pageNumber}.png`,
167
+ pagesToProcess: [1, 3],
168
+ verbosityLevel: 0,
94
169
  },
95
- ],
96
-
97
- pdfToPngConvertOptions: {
98
- viewportScale: 2.0, // Rendering scale — higher means more detail. Default: 2.0.
99
- disableFontFace: true, // Use built-in font renderer. Default: true.
100
- useSystemFonts: false, // Fall back to system fonts for non-embedded fonts. Default: false.
101
- enableXfa: false, // Enable XFA form rendering. Default: false.
102
- pdfFilePassword: 'pa$$word', // Password for encrypted PDFs.
103
- outputFolder: 'output/pngs', // Save intermediate PNG files here. Omit to keep in memory only.
104
- outputFileMaskFunc: (pageNumber) => `page_${pageNumber}.png`, // Custom PNG filename.
105
- pagesToProcess: [1, 3], // Limit comparison to specific pages (1-based). Default: all pages.
106
- verbosityLevel: 0, // 0 = errors only, 1 = warnings, 5 = info. Default: 0.
107
- },
108
- });
170
+ };
171
+
172
+ const isEqual = await comparePdf('./actual.pdf', './expected.pdf', options);
109
173
  ```
110
174
 
111
- ### Comparing PDF buffers
175
+ ### Comparing PDF binary inputs
176
+
177
+ `comparePdf` accepts file paths plus these binary inputs: `Buffer`, `ArrayBuffer`, and
178
+ `SharedArrayBuffer`. `SharedArrayBuffer` inputs are normalized internally before rendering.
179
+
180
+ String paths are intended for trusted local usage by default. If you need to accept caller-provided
181
+ path strings, set `allowedInputRoot` to constrain them to a specific workspace, or prefer binary
182
+ inputs instead.
112
183
 
113
- Both file paths and `Buffer` instances are accepted as inputs:
184
+ Diff PNGs are written only when `writeDiffs: true`. When enabled, treat `diffsOutputFolder` as a
185
+ trusted write root and keep any `diffFilePath` overrides inside that directory. This boundary
186
+ assumes a trusted local filesystem while the comparison is running.
114
187
 
115
188
  ```typescript
116
189
  import { readFileSync } from 'node:fs';
@@ -122,43 +195,224 @@ const expected = readFileSync('./expected.pdf');
122
195
  const isEqual = await comparePdf(actual, expected);
123
196
  ```
124
197
 
198
+ ```typescript
199
+ import { readFileSync } from 'node:fs';
200
+ import { comparePdf, type PdfInput } from 'pdf-visual-compare';
201
+
202
+ const actualBuffer = readFileSync('./actual.pdf');
203
+ const expectedBuffer = readFileSync('./expected.pdf');
204
+
205
+ const actual: PdfInput = actualBuffer.buffer.slice(
206
+ actualBuffer.byteOffset,
207
+ actualBuffer.byteOffset + actualBuffer.byteLength,
208
+ );
209
+ const expected: PdfInput = expectedBuffer.buffer.slice(
210
+ expectedBuffer.byteOffset,
211
+ expectedBuffer.byteOffset + expectedBuffer.byteLength,
212
+ );
213
+
214
+ const isEqual = await comparePdf(actual, expected);
215
+ ```
216
+
125
217
  ---
126
218
 
127
219
  ## API
128
220
 
129
221
  ### `comparePdf(actualPdf, expectedPdf, options?)`
130
222
 
131
- | Parameter | Type | Description |
132
- | ------------- | ------------------------------------------- | ----------------------------------------- |
133
- | `actualPdf` | `string \| Buffer \| ArrayBufferLike` | File path or buffer of the PDF under test |
134
- | `expectedPdf` | `string \| Buffer \| ArrayBufferLike` | File path or buffer of the reference PDF |
135
- | `options` | `ComparePdfOptions` _(optional)_ | Comparison configuration |
223
+ | Parameter | Type | Description |
224
+ | ------------- | -------------------------------- | -------------------------------------------------- |
225
+ | `actualPdf` | `PdfInput` | File path or supported binary PDF input under test |
226
+ | `expectedPdf` | `PdfInput` | File path or supported binary reference PDF |
227
+ | `options` | `ComparePdfOptions` _(optional)_ | Comparison configuration |
228
+
229
+ Returns `Promise<boolean>` — a backwards-compatible convenience wrapper over
230
+ `comparePdfDetailed()` that resolves to `result.isEqual`.
231
+
232
+ Rendered pages are paired by the renderer-reported `pageNumber` (1-based), not by generated PNG
233
+ file names or by the position of entries inside `excludedAreas`. If one side is missing a rendered
234
+ counterpart for a page number in the comparison plan, the comparison returns `false`.
136
235
 
137
- Returns `Promise<boolean>` `true` if the PDFs are visually equivalent within the configured threshold, `false` otherwise.
236
+ The library discovers the page plan first, then renders and compares one page at a time to keep
237
+ memory bounded on multi-page PDFs. `png-visual-compare` 6.x now exposes additional async/ported
238
+ comparison entry points, but this library still uses sequential `comparePng()` calls until a
239
+ benchmark and dependency-level safety review justify parallel execution.
240
+
241
+ String inputs are trusted caller-controlled file paths unless `options.allowedInputRoot` is set. If
242
+ `allowedInputRoot` is configured, both string inputs must resolve within that directory. After
243
+ boundary validation, string inputs are opened and read immediately so rendering operates on bytes
244
+ instead of reopening caller paths later.
245
+
246
+ When `options.writeDiffs` is `true`, diff outputs are written under `options.diffsOutputFolder`,
247
+ which acts as a trusted write root. Auto-generated diff paths and any
248
+ `excludedAreas[].diffFilePath` overrides must resolve within that directory. This boundary assumes a
249
+ trusted local filesystem while the comparison is running. If `diffsOutputFolder` is provided, it is
250
+ still validated even when `writeDiffs` is `false`.
138
251
 
139
252
  **Throws:**
140
- - `Error: PDF file not found: <path>` — when a string argument points to a non-existent file.
141
- - `Error: Unknown input file type.` — when an argument is neither a string nor a Buffer.
142
- - `Error: Compare Threshold cannot be less than 0.` — when `options.compareThreshold < 0`.
253
+
254
+ - `ComparePdfInputError: PDF file not found: <path>` — when a string argument points to a non-existent file.
255
+ - `ComparePdfInputError: PDF path is not a file: <path>` — when a string argument points to an existing directory or other non-file path.
256
+ - `ComparePdfInputError: Failed to read PDF file: <path>` — when a string argument exists but the library cannot open or read it. The original filesystem exception is attached as `cause`.
257
+ - `ComparePdfInputError: Unknown input file type.` — when an argument is not a `string`, `Buffer`, `ArrayBuffer`, or `SharedArrayBuffer`.
258
+ - `ComparePdfConfigurationError: Options must be an object.` — when an untyped caller passes a non-object third argument.
259
+ - `ComparePdfConfigurationError: diffsOutputFolder must be a non-empty string.` — when an untyped caller passes a non-string or blank diff root.
260
+ - `ComparePdfConfigurationError: diffsOutputFolder must point to a directory when it already exists: <path>` — when the configured diff root already exists as a file.
261
+ - `ComparePdfConfigurationError: allowedInputRoot must be a non-empty string.` — when an untyped caller passes a non-string or blank path boundary.
262
+ - `ComparePdfConfigurationError: allowedInputRoot does not exist: <path>` — when `allowedInputRoot` points to a missing path.
263
+ - `ComparePdfConfigurationError: allowedInputRoot must point to an existing directory: <path>` — when `allowedInputRoot` points to a file instead of a directory.
264
+ - `ComparePdfConfigurationError: actualPdf must resolve within allowedInputRoot: <path>` / `expectedPdf must resolve within allowedInputRoot: <path>` — when a string PDF input escapes the configured root.
265
+ - `ComparePdfConfigurationError: excludedAreas must be an array.` — when an untyped caller passes a non-array `excludedAreas` value.
266
+ - `ComparePdfConfigurationError: Each excludedAreas entry must be an object.` — when an untyped caller passes a non-object item inside `excludedAreas`.
267
+ - `ComparePdfConfigurationError: Diff output path must stay within diffsOutputFolder: <path>` — when an auto-generated diff path or `excludedAreas[].diffFilePath` override escapes the configured diff root.
268
+ - `ComparePdfConfigurationError: Compare Threshold must be a finite non-negative integer.` — when `options.compareThreshold` is negative, fractional, `NaN`, or infinite.
269
+ - `ComparePdfConfigurationError: Matching Threshold must be a finite non-negative integer.` — when `excludedAreas[].matchingThreshold` is negative, fractional, `NaN`, or infinite.
270
+ - `ComparePdfConfigurationError: Page Number must be a finite positive integer.` — when `excludedAreas[].pageNumber` is `<= 0`, fractional, `NaN`, or infinite.
271
+ - `ComparePdfConfigurationError: pdfToPngConvertOptions must be an object.` — when an untyped caller passes a non-object render configuration.
272
+ - `ComparePdfConfigurationError: Unsupported pdfToPngConvertOptions properties: ...` — when an untyped caller passes render settings that would skip page content or enable parallel rendering.
273
+ - `ComparePdfConfigurationError: pdfToPngConvertOptions.outputFolder must be a non-empty string.` — when an untyped caller passes a non-string or blank render output path.
274
+ - `ComparePdfConfigurationError: pdfToPngConvertOptions.outputFolder must point to a directory when it already exists: <path>` — when the configured render output path already exists as a file.
275
+ - `ComparePdfConfigurationError: pdfToPngConvertOptions.outputFolder must not traverse a symbolic link: <path>` — when the render output path itself or an existing ancestor is a symbolic link, or when the post-mkdir leaf check finds a symbolic link at the resolved folder or at the pre-created `actual/` / `expected/` namespace subdirectory (sandbox parity with `diffsOutputFolder`).
276
+ - `ComparePdfConfigurationError: pdfToPngConvertOptions.outputFolder must point to a writable directory: <path>` — when the library cannot create the resolved render output folder or its `actual/` / `expected/` namespace subdirectories (for example, a regular file blocks the leaf path).
277
+ - `ComparePdfRenderingError: Failed to render actual PDF pages.` / `Failed to render expected PDF pages.` — when the PDF renderer dependency fails. The original dependency exception is attached as `cause`.
278
+ - `ComparePdfRenderingError: Rendered page content is missing for page: <page-name>.` — when the renderer returns a page without binary PNG content.
279
+ - `ComparePdfComparisonError: Failed to compare rendered PDF page <page-number>.` — when the PNG comparator dependency fails. The original dependency exception is attached as `cause`.
280
+
281
+ ### `comparePdfDetailed(actualPdf, expectedPdf, options?)`
282
+
283
+ Accepts the same parameters as `comparePdf()` and returns
284
+ `Promise<ComparePdfDetailedResult>`.
285
+
286
+ Use this API when you need page-level mismatch counts, applied thresholds, diff output paths, or
287
+ deterministic missing-page information without inspecting the filesystem.
288
+
289
+ ### `PdfInput`
290
+
291
+ ```typescript
292
+ type PdfInput = string | Buffer | ArrayBufferLike;
293
+ ```
294
+
295
+ `string` values are trusted local file paths by default. They are opened and read before rendering.
296
+ For untrusted environments, prefer binary inputs or set `ComparePdfOptions.allowedInputRoot`.
143
297
 
144
298
  ### `ComparePdfOptions`
145
299
 
146
- | Property | Type | Default | Description |
147
- | ------------------------ | --------------------- | -------------------- | --------------------------------------------------------------------------- |
148
- | `diffsOutputFolder` | `string` | `./comparePdfOutput` | Folder where diff PNG images are written |
149
- | `compareThreshold` | `number` | `0` | Maximum number of differing pixels allowed before comparison fails |
150
- | `excludedAreas` | `ExcludedPageArea[]` | `[]` | Per-page exclusion zones matched by the `pageNumber` field of each entry (1-based) |
151
- | `pdfToPngConvertOptions` | `PdfToPngOptions` | see below | Options forwarded to [`pdf-to-png-converter`](https://github.com/dichovsky/pdf-to-png-converter) |
152
-
153
- ### `ExcludedPageArea`
154
-
155
- | Property | Type | Description |
156
- | ------------------- | -------- | --------------------------------------------------------------------------------------------------- |
157
- | `pageNumber` | `number` | 1-based page number this exclusion applies to (`1` = first page, `2` = second page, etc.) |
158
- | `excludedAreas` | `Area[]` | Rectangles to exclude. `Area` = `{ x1, y1, x2, y2 }` in pixels at the configured `viewportScale` |
159
- | `excludedAreaColor` | `Color` | Fill color for excluded regions in diff images. `Color` = `{ r, g, b }` with values 0–255 |
160
- | `diffFilePath` | `string` | Override the diff image output path for this page |
161
- | `matchingThreshold` | `number` | Per-page pixel threshold, overrides the document-level `compareThreshold` for this page |
300
+ | Property | Type | Default | Description |
301
+ | ------------------------ | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
302
+ | `writeDiffs` | `boolean` | `false` | Enables diff PNG output on disk |
303
+ | `diffsOutputFolder` | `string` | `./comparePdfOutput` | Trusted diff-output root; validated when provided, and used for written diff PNGs only when `writeDiffs` is `true` |
304
+ | `allowedInputRoot` | `string` | `undefined` | Optional root directory that constrains string PDF inputs; when omitted, string paths are trusted caller-controlled inputs |
305
+ | `compareThreshold` | `number` | `0` | Maximum number of differing pixels allowed before comparison fails; must be a finite non-negative integer |
306
+ | `excludedAreas` | `PageExclusion[]` | `[]` | Per-page exclusion zones matched by rendered `pageNumber` (1-based); non-rendered pages are ignored and the first duplicate entry for a page wins |
307
+ | `pdfToPngConvertOptions` | `PdfRenderOptions` | see below | Options for rendering PDF pages before comparison |
308
+
309
+ ### `ComparePdfDetailedResult`
310
+
311
+ | Property | Type | Description |
312
+ | ------------------- | ------------------------ | ------------------------------------------------------------- |
313
+ | `isEqual` | `boolean` | `true` when every planned page comparison is within threshold |
314
+ | `actualPageCount` | `number` | Number of rendered pages produced from the actual PDF |
315
+ | `expectedPageCount` | `number` | Number of rendered pages produced from the expected PDF |
316
+ | `compareThreshold` | `number` | Document-level threshold supplied to the comparison |
317
+ | `writeDiffs` | `boolean` | `true` when diff PNG writing was enabled for the comparison |
318
+ | `diffsOutputFolder` | `string \| null` | Resolved base diff output folder, or `null` when disabled |
319
+ | `pages` | `ComparePdfPageResult[]` | Page-level outcomes sorted by `pageNumber` |
320
+
321
+ ### `ComparePdfPageResult`
322
+
323
+ | Property | Type | Description |
324
+ | ------------------ | ---------------------- | ------------------------------------------------------------------- |
325
+ | `pageNumber` | `number` | 1-based rendered page number |
326
+ | `status` | `ComparePdfPageStatus` | `matched`, `mismatched`, `missing-actual`, or `missing-expected` |
327
+ | `isEqual` | `boolean` | `true` when this page is within its applicable threshold |
328
+ | `threshold` | `number` | Threshold actually applied to this page |
329
+ | `mismatchCount` | `number \| null` | Comparator mismatch count, or `null` when the page was not compared |
330
+ | `diffFilePath` | `string \| null` | Diff PNG output path, or `null` when the page was not compared |
331
+ | `actualPageName` | `string \| null` | Renderer-reported actual page image name |
332
+ | `expectedPageName` | `string \| null` | Renderer-reported expected page image name |
333
+
334
+ ### `ComparePdfPageStatus`
335
+
336
+ ```typescript
337
+ type ComparePdfPageStatus = 'matched' | 'mismatched' | 'missing-actual' | 'missing-expected';
338
+ ```
339
+
340
+ ### `PdfRenderOptions`
341
+
342
+ `PdfRenderOptions` is this library's stable rendering contract. It is adapted internally to the
343
+ current PDF renderer and is not a direct re-export of an upstream dependency type.
344
+
345
+ | Property | Type | Description |
346
+ | -------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
+ | `viewportScale` | `number` | Scale factor applied before rendering |
348
+ | `disableFontFace` | `boolean` | Use built-in fonts instead of embedded fonts |
349
+ | `useSystemFonts` | `boolean` | Allow system font fallbacks |
350
+ | `enableXfa` | `boolean` | Render XFA form data |
351
+ | `pdfFilePassword` | `string` | Password for encrypted PDFs |
352
+ | `outputFolder` | `string` | Folder for intermediate PNG files; validated as a non-empty path that does not traverse a symbolic link. This library writes under `actual/` and `expected/` subfolders to avoid collisions |
353
+ | `outputFileMaskFunc` | `(pageNumber: number) => string` | Custom PNG filename generator |
354
+ | `pagesToProcess` | `number[]` | 1-based pages to render |
355
+ | `verbosityLevel` | `number` | Renderer verbosity level |
356
+
357
+ `comparePdf()` always renders page content and always calls the renderer sequentially. The following
358
+ upstream renderer flags are intentionally excluded from `PdfRenderOptions` and are rejected at runtime
359
+ for untyped JavaScript callers: `returnPageContent`, `returnMetadataOnly`, `processPagesInParallel`,
360
+ and `concurrencyLimit`.
361
+
362
+ `pdfToPngConvertOptions.outputFolder` controls renderer intermediate files independently from
363
+ `diffsOutputFolder`, which only constrains diff PNG output paths. When `outputFolder` is set, this
364
+ library writes the two compared PDFs into `actual/` and `expected/` subfolders beneath that root so
365
+ custom filename masks do not collide.
366
+
367
+ The render output path is validated with the same sandbox-parity contract applied to
368
+ `diffsOutputFolder`: it must be a non-empty string, must resolve to a directory when the path
369
+ already exists, and may not traverse a symbolic link at the leaf or at any existing ancestor.
370
+ On top of the path-walker check, this library takes ownership of leaf creation: the resolved
371
+ path and both the `actual/` and `expected/` namespace subdirectories are pre-created and then
372
+ re-asserted to be real (non-symlink) directories before any render call runs. That closes the
373
+ residual validate→render TOCTOU window the path walker cannot cover on its own, so an attacker
374
+ cannot replace a future write destination with a symbolic link between validation and the
375
+ renderer's first write (CWE-59 / CWE-61 / CWE-367).
376
+
377
+ ### `PageExclusion`
378
+
379
+ | Property | Type | Description |
380
+ | ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
381
+ | `pageNumber` | `number` | 1-based page number this exclusion applies to (`1` = first page, `2` = second page, etc.); must be a finite positive integer |
382
+ | `excludedAreas` | `PageArea[]` | Rectangles to exclude from comparison |
383
+ | `excludedAreaColor` | `RgbColor` | Fill colour applied to `excludedAreas` before comparison. When omitted, `png-visual-compare` uses its default blue `{ r: 0, g: 0, b: 255 }` |
384
+ | `diffFilePath` | `string` | Override the diff image output path for this page; the resolved path must stay inside `diffsOutputFolder` |
385
+ | `matchingThreshold` | `number` | Per-page pixel threshold, overrides the document-level `compareThreshold` for this page; must be a finite non-negative integer |
386
+
387
+ `ExcludedPageArea` remains exported as a backwards-compatible alias of `PageExclusion`.
388
+
389
+ Entries whose `pageNumber` does not correspond to a rendered page are ignored. If multiple entries
390
+ target the same `pageNumber`, only the first matching entry is used.
391
+
392
+ ### `PageArea`
393
+
394
+ `PageArea` is a rectangle on a rendered PDF page:
395
+
396
+ ```typescript
397
+ {
398
+ x1: number;
399
+ y1: number;
400
+ x2: number;
401
+ y2: number;
402
+ }
403
+ ```
404
+
405
+ ### `RgbColor`
406
+
407
+ `RgbColor` is an RGB colour object used in diff-related configuration:
408
+
409
+ ```typescript
410
+ {
411
+ r: number;
412
+ g: number;
413
+ b: number;
414
+ }
415
+ ```
162
416
 
163
417
  ---
164
418
 
@@ -1,11 +1,53 @@
1
1
  import type { ComparePdfOptions } from './types/ComparePdfOptions.js';
2
+ import type { ComparePdfDetailedResult } from './types/ComparePdfDetailedResult.js';
3
+ import type { PdfInput } from './types/PdfInput.js';
2
4
  /**
3
- * Compares two PDF files or buffers and returns a boolean indicating whether they are similar.
5
+ * Compares two PDF inputs and returns a boolean indicating whether they are visually equivalent.
4
6
  *
5
- * @param actualPdf - The file path or buffer of the actual PDF to compare.
6
- * @param expectedPdf - The file path or buffer of the expected PDF to compare against.
7
+ * Supported PDF inputs are file paths, `Buffer`, `ArrayBuffer`, and `SharedArrayBuffer`. String
8
+ * paths are trusted caller-controlled inputs by default. To constrain them to a specific
9
+ * workspace, set `options.allowedInputRoot`. `SharedArrayBuffer` inputs are normalized to
10
+ * `ArrayBuffer` before rendering.
11
+ *
12
+ * Diff PNGs are not written unless `options.writeDiffs` is explicitly set to `true`. When enabled,
13
+ * `options.diffsOutputFolder` acts as a trusted write root on a trusted local filesystem.
14
+ *
15
+ * Rendered pages are paired by renderer-reported `pageNumber`, not by generated PNG filename
16
+ * or `excludedAreas` array position. If either PDF is missing a rendered counterpart for a
17
+ * page number present in the comparison plan, the overall comparison returns `false`.
18
+ *
19
+ * @param actualPdf - The file path or binary content of the actual PDF to compare.
20
+ * @param expectedPdf - The file path or binary content of the expected PDF to compare against.
21
+ * @param opts - Optional comparison options.
22
+ * @returns A promise that resolves to `true` when every compared page stays within its
23
+ * applicable threshold, otherwise `false`.
24
+ * @throws {ComparePdfInputError} When a PDF input has an unsupported type or points to a missing file.
25
+ * @throws {ComparePdfConfigurationError} When runtime comparison configuration is invalid.
26
+ * @throws {ComparePdfRenderingError} When PDF rendering fails.
27
+ * @throws {ComparePdfComparisonError} When PNG comparison fails.
28
+ */
29
+ export declare function comparePdf(actualPdf: PdfInput, expectedPdf: PdfInput, opts?: ComparePdfOptions): Promise<boolean>;
30
+ /**
31
+ * Compares two PDF inputs and returns structured page-level comparison details.
32
+ *
33
+ * Supported PDF inputs are file paths, `Buffer`, `ArrayBuffer`, and `SharedArrayBuffer`. String
34
+ * paths are trusted caller-controlled inputs by default. To constrain them to a specific
35
+ * workspace, set `options.allowedInputRoot`. `SharedArrayBuffer` inputs are normalized to
36
+ * `ArrayBuffer` before rendering.
37
+ *
38
+ * Diff PNGs are not written unless `options.writeDiffs` is explicitly set to `true`.
39
+ *
40
+ * Rendered pages are paired by renderer-reported `pageNumber`, not by generated PNG filename
41
+ * or `excludedAreas` array position. Missing rendered counterpart pages are surfaced in the
42
+ * returned page results without requiring callers to inspect diff files on disk.
43
+ *
44
+ * @param actualPdf - The file path or binary content of the actual PDF to compare.
45
+ * @param expectedPdf - The file path or binary content of the expected PDF to compare against.
7
46
  * @param opts - Optional comparison options.
8
- * @returns A promise that resolves to a boolean indicating whether the PDFs are similar.
9
- * @throws Will throw an error if the compare threshold is less than 0.
47
+ * @returns A promise that resolves to a structured comparison result.
48
+ * @throws {ComparePdfInputError} When a PDF input has an unsupported type or points to a missing file.
49
+ * @throws {ComparePdfConfigurationError} When runtime comparison configuration is invalid.
50
+ * @throws {ComparePdfRenderingError} When PDF rendering fails.
51
+ * @throws {ComparePdfComparisonError} When PNG comparison fails.
10
52
  */
11
- export declare function comparePdf(actualPdf: string | ArrayBufferLike, expectedPdf: string | ArrayBufferLike, opts?: ComparePdfOptions): Promise<boolean>;
53
+ export declare function comparePdfDetailed(actualPdf: PdfInput, expectedPdf: PdfInput, opts?: ComparePdfOptions): Promise<ComparePdfDetailedResult>;