@yofix/comparator
v1.0.2
Published
Pure image comparison for visual regression testing - pixel-level diff analysis without AI/LLM
Maintainers
Readme
@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
# or
yarn add @yofix/comparatorQuick 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 // 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)
}Output
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
}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 // Process 3 at a time
},
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
}
})
// 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)
}6. Integration with @yofix/storage
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'
}]
})
}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 // Number of different bits
}MSE (Mean Squared Error)
Lower is better. 0 = identical.
metrics.mse: 12.45PSNR (Peak Signal-to-Noise Ratio)
Higher is better. Infinity = identical.
metrics.psnr: 35.2 // dBPSNR 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', // > 1000 pixels
pixelCount: 1250
}
]Severity Levels:
critical: > 1000 pixels differentmoderate: 500-1000 pixels differentminor: < 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 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)
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
pixelmatchlibrary - 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 processingpixelmatch: Pixel-level comparisonpngjs: PNG manipulationzod: 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
