compare-pdf-plus
Version:
Standalone node module that compares PDFs
552 lines (544 loc) • 20.1 kB
JavaScript
;
var gm = require('gm');
var path4 = require('path');
var pdf_mjs = require('pdfjs-dist/legacy/build/pdf.mjs');
var fs4 = require('fs-extra');
var canvas = require('canvas');
var filter = require('lodash/filter');
var pngjs = require('pngjs');
var pixelmatch = require('pixelmatch');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var gm__default = /*#__PURE__*/_interopDefault(gm);
var path4__default = /*#__PURE__*/_interopDefault(path4);
var fs4__default = /*#__PURE__*/_interopDefault(fs4);
var filter__default = /*#__PURE__*/_interopDefault(filter);
var pixelmatch__default = /*#__PURE__*/_interopDefault(pixelmatch);
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/engines/graphicsMagick.ts
var graphicsMagick_exports = {};
__export(graphicsMagick_exports, {
applyCrop: () => applyCrop,
applyMask: () => applyMask,
pdfToPng: () => pdfToPng
});
var imageMagick, pdfToPng, applyMask, applyCrop;
var init_graphicsMagick = __esm({
"src/engines/graphicsMagick.ts"() {
imageMagick = gm__default.default.subClass({ imageMagick: "7+" });
pdfToPng = (pdfDetails, pngFilePath, config2) => {
return new Promise((resolve, reject) => {
const pdfBuffer = pdfDetails.buffer;
const pdfFilename = path4__default.default.parse(pdfDetails.filename).name;
imageMagick(pdfBuffer, pdfFilename).command("convert").density(config2.settings.density, config2.settings.density).quality(config2.settings.quality).write(pngFilePath, (err) => {
err ? reject(err) : resolve();
});
});
};
applyMask = (pngFilePath, coordinates = { x0: 0, y0: 0, x1: 0, y1: 0 }, color = "black") => {
return new Promise((resolve, reject) => {
imageMagick(pngFilePath).command("convert").drawRectangle(
coordinates.x0,
coordinates.y0,
coordinates.x1,
coordinates.y1
).fill(color).write(pngFilePath, (err) => {
err ? reject(err) : resolve();
});
});
};
applyCrop = (pngFilePath, coordinates = { width: 0, height: 0, x: 0, y: 0 }, index = 0) => {
return new Promise((resolve, reject) => {
imageMagick(pngFilePath).command("convert").crop(coordinates.width, coordinates.height, coordinates.x, coordinates.y).write(pngFilePath.replace(".png", `-${index}.png`), (err) => {
err ? reject(err) : resolve();
});
});
};
}
});
// src/engines/native.ts
var native_exports = {};
__export(native_exports, {
applyCrop: () => applyCrop2,
applyMask: () => applyMask2,
pdfPageToPng: () => pdfPageToPng,
pdfToPng: () => pdfToPng2
});
var CMAP_URL, CMAP_PACKED, STANDARD_FONT_DATA_URL, pdfPageToPng, pdfToPng2, applyMask2, applyCrop2;
var init_native = __esm({
"src/engines/native.ts"() {
CMAP_URL = "../../node_modules/pdfjs-dist/cmaps/";
CMAP_PACKED = true;
STANDARD_FONT_DATA_URL = "../../node_modules/pdfjs-dist/standard_fonts/";
pdfPageToPng = async (pdfDocument, pageNumber, filename, isSinglePage = false) => {
const page = await pdfDocument.getPage(pageNumber);
const viewport = page.getViewport({ scale: 1.38889 });
const canvasFactory = pdfDocument.canvasFactory;
const canvasAndContext = canvasFactory.create(
viewport.width,
viewport.height
);
const renderContext = {
canvasContext: canvasAndContext.context,
viewport
};
await page.render(renderContext).promise;
const image = canvasAndContext.canvas.toBuffer("image/png");
const pngFileName = isSinglePage ? filename : filename.replace(".png", `-${pageNumber - 1}.png`);
fs4__default.default.writeFileSync(pngFileName, image);
};
pdfToPng2 = async (pdfDetails, pngFilePath, config2) => {
const pdfData = new Uint8Array(pdfDetails.buffer);
const pdfDocument = await pdf_mjs.getDocument({
disableFontFace: config2.settings?.disableFontFace ?? true,
data: pdfData,
cMapUrl: CMAP_URL,
cMapPacked: CMAP_PACKED,
standardFontDataUrl: STANDARD_FONT_DATA_URL,
verbosity: config2.settings?.verbosity ?? 0
}).promise;
for (let index = 1; index <= pdfDocument.numPages; index++) {
await pdfPageToPng(
pdfDocument,
index,
pngFilePath,
pdfDocument.numPages === 1
);
}
};
applyMask2 = async (pngFilePath, coordinates = { x0: 0, y0: 0, x1: 0, y1: 0 }, color = "black") => {
return new Promise((resolve, reject) => {
const data = fs4__default.default.readFileSync(pngFilePath);
const img = new canvas.Image();
img.src = data;
const canvas$1 = canvas.createCanvas(img.width, img.height);
const ctx = canvas$1.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.beginPath();
ctx.fillRect(
coordinates.x0,
coordinates.y0,
coordinates.x1 - coordinates.x0,
coordinates.y1 - coordinates.y0
);
fs4__default.default.writeFileSync(pngFilePath, canvas$1.toBuffer());
resolve();
});
};
applyCrop2 = async (pngFilePath, coordinates = { width: 0, height: 0, x: 0, y: 0 }, index = 0) => {
return new Promise((resolve, reject) => {
const data = fs4__default.default.readFileSync(pngFilePath);
const img = new canvas.Image();
img.src = data;
const canvas$1 = canvas.createCanvas(coordinates.width, coordinates.height);
const ctx = canvas$1.getContext("2d");
ctx.drawImage(
img,
coordinates.x,
coordinates.y,
coordinates.width,
coordinates.height,
0,
0,
coordinates.width,
coordinates.height
);
fs4__default.default.writeFileSync(
pngFilePath.replace(".png", `-${index}.png`),
canvas$1.toBuffer()
);
resolve();
});
};
}
});
var copyJsonObject = (jsonObject) => {
return JSON.parse(JSON.stringify(jsonObject));
};
var ensureAndCleanupPath = (filepath) => {
fs4__default.default.ensureDirSync(filepath);
fs4__default.default.emptyDirSync(filepath);
};
var ensurePathsExist = (config2) => {
fs4__default.default.ensureDirSync(config2.paths.actualPdfRootFolder);
fs4__default.default.ensureDirSync(config2.paths.baselinePdfRootFolder);
};
var comparePdfByBase64 = async ({
actualPdfFilename,
baselinePdfFilename,
actualPdfBuffer,
baselinePdfBuffer
}) => {
return new Promise((resolve) => {
const actualPdfBaseName = path4__default.default.parse(actualPdfFilename).name;
const baselinePdfBaseName = path4__default.default.parse(baselinePdfFilename).name;
const actualPdfBase64 = Buffer.from(actualPdfBuffer).toString("base64");
const baselinePdfBase64 = Buffer.from(baselinePdfBuffer).toString("base64");
if (actualPdfBase64 !== baselinePdfBase64) {
resolve({
status: "failed",
message: `${actualPdfBaseName}.pdf is not the same as ${baselinePdfBaseName}.pdf compared by their base64 values.`
});
} else {
resolve({ status: "passed" });
}
});
};
var comparePngs = async (actual, baseline, diff, config2) => {
return new Promise((resolve) => {
try {
const actualPng = pngjs.PNG.sync.read(fs4__default.default.readFileSync(actual));
const baselinePng = pngjs.PNG.sync.read(fs4__default.default.readFileSync(baseline));
const { width, height } = actualPng;
const diffPng = new pngjs.PNG({ width, height });
const threshold = config2.settings?.threshold ?? 0.05;
const tolerance = config2.settings?.tolerance ?? 0;
const numDiffPixels = pixelmatch__default.default(
actualPng.data,
baselinePng.data,
diffPng.data,
width,
height,
{
threshold
}
);
if (numDiffPixels > tolerance) {
fs4__default.default.writeFileSync(diff, pngjs.PNG.sync.write(diffPng));
resolve({ status: "failed", numDiffPixels, diffPng: diff });
} else {
resolve({ status: "passed" });
}
} catch (error) {
resolve({ status: "failed", actual, error });
}
});
};
var comparePdfByImage = async ({
actualPdfFilename,
baselinePdfFilename,
actualPdfBuffer,
baselinePdfBuffer,
config: config2,
opts
}) => {
return new Promise(async (resolve) => {
try {
const imageEngine = await (config2.settings.imageEngine === "graphicsMagick" ? Promise.resolve().then(() => (init_graphicsMagick(), graphicsMagick_exports)) : Promise.resolve().then(() => (init_native(), native_exports)));
const actualPdfBaseName = path4__default.default.parse(actualPdfFilename).name;
const baselinePdfBaseName = path4__default.default.parse(baselinePdfFilename).name;
if (config2.paths.actualPngRootFolder && config2.paths.baselinePngRootFolder && config2.paths.diffPngRootFolder) {
const actualPngDirPath = `${config2.paths.actualPngRootFolder}/${actualPdfBaseName}`;
const baselinePngDirPath = `${config2.paths.baselinePngRootFolder}/${baselinePdfBaseName}`;
const diffPngDirPath = `${config2.paths.diffPngRootFolder}/${actualPdfBaseName}`;
ensureAndCleanupPath(actualPngDirPath);
ensureAndCleanupPath(baselinePngDirPath);
ensureAndCleanupPath(diffPngDirPath);
const actualPngFilePath = `${actualPngDirPath}/${actualPdfBaseName}.png`;
const baselinePngFilePath = `${baselinePngDirPath}/${baselinePdfBaseName}.png`;
const actualPdfDetails = {
filename: actualPdfFilename,
buffer: actualPdfBuffer
};
await imageEngine.pdfToPng(actualPdfDetails, actualPngFilePath, config2);
const baselinePdfDetails = {
filename: baselinePdfFilename,
buffer: baselinePdfBuffer
};
await imageEngine.pdfToPng(
baselinePdfDetails,
baselinePngFilePath,
config2
);
const actualPngs = fs4__default.default.readdirSync(actualPngDirPath).filter(
(pngFile) => path4__default.default.parse(pngFile).name.startsWith(actualPdfBaseName)
);
const baselinePngs = fs4__default.default.readdirSync(baselinePngDirPath).filter(
(pngFile) => path4__default.default.parse(pngFile).name.startsWith(baselinePdfBaseName)
);
if (config2.settings.matchPageCount === true) {
if (actualPngs.length !== baselinePngs.length) {
resolve({
status: "failed",
message: `Actual pdf page count (${actualPngs.length}) is not the same as Baseline pdf (${baselinePngs.length}).`
});
return;
}
}
const comparisonResults = [];
for (let index = 0; index < baselinePngs.length; index++) {
let suffix = "";
if (baselinePngs.length > 1) {
suffix = `-${index}`;
}
const actualPng = actualPngs.length > 1 ? `${actualPngDirPath}/${actualPdfBaseName}${suffix}.png` : `${actualPngDirPath}/${actualPdfBaseName}.png`;
const baselinePng = `${baselinePngDirPath}/${baselinePdfBaseName}${suffix}.png`;
const diffPng = `${diffPngDirPath}/${actualPdfBaseName}_diff${suffix}.png`;
if (opts.skipPageIndexes?.length && opts.skipPageIndexes.includes(index)) {
continue;
}
if (opts.onlyPageIndexes?.length && !opts.onlyPageIndexes.includes(index)) {
continue;
}
if (opts.masks) {
const pageMasks = filter__default.default(opts.masks, { pageIndex: index });
if (pageMasks?.length) {
for (const pageMask of pageMasks) {
await imageEngine.applyMask(
actualPng,
pageMask.coordinates,
pageMask.color
);
await imageEngine.applyMask(
baselinePng,
pageMask.coordinates,
pageMask.color
);
}
}
}
if (opts.crops?.length) {
const pageCroppings = filter__default.default(opts.crops, { pageIndex: index });
if (pageCroppings?.length) {
for (let cropIndex = 0; cropIndex < pageCroppings.length; cropIndex++) {
await imageEngine.applyCrop(
actualPng,
pageCroppings[cropIndex]?.coordinates,
cropIndex
);
await imageEngine.applyCrop(
baselinePng,
pageCroppings[cropIndex]?.coordinates,
cropIndex
);
comparisonResults.push(
await comparePngs(
actualPng.replace(".png", `-${cropIndex}.png`),
baselinePng.replace(".png", `-${cropIndex}.png`),
diffPng,
config2
)
);
}
}
} else {
comparisonResults.push(
await comparePngs(actualPng, baselinePng, diffPng, config2)
);
}
}
if (config2.settings.cleanPngPaths) {
ensureAndCleanupPath(config2.paths.actualPngRootFolder);
ensureAndCleanupPath(config2.paths.baselinePngRootFolder);
}
const failedResults = filter__default.default(
comparisonResults,
(res) => res.status === "failed"
);
if (failedResults.length > 0) {
resolve({
status: "failed",
message: `${actualPdfBaseName}.pdf is not the same as ${baselinePdfBaseName}.pdf compared by their images.`,
details: failedResults
});
} else {
resolve({ status: "passed" });
}
} else {
resolve({
status: "failed",
message: "PNG directory is not set. Please define correctly then try again."
});
}
} catch (error) {
resolve({
status: "failed",
message: `An error occurred.
${error}`
});
}
});
};
var config = {
paths: {
actualPdfRootFolder: path4__default.default.resolve(process.cwd(), "data/actualPdfs"),
baselinePdfRootFolder: path4__default.default.join(process.cwd(), "data/baselinePdfs"),
actualPngRootFolder: path4__default.default.join(process.cwd(), "data/actualPngs"),
baselinePngRootFolder: path4__default.default.join(process.cwd(), "data/baselinePngs"),
diffPngRootFolder: path4__default.default.join(process.cwd(), "data/diffPngs")
},
settings: {
imageEngine: "graphicsMagick",
density: 100,
quality: 70,
tolerance: 0,
threshold: 0.05,
cleanPngPaths: false,
matchPageCount: true,
disableFontFace: true,
verbosity: 0
}
};
var config_default = config;
// src/comparePdf.ts
var ComparePdf = class {
config;
opts;
result;
baselinePdfBufferData;
actualPdfBufferData;
baselinePdf;
actualPdf;
constructor(config2 = copyJsonObject(config_default)) {
this.config = config2;
ensurePathsExist(this.config);
this.opts = {
masks: [],
crops: [],
onlyPageIndexes: [],
skipPageIndexes: []
};
this.result = {
status: "not executed"
};
}
baselinePdfBuffer(baselinePdfBuffer, baselinePdfFilename) {
if (baselinePdfBuffer) {
this.baselinePdfBufferData = baselinePdfBuffer;
if (baselinePdfFilename) {
this.baselinePdf = baselinePdfFilename;
}
} else {
this.result = {
status: "failed",
message: "Baseline pdf buffer is invalid or filename is missing. Please define correctly then try again."
};
}
return this;
}
baselinePdfFile(baselinePdf) {
if (baselinePdf) {
const baselinePdfBaseName = path4__default.default.parse(baselinePdf).name;
if (fs4__default.default.existsSync(baselinePdf)) {
this.baselinePdf = baselinePdf;
} else if (fs4__default.default.existsSync(
`${this.config.paths.baselinePdfRootFolder}/${baselinePdfBaseName}.pdf`
)) {
this.baselinePdf = `${this.config.paths.baselinePdfRootFolder}/${baselinePdfBaseName}.pdf`;
} else {
this.result = {
status: "failed",
message: "Baseline pdf file path does not exists. Please define correctly then try again."
};
}
} else {
this.result = {
status: "failed",
message: "Baseline pdf file path was not set. Please define correctly then try again."
};
}
return this;
}
actualPdfBuffer(actualPdfBuffer, actualPdfFilename) {
if (actualPdfBuffer) {
this.actualPdfBufferData = actualPdfBuffer;
if (actualPdfFilename) {
this.actualPdf = actualPdfFilename;
}
} else {
this.result = {
status: "failed",
message: "Actual pdf buffer is invalid or filename is missing. Please define correctly then try again."
};
}
return this;
}
actualPdfFile(actualPdf) {
if (actualPdf) {
const actualPdfBaseName = path4__default.default.parse(actualPdf).name;
if (fs4__default.default.existsSync(actualPdf)) {
this.actualPdf = actualPdf;
} else if (fs4__default.default.existsSync(
`${this.config.paths.actualPdfRootFolder}/${actualPdfBaseName}.pdf`
)) {
this.actualPdf = `${this.config.paths.actualPdfRootFolder}/${actualPdfBaseName}.pdf`;
} else {
this.result = {
status: "failed",
message: "Actual pdf file path does not exists. Please define correctly then try again."
};
}
} else {
this.result = {
status: "failed",
message: "Actual pdf file path was not set. Please define correctly then try again."
};
}
return this;
}
addMask(pageIndex, coordinates = { x0: 0, y0: 0, x1: 0, y1: 0 }, color = "black") {
this.opts.masks.push({
pageIndex,
coordinates,
color
});
return this;
}
addMasks(masks) {
this.opts.masks = [...this.opts.masks, ...masks];
return this;
}
onlyPageIndexes(pageIndexes) {
this.opts.onlyPageIndexes = [...this.opts.onlyPageIndexes, ...pageIndexes];
return this;
}
skipPageIndexes(pageIndexes) {
this.opts.skipPageIndexes = [...this.opts.skipPageIndexes, ...pageIndexes];
return this;
}
cropPage(pageIndex, coordinates = { width: 0, height: 0, x: 0, y: 0 }) {
this.opts.crops.push({
pageIndex,
coordinates
});
return this;
}
cropPages(cropPagesList) {
this.opts.crops = [...this.opts.crops, ...cropPagesList];
return this;
}
async compare(comparisonType = "byImage") {
if (this.result.status === "not executed" || this.result.status !== "failed") {
if (!this.actualPdf || !this.baselinePdf) {
throw new Error("PDF files not properly initialized");
}
const compareDetails = {
actualPdfFilename: this.actualPdf,
baselinePdfFilename: this.baselinePdf,
actualPdfBuffer: this.actualPdfBufferData ?? fs4__default.default.readFileSync(this.actualPdf),
baselinePdfBuffer: this.baselinePdfBufferData ?? fs4__default.default.readFileSync(this.baselinePdf),
config: this.config,
opts: this.opts
};
switch (comparisonType) {
case "byBase64":
this.result = await comparePdfByBase64(compareDetails);
break;
default:
this.result = await comparePdfByImage(compareDetails);
break;
}
}
return this.result;
}
};
exports.ComparePdf = ComparePdf;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map