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.
- package/README.md +326 -72
- package/out/comparePdf.d.ts +48 -6
- package/out/comparePdf.js +44 -78
- package/out/const.d.ts +2 -2
- package/out/const.js +5 -3
- package/out/errors/ComparePdfComparisonError.d.ts +6 -0
- package/out/errors/ComparePdfComparisonError.js +10 -0
- package/out/errors/ComparePdfConfigurationError.d.ts +6 -0
- package/out/errors/ComparePdfConfigurationError.js +10 -0
- package/out/errors/ComparePdfError.d.ts +6 -0
- package/out/errors/ComparePdfError.js +13 -0
- package/out/errors/ComparePdfInputError.d.ts +6 -0
- package/out/errors/ComparePdfInputError.js +10 -0
- package/out/errors/ComparePdfRenderingError.d.ts +6 -0
- package/out/errors/ComparePdfRenderingError.js +10 -0
- package/out/index.d.ts +16 -2
- package/out/index.js +14 -2
- package/out/internal/adapters/comparePngOptions.d.ts +3 -0
- package/out/internal/adapters/comparePngOptions.js +41 -0
- package/out/internal/adapters/pdfRenderOptions.d.ts +3 -0
- package/out/internal/adapters/pdfRenderOptions.js +19 -0
- package/out/internal/comparePlannedPage.d.ts +3 -0
- package/out/internal/comparePlannedPage.js +72 -0
- package/out/internal/diffOutputGuards.d.ts +56 -0
- package/out/internal/diffOutputGuards.js +176 -0
- package/out/internal/normalizeComparisonOptions.d.ts +2 -0
- package/out/internal/normalizeComparisonOptions.js +104 -0
- package/out/internal/normalizePdfInput.d.ts +13 -0
- package/out/internal/normalizePdfInput.js +110 -0
- package/out/internal/pageComparisonPlan.d.ts +4 -0
- package/out/internal/pageComparisonPlan.js +35 -0
- package/out/internal/renderOutputFolderGuards.d.ts +22 -0
- package/out/internal/renderOutputFolderGuards.js +70 -0
- package/out/internal/renderPdfPages.d.ts +10 -0
- package/out/internal/renderPdfPages.js +105 -0
- package/out/internal/securePath.d.ts +34 -0
- package/out/internal/securePath.js +75 -0
- package/out/internal/types.d.ts +26 -0
- package/out/internal/types.js +2 -0
- package/out/types/ComparePdfDetailedResult.d.ts +35 -0
- package/out/types/ComparePdfDetailedResult.js +2 -0
- package/out/types/ComparePdfOptions.d.ts +33 -11
- package/out/types/ComparePdfPageResult.d.ts +42 -0
- package/out/types/ComparePdfPageResult.js +2 -0
- package/out/types/ComparePdfPageStatus.d.ts +4 -0
- package/out/types/ComparePdfPageStatus.js +2 -0
- package/out/types/ExcludedPageArea.d.ts +3 -33
- package/out/types/PageArea.d.ts +13 -0
- package/out/types/PageArea.js +2 -0
- package/out/types/PageExclusion.d.ts +46 -0
- package/out/types/PageExclusion.js +2 -0
- package/out/types/PdfInput.d.ts +11 -0
- package/out/types/PdfInput.js +2 -0
- package/out/types/PdfRenderOptions.d.ts +51 -0
- package/out/types/PdfRenderOptions.js +2 -0
- package/out/types/RgbColor.d.ts +11 -0
- package/out/types/RgbColor.js +2 -0
- 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
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
2
|
-
export declare
|
|
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.
|
|
3
|
+
exports.getDefaultDiffsFolder = getDefaultDiffsFolder;
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
|
-
/**
|
|
6
|
-
|
|
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,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,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,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,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,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
|
|
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
|
|
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,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;
|