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
10 changes: 10 additions & 0 deletions .changeset/swap-pixelmatch-blazediff-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@wdio/image-comparison-core": patch
---

perf: swap pixelmatch for @blazediff/core in the golden-PNG comparison path

`@blazediff/core` is a drop-in replacement for pixelmatch: same call signature
(`img1, img2, output, width, height, options`), same options, and identical
diff output semantics. It produces the same pixel-diff results ~1.5x faster with
zero dependencies, so no comparison logic changes.
2 changes: 1 addition & 1 deletion packages/image-comparison-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"watch:tsc": "pnpm run build:tsc -w"
},
"dependencies": {
"@blazediff/core": "^1.9.3",
"fast-png": "^8.0.0",
"pixelmatch": "^7.2.0",
"@wdio/logger": "^9.29.1",
"@wdio/types": "^9.29.1"
},
Expand Down
76 changes: 38 additions & 38 deletions packages/image-comparison-core/src/pixelmatch/compareImages.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'

vi.mock('pixelmatch', () => ({
default: vi.fn()
vi.mock('@blazediff/core', () => ({
diff: vi.fn()
}))

vi.mock('../utils/imageUtils.js', () => {
Expand All @@ -19,10 +19,10 @@ vi.mock('../utils/imageUtils.js', () => {
})

import compareImages from './compareImages.js'
import pixelmatch from 'pixelmatch'
import { diff } from '@blazediff/core'
import * as imageUtils from '../utils/imageUtils.js'

const pixelmatchFn = vi.mocked(pixelmatch)
const diffFn = vi.mocked(diff)
const decodeImageFn = vi.mocked(imageUtils.decodeImage)
const resizeBilinearFn = vi.mocked(imageUtils.resizeBilinear)

Expand All @@ -34,7 +34,7 @@ describe('pixelmatch adapter - compareImages', () => {

describe('basic comparison', () => {
it('returns zero mismatch percentage when images are identical', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

Expand All @@ -45,7 +45,7 @@ describe('pixelmatch adapter - compareImages', () => {

it('returns correct mismatch percentage for a known diff count', async () => {
// 100x100 = 10000 total pixels, 100 diff pixels = 1%
pixelmatchFn.mockImplementation(() => 100)
diffFn.mockImplementation(() => 100)

const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

Expand All @@ -54,15 +54,15 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('returns analysisTime greater than or equal to 0', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

expect(result.analysisTime).toBeGreaterThanOrEqual(0)
})

it('resolves getBuffer to a Buffer', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})
const buf = await result.getBuffer()
Expand All @@ -76,7 +76,7 @@ describe('pixelmatch adapter - compareImages', () => {
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 }) // baseline
.mockReturnValueOnce({ data: actual, width: 100, height: 100 }) // actual

pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
// Mark pixel at (0,0) as a diff (magenta)
output[0] = 255
output[1] = 0
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('pixelmatch adapter - compareImages', () => {

describe('diffPixels and diffBounds', () => {
it('collects magenta pixels as diff pixel coordinates', async () => {
pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
// Place a magenta pixel at x=5, y=3
const pos = (3 * width + 5) * 4
output[pos] = 255
Expand All @@ -124,7 +124,7 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('does not count grayscale matching pixels as diff pixels', async () => {
pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
const pos = (2 * width + 10) * 4
output[pos] = 200
output[pos + 1] = 200
Expand All @@ -139,7 +139,7 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('computes correct diffBounds from multiple diff pixels', async () => {
pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array, width: number) => {
const mark = (x: number, y: number) => {
const pos = (y * width + x) * 4
output[pos] = 255
Expand All @@ -159,7 +159,7 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('returns sentinel diffBounds when there are no diff pixels', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

const result = await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

Expand All @@ -176,65 +176,65 @@ describe('pixelmatch adapter - compareImages', () => {
['alpha', { threshold: 0.063, includeAA: true }],
['colors', { threshold: 0.063, includeAA: true }],
] as const)('passes preset settings for ignore: %s', async (ignore, expected) => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), { ignore })

expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining(expected)
)
})

it('passes threshold=0.063 and includeAA=true when no ignore option is given', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0.063, includeAA: true })
)
})

it('uses last-wins preset when ignoreLess follows ignoreAntialiasing', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {
ignore: ['antialiasing', 'less']
})

expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0.063, includeAA: true })
)
})

it('uses last-wins preset when ignoreNothing follows other flags', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {
ignore: ['antialiasing', 'less', 'nothing']
})

expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0, includeAA: true })
)
})

it('uses last-wins preset when ignoreLess follows ignoreAlpha', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {
ignore: ['alpha', 'less']
})

expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0.063, includeAA: true })
Expand All @@ -251,7 +251,7 @@ describe('pixelmatch adapter - compareImages', () => {
})
let capturedPixels1: Uint8Array | undefined

pixelmatchFn.mockImplementation((img1: Uint8Array) => {
diffFn.mockImplementation((img1: Uint8Array) => {
capturedPixels1 = new Uint8Array(img1)
return 0
})
Expand All @@ -272,7 +272,7 @@ describe('pixelmatch adapter - compareImages', () => {
})
let capturedPixels1: Uint8Array | undefined

pixelmatchFn.mockImplementation((img1: Uint8Array) => {
diffFn.mockImplementation((img1: Uint8Array) => {
capturedPixels1 = new Uint8Array(img1)
return 0
})
Expand All @@ -288,12 +288,12 @@ describe('pixelmatch adapter - compareImages', () => {
decodeImageFn
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 })
.mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4).fill(64), width: 50, height: 50 })
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

// Canvas is 100×100; img2 (50×50) is padded to fill it
expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object), expect.any(Uint8Array),
100, 100, expect.any(Object)
)
Expand All @@ -303,12 +303,12 @@ describe('pixelmatch adapter - compareImages', () => {
decodeImageFn
.mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4).fill(64), width: 50, height: 50 })
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 })
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {})

// Canvas is 100×100; img1 (50×50) is padded to fill it
expect(pixelmatchFn).toHaveBeenCalledWith(
expect(diffFn).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object), expect.any(Uint8Array),
100, 100, expect.any(Object)
)
Expand All @@ -320,7 +320,7 @@ describe('pixelmatch adapter - compareImages', () => {
let capturedImg1: Uint8Array | undefined
let capturedImg2: Uint8Array | undefined

pixelmatchFn.mockImplementation((img1: Uint8Array, img2: Uint8Array) => {
diffFn.mockImplementation((img1: Uint8Array, img2: Uint8Array) => {
capturedImg1 = new Uint8Array(img1)
capturedImg2 = new Uint8Array(img2)
return 0
Expand All @@ -345,7 +345,7 @@ describe('pixelmatch adapter - compareImages', () => {
decodeImageFn
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4), width: 100, height: 100 })
.mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4), width: 50, height: 50 })
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true })

Expand All @@ -360,7 +360,7 @@ describe('pixelmatch adapter - compareImages', () => {
decodeImageFn
.mockReturnValueOnce({ data: new Uint8Array(50 * 50 * 4), width: 50, height: 50 })
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4), width: 100, height: 100 })
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true })

Expand All @@ -372,15 +372,15 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('does not call resizeBilinear when scaleToSameSize is false', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: false })

expect(resizeBilinearFn).not.toHaveBeenCalled()
})

it('does not call resizeBilinear when images are the same size', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), { scaleToSameSize: true })

Expand All @@ -390,7 +390,7 @@ describe('pixelmatch adapter - compareImages', () => {

describe('direct pixelmatch mode', () => {
it('forwards resolved pixelmatch settings to pixelmatch', async () => {
pixelmatchFn.mockImplementation(() => 0)
diffFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {
pixelmatch: {
Expand All @@ -405,7 +405,7 @@ describe('pixelmatch adapter - compareImages', () => {
},
})

expect(pixelmatchFn.mock.calls[0]?.[5]).toMatchSnapshot()
expect(diffFn.mock.calls[0]?.[5]).toMatchSnapshot()
})

it('uses custom diffColor for diff pixel detection and compositing', async () => {
Expand All @@ -414,7 +414,7 @@ describe('pixelmatch adapter - compareImages', () => {
.mockReturnValueOnce({ data: new Uint8Array(100 * 100 * 4).fill(128), width: 100, height: 100 })
.mockReturnValueOnce({ data: actual, width: 100, height: 100 })

pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
output[0] = 255
output[1] = 0
output[2] = 0
Expand Down Expand Up @@ -443,7 +443,7 @@ describe('pixelmatch adapter - compareImages', () => {
})

it('uses pixelmatch output directly when diffMask is true', async () => {
pixelmatchFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
diffFn.mockImplementation((_img1, _img2, output: Uint8Array) => {
output[0] = 10
output[1] = 20
output[2] = 30
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pixelmatch from 'pixelmatch'
import { diff } from '@blazediff/core'
import { resolveComparePreset } from '../helpers/options.js'
import { DEFAULT_PIXELMATCH_OPTIONS } from '../helpers/constants.js'
import { applyResembleGrayscale } from './compareBrightness.js'
Expand Down Expand Up @@ -140,7 +140,7 @@ export default async function compareImages(

const outputPixels = new Uint8Array(totalPixels * 4)

const diffCount: number = pixelmatch(pixels1, pixels2, outputPixels, width, height, {
const diffCount: number = diff(pixels1, pixels2, outputPixels, width, height, {
threshold: resolvedPixelmatch.threshold,
includeAA: resolvedPixelmatch.includeAA,
diffColor: resolvedPixelmatch.diffColor,
Expand Down
Loading