
Security News
The Nightmare Before Deployment
Season’s greetings from Socket, and here’s to a calm end of year: clean dependencies, boring pipelines, no surprises.
@yofix/comparator
Advanced tools
Pure image comparison for visual regression testing - pixel-level diff analysis without AI/LLM
Pure image comparison for visual regression testing - pixel-level diff analysis without AI/LLM
Pure TypeScript library for comparing baseline vs current screenshots. Generates pixel-perfect diff images, calculates similarity metrics, and detects visual regression regions.
✅ Pure image comparison - No AI/LLM dependencies ✅ Multiple diff formats - Raw, side-by-side, overlay ✅ Parallel processing - Configurable concurrency for batch comparisons ✅ Perceptual hashing - Fast similarity detection ✅ Quality metrics - MSE, PSNR calculations ✅ Region detection - Identify areas with differences ✅ Auto-detect sources - Supports Buffer, file paths, and URLs ✅ Storage-agnostic - Works with any image source
npm install @yofix/comparator
# or
yarn add @yofix/comparator
import { compareBaselines } from '@yofix/comparator'
const result = await compareBaselines({
comparisons: [
{
route: '/dashboard',
viewport: 'desktop',
current: './screenshots/current/dashboard-desktop.png',
baseline: './screenshots/baseline/dashboard-desktop.png'
}
],
options: {
threshold: 0.01,
diffFormat: 'side-by-side',
parallel: { enabled: true, concurrency: 3 },
generateHash: true,
detectRegions: true,
verbose: true
}
})
if (result.success) {
console.log(`Overall similarity: ${(result.summary.overallSimilarity * 100).toFixed(2)}%`)
console.log(`Differences found: ${result.metadata.differences}`)
result.comparisons.forEach(comp => {
console.log(`${comp.route}: ${comp.match ? 'Match ✅' : 'Diff ⚠️'}`)
if (comp.diff) {
console.log(` Diff image available: ${comp.diff.buffer.length} bytes`)
console.log(` Regions: ${comp.diff.regions?.length || 0}`)
}
})
}
compareBaselines(input: CompareBaselinesInput): Promise<ComparisonResult>Main comparison function.
interface CompareBaselinesInput {
comparisons: ImagePair[]
options?: CompareOptions
}
interface ImagePair {
route: string // e.g., "/dashboard"
viewport: string // e.g., "desktop", "1920x1080"
current: ImageSource // Buffer | string (path or URL)
baseline: ImageSource // Buffer | string (path or URL)
}
interface CompareOptions {
threshold?: number // Diff threshold (0-1, default: 0.01)
diffFormat?: DiffFormat // 'side-by-side' | 'overlay' | 'raw'
parallel?: {
enabled: boolean
concurrency: number // Default: 3
}
optimization?: {
enabled: boolean
quality: number // 0-100
format: 'png' | 'webp'
}
generateHash?: boolean // Perceptual hash (default: true)
detectRegions?: boolean // Find diff regions (default: false)
verbose?: boolean // Logging (default: false)
}
interface ComparisonResult {
success: boolean
metadata: {
timestamp: number
totalComparisons: number
matches: number
differences: number
duration: number
}
comparisons: Comparison[]
summary: {
overallSimilarity: number // 0-1 (1 = identical)
totalPixelDifference: number
categorizedDiffs: {
critical: number // > 30% diff
moderate: number // 10-30% diff
minor: number // < 10% diff
}
}
errors?: ComparisonError[]
}
interface Comparison {
route: string
viewport: string
current: ImageMetadata
baseline: ImageMetadata
match: boolean
similarity: number // 0-1 (1 = identical)
pixelDifference: number
diffPercentage: number
diff?: {
buffer: Buffer // Diff image buffer
format: DiffFormat
dimensions: { width: number; height: number }
regions?: DiffRegion[] // Diff areas
}
metrics: {
perceptualHash?: {
current: string
baseline: string
hammingDistance: number
}
mse?: number // Mean squared error
psnr?: number // Peak signal-to-noise ratio
}
duration: number
error?: string
}
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: './screenshots/current/home-desktop.png',
baseline: './screenshots/baseline/home-desktop.png'
}
]
})
import { readFile } from 'fs/promises'
const currentBuffer = await readFile('./current.png')
const baselineBuffer = await readFile('./baseline.png')
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'mobile',
current: currentBuffer,
baseline: baselineBuffer
}
]
})
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: 'https://example.com/screenshots/current.png',
baseline: 'https://example.com/screenshots/baseline.png'
}
]
})
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: './current/home-desktop.png',
baseline: './baseline/home-desktop.png'
},
{
route: '/dashboard',
viewport: 'desktop',
current: './current/dashboard-desktop.png',
baseline: './baseline/dashboard-desktop.png'
},
{
route: '/settings',
viewport: 'mobile',
current: './current/settings-mobile.png',
baseline: './baseline/settings-mobile.png'
}
],
options: {
parallel: {
enabled: true,
concurrency: 3 // Process 3 at a time
},
verbose: true
}
})
import { writeFile } from 'fs/promises'
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: './current/home.png',
baseline: './baseline/home.png'
}
],
options: {
diffFormat: 'side-by-side',
detectRegions: true
}
})
// Save diff image
if (result.comparisons[0].diff) {
await writeFile(
'./diffs/home-diff.png',
result.comparisons[0].diff.buffer
)
console.log('Diff regions:', result.comparisons[0].diff.regions)
}
import { compareBaselines } from '@yofix/comparator'
import { downloadFiles, uploadFiles } from '@yofix/storage'
// 1. Download baselines from storage
const baselines = await downloadFiles({
storage: {
provider: 'firebase',
config: {
bucket: 'my-bucket',
credentials: process.env.FIREBASE_CREDENTIALS
}
},
files: ['baselines/dashboard/desktop.png']
})
// 2. Compare
const result = await compareBaselines({
comparisons: [
{
route: '/dashboard',
viewport: 'desktop',
current: './screenshots/dashboard-desktop.png',
baseline: baselines[0].buffer
}
],
options: {
diffFormat: 'side-by-side',
detectRegions: true
}
})
// 3. Upload diffs to storage
if (result.comparisons[0].diff) {
await uploadFiles({
storage: {
provider: 'firebase',
config: {
bucket: 'my-bucket',
credentials: process.env.FIREBASE_CREDENTIALS,
basePath: 'diffs'
}
},
files: [{
path: result.comparisons[0].diff.buffer,
destination: 'dashboard/desktop-diff.png'
}]
})
}
Red highlights on differences (default)
diffFormat: 'raw'
Baseline | Diff | Current
diffFormat: 'side-by-side'
Baseline with current overlaid at 50% opacity
diffFormat: 'overlay'
Fast similarity check using average hash algorithm. Returns binary string and Hamming distance.
metrics.perceptualHash: {
current: '1010110110101...',
baseline: '1010110110101...',
hammingDistance: 2 // Number of different bits
}
Lower is better. 0 = identical.
metrics.mse: 12.45
Higher is better. Infinity = identical.
metrics.psnr: 35.2 // dB
PSNR Interpretation:
> 40 dB: Excellent (virtually identical)30-40 dB: Good (minor differences)20-30 dB: Fair (noticeable differences)< 20 dB: Poor (significant differences)Detects contiguous areas with differences and categorizes by severity:
diff.regions: [
{
x: 120,
y: 45,
width: 200,
height: 150,
severity: 'critical', // > 1000 pixels
pixelCount: 1250
}
]
Severity Levels:
critical: > 1000 pixels differentmoderate: 500-1000 pixels differentminor: < 500 pixels differentconst result = await compareBaselines({...})
if (!result.success) {
result.errors?.forEach(error => {
console.error(`${error.code}: ${error.message}`)
console.error('Route:', error.route)
console.error('Phase:', error.phase)
})
}
Error Codes:
VALIDATION_ERROR: Invalid inputIMAGE_LOAD_ERROR: Failed to load imageIMAGE_DIMENSION_ERROR: Image dimensions don't matchCOMPARISON_ERROR: Pixel comparison failedDIFF_GENERATION_ERROR: Diff image creation failedNETWORK_ERROR: Download failed (for URLs)import { Comparator } from '@yofix/comparator'
const comparator = new Comparator()
const comparison = await comparator.compareImages(
{
route: '/home',
viewport: 'desktop',
current: './current.png',
baseline: './baseline.png'
},
{
threshold: 0.01,
diffFormat: 'raw',
generateHash: true
}
)
pixelmatch libraryBenchmark (1920x1080 images):
sharp: Image processingpixelmatch: Pixel-level comparisonpngjs: PNG manipulationzod: Input validation@yofix/browser: Screenshot capture@yofix/storage: Multi-provider storage@yofix/analyzer: Route impact analysisMIT
Issues and PRs welcome at https://github.com/yofix/yofix
FAQs
Pure image comparison for visual regression testing - pixel-level diff analysis without AI/LLM
We found that @yofix/comparator demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Season’s greetings from Socket, and here’s to a calm end of year: clean dependencies, boring pipelines, no surprises.

Research
/Security News
Impostor NuGet package Tracer.Fody.NLog typosquats Tracer.Fody and its author, using homoglyph tricks, and exfiltrates Stratis wallet JSON/passwords to a Russian IP address.

Security News
Deno 2.6 introduces deno audit with a new --socket flag that plugs directly into Socket to bring supply chain security checks into the Deno CLI.