Skip to main content

proofsheet

Exact-pixel App Store and Google Play screenshots from a real browser. Deterministic, local-first, no cloud.

Source: github.com/interchained/proofsheet

pip install proofsheet
import proofsheet

report = proofsheet.capture(
    "https://your.app",
    out_dir="./shots",
    store="apple",
)

print(report.summary())
if not report.ok:
    raise SystemExit(1)
33 exact, 0 off-size, 0 failed in 12043ms

Point it at anything a browser can open

There is no "local mode" and no "remote mode" — the first argument is a URL. All four of these are ordinary usage:

proofsheet.capture("http://localhost:5173", store="apple", out_dir="./shots")   # dev server
proofsheet.capture(f"file://{os.getcwd()}/dist/index.html", store="apple")      # static build
proofsheet.capture("https://pr-482.preview.example.com", store="apple")         # preview deploy
proofsheet.capture("https://your.app", store="apple")                           # production

Prefer localhost. Not as a fallback — as the default:

  • You capture before you ship. The set is built from the branch you're about to release, so it can gate the release rather than document it afterwards.
  • CI needs no deploy and no public URL. Start your dev server, capture, upload.
  • It's hermetic. A live domain drags in CDN state, cookie banners, A/B buckets and analytics — all of which move between runs and destroy byte-identical determinism. Localhost doesn't.
  • It's faster. Measured on the same page: 366ms local vs 1.6s over the network. Across a 44-device matrix that's ~25 seconds versus a couple of minutes.
  • It works on apps that aren't public yet, or are behind auth.

A complete pre-release script:

import contextlib
import socket
import subprocess
import time

import proofsheet


@contextlib.contextmanager
def dev_server(cmd, port, timeout=30):
    proc = subprocess.Popen(cmd, shell=True)
    try:
        # Wait for the port to accept connections rather than sleeping a
        # hopeful number of seconds.
        deadline = time.time() + timeout
        while time.time() < deadline:
            with socket.socket() as s:
                s.settimeout(0.5)
                if s.connect_ex(("127.0.0.1", port)) == 0:
                    break
            time.sleep(0.25)
        else:
            raise TimeoutError(f"nothing listening on {port} after {timeout}s")
        yield f"http://localhost:{port}"
    finally:
        proc.terminate()
        proc.wait(timeout=10)


with dev_server("npm run preview", 4173) as url:
    report = proofsheet.capture(url, store="apple", out_dir="./shots")

print(report.summary())
raise SystemExit(0 if report.ok else 1)

Two things worth knowing before they bite you:

file:// is not a real origin. Fastest path, fine for genuinely static pages, but fetch, service workers, ES module imports and anything CORS-sensitive behave differently there than in production. If your app does real work, run the dev server and point at localhost.

Inside Docker, localhost means the container. If this runs in a container while your dev server runs on the host, use host.docker.internal or --network host.

Why

Store screenshot sets rot. They get taken by hand, at different moments, on different builds — one shot says 3:47 and the next says 9:12, a list reshuffles between frames, "2 hours ago" becomes "3 days ago". Then a redesign lands and somebody spends two days redoing all of them.

proofsheet makes the set a build artifact. Same seed, same bytes. Rerun after a redesign and the only things that changed are the things you changed.

Exact pixels, structurally

Apple and Google publish requirements in output pixels1320 × 2868, 1024 × 500. A browser is driven in CSS pixels plus a device pixel ratio. Storing the CSS size and multiplying is the obvious design and it's the wrong one: it permits a preset that cannot produce a required size, and you find out at upload.

So a preset stores the required output size and derives the viewport as output / scale. Any preset where that division isn't exact is rejected before a browser starts.

d = proofsheet.devices("apple")[0]
d.output_size   # (1320, 2868)  <- what the store requires
d.viewport      # (440, 956)    <- what the browser is driven at
d.scale         # 3
d.source        # the Apple doc URL this came from

Determinism

A preamble is injected before any page script on every document: seeded PRNG, frozen clock, virtual requestAnimationFrame, seeded crypto.getRandomValues. Locale and timezone are pinned through CDP rather than script, because the script-level overrides don't reach Intl's internal data.

Verified in both directions — the same seed produces byte-identical PNGs across independent browser launches, and a different seed produces different bytes. The second half is what makes the first half evidence.

a = proofsheet.capture(url, device_ids=["apple-iphone-6-9-1320"], seed=42)
b = proofsheet.capture(url, device_ids=["apple-iphone-6-9-1320"], seed=42)
assert a.results[0].capture.sha256 == b.results[0].capture.sha256

Presets

46 presets, every store size read from official documentation on 2026-08-22 and carrying the URL it came from.

  • Apple — iPhone 6.9″/6.5″/6.3″/6.1″/5.5″/4.7″, iPad 13″/11″/10.5″/9.7″, Mac, Apple TV, Vision Pro, Apple Watch
  • Google Play — feature graphic, phone, 7″/10″ tablet, Wear OS, Automotive, TV
for d in proofsheet.devices("play"):
    if d.mandatory:
        print(d.id, d.output_size)

Pass presets="my-devices.json" to use your own table. Stores change these numbers without warning, and a requirement you can only fix by cutting a release is a requirement that will be wrong.

report.ok, not failed == 0

if report.ok: ...

ok requires that at least one capture happened and nothing went wrong. A run that captured nothing has an empty problem list, so failed == 0 is true for it — an empty green is the most misleading result a tool can produce.

Browser

You need a Chromium. The quickest route is proofsheet's own installer, which fetches a pinned Chrome for Testing build:

This package is a librarypip install proofsheet adds no proofsheet command to your PATH. The installer lives in the CLI:

cargo install proofsheet && proofsheet install-browser

It fetches a pinned Chrome for Testing build into ~/.proofsheet/browser (PROOFSHEET_HOME overrides), and find_browser() picks it up with no configuration. If you would rather not install the CLI, set PROOFSHEET_CHROME to any Chromium, or pass browser=.

proofsheet.find_browser()   # raises with instructions if none found

Scope, honestly

In scope: anything that renders in a browser engine — web apps, PWAs, and Capacitor / web-view apps, where the web view genuinely is the app.

Out of scope: true native Swift or Kotlin apps. Those need a simulator, and no amount of Chromium gets you there. Better you know that now than after installing.

Also available

  • Rust: cargo install proofsheet (CLI) / proofsheet-core (library)
  • Node: npm i @interchained/proofsheet

Same core, same behaviour, one release.

Contact

License

BUSL-1.1, converting to MIT on 2030-08-22.

Property made in part by Interchained LLC Labs.

© 2026 Interchained LLC

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proofsheet-0.1.11.tar.gz (50.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

proofsheet-0.1.11-cp38-abi3-win_amd64.whl (352.3 kB view details)

Uploaded CPython 3.8+Windows x86-64

proofsheet-0.1.11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (494.7 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ x86-64

proofsheet-0.1.11-cp38-abi3-macosx_11_0_arm64.whl (427.9 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

proofsheet-0.1.11-cp38-abi3-macosx_10_12_x86_64.whl (444.3 kB view details)

Uploaded CPython 3.8+macOS 10.12+ x86-64

File details

Details for the file proofsheet-0.1.11.tar.gz.

File metadata

  • Download URL: proofsheet-0.1.11.tar.gz
  • Upload date:
  • Size: 50.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.14.1

File hashes

Hashes for proofsheet-0.1.11.tar.gz
Algorithm Hash digest
SHA256 4e91411809fc0934168e6f1b6e70d4df43d084ec4cc5ed367c46680d6b1e4780
MD5 9c7518bba6279b7c4f00b1e88a64798c
BLAKE2b-256 ae9df8d3fd4da54c1a57f046b738a3b0b5cedc5ddac36829da9b3de8da372373

See more details on using hashes here.

File details

Details for the file proofsheet-0.1.11-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for proofsheet-0.1.11-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 ffe021fa204726c8b7636435b6586d1150abd090a2a4f6772198e6dcdf4d2a7a
MD5 1351569abdf114355e3977e311d1e8f9
BLAKE2b-256 02b625c8545ecc67683d42b3bc79bad219266fe1761eab0fc9cba506b90b3a65

See more details on using hashes here.

File details

Details for the file proofsheet-0.1.11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for proofsheet-0.1.11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2db2cecd9830118173a303b181d28407db2f4e1d62b0f55029c1de828ae36f7e
MD5 b3058bb260b562ddf956fbecbd4dd822
BLAKE2b-256 d09d8c8e269befce54c49c5acde29f8dffe1992647fb5d44e6aec0263d935b8c

See more details on using hashes here.

File details

Details for the file proofsheet-0.1.11-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for proofsheet-0.1.11-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2df37cbceacc89806538e2140af0f151e17b2737cd54a3af055904bd3ba5e25b
MD5 168a74e451ca324a4e24a7733420bead
BLAKE2b-256 0659606a8fef5e9bca53f702afab3c444a50c5039d6d83c0ae5a4d1421957e73

See more details on using hashes here.

File details

Details for the file proofsheet-0.1.11-cp38-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for proofsheet-0.1.11-cp38-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0d9537b4d561a36bf54fdfee47d96287ff4b4c2ed3ef71c0fcb1c86984ed56f3
MD5 3487c4f2c1839e74aec6eb52da8344e4
BLAKE2b-256 2b150ad134674c449f777fc49f696acf55de94bc2d6e4b3c321f9b8c8bf4577b

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.14

5 files

0.1.13

5 files

0.1.12

5 files

This release

0.1.11 This release

5 files

0.1.10

5 files

0.1.9

5 files

0.1.8

5 files

0.1.7

5 files

0.1.6

5 files

0.1.5

5 files

0.1.4

5 files

0.1.1

5 files

0.1.0

5 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page