UNPKG

compare-pdf-plus

Version:

Standalone node module that compares PDFs

552 lines (544 loc) 20.1 kB
'use strict'; 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