Skip to content

Commit e12515a

Browse files
authored
fix(screenshot): white sampling (#636)
* fix(screenshot): white sampling * chore(screenshot): handle max memory limit * refactor: better jimp helper
1 parent 74c3fc4 commit e12515a

File tree

2 files changed

+85
-12
lines changed

2 files changed

+85
-12
lines changed

packages/screenshot/src/is-white-screenshot.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,30 @@
33
const { Jimp } = require('jimp')
44

55
module.exports = async uint8array => {
6-
const image = await Jimp.fromBuffer(Buffer.from(uint8array))
7-
const firstPixel = image.getPixelColor(0, 0)
8-
const height = image.bitmap.height
9-
const width = image.bitmap.width
6+
try {
7+
const image = await Jimp.fromBuffer(Buffer.from(uint8array))
8+
const firstPixel = image.getPixelColor(0, 0)
9+
const height = image.bitmap.height
10+
const width = image.bitmap.width
1011

11-
const samplePercentage = 0.25 // Sample 25% of the image
12-
const sampleSize = Math.floor(width * height * samplePercentage) // Calculate sample size based on percentage
13-
const stepSize = Math.max(1, Math.floor((width * height) / sampleSize)) // Calculate step size based on sample size
12+
// For 2D grid sampling, calculate stepSize to achieve approximately the target sample percentage.
13+
// When sampling every 'stepSize' pixels in both dimensions, actual samples = (height/stepSize) * (width/stepSize).
14+
// To achieve samplePercentage, we need: (h*w)/(stepSize²) ≈ samplePercentage*(h*w)
15+
// Therefore: stepSize ≈ sqrt(1 / samplePercentage)
16+
const samplePercentage = 0.25 // Sample ~25% of the image
17+
const stepSize = Math.max(1, Math.ceil(Math.sqrt(1 / samplePercentage)))
1418

15-
for (let i = 0; i < height; i += stepSize) {
16-
for (let j = 0; j < width; j += stepSize) {
17-
if (firstPixel !== image.getPixelColor(j, i)) return false
19+
for (let i = 0; i < height; i += stepSize) {
20+
for (let j = 0; j < width; j += stepSize) {
21+
if (firstPixel !== image.getPixelColor(j, i)) return false
22+
}
1823
}
19-
}
2024

21-
return true
25+
return true
26+
} catch (error) {
27+
if (error.message.includes('maxMemoryUsageInMB')) {
28+
return false
29+
}
30+
throw error
31+
}
2232
}

packages/screenshot/test/is-white-screenshot.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,36 @@
33
const test = require('ava')
44
const { readFile } = require('fs/promises')
55

6+
const { Jimp } = require('jimp')
7+
68
const isWhite = require('../src/is-white-screenshot')
79

10+
const createJimpSpy = () => {
11+
const originalFromBuffer = Jimp.fromBuffer
12+
const spy = { callCount: 0 }
13+
14+
const wrappedFromBuffer = async function (buffer, options) {
15+
const image = await originalFromBuffer.call(this, buffer, options)
16+
const originalGetPixelColor = image.getPixelColor.bind(image)
17+
18+
image.getPixelColor = function (x, y) {
19+
spy.callCount++
20+
return originalGetPixelColor(x, y)
21+
}
22+
23+
return image
24+
}
25+
26+
Jimp.fromBuffer = wrappedFromBuffer
27+
28+
return {
29+
spy,
30+
restore: () => {
31+
Jimp.fromBuffer = originalFromBuffer
32+
}
33+
}
34+
}
35+
836
test('true', async t => {
937
t.true(await isWhite(await readFile('./test/fixtures/white-5k.jpg')))
1038
t.true(await isWhite(await readFile('./test/fixtures/white-5k.png')))
@@ -14,3 +42,38 @@ test('false', async t => {
1442
t.false(await isWhite(await readFile('./test/fixtures/no-white-5k.jpg')))
1543
t.false(await isWhite(await readFile('./test/fixtures/no-white-5k.png')))
1644
})
45+
46+
test('sampling algorithm correctly samples ~25% of pixels', async t => {
47+
const imageBuffer = await readFile('./test/fixtures/white-5k.png')
48+
const tempImage = await Jimp.fromBuffer(imageBuffer)
49+
const totalPixels = tempImage.bitmap.width * tempImage.bitmap.height
50+
const { spy, restore } = createJimpSpy()
51+
52+
await isWhite(imageBuffer)
53+
restore()
54+
55+
const percentageChecked = (spy.callCount / totalPixels) * 100
56+
57+
t.true(
58+
percentageChecked >= 20 && percentageChecked <= 30,
59+
`Expected to check ~25% of pixels, but checked ${percentageChecked.toFixed(2)}% (${
60+
spy.callCount
61+
}/${totalPixels})`
62+
)
63+
})
64+
65+
test('handles memory errors gracefully on very large images', async t => {
66+
const { createCanvas } = require('canvas')
67+
68+
const width = 10000
69+
const height = 10000
70+
const canvas = createCanvas(width, height)
71+
const ctx = canvas.getContext('2d')
72+
73+
ctx.fillStyle = '#ffffff'
74+
ctx.fillRect(0, 0, width, height)
75+
76+
const largeImageBuffer = canvas.toBuffer('image/jpeg', { quality: 0.9 })
77+
const result = await isWhite(largeImageBuffer)
78+
t.is(typeof result, 'boolean', 'Should return a boolean, not throw an error')
79+
})

0 commit comments

Comments
 (0)