pdf-visual-compare 3.4.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 -78
  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 -33
  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 -74
package/out/comparePdf.js CHANGED
@@ -1,85 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.comparePdf = comparePdf;
4
- const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
- const pdf_to_png_converter_1 = require("pdf-to-png-converter");
7
- const png_visual_compare_1 = require("png-visual-compare");
8
- const const_js_1 = require("./const.js");
9
- /**
10
- * Compares two PDF files or buffers and returns a boolean indicating whether they are similar.
11
- *
12
- * @param actualPdf - The file path or buffer of the actual PDF to compare.
13
- * @param expectedPdf - The file path or buffer of the expected PDF to compare against.
14
- * @param opts - Optional comparison options.
15
- * @returns A promise that resolves to a boolean indicating whether the PDFs are similar.
16
- * @throws Will throw an error if the compare threshold is less than 0.
17
- */
18
- async function comparePdf(actualPdf, expectedPdf, opts = {}) {
19
- // Validate input file types
20
- validateInputFileType(actualPdf);
21
- validateInputFileType(expectedPdf);
22
- // Set default options
23
- const pdfToPngConvertOpts = { ...opts.pdfToPngConvertOptions };
24
- if (!pdfToPngConvertOpts.viewportScale) {
25
- pdfToPngConvertOpts.viewportScale = 2.0;
26
- }
27
- if (!pdfToPngConvertOpts.outputFileMaskFunc) {
28
- pdfToPngConvertOpts.outputFileMaskFunc = (pageNumber) => `comparePdf_${pageNumber}.png`;
29
- }
30
- const diffsOutputFolder = opts?.diffsOutputFolder ?? const_js_1.DEFAULT_DIFFS_FOLDER;
31
- const compareThreshold = opts?.compareThreshold ?? 0;
32
- const excludedAreas = opts?.excludedAreas ?? [];
33
- if (compareThreshold < 0) {
34
- throw Error('Compare Threshold cannot be less than 0.');
35
- }
36
- // Convert PDFs to PNGs
37
- let [actualPdfPngPages, expectedPdfPngPages] = await Promise.all([
38
- (0, pdf_to_png_converter_1.pdfToPng)(actualPdf, pdfToPngConvertOpts),
39
- (0, pdf_to_png_converter_1.pdfToPng)(expectedPdf, pdfToPngConvertOpts),
40
- ]);
41
- // Ensure actualPdfPngPages is always the longer array to avoid index out of bounds errors
42
- if (actualPdfPngPages.length < expectedPdfPngPages.length) {
43
- [actualPdfPngPages, expectedPdfPngPages] = [expectedPdfPngPages, actualPdfPngPages];
44
- }
45
- let documentCompareResult = true;
46
- actualPdfPngPages.forEach((pngPage, index) => {
47
- const comparePngOpts = {
48
- ...opts?.pdfToPngConvertOptions,
49
- ...excludedAreas[index],
50
- throwErrorOnInvalidInputData: false,
51
- };
52
- comparePngOpts.diffFilePath = (0, node_path_1.resolve)(diffsOutputFolder, `diff_${pngPage.name}`);
53
- const pngPageOutputToCompareWith = expectedPdfPngPages.find((p) => p.name === pngPage.name);
54
- const pageCompareResult = (0, png_visual_compare_1.comparePng)(pngPage.content, pngPageOutputToCompareWith?.content ?? '', comparePngOpts);
55
- if (pageCompareResult > compareThreshold) {
56
- documentCompareResult = false;
57
- }
58
- });
59
- return documentCompareResult;
4
+ exports.comparePdfDetailed = comparePdfDetailed;
5
+ const comparePlannedPage_js_1 = require("./internal/comparePlannedPage.js");
6
+ const pageComparisonPlan_js_1 = require("./internal/pageComparisonPlan.js");
7
+ const normalizeComparisonOptions_js_1 = require("./internal/normalizeComparisonOptions.js");
8
+ const normalizePdfInput_js_1 = require("./internal/normalizePdfInput.js");
9
+ const renderPdfPages_js_1 = require("./internal/renderPdfPages.js");
10
+ async function comparePdf(actualPdf, expectedPdf, opts) {
11
+ const result = await comparePdfDetailed(actualPdf, expectedPdf, opts);
12
+ return result.isEqual;
60
13
  }
61
- /**
62
- * Validates the type of the input file. The input file can either be a Buffer or a string representing a file path.
63
- * If the input file is a Buffer, the function returns without any error.
64
- * If the input file is a string, the function checks if the file exists at the given path.
65
- * If the file does not exist, an error is thrown.
66
- * If the input file is neither a Buffer nor a string, an error is thrown.
67
- *
68
- * @param inputFile - The input file to validate. It can be a Buffer or a string representing a file path.
69
- * @throws {Error} If the input file is a string and the file does not exist.
70
- * @throws {Error} If the input file is neither a Buffer nor a string.
71
- */
72
- function validateInputFileType(inputFile) {
73
- if (Buffer.isBuffer(inputFile)) {
74
- return;
14
+ async function comparePdfDetailed(actualPdf, expectedPdf, opts) {
15
+ const normalizedOptions = (0, normalizeComparisonOptions_js_1.normalizeComparisonOptions)(opts);
16
+ const normalizedActualPdf = (0, normalizePdfInput_js_1.normalizePdfInput)(actualPdf, 'actualPdf', normalizedOptions.allowedInputRoot);
17
+ const normalizedExpectedPdf = (0, normalizePdfInput_js_1.normalizePdfInput)(expectedPdf, 'expectedPdf', normalizedOptions.allowedInputRoot);
18
+ const actualPlanningPages = await (0, renderPdfPages_js_1.listRenderedPageNumbers)(normalizedActualPdf, normalizedOptions.pdfToPngConvertOpts, 'actual');
19
+ const expectedPlanningPages = await (0, renderPdfPages_js_1.listRenderedPageNumbers)(normalizedExpectedPdf, normalizedOptions.pdfToPngConvertOpts, 'expected');
20
+ const actualPageNumberSet = new Set(actualPlanningPages.pageNumbers);
21
+ const expectedPageNumberSet = new Set(expectedPlanningPages.pageNumbers);
22
+ const comparisonPlan = (0, pageComparisonPlan_js_1.buildPageNumberComparisonPlan)(actualPlanningPages.pageNumbers, expectedPlanningPages.pageNumbers, normalizedOptions.excludedAreas);
23
+ const pages = [];
24
+ for (const planEntry of comparisonPlan) {
25
+ const actualPage = actualPageNumberSet.has(planEntry.pageNumber)
26
+ ? await resolvePlannedPage(actualPlanningPages.prefetchedPages.get(planEntry.pageNumber), normalizedActualPdf, normalizedOptions.pdfToPngConvertOpts, planEntry.pageNumber, 'actual')
27
+ : undefined;
28
+ const expectedPage = expectedPageNumberSet.has(planEntry.pageNumber)
29
+ ? await resolvePlannedPage(expectedPlanningPages.prefetchedPages.get(planEntry.pageNumber), normalizedExpectedPdf, normalizedOptions.pdfToPngConvertOpts, planEntry.pageNumber, 'expected')
30
+ : undefined;
31
+ pages.push((0, comparePlannedPage_js_1.comparePlannedPage)({ ...planEntry, actualPage, expectedPage }, normalizedOptions));
75
32
  }
76
- if (typeof inputFile === 'string') {
77
- if ((0, node_fs_1.existsSync)(inputFile)) {
78
- return;
79
- }
80
- else {
81
- throw Error(`PDF file not found: ${inputFile}`);
82
- }
33
+ return {
34
+ isEqual: pages.every((page) => page.isEqual),
35
+ actualPageCount: actualPlanningPages.pageNumbers.length,
36
+ expectedPageCount: expectedPlanningPages.pageNumbers.length,
37
+ compareThreshold: normalizedOptions.compareThreshold,
38
+ diffsOutputFolder: normalizedOptions.writeDiffs ? normalizedOptions.diffsOutputFolder : null,
39
+ pages,
40
+ writeDiffs: normalizedOptions.writeDiffs,
41
+ };
42
+ }
43
+ async function resolvePlannedPage(prefetchedPage, pdfInput, pdfToPngConvertOpts, pageNumber, sourceLabel) {
44
+ if (prefetchedPage && isRenderedPngPageOutput(prefetchedPage)) {
45
+ return prefetchedPage;
83
46
  }
84
- throw Error(`Unknown input file type.`);
47
+ return (0, renderPdfPages_js_1.renderPdfPage)(pdfInput, pdfToPngConvertOpts, pageNumber, sourceLabel);
48
+ }
49
+ function isRenderedPngPageOutput(page) {
50
+ return page.kind !== 'metadata' && Buffer.isBuffer(page.content);
85
51
  }
package/out/const.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- /** Default folder path for diff PNG images produced during PDF comparison. */
2
- export declare const DEFAULT_DIFFS_FOLDER: string;
1
+ /** Resolves the default folder path for diff PNG images produced during PDF comparison. */
2
+ export declare function getDefaultDiffsFolder(): string;
package/out/const.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_DIFFS_FOLDER = void 0;
3
+ exports.getDefaultDiffsFolder = getDefaultDiffsFolder;
4
4
  const node_path_1 = require("node:path");
5
- /** Default folder path for diff PNG images produced during PDF comparison. */
6
- exports.DEFAULT_DIFFS_FOLDER = (0, node_path_1.resolve)(`./comparePdfOutput`);
5
+ /** Resolves the default folder path for diff PNG images produced during PDF comparison. */
6
+ function getDefaultDiffsFolder() {
7
+ return (0, node_path_1.resolve)('./comparePdfOutput');
8
+ }
@@ -0,0 +1,6 @@
1
+ import { ComparePdfError } from './ComparePdfError.js';
2
+ /**
3
+ * Thrown when rendered PDF pages cannot be compared reliably.
4
+ */
5
+ export declare class ComparePdfComparisonError extends ComparePdfError {
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComparePdfComparisonError = void 0;
4
+ const ComparePdfError_js_1 = require("./ComparePdfError.js");
5
+ /**
6
+ * Thrown when rendered PDF pages cannot be compared reliably.
7
+ */
8
+ class ComparePdfComparisonError extends ComparePdfError_js_1.ComparePdfError {
9
+ }
10
+ exports.ComparePdfComparisonError = ComparePdfComparisonError;
@@ -0,0 +1,6 @@
1
+ import { ComparePdfError } from './ComparePdfError.js';
2
+ /**
3
+ * Thrown when comparePdf receives invalid runtime configuration values.
4
+ */
5
+ export declare class ComparePdfConfigurationError extends ComparePdfError {
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComparePdfConfigurationError = void 0;
4
+ const ComparePdfError_js_1 = require("./ComparePdfError.js");
5
+ /**
6
+ * Thrown when comparePdf receives invalid runtime configuration values.
7
+ */
8
+ class ComparePdfConfigurationError extends ComparePdfError_js_1.ComparePdfError {
9
+ }
10
+ exports.ComparePdfConfigurationError = ComparePdfConfigurationError;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Base error for all public library exceptions thrown by `comparePdf`.
3
+ */
4
+ export declare class ComparePdfError extends Error {
5
+ constructor(message: string, options?: ErrorOptions);
6
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComparePdfError = void 0;
4
+ /**
5
+ * Base error for all public library exceptions thrown by `comparePdf`.
6
+ */
7
+ class ComparePdfError extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = new.target.name;
11
+ }
12
+ }
13
+ exports.ComparePdfError = ComparePdfError;
@@ -0,0 +1,6 @@
1
+ import { ComparePdfError } from './ComparePdfError.js';
2
+ /**
3
+ * Thrown when comparePdf receives invalid PDF input arguments.
4
+ */
5
+ export declare class ComparePdfInputError extends ComparePdfError {
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComparePdfInputError = void 0;
4
+ const ComparePdfError_js_1 = require("./ComparePdfError.js");
5
+ /**
6
+ * Thrown when comparePdf receives invalid PDF input arguments.
7
+ */
8
+ class ComparePdfInputError extends ComparePdfError_js_1.ComparePdfError {
9
+ }
10
+ exports.ComparePdfInputError = ComparePdfInputError;
@@ -0,0 +1,6 @@
1
+ import { ComparePdfError } from './ComparePdfError.js';
2
+ /**
3
+ * Thrown when PDF rendering fails before visual comparison can start.
4
+ */
5
+ export declare class ComparePdfRenderingError extends ComparePdfError {
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComparePdfRenderingError = void 0;
4
+ const ComparePdfError_js_1 = require("./ComparePdfError.js");
5
+ /**
6
+ * Thrown when PDF rendering fails before visual comparison can start.
7
+ */
8
+ class ComparePdfRenderingError extends ComparePdfError_js_1.ComparePdfError {
9
+ }
10
+ exports.ComparePdfRenderingError = ComparePdfRenderingError;
package/out/index.d.ts CHANGED
@@ -3,10 +3,24 @@
3
3
  *
4
4
  * Visual regression testing library for PDFs in JavaScript/TypeScript.
5
5
  * Converts PDF pages to PNG images and performs pixel-level comparison,
6
- * requiring no native binaries or OS-level dependencies.
6
+ * without requiring external system packages. The current renderer uses
7
+ * prebuilt native `@napi-rs/canvas` binaries bundled through its dependency tree.
7
8
  *
8
9
  * @module pdf-visual-compare
9
10
  */
10
- export { comparePdf } from './comparePdf.js';
11
+ export { comparePdf, comparePdfDetailed } from './comparePdf.js';
12
+ export { ComparePdfComparisonError } from './errors/ComparePdfComparisonError.js';
13
+ export { ComparePdfConfigurationError } from './errors/ComparePdfConfigurationError.js';
14
+ export { ComparePdfError } from './errors/ComparePdfError.js';
15
+ export { ComparePdfInputError } from './errors/ComparePdfInputError.js';
16
+ export { ComparePdfRenderingError } from './errors/ComparePdfRenderingError.js';
11
17
  export type { ComparePdfOptions } from './types/ComparePdfOptions.js';
18
+ export type { ComparePdfDetailedResult } from './types/ComparePdfDetailedResult.js';
19
+ export type { ComparePdfPageResult } from './types/ComparePdfPageResult.js';
20
+ export type { ComparePdfPageStatus } from './types/ComparePdfPageStatus.js';
12
21
  export type { ExcludedPageArea } from './types/ExcludedPageArea.js';
22
+ export type { PageArea } from './types/PageArea.js';
23
+ export type { PageExclusion } from './types/PageExclusion.js';
24
+ export type { PdfInput } from './types/PdfInput.js';
25
+ export type { PdfRenderOptions } from './types/PdfRenderOptions.js';
26
+ export type { RgbColor } from './types/RgbColor.js';
package/out/index.js CHANGED
@@ -1,14 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.comparePdf = void 0;
3
+ exports.ComparePdfRenderingError = exports.ComparePdfInputError = exports.ComparePdfError = exports.ComparePdfConfigurationError = exports.ComparePdfComparisonError = exports.comparePdfDetailed = exports.comparePdf = void 0;
4
4
  /**
5
5
  * pdf-visual-compare
6
6
  *
7
7
  * Visual regression testing library for PDFs in JavaScript/TypeScript.
8
8
  * Converts PDF pages to PNG images and performs pixel-level comparison,
9
- * requiring no native binaries or OS-level dependencies.
9
+ * without requiring external system packages. The current renderer uses
10
+ * prebuilt native `@napi-rs/canvas` binaries bundled through its dependency tree.
10
11
  *
11
12
  * @module pdf-visual-compare
12
13
  */
13
14
  var comparePdf_js_1 = require("./comparePdf.js");
14
15
  Object.defineProperty(exports, "comparePdf", { enumerable: true, get: function () { return comparePdf_js_1.comparePdf; } });
16
+ Object.defineProperty(exports, "comparePdfDetailed", { enumerable: true, get: function () { return comparePdf_js_1.comparePdfDetailed; } });
17
+ var ComparePdfComparisonError_js_1 = require("./errors/ComparePdfComparisonError.js");
18
+ Object.defineProperty(exports, "ComparePdfComparisonError", { enumerable: true, get: function () { return ComparePdfComparisonError_js_1.ComparePdfComparisonError; } });
19
+ var ComparePdfConfigurationError_js_1 = require("./errors/ComparePdfConfigurationError.js");
20
+ Object.defineProperty(exports, "ComparePdfConfigurationError", { enumerable: true, get: function () { return ComparePdfConfigurationError_js_1.ComparePdfConfigurationError; } });
21
+ var ComparePdfError_js_1 = require("./errors/ComparePdfError.js");
22
+ Object.defineProperty(exports, "ComparePdfError", { enumerable: true, get: function () { return ComparePdfError_js_1.ComparePdfError; } });
23
+ var ComparePdfInputError_js_1 = require("./errors/ComparePdfInputError.js");
24
+ Object.defineProperty(exports, "ComparePdfInputError", { enumerable: true, get: function () { return ComparePdfInputError_js_1.ComparePdfInputError; } });
25
+ var ComparePdfRenderingError_js_1 = require("./errors/ComparePdfRenderingError.js");
26
+ Object.defineProperty(exports, "ComparePdfRenderingError", { enumerable: true, get: function () { return ComparePdfRenderingError_js_1.ComparePdfRenderingError; } });
@@ -0,0 +1,3 @@
1
+ import type { ComparePngOptions } from 'png-visual-compare';
2
+ import type { PageExclusion } from '../../types/PageExclusion.js';
3
+ export declare function toComparePngOptions(pageExclusion: PageExclusion | undefined, diffsOutputFolder: string, pageName: string, writeDiffs?: boolean): ComparePngOptions;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toComparePngOptions = toComparePngOptions;
4
+ const node_path_1 = require("node:path");
5
+ const ComparePdfConfigurationError_js_1 = require("../../errors/ComparePdfConfigurationError.js");
6
+ function toComparePngOptions(pageExclusion, diffsOutputFolder, pageName, writeDiffs = true) {
7
+ const diffFilePath = writeDiffs
8
+ ? resolveDiffFilePath(pageExclusion?.diffFilePath, diffsOutputFolder, pageName)
9
+ : undefined;
10
+ return {
11
+ excludedAreas: pageExclusion?.excludedAreas?.map((area) => ({ ...area })),
12
+ excludedAreaColor: pageExclusion?.excludedAreaColor && { ...pageExclusion.excludedAreaColor },
13
+ diffFilePath,
14
+ throwErrorOnInvalidInputData: true,
15
+ };
16
+ }
17
+ function resolveDiffFilePath(configuredDiffFilePath, diffsOutputFolder, pageName) {
18
+ if (configuredDiffFilePath !== undefined) {
19
+ if (typeof configuredDiffFilePath !== 'string' || configuredDiffFilePath.trim() === '') {
20
+ throw new ComparePdfConfigurationError_js_1.ComparePdfConfigurationError('diffFilePath must be a non-empty string.');
21
+ }
22
+ return assertPathWithinDiffsOutputFolder(configuredDiffFilePath, diffsOutputFolder);
23
+ }
24
+ assertSafePageName(pageName, diffsOutputFolder);
25
+ return assertPathWithinDiffsOutputFolder((0, node_path_1.resolve)(diffsOutputFolder, `diff_${pageName}`), diffsOutputFolder);
26
+ }
27
+ function assertSafePageName(pageName, diffsOutputFolder) {
28
+ if (pageName.trim() !== '' && pageName === (0, node_path_1.basename)(pageName)) {
29
+ return;
30
+ }
31
+ throw new ComparePdfConfigurationError_js_1.ComparePdfConfigurationError(`Diff output path must stay within diffsOutputFolder: ${(0, node_path_1.resolve)(diffsOutputFolder)}`);
32
+ }
33
+ function assertPathWithinDiffsOutputFolder(candidatePath, diffsOutputFolder) {
34
+ const resolvedDiffsOutputFolder = (0, node_path_1.resolve)(diffsOutputFolder);
35
+ const resolvedCandidatePath = (0, node_path_1.resolve)(candidatePath);
36
+ const relativePath = (0, node_path_1.relative)(resolvedDiffsOutputFolder, resolvedCandidatePath);
37
+ if (relativePath === '' || (!relativePath.startsWith('..') && !(0, node_path_1.isAbsolute)(relativePath))) {
38
+ return resolvedCandidatePath;
39
+ }
40
+ throw new ComparePdfConfigurationError_js_1.ComparePdfConfigurationError(`Diff output path must stay within diffsOutputFolder: ${resolvedDiffsOutputFolder}`);
41
+ }
@@ -0,0 +1,3 @@
1
+ import type { PdfToPngOptions } from 'pdf-to-png-converter';
2
+ import type { PdfRenderOptions } from '../../types/PdfRenderOptions.js';
3
+ export declare function toPdfToPngOptions(options: PdfRenderOptions | undefined): PdfToPngOptions;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toPdfToPngOptions = toPdfToPngOptions;
4
+ function toPdfToPngOptions(options) {
5
+ return {
6
+ viewportScale: options?.viewportScale,
7
+ disableFontFace: options?.disableFontFace,
8
+ useSystemFonts: options?.useSystemFonts,
9
+ enableXfa: options?.enableXfa,
10
+ pdfFilePassword: options?.pdfFilePassword,
11
+ outputFolder: options?.outputFolder,
12
+ outputFileMaskFunc: options?.outputFileMaskFunc,
13
+ pagesToProcess: options?.pagesToProcess,
14
+ verbosityLevel: options?.verbosityLevel,
15
+ returnPageContent: true,
16
+ returnMetadataOnly: false,
17
+ processPagesInParallel: false,
18
+ };
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { ComparePdfPageResult } from '../types/ComparePdfPageResult.js';
2
+ import type { NormalizedComparePdfOptions, PageComparisonPlanEntry } from './types.js';
3
+ export declare function comparePlannedPage(planEntry: PageComparisonPlanEntry, normalizedOptions: NormalizedComparePdfOptions): ComparePdfPageResult;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.comparePlannedPage = comparePlannedPage;
4
+ const png_visual_compare_1 = require("png-visual-compare");
5
+ const ComparePdfComparisonError_js_1 = require("../errors/ComparePdfComparisonError.js");
6
+ const comparePngOptions_js_1 = require("./adapters/comparePngOptions.js");
7
+ const diffOutputGuards_js_1 = require("./diffOutputGuards.js");
8
+ function comparePlannedPage(planEntry, normalizedOptions) {
9
+ const threshold = planEntry.pageExclusion?.matchingThreshold ?? normalizedOptions.compareThreshold;
10
+ const actualPageName = planEntry.actualPage?.name ?? null;
11
+ const expectedPageName = planEntry.expectedPage?.name ?? null;
12
+ if (!planEntry.actualPage || !planEntry.expectedPage) {
13
+ return {
14
+ pageNumber: planEntry.pageNumber,
15
+ status: planEntry.actualPage ? 'missing-expected' : 'missing-actual',
16
+ isEqual: false,
17
+ threshold,
18
+ mismatchCount: null,
19
+ diffFilePath: null,
20
+ actualPageName,
21
+ expectedPageName,
22
+ };
23
+ }
24
+ const resolvedComparePngOptions = (0, comparePngOptions_js_1.toComparePngOptions)(planEntry.pageExclusion, normalizedOptions.diffsOutputFolder, planEntry.actualPage.name, normalizedOptions.writeDiffs);
25
+ const diffFilePath = resolvedComparePngOptions.diffFilePath ?? null;
26
+ let stagedTempfilePath;
27
+ let scopedComparePngOptions = resolvedComparePngOptions;
28
+ if (diffFilePath) {
29
+ (0, diffOutputGuards_js_1.assertDiffOutputPathUsesRealFilesystemEntries)(diffFilePath, normalizedOptions.diffsOutputFolder);
30
+ (0, diffOutputGuards_js_1.ensureDiffOutputDirectory)(diffFilePath, normalizedOptions.diffsOutputFolder);
31
+ (0, diffOutputGuards_js_1.assertDiffOutputPathUsesRealFilesystemEntries)(diffFilePath, normalizedOptions.diffsOutputFolder);
32
+ (0, diffOutputGuards_js_1.assertCanonicalDiffOutputPath)(diffFilePath, normalizedOptions.diffsOutputFolder);
33
+ // Stage the comparator's write at a securely-created random-named tempfile in the
34
+ // same directory, then atomically rename into diffFilePath afterwards. rename()
35
+ // replaces any symlink planted at diffFilePath during the write window without
36
+ // following it, which is the only way to actually prevent — not just detect — a
37
+ // redirected write when the comparator's API takes a path rather than an fd
38
+ // (CWE-61 / CWE-367).
39
+ stagedTempfilePath = (0, diffOutputGuards_js_1.createSecureDiffOutputTempfile)(diffFilePath, normalizedOptions.diffsOutputFolder);
40
+ scopedComparePngOptions = { ...resolvedComparePngOptions, diffFilePath: stagedTempfilePath };
41
+ }
42
+ let mismatchCount;
43
+ try {
44
+ mismatchCount = (0, png_visual_compare_1.comparePng)(planEntry.actualPage.content, planEntry.expectedPage.content, scopedComparePngOptions);
45
+ }
46
+ catch (cause) {
47
+ // Roll back the staged tempfile so a zero-byte file does not leak into the diffs
48
+ // folder on the comparator's error path (would otherwise confuse CI artifacts).
49
+ if (stagedTempfilePath) {
50
+ (0, diffOutputGuards_js_1.discardDiffOutputLeaf)(stagedTempfilePath);
51
+ }
52
+ throw new ComparePdfComparisonError_js_1.ComparePdfComparisonError(`Failed to compare rendered PDF page ${planEntry.pageNumber}.`, {
53
+ cause,
54
+ });
55
+ }
56
+ if (stagedTempfilePath && diffFilePath) {
57
+ (0, diffOutputGuards_js_1.publishDiffOutputTempfile)(stagedTempfilePath, diffFilePath, normalizedOptions.diffsOutputFolder, {
58
+ expectDiffWritten: mismatchCount > threshold,
59
+ });
60
+ }
61
+ const isEqual = mismatchCount <= threshold;
62
+ return {
63
+ pageNumber: planEntry.pageNumber,
64
+ status: isEqual ? 'matched' : 'mismatched',
65
+ isEqual,
66
+ threshold,
67
+ mismatchCount,
68
+ diffFilePath,
69
+ actualPageName,
70
+ expectedPageName,
71
+ };
72
+ }
@@ -0,0 +1,56 @@
1
+ import type { ComparePdfOptions } from '../types/ComparePdfOptions.js';
2
+ export declare function validateDiffsOutputFolder(diffsOutputFolder: ComparePdfOptions['diffsOutputFolder']): string;
3
+ export declare function ensureDiffOutputDirectory(outputPath: string, diffsOutputFolder: string): void;
4
+ export declare function assertDiffOutputPathUsesRealFilesystemEntries(diffFilePath: string, diffsOutputFolder: string): void;
5
+ /**
6
+ * Atomically creates a fresh, randomly-named tempfile in the same directory as the diff leaf
7
+ * and returns its path. The third-party comparator is then directed at this tempfile, and the
8
+ * result is later promoted into `diffFilePath` via {@link publishDiffOutputTempfile}.
9
+ *
10
+ * This two-step "stage then rename" pattern is materially stronger than writing directly to
11
+ * `diffFilePath`:
12
+ * - The tempfile name carries an unpredictable random suffix, so an attacker with write
13
+ * access in `diffsOutputFolder` cannot pre-plant a symlink at the future tempfile path.
14
+ * - The tempfile is opened with `O_CREAT | O_EXCL | O_NOFOLLOW`, so any racing attempt to
15
+ * pre-create a regular file or symlink at the same path during the open is rejected
16
+ * (CWE-61 / CWE-367).
17
+ * - The eventual atomic `renameSync` (see {@link publishDiffOutputTempfile}) replaces any
18
+ * pre-existing entry at `diffFilePath` — including a symlink planted there during the
19
+ * write window — without ever following the destination's symlink.
20
+ */
21
+ export declare function createSecureDiffOutputTempfile(diffFilePath: string, diffsOutputFolder: string): string;
22
+ export type PublishDiffOutputTempfileOptions = {
23
+ /**
24
+ * `true` when the comparator was expected to produce a diff PNG (mismatched pages above
25
+ * threshold). A missing or empty tempfile in that case becomes a
26
+ * `ComparePdfConfigurationError` because the diff bytes must already be on disk; their
27
+ * absence implies attacker deletion, a symlink-redirected write, or a silent comparator
28
+ * failure that callers must surface.
29
+ *
30
+ * `false` when the comparator was expected to skip writing (matching pages). Missing or
31
+ * empty tempfile is then the normal "no diff to report" case; any stale leaf at the final
32
+ * path is removed so on-disk state matches the pre-fix behavior.
33
+ */
34
+ expectDiffWritten: boolean;
35
+ };
36
+ /**
37
+ * Promotes the staged tempfile to `diffFilePath` via atomic `renameSync`. This is the
38
+ * core anti-TOCTOU primitive: `rename()` replaces any pre-existing file or symlink at
39
+ * the destination as a single directory-entry operation without ever following the
40
+ * destination. A symlink planted at `diffFilePath` during the write window is therefore
41
+ * harmlessly overwritten — the attacker's chosen target is never opened by this library.
42
+ *
43
+ * Throws `ComparePdfConfigurationError` when:
44
+ * - the tempfile is no longer a regular file (symlink swap during the comparator's write
45
+ * — bytes may have leaked through the swapped symlink to an attacker-chosen target);
46
+ * - a diff was expected but the tempfile is missing or zero bytes;
47
+ * - the tempfile cannot be inspected, or rename fails for reasons other than absence.
48
+ */
49
+ export declare function publishDiffOutputTempfile(tempfilePath: string, diffFilePath: string, diffsOutputFolder: string, options: PublishDiffOutputTempfileOptions): void;
50
+ /**
51
+ * Best-effort removal of a diff output file. Callers use this to roll back the staged
52
+ * tempfile when the third-party comparator throws before writing, and to clear stale
53
+ * leaves at the published path when no diff was produced for matching pages.
54
+ */
55
+ export declare function discardDiffOutputLeaf(diffFilePath: string): void;
56
+ export declare function assertCanonicalDiffOutputPath(diffFilePath: string, diffsOutputFolder: string): void;