Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .changeset/fix-ignore-antialiasing-pixelmatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@wdio/image-comparison-core": patch
"@wdio/visual-service": patch
---

fix: wire ignoreAntialiasing to pixelmatch AA forgiveness toggle

In v10 the pixelmatch engine always ran with anti-aliasing forgiveness enabled, even when `ignoreAntialiasing` was `false`. The option had no effect on comparison behaviour.

**What changed**

- `ignoreAntialiasing` now toggles pixelmatch's AA handling: `true` forgives anti-aliased pixels, `false` counts them as mismatches.
- The default is now `ignoreAntialiasing: true`, matching the forgiving pixelmatch behaviour users already get in v10.
- `ignoreLess` and `ignoreNothing` keep their own threshold behaviour; AA forgiveness is controlled independently via `ignoreAntialiasing`.

**Migration**

- No action needed if you rely on the current forgiving defaults, comparison behaviour stays the same.
- If you explicitly set `ignoreAntialiasing: true` today, that remains redundant but harmless.
- Set `ignoreAntialiasing: false` when you need strict comparison where anti-aliased pixels count as differences.

### Committers: 1

- Wim Selles ([@wswebcreation](https://github.com/wswebcreation))
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exports[`BaseClass > initializes default options correctly 1`] = `
"createJsonReportFiles": false,
"diffPixelBoundingBoxProximity": 5,
"ignoreAlpha": false,
"ignoreAntialiasing": false,
"ignoreAntialiasing": true,
"ignoreColors": false,
"ignoreLess": false,
"ignoreNothing": false,
Expand Down
4 changes: 2 additions & 2 deletions packages/image-comparison-core/src/base.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export interface BaseImageCompareOptions {
*/
ignoreAlpha?: boolean;
/**
* Compare images and discard anti aliasing
* @default false
* Compare images and forgive anti-aliasing differences
* @default true
*/
ignoreAntialiasing?: boolean;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ exports[`options > defaultOptions > should return the default options when no op
"createJsonReportFiles": false,
"diffPixelBoundingBoxProximity": 5,
"ignoreAlpha": false,
"ignoreAntialiasing": false,
"ignoreAntialiasing": true,
"ignoreColors": false,
"ignoreLess": false,
"ignoreNothing": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/image-comparison-core/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const DEFAULT_COMPARE_OPTIONS = {
createJsonReportFiles: false,
diffPixelBoundingBoxProximity: 5,
ignoreAlpha: false,
ignoreAntialiasing: false,
ignoreAntialiasing: true,
ignoreColors: false,
ignoreLess: false,
ignoreNothing: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ export interface ClassOptions {
ignoreAlpha?: boolean;

/**
* Ignore anti-aliasing when comparing images.
* Forgive anti-aliasing differences when comparing images.
* Defaults to `true` so sub-pixel rendering noise is ignored out of the box.
* Set to `false` for strict pixel comparison where AA pixels count as mismatches.
*/
ignoreAntialiasing?: boolean;

Expand Down Expand Up @@ -440,7 +442,7 @@ export interface CompareOptions {
ignoreAlpha: boolean;

/**
* Compare images and ignore anti-aliasing effects.
* Forgive anti-aliasing differences when comparing images.
*/
ignoreAntialiasing: boolean;

Expand Down
20 changes: 19 additions & 1 deletion packages/image-comparison-core/src/helpers/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { defaultOptions, methodCompareOptions, screenMethodCompareOptions, createBeforeScreenshotOptions, buildAfterScreenshotOptions } from './options.js'
import { defaultOptions, methodCompareOptions, screenMethodCompareOptions, createBeforeScreenshotOptions, buildAfterScreenshotOptions, prepareIgnoreOptions } from './options.js'
import type { ClassOptions } from './options.interfaces.js'
import type { ScreenMethodImageCompareCompareOptions } from '../methods/images.interfaces.js'
import type { InstanceData } from '../methods/instanceData.interfaces.js'
Expand Down Expand Up @@ -472,4 +472,22 @@ describe('options', () => {
expect(result.platformName).toBe('iOS')
})
})

describe('prepareIgnoreOptions', () => {
it('includes antialiasing when ignoreAntialiasing is true', () => {
expect(prepareIgnoreOptions({ ignoreAntialiasing: true })).toEqual(['antialiasing'])
})

it('omits antialiasing when ignoreAntialiasing is false', () => {
expect(prepareIgnoreOptions({ ignoreAntialiasing: false })).toEqual([])
})

it('collects multiple enabled ignore flags', () => {
expect(prepareIgnoreOptions({
ignoreAlpha: true,
ignoreAntialiasing: true,
ignoreLess: true,
})).toEqual(['alpha', 'antialiasing', 'less'])
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,34 @@ describe('executeImageCompare', () => {
})
})

it('should pass antialiasing ignore when ignoreAntialiasing is enabled', async () => {
const optionsWithAntialiasing = {
...mockOptions,
compareOptions: {
...mockOptions.compareOptions,
method: {
ignoreAntialiasing: true,
}
}
}

await executeImageCompare({
isViewPortScreenshot: true,
isNativeContext: false,
options: optionsWithAntialiasing,
testContext: mockTestContext
})

expect(compareImagesPixelmatch.default).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Buffer),
{
ignore: ['antialiasing'],
scaleToSameSize: true
}
)
})

it('should handle ignore options from compareOptions', async () => {
const optionsWithIgnore = {
...mockOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ describe('pixelmatch adapter - compareImages', () => {
)
})

it('passes threshold=0.063 and includeAA=false for ignore: less', async () => {
it('passes threshold=0.063 and includeAA=true for ignore: less', async () => {
pixelmatchFn.mockImplementation(() => 0)

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

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

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

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

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

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

it('accepts ignore as an array and uses the highest-priority mode', async () => {
it('accepts ignore as an array and uses less threshold with antialiasing forgiveness', async () => {
pixelmatchFn.mockImplementation(() => 0)

await compareImages(Buffer.from('img1'), Buffer.from('img2'), {
Expand All @@ -227,7 +227,19 @@ describe('pixelmatch adapter - compareImages', () => {
expect(pixelmatchFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0.063 })
expect.objectContaining({ threshold: 0.063, includeAA: false })
)
})

it('passes threshold=0.063 and includeAA=true for ignore: alpha without antialiasing', async () => {
pixelmatchFn.mockImplementation(() => 0)

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

expect(pixelmatchFn).toHaveBeenCalledWith(
expect.anything(), expect.anything(), expect.anything(),
expect.any(Number), expect.any(Number),
expect.objectContaining({ threshold: 0.063, includeAA: true })
)
})
})
Expand Down
23 changes: 15 additions & 8 deletions packages/image-comparison-core/src/pixelmatch/compareImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@ function toPixelmatchOptions(ignoreList: ComparisonIgnoreOption[]): { threshold:
if (ignoreList.includes('nothing')) {
return { threshold: 0, includeAA: true }
}
if (ignoreList.includes('less')) {
// 16/255 per channel in resemble maps roughly to ~6.3% of max YIQ distance
return { threshold: 0.063, includeAA: false }

const forgivesAA = ignoreList.includes('antialiasing')
const threshold = ignoreList.includes('less')
? 0.063
: forgivesAA
// Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance which
// corresponds to ~0.13 in YIQ perceptual distance.
? 0.13
// Default strict tolerance: 16/255 per channel maps to ~6.3% of max YIQ distance.
: 0.063

return {
threshold,
// pixelmatch includeAA=true disables AA forgiveness; false enables it.
includeAA: !forgivesAA,
}
// 'antialiasing', 'alpha', 'colors' and the default.
// Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance which
// corresponds to ~0.13 in YIQ perceptual distance. Using 0.1 is stricter
// and causes invisible sub-pixel differences to register as failures.
return { threshold: 0.13, includeAA: false }
}

function grayscalePixels(pixels: Buffer, totalPixels: number): void {
Expand Down
Loading