Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 5 min read

Checking Amazon's "pure white background" rule in the browser with canvas and a flood fill

Amazon's main product image has two rules that sellers trip over all the time: the background has to be pure white (RGB 255, 255, 255), and the product should fill about 85% of the frame. The problem is that "looks white

Amazon's main product image has two rules that sellers trip over all the time: the background has to be pure white (RGB 255, 255, 255), and the product should fill about 85% of the frame. The problem is that "looks white" and "is 255" are different things. A background at 248 looks perfectly white on a monitor, and that's exactly the kind of image that gets flagged.

I wanted a checker that runs entirely in the browser (no upload, no server), so here is how it works with plain canvas and typed arrays.

1. Downscale without smoothing

Big photos are slow to scan pixel by pixel, so the image is drawn onto a canvas capped at 1200px on the long side. The important part is turning off smoothing:

const scale = Math.min(1, 1200 / Math.max(bmp.width, bmp.height))
const w = Math.round(bmp.width * scale)
const h = Math.round(bmp.height * scale)
const ctx = canvas.getContext('2d', { willReadFrequently: true })
ctx.fillStyle = '#fff'          // transparent pixels count as white
ctx.fillRect(0, 0, w, h)
ctx.imageSmoothingEnabled = false
ctx.drawImage(bmp, 0, 0, w, h)
const d = ctx.getImageData(0, 0, w, h).data

With smoothing on, the browser interpolates neighbouring pixels while scaling. A 255 next to a 250 can turn into a 253 that doesn't exist in the original file. Nearest-neighbour sampling keeps the real values, which matters when the whole test is "is it exactly 255".

2. Quick signal: the border ring

The cheapest check is a 1% wide ring around the edges. Count how many of those pixels are exactly 255,255,255:

const ring = Math.max(1, Math.round(Math.min(w, h) * 0.01))
let n = 0, pure = 0
for (let y = 0; y < h; y++) {
  for (let x = 0; x < w; x++) {
    if (x >= ring && x < w - ring && y >= ring && y < h - ring) continue
    const i = (y * w + x) * 4
    n++
    if (d[i] === 255 && d[i + 1] === 255 && d[i + 2] === 255) pure++
  }
}

If this is well below 100%, the background is off-white, or the product (or its shadow) touches the edge.

3. Find the background with a flood fill from the edges

The ring alone isn't enough. What you actually want is "the background area", and you can't just take every light pixel, because a white label or a white mug handle would count as background.

So the background is defined as: light pixels connected to the image border. A stack-based flood fill seeded from all four edges does it, with a threshold (every channel β‰₯ 235 by default) so near-white counts as "background that should have been white":

function backgroundMask(d, w, h, threshold) {
  const mask = new Uint8Array(w * h)
  const stack = new Int32Array(w * h)
  let sp = 0
  const light = p => d[p*4] >= threshold && d[p*4+1] >= threshold && d[p*4+2] >= threshold
  const push = p => { if (!mask[p] && light(p)) { mask[p] = 1; stack[sp++] = p } }
  for (let x = 0; x < w; x++) { push(x); push((h - 1) * w + x) }
  for (let y = 0; y < h; y++) { push(y * w); push(y * w + w - 1) }
  while (sp > 0) {
    const p = stack[--sp]
    const x = p % w
    if (x > 0) push(p - 1)
    if (x < w - 1) push(p + 1)
    if (p >= w) push(p - w)
    if (p < w * (h - 1)) push(p + w)
  }
  return mask
}

A few notes:

  • An Int32Array sized to the pixel count is the stack. Every pixel is pushed at most once (the mask is set on push), so it can never overflow, and there's no recursion to blow up.
  • 4-connectivity is enough here; 8-connectivity tends to leak through thin dark outlines.
  • Inside the mask, every pixel that isn't exactly 255 gets counted and painted magenta on an overlay canvas. That overlay is the most useful output: people immediately see the grey vignette in the corners or the soft shadow under the product.

4. How much of the frame does the product fill?

Everything that is not background is "product". To get a bounding box that ignores stray specks (dust, JPEG noise), count non-background pixels per row and per column, and only accept rows/columns above a tiny minimum (0.3% of the other dimension):

const rows = new Int32Array(h), cols = new Int32Array(w)
for (let p = 0; p < w * h; p++) if (!mask[p]) { rows[(p / w) | 0]++; cols[p % w]++ }
const minRow = Math.max(1, Math.round(w * 0.003))
const minCol = Math.max(1, Math.round(h * 0.003))
// first/last row and column above the minimum = bounding box

Fill is then max(boxWidth / w, boxHeight / h), i.e. how much of the frame the product's longest side covers. Under ~0.85 means the product is probably too small for the main image.

5. Fixing it

Once you have the mask, the fix is almost free:

  • Whiten: redraw at full resolution (capped at 5000px), recompute the mask, and set every background pixel to 255. The threshold is a slider (215–252), because a soft shadow you want to keep and a grey backdrop you want to remove can sit at similar values.
  • Reframe: crop to the bounding box and draw it centred on a white square where the product's long side is 90% of the frame, with the side at least 2000px so zoom works.

Limitations

  • A white product on a white background is the hard case: the flood fill can eat into the product's edges. That's why the threshold is adjustable and the overlay is shown, instead of silently "fixing" things.
  • This checks pixels, not policy. Amazon also requires the main image to be a real photo of the product, without added text or graphics, and no script can verify that.
  • Transparent PNGs are treated as white here. That's a choice, not necessarily how every marketplace renders them.

Try it

I built this as a free tool for my side project ImageWhim. It runs fully in the browser, and your image is never uploaded: https://imagewhim.com/tools/amazon-main-image-checker

Disclosure: I'm the developer of ImageWhim. This post was written with AI assistance; the code is taken from the tool's actual source.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.