Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/compare-pdf-to-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ describe('comparePdfToSnapshot()', () => {
}))
it('two-page.pdf', () => testPdf2png(twoPage, 'two-page'))
it('two-page.pdf buffer', () => readFile(twoPage).then((x) => testPdf2png(x, 'two-page')))
})
it('two-page-separate-pages (combinePages: false)', () => {
return testPdf2png(twoPagePdfPath, 'two-page-separate-pages', {
combinePages: false
})
})
});

describe('mask regions', () => {
const blueMask: RegionMask = {
Expand Down
84 changes: 70 additions & 14 deletions src/compare-pdf-to-snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'node:path'
import { access, mkdir, unlink } from 'node:fs/promises'
import { access, mkdir, unlink, readdir } from 'node:fs/promises'
import { pdf2png } from './pdf2png/pdf2png'
import { compareImages } from './compare-images'
import { Jimp, JimpInstance } from 'jimp'
Expand Down Expand Up @@ -90,6 +90,12 @@ const maskImgWithRegions =
* fields is inlined.
*/
export type CompareOptions = {
/**
* Whether to combine all pages into a single image.
*
* @defaultValue true
*/
combinePages?: boolean
/**
* Number value for error tolerance in the range [0, 1].
*
Expand Down Expand Up @@ -142,10 +148,25 @@ export async function comparePdfToSnapshot(
options?: CompareOptions,
): Promise<boolean> {
const mergedOptions = mergeOptionsWithDefaults(options)
const snapshotContext = await createSnapshotContext(snapshotDir, snapshotName)
const snapshotContext = await createSnapshotContext(snapshotDir, snapshotName, mergedOptions)

// When combinePages is false, we need to process each page as a separate context
if (Array.isArray(snapshotContext)) {
try {
// Ensure snapshots exists for all pages. If any are missing, we
// should re-generate all of them.
for (const context of snapshotContext) {
await access(context.path)
}

return compareWithSnapshot(pdf, snapshotContext, mergedOptions)
} catch {
return handleMissingSnapshot(pdf, snapshotContext[0], mergedOptions)
}
}

// Check if snapshot exits and handle accordingly
try {
// Check if snapshot exits and handle accordingly
await access(snapshotContext.path)
return compareWithSnapshot(pdf, snapshotContext, mergedOptions)
} catch {
Expand All @@ -163,6 +184,7 @@ type SnapshotContext = {

function mergeOptionsWithDefaults(options?: CompareOptions): Required<CompareOptions> {
return {
combinePages: options?.combinePages ?? true,
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default for this option is kept as true to ensure this is a non-breaking change.

maskRegions: options?.maskRegions ?? (() => []),
pdf2PngOptions: options?.pdf2PngOptions ?? { dpi: Dpi.High },
failOnMissingSnapshot: options?.failOnMissingSnapshot ?? false,
Expand All @@ -180,7 +202,8 @@ export const snapshotsDirName = SNAPSHOTS_DIR_NAME
async function createSnapshotContext(
snapshotDir: string,
snapshotName: string,
): Promise<SnapshotContext> {
options: Required<CompareOptions>,
): Promise<SnapshotContext | Array<SnapshotContext>> {
const dirPath = path.join(snapshotDir, SNAPSHOTS_DIR_NAME)
try {
await access(dirPath)
Expand All @@ -190,6 +213,21 @@ async function createSnapshotContext(

const basePath = path.join(dirPath, snapshotName)

// When combinePages is false, we need to create a separate snapshot for each page
if (options.combinePages === false) {
const files = await readdir(dirPath)
return files.filter((file: string) => file.startsWith(snapshotName)).map((file: string) => {
const fileNameWithoutExt = file.substring(0, file.lastIndexOf('.'))
return ({
name: snapshotName,
dirPath,
path: path.join(dirPath, file),
diffPath: path.join(dirPath, `${fileNameWithoutExt}.diff.png`),
newPath: path.join(dirPath, `${fileNameWithoutExt}.new.png`),
})
})
}

return {
name: snapshotName,
dirPath,
Expand All @@ -202,40 +240,58 @@ async function createSnapshotContext(
async function handleMissingSnapshot(
pdf: string | Buffer,
snapshotContext: SnapshotContext,
{ failOnMissingSnapshot, maskRegions, pdf2PngOptions }: Required<CompareOptions>,
{ combinePages, failOnMissingSnapshot, maskRegions, pdf2PngOptions }: Required<CompareOptions>,
): Promise<boolean> {
if (failOnMissingSnapshot) {
return false
}

// Generate snapshot if missing
const images = await pdf2png(pdf, pdf2PngOptions).then(maskImgWithRegions(maskRegions))
await writeImages(snapshotContext.path)(images)
await writeImages(snapshotContext.path, combinePages)(images)

return true
}

async function compareWithSnapshot(
pdf: string | Buffer,
async function compareContext(
snapshotContext: SnapshotContext,
{ maskRegions, pdf2PngOptions, tolerance }: Required<CompareOptions>,
): Promise<boolean> {
const images = await pdf2png(pdf, pdf2PngOptions).then(maskImgWithRegions(maskRegions))
images: ReadonlyArray<JimpInstance>,
{ combinePages, tolerance }: Required<CompareOptions>
) {
const result = await compareImages(snapshotContext.path, images, { tolerance })

if (result.equal) {
await removeIfExists(snapshotContext.diffPath)
await removeIfExists(snapshotContext.newPath)

return true
}

await writeImages(snapshotContext.newPath)(images)
await writeImages(snapshotContext.diffPath)(result.diffs.map((x) => x.diff))
await writeImages(snapshotContext.newPath, combinePages)(images)
await writeImages(snapshotContext.diffPath, combinePages)(result.diffs.map((x) => x.diff))

return false
}

async function compareWithSnapshot(
pdf: string | Buffer,
snapshotContext: SnapshotContext | Array<SnapshotContext>,
options: Required<CompareOptions>,
): Promise<boolean> {
const { maskRegions, pdf2PngOptions } = options;
const images = await pdf2png(pdf, pdf2PngOptions).then(maskImgWithRegions(maskRegions))

if (Array.isArray(snapshotContext)) {
let results: Array<Promise<boolean>> = [];
for (let i = 0, l = snapshotContext.length; i < l; i++) {
results.push(compareContext(snapshotContext[i], [images[i]], options));
}

return (await Promise.all(results)).every((result) => result)
}

return await compareContext(snapshotContext, images, options)
}

async function removeIfExists(filePath: string): Promise<void> {
try {
await unlink(filePath)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading