Building a Screenshot Diffing Pipeline With Just Curl and ImageMagick

Wanted to set up visual regression testing without pulling in Playwright, Cypress, or any heavy framework. Just needed to know: "did any of our key pages change visually after this deploy?"

Turns out you can get pretty far with curl, a screenshot API, and ImageMagick.

The pipeline

Three steps. Capture before deploy, capture after deploy, diff.

PAGES=("https://myapp.com" "https://myapp.com/pricing" "https://myapp.com/docs")
API="https://api.screenshotrun.com/capture"

# Before deploy
for page in "${PAGES[@]}"; do
  hash=$(echo -n "$page" | md5sum | cut -d' ' -f1)
  curl -s -o "before_${hash}.png" "${API}?url=${page}&width=1280&format=png&api_key=${SR_KEY}"
done

# ... deploy happens here ...

# After deploy
for page in "${PAGES[@]}"; do
  hash=$(echo -n "$page" | md5sum | cut -d' ' -f1)
  curl -s -o "after_${hash}.png" "${API}?url=${page}&width=1280&format=png&api_key=${SR_KEY}"
done

The diff

ImageMagick's compare command does pixel-level diffing. The -metric AE flag returns the number of pixels that differ.

for page in "${PAGES[@]}"; do
  hash=$(echo -n "$page" | md5sum | cut -d' ' -f1)
  DIFF=$(compare -metric AE "before_${hash}.png" "after_${hash}.png" "diff_${hash}.png" 2>&1)

  if [ "$DIFF" -gt 500 ]; then
    echo "CHANGED: $page ($DIFF pixels differ)"
  fi
done

The threshold of 500 pixels is arbitrary — adjust based on how sensitive you want it. Too low and you get noise from font rendering differences, too high and you miss real changes.

Why an external screenshot API instead of local Chrome

Tried running headless Chrome locally first. Two problems:

The render environment wasn't consistent. Different font stacks, GPU vs no-GPU rendering, scrollbar visibility — all of these create phantom diffs that waste your time investigating nothing.

Second issue: Chrome in Docker is a pain. You need specific packages, specific flags, and it eats memory. For a simple "did the page change" check, that's too much overhead.

Using an API means the screenshots always come from the same environment. Same browser config, same fonts, same rendering pipeline. The diffs are actually meaningful.

What this won't catch

Anything interactive. Hover states, dropdown menus, form validation, anything that requires user input. For those you still need real E2E tests.

But for "someone pushed CSS that moved the checkout button off-screen" or "the hero image stopped loading" — this 30-line bash script handles it. We run it in CI after every deploy to staging. Takes about 40 seconds for 8 pages.

Not sophisticated. But it's caught three real bugs in the past month that would've shipped to production otherwise. Good enough.