Privacy and compliance
EXIF can carry GPS coordinates, device model and capture timestamps. Storing that unmodified is a GDPR/CCPA liability you did not ask for — and one nobody notices until an audit.
A photo off a phone carries GPS coordinates, a device serial and a capture timestamp. pixelscrub draws it to a canvas and reads it back — metadata is gone by construction, not by being edited out. The bytes never leave the tab.
Both previews render with image-orientation: none, so you see the raw
pixel grid. A portrait photo that looks sideways as the original and upright once
sanitized is the orientation fix doing its job.
Every one of them is fixed in the same pass, in the browser, before a single byte is uploaded.
EXIF can carry GPS coordinates, device model and capture timestamps. Storing that unmodified is a GDPR/CCPA liability you did not ask for — and one nobody notices until an audit.
Phone photos run 5–10 MB each. Uploading those over a weak connection fails often, and costs real money in transfer and storage every time it succeeds.
Resizing server-side with sharp or imagemagick burns CPU you could avoid entirely. The client already has a decoder and a rasteriser sitting idle.
Draw the image to a canvas, read it back out. Metadata is gone by construction rather than by being edited out — there is nowhere left for it to live. Resizing and format conversion happen in the same pass, for free.
The one tag with visual meaning is parsed off the raw JPEG bytes first, before any decoding.
Large downscales step down by halving. One big drawImage aliases badly in every engine; the extra draws are cheap.
The transform is applied exactly once, to the pixels. The output carries no orientation tag of its own.
WebP, falling back to JPEG then PNG where the encoder cannot. You get a File, not a Blob.
Chrome rotates during decode whatever you ask of it; older engines do not rotate at all. No feature flag tells you which you are on, so assume either and half your users get sideways photos.
pixelscrub settles it by measurement. Once per session it encodes a 2×1 JPEG with the browser's own encoder, splices an orientation-6 tag into it, and decodes it back. A decoder that honours EXIF hands back a 1×2 image.
| Option | Type | Default | Notes |
|---|---|---|---|
| maxWidth | number | no limit | Bounds the displayed width, after orientation is applied. |
| maxHeight | number | no limit | Aspect ratio is always preserved; a smaller source is never stretched up. |
| quality | number (0–1) | 0.85 | Ignored for image/png. Values outside the range are clamped. |
| outputFormat | webp | jpeg | png | image/webp | Falls back automatically where the browser cannot encode it. |
| watermark | Watermark | none | Text or a logo, drawn after resizing and rotation. |
| maxCanvasDimension | number | 4096 | Older iOS Safari returns a blank canvas past this, without erroring. |
| concurrency | number | 4 | How many images are decoded at once. A memory ceiling, not a speed dial. |
| signal | AbortSignal | — | Cancels the run. Finished results are discarded, matching fetch. |
| onProgress | (p: BatchProgress) => void | — | Called as each image settles, in completion order. |
Every rejection is a PixelScrubError carrying one of these codes. An unsupported output format is never an error — it falls back.
Results are settled, not all-or-nothing, and stay in input order — results[i] always describes files[i]. Pick several photos and watch it run.
A watermark specified as 24px is either illegible on a thumbnail or enormous on a full-resolution photo, and you rarely control which you are handed. One config reads correctly at both ends.
No build step, no worker, no server. Import one function and hand it a File.