What a humanizer can and cannot change in a picture file
An image file on disk is two things stacked together: a container of metadata segments, and compressed pixel data. Those two layers fail differently. Metadata can be removed surgically — the byte ranges are skipped and everything else is copied unchanged. Pixels cannot be removed; they can only be replaced with different pixels, which is what the noise and re-compression stages do.
That split is the honest way to describe a picture humanizer. Stripping is reversible in principle (keep the original file) and exact: the removed bytes are gone and their absence is verifiable. Adding grain is irreversible by nature and approximate: it changes every pixel slightly, and no record of the original remains in the output. So the tool does two different jobs with two different guarantees, and this page keeps them separate.
The four stages, in the order the code runs them
The pipeline is fixed — a stage cannot be skipped, and each one sees the output of the last. That ordering matters for one specific reason: metadata is stripped before the pixels are re-encoded, and the encoded output is stripped a second time, so a browser encoder that writes its own segments cannot put metadata back.
| Stage | What happens | Reversible? |
|---|---|---|
| 1 · decoding | Format sniffed from magic bytes, EXIF orientation read, bitmap decoded with orientation baking turned off so the browser cannot rotate twice. | n/a — read only |
| 2 · stripping | Every JPEG APP0–APP15 segment and COM block is removed; for PNG, only a 14-chunk whitelist survives (IHDR, PLTE, IDAT, IEND, tRNS, gAMA, cHRM, sRGB, bKGD, pHYs, sBIT, acTL, fcTL, fdAT). | Exact, verifiable removal |
| 3 · rendering | Orientation baked into pixels, 16 MP clamp applied, sensor noise added per pixel, film grain overlaid at 5–17 % alpha in overlay blend mode. | No — every pixel shifts |
| 4 · encoding | Re-encoded at the chosen JPEG quality, then stripped a second time so nothing the encoder wrote survives into the download. | No — re-compression is lossy |
Which metadata segments actually leave a JPEG
The stripper walks the JPEG marker chain from the start of the file until it reaches the compressed image data, and drops anything in the APPn range or a comment block. It keeps the segments a decoder needs: SOI, the frame header, quantisation tables, Huffman tables, the scan header and the compressed scan itself.
Removed
- APP0 — JFIF
- APP1 — EXIF / XMP
- APP2 — ICC profile
- APP3–APP10 — vendor metadata
- APP11 — JUMBF / C2PA
- APP12–APP15 — vendor metadata
- COM — free-text comment
Kept
- SOI / EOI — file boundaries
- SOF — frame dimensions
- DQT — quantisation tables
- DHT — Huffman tables
- SOS + scan data — the picture itself
One consequence worth stating plainly: the ICC colour profile lives in APP2, so a colour-managed photograph in a wide-gamut space (Display P3, Adobe RGB) loses that profile when stripped this way. If colour accuracy matters more than metadata removal for a given file, convert to sRGB before processing rather than after.
The grain slider, converted to actual pixel values
“Grain intensity” is a number between 0 and 100 on a slider, which tells you nothing about what happens to the pixels. The tool converts it to an amplitude in 0–255 channel levels, weighted by local brightness so shadows receive more noise than highlights — the physical behaviour of a real sensor, where read noise is a larger fraction of a small signal.
| Slider value | Noise in shadows (luma 0) | Noise in highlights (luma 1) | Grain overlay alpha |
|---|---|---|---|
| 0 | 0 | 0 | 0 (pass skipped) |
| 35 (default) | ±4.2 levels | ±1.26 levels | 0.092 |
| 70 | ±8.4 levels | ±2.52 levels | 0.134 |
| 100 | ±12.0 levels | ±3.6 levels | 0.170 |
The grain is a separate component, not the same noise counted twice. A grey-noise tile roughly half the image width (clamped to 64–512 px) is generated, scaled up to the frame, and composited in overlay mode at the alpha in the last column. The cap at 0.17 is deliberate: past that point the overlay starts flattening real detail instead of adding texture. Channel values are written through a clamped 8-bit buffer, so bright pixels cannot roll over into black.
Parameter reference — every control and its real range
| Control | Range / default | Observable effect | Source constant |
|---|---|---|---|
| Grain & sensor noise slider | 0 – 100, step 1, default 35 | Sets noise amplitude. At 0 the pixel pass is skipped entirely, so only metadata stripping and re-compression happen. | components/HumanizeTool.tsx (min/max/step) · lib/image/types.ts DEFAULT_SETTINGS |
| JPEG quality slider | 50 – 100, step 1, default 92 | Passed to the browser encoder as quality/100. Disabled and ignored when the input is PNG — PNG output stays lossless. | components/HumanizeTool.tsx · lib/image/humanize.ts clamp(settings.quality, 50, 100) |
| File size ceiling | 60 MB | Rejected above the limit with a too-large error before any decoding happens. | lib/image/format.ts MAX_FILE_BYTES = 60 * 1024 * 1024 |
| Pixel ceiling | 16,000,000 px | Images above 16 MP are scaled down by sqrt(16e6 / pixels) before effects are applied. A 24 MP 6000×4000 file becomes 4898×3265; a 19.96 MP 5472×3648 file becomes 4898×3265. | lib/image/humanize.ts MAX_PIXELS = 16_000_000 · renderCanvas() |
| Accepted input formats | JPEG, PNG | Format is detected from magic bytes, not from the file extension or the browser-reported MIME type. WebP, GIF, AVIF and HEIC are recognised and refused with an unsupported-format error. | lib/image/format.ts SUPPORTED_FORMATS · sniffFormat() |
| Determinism | Same input + same settings = same output | The noise seed is derived from input byte length and the intensity value, using a seeded PRNG, so a repeated run reproduces the same grain instead of a new random pattern. | lib/image/noise.ts deriveSeed() · mulberry32() |
Why the output never claims to be a camera file
A file that has just had its EXIF block removed looks, to a metadata reader, like a file with no camera information — which is also what an exported screenshot or a platform-recompressed upload looks like. The tool does not write a camera make, a lens model, a capture timestamp or GPS coordinates into that gap. Doing so would be forging provenance, which is a different product and one this site does not build.
The same restraint applies to detector claims. Detectors weigh frequency statistics, watermark records, provenance boxes and metadata together, and they are retrained on new data continuously. What is true here is narrower and checkable: the identified metadata segments are gone, the output is re-encoded at the quality you chose, and the pixel statistics differ from the input in the shadows by the amounts in the table above. Anything beyond that would be a guess.
Where the processing happens
All four stages run in the browser tab using the Canvas API. The file is read from disk into memory, decoded, transformed and re-encoded there, and the download is assembled from bytes that never left the machine. There is no upload endpoint in the codebase, so the tool also works with the network disabled. That is why a 60 MB ceiling exists at all: the whole file has to fit in the tab's memory alongside its decoded bitmap, and a decoded bitmap is uncompressed — a 16 MP frame occupies roughly 64 MB of memory on its own, several times the size of the JPEG it came from.