@yofix/comparator
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.
Features
✅ 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
Installation
npm install @yofix/comparator
yarn add @yofix/comparator
Quick Start
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}`)
}
})
}
API Reference
compareBaselines(input: CompareBaselinesInput): Promise<ComparisonResult>
Main comparison function.
Input
interface CompareBaselinesInput {
comparisons: ImagePair[]
options?: CompareOptions
}
interface ImagePair {
route: string
viewport: string
current: ImageSource
baseline: ImageSource
}
interface CompareOptions {
threshold?: number
diffFormat?: DiffFormat
parallel?: {
enabled: boolean
concurrency: number
}
optimization?: {
enabled: boolean
quality: number
format: 'png' | 'webp'
}
generateHash?: boolean
detectRegions?: boolean
verbose?: boolean
}
Output
interface ComparisonResult {
success: boolean
metadata: {
timestamp: number
totalComparisons: number
matches: number
differences: number
duration: number
}
comparisons: Comparison[]
summary: {
overallSimilarity: number
totalPixelDifference: number
categorizedDiffs: {
critical: number
moderate: number
minor: number
}
}
errors?: ComparisonError[]
}
interface Comparison {
route: string
viewport: string
current: ImageMetadata
baseline: ImageMetadata
match: boolean
similarity: number
pixelDifference: number
diffPercentage: number
diff?: {
buffer: Buffer
format: DiffFormat
dimensions: { width: number; height: number }
regions?: DiffRegion[]
}
metrics: {
perceptualHash?: {
current: string
baseline: string
hammingDistance: number
}
mse?: number
psnr?: number
}
duration: number
error?: string
}
Usage Examples
1. Compare with File Paths
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: './screenshots/current/home-desktop.png',
baseline: './screenshots/baseline/home-desktop.png'
}
]
})
2. Compare with Buffers
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
}
]
})
3. Compare with URLs
const result = await compareBaselines({
comparisons: [
{
route: '/home',
viewport: 'desktop',
current: 'https://example.com/screenshots/current.png',
baseline: 'https://example.com/screenshots/baseline.png'
}
]
})
4. Batch Comparison with Parallel Processing
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
},
verbose: true
}
})
5. Generate Diff Images
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
}
})
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)
}
6. Integration with @yofix/storage
import { compareBaselines } from '@yofix/comparator'
import { downloadFiles, uploadFiles } from '@yofix/storage'
const baselines = await downloadFiles({
storage: {
provider: 'firebase',
config: {
bucket: 'my-bucket',
credentials: process.env.FIREBASE_CREDENTIALS
}
},
files: ['baselines/dashboard/desktop.png']
})
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
}
})
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'
}]
})
}
Diff Formats
Raw
Red highlights on differences (default)
diffFormat: 'raw'
Side-by-Side
Baseline | Diff | Current
diffFormat: 'side-by-side'
Overlay
Baseline with current overlaid at 50% opacity
diffFormat: 'overlay'
Metrics
Perceptual Hash
Fast similarity check using average hash algorithm. Returns binary string and Hamming distance.
metrics.perceptualHash: {
current: '1010110110101...',
baseline: '1010110110101...',
hammingDistance: 2
}
MSE (Mean Squared Error)
Lower is better. 0 = identical.
metrics.mse: 12.45
PSNR (Peak Signal-to-Noise Ratio)
Higher is better. Infinity = identical.
metrics.psnr: 35.2
PSNR Interpretation:
> 40 dB: Excellent (virtually identical)
30-40 dB: Good (minor differences)
20-30 dB: Fair (noticeable differences)
< 20 dB: Poor (significant differences)
Region Detection
Detects contiguous areas with differences and categorizes by severity:
diff.regions: [
{
x: 120,
y: 45,
width: 200,
height: 150,
severity: 'critical',
pixelCount: 1250
}
]
Severity Levels:
critical: > 1000 pixels different
moderate: 500-1000 pixels different
minor: < 500 pixels different
Error Handling
const 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 input
IMAGE_LOAD_ERROR: Failed to load image
IMAGE_DIMENSION_ERROR: Image dimensions don't match
COMPARISON_ERROR: Pixel comparison failed
DIFF_GENERATION_ERROR: Diff image creation failed
NETWORK_ERROR: Download failed (for URLs)
Advanced Usage
Using the Comparator Class Directly
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
}
)
Performance
- Parallel processing: Configurable concurrency (default: 3)
- Fast perceptual hashing: 8x8 average hash
- Optimized pixel diff: Uses
pixelmatch library
- Memory efficient: Streams large images
Benchmark (1920x1080 images):
- Load: ~50ms per image
- Compare: ~100ms per pair
- Diff generation: ~150ms
- Perceptual hash: ~20ms
Dependencies
sharp: Image processing
pixelmatch: Pixel-level comparison
pngjs: PNG manipulation
zod: Input validation
Related Packages
@yofix/browser: Screenshot capture
@yofix/storage: Multi-provider storage
@yofix/analyzer: Route impact analysis
License
MIT
Contributing
Issues and PRs welcome at https://github.com/yofix/yofix
comparator