@thiagocolen/corruptatron
v1.0.0
Published
Generate random algorithmic glitch variations of an image
Maintainers
Readme
CORRUPTATRON
Generate N random glitch variations of an image. Every batch is reproducible from its seed, and every variation records the exact effect chain that made it — so a result you like can be recovered or hand-tuned instead of re-rolled.
Install
npm install -g @thiagocolen/corruptatron
corrup photo.jpg -n 20 --intensity heavyOr run it once with no install (the package name doesn't match either bin,
so -p is required to tell npx which package to fetch):
npx -p @thiagocolen/corruptatron corrup photo.jpg -n 20 --intensity heavyOutput lands in glitch-out/: the variations and a labelled
contact-sheet.png for judging the batch at a glance. Pass --manifest to
also save a manifest.json recording the exact params behind every
variation — needed if you want to --replay the batch later.
Portrait cropping (see Portrait crop below) pulls in
@tensorflow/tfjs-core/-converter/-backend-cpu and
@tensorflow-models/blazeface for local, offline face detection — the first
non-native dependency this project has needed, so expect a noticeably bigger
npm install than sharp/zod/the MCP SDK alone. The blazeface model
itself ships vendored under models/, so there's no network fetch at
runtime even the first time you use it.
Working from a clone
npm install
node scripts/make-test-image.js test.png # optional sample input
node glitch.js test.png -n 20 --intensity heavyEverything below applies the same way whether you're running corrup after
a global install or node glitch.js from a clone — the flags are identical.
Options
-n, --count <n> variations to generate (default 8)
-o, --out <dir> output directory (default ./glitch-out)
-s, --seed <n> base seed (default random)
-i, --intensity <lvl> subtle | medium | heavy (default medium)
--format <fmt> png | jpeg | webp (default png)
--quality <n> encode quality, jpeg/webp (default 92)
--effects <list> comma-separated pool restriction
--max-dim <px> downscale longest edge first (default 2000, 0 = off)
--no-databend exclude byte-level corruption
--no-sheet skip the contact sheet
--crop-direction <dir> pin randomCrop's edge instead of random
(top-to-bottom | bottom-to-top | left-to-right | right-to-left)
--crop-size <pct> pin randomCrop's size, 1-100%, instead of random 7-23%
--crop-aspect-ratio <ratio> switch randomCrop to aspect-ratio mode
named format (e.g. youtube_thumbnail) or raw W:H (e.g. 16:9)
— see --list-formats for the full catalog
--portrait-crop crop to a face-centred portrait before glitching
--portrait-aspect-ratio <ratio> portrait crop's aspect ratio (default 4:5)
named format or raw W:H, same as --crop-aspect-ratio
--portrait-margin <n> portrait crop's headroom multiplier (default 1.8)
--manifest save manifest.json, needed to --replay later (default off)
--replay <file> regenerate exactly from a manifest.json
--list list available effects
--list-formats list named aspect-ratio formats for --crop-aspect-ratio/--portrait-aspect-ratio
-h, --help this textEffects
Eleven are pixel math: blockGlitch, channelShift, channelSwap,
chromaticAberration, noiseDropout, pixelSort, quantize, randomCrop,
scanlines, sliceDisplace, wave.
jpegDatabend is the odd one out — it is actual data corruption. The image
is encoded to JPEG, bytes inside the entropy-coded scan are damaged, and the
decoder renders the wreckage. Because JPEG blocks are DC-delta-coded against
their predecessor, one bad byte discolours everything downstream of it. No
algorithm reproduces that smear convincingly.
Three rules separate a glitched image from a dead file, and
src/effects/jpegDatabend.js enforces all three:
- Only touch bytes after the SOS marker — damage the header and there is nothing left to decode.
- Never write
0xFF. A strayFFD9reads as end-of-image and truncates everything after it. - Never overwrite an
0xFFor the byte following one — those are real markers, and restart markers let the decoder resync, keeping damage local.
Decoding uses sharp's failOn: 'none'; the default setting rejects corrupt
input outright. If a stream still fails to decode, corruption is halved and
retried up to three times, then the step passes the image through untouched and
records fallback in the manifest — one bad roll never kills a batch.
randomCrop
The only effect that changes the canvas size — a true crop, not a pad or rescale. Two modes, chosen by whether an aspect ratio is requested:
- Slice mode (default): cuts a full-width or full-height band flush
against one of four edges (
top-to-bottom,bottom-to-top,left-to-right,right-to-left), sized as a random 7-23% of that dimension. - Aspect-ratio mode (
--crop-aspect-ratio): crops a rectangle matching a ratio — either a named format from the crop format catalog below (e.g.youtube_thumbnail,instagram_portrait) or a rawW:Hstring like16:9— scaled off the largest rectangle of that ratio that fits the image, anchored to the requested edge.
Direction and size are independently optional overrides — whichever you don't specify is still sampled randomly:
node glitch.js photo.jpg --effects randomCrop --crop-direction left-to-right --crop-size 15
node glitch.js photo.jpg --effects randomCrop --crop-aspect-ratio youtube_thumbnail --crop-direction top-to-bottom
node glitch.js photo.jpg --effects randomCrop --crop-aspect-ratio 16:9 --crop-direction top-to-bottomIt's meant to be stacked — applying it again (more variations, or repeatedly through the MCP server) slices a fresh rectangle out of whatever the previous crop left behind.
Portrait crop
--portrait-crop detects a face (via a locally-run
blazeface
model — bounding box only, no identity/recognition) and crops around it with
headroom above and shoulder room below, like a portrait rather than a tight
face-only box. Unlike randomCrop, this is content-dependent — it looks at
actual pixels, not just image dimensions — so it runs once as preprocessing
on the source image before the batch, not as a per-variation effect:
node glitch.js portrait.jpg --portrait-crop --intensity subtle
node glitch.js portrait.jpg --portrait-crop --portrait-aspect-ratio square --portrait-margin 2.2Every variation in the batch is a glitched version of the same crop, and an
unglitched portrait-crop.<ext> reference file is written alongside them.
--portrait-aspect-ratio accepts the same named formats or raw ratios as
--crop-aspect-ratio (default 4:5); --portrait-margin scales how much
headroom/shoulder room is added above the tight face box (default 1.8,
must be > 1).
If no face is detected, generation continues on the uncropped image with a
warning on stderr — one photo without a detectable face doesn't kill a
batch. The resolved crop rectangle is recorded in manifest.json when
--manifest is set, so --replay reproduces the exact same crop without
re-running face detection.
Crop format catalog
--crop-aspect-ratio and --portrait-aspect-ratio (and their MCP
equivalents, cropAspectRatio/aspectRatio) accept either a named format
from this catalog or a raw W:H ratio, decimals included (e.g. 1.91:1).
Several names intentionally share a ratio — they're aliases with different
real-world intent (square, app_icon, and square_story_card are all
1:1). Run corrup --list-formats or call the MCP list_crop_formats tool
for this same list at any time.
| Name | Ratio | Description | Common uses |
| --- | --- | --- | --- |
| square | 1:1 | Square, equal width and height | social media post, profile picture, product thumbnail |
| landscape_widescreen | 16:9 | Standard widescreen video/display | video, presentation slide, YouTube thumbnail, desktop wallpaper |
| portrait_widescreen | 9:16 | Vertical widescreen | Instagram Reels, TikTok, Stories, mobile video |
| landscape_standard | 4:3 | Traditional standard display | older TV/monitor, presentation slide, print |
| portrait_standard | 3:4 | Vertical standard | portrait photo, print poster, mobile app screen |
| classic_photo_landscape | 3:2 | Classic 35mm film / DSLR photo ratio | photography, print |
| classic_photo_portrait | 2:3 | Vertical classic photo ratio | photography, print poster |
| landscape_wide | 3:1 | Wide panoramic banner | website header, email banner |
| banner_ultrawide | 6:1 | Ultra-wide banner strip | site banner, leaderboard ad |
| cinematic_widescreen | 21:9 | Cinematic ultrawide | film, ultrawide monitor, cinematic video |
| instagram_portrait | 4:5 | Tall portrait for feed | Instagram post, Facebook post |
| instagram_landscape | 1.91:1 | Wide landscape for feed | Instagram post, Facebook link preview |
| golden_ratio_landscape | 1.618:1 | Golden ratio, landscape | design layout, art composition |
| us_letter_portrait | 8.5:11 | US Letter document, portrait | document, print, PDF |
| us_letter_landscape | 11:8.5 | US Letter document, landscape | document, print, PDF |
| a4_portrait | 1:1.414 | ISO A4 paper, portrait | document, print, PDF |
| a4_landscape | 1.414:1 | ISO A4 paper, landscape | document, print, PDF |
| presentation_widescreen | 16:10 | Widescreen presentation/monitor | presentation slide, laptop display |
| skyscraper_banner | 1:4 | Tall narrow banner | sidebar ad, vertical banner |
| leaderboard_banner | 8:1 | Very wide thin banner | top web banner ad |
| twitter_post | 16:9 | Standard X/Twitter image post | social media post |
| pinterest_pin | 2:3 | Tall pin format | Pinterest pin |
| youtube_thumbnail | 16:9 | Video thumbnail | YouTube thumbnail, video cover |
| app_icon | 1:1 | Square app icon | mobile app icon, favicon |
| book_cover | 2:3 | Standard book cover proportions | ebook cover, print book cover |
| widescreen_cinema | 1.85:1 | Standard theatrical widescreen | film |
| anamorphic_cinema | 2.39:1 | Anamorphic widescreen cinema | film |
| square_story_card | 1:1 | Square card for chat/story previews | link preview card |
The catalog lives in src/cropFormats.js, one source of truth shared by
randomCrop and portraitCrop — add a format there and both crop tools
pick it up automatically.
Reproducibility
Per-variation seeds derive from hash(baseSeed, index), not from one running
stream, so variation #7 is the same image whether you asked for 10 or 100.
node glitch.js photo.jpg -n 12 --seed 42 --manifest
node glitch.js photo.jpg --replay glitch-out/manifest.json -o rerun # identical--replay reads a manifest.json, so the original run needs --manifest
for one to exist. To hand-tune a result, edit its params in
manifest.json and replay.
MCP server
corrup-mcp exposes the effects as MCP tools, so an agent can pick effects
one at a time (or let it pick for you) and keep stacking them on the same
working image across many calls, instead of only getting one random batch per
run.
npm install -g @thiagocolen/corruptatronPoint an MCP client at the corrup-mcp command (stdio transport). For
Claude Code:
claude mcp add corrup -- corrup-mcpOr, from a clone, point it at node mcp/server.js instead.
Tools:
list_effects list every effect with a one-line description
list_crop_formats list named aspect-ratio formats for cropAspectRatio/aspectRatio
load_image load a file, starts a session (default id "default")
apply_effect apply one named effect, optionally repeat <n> times
apply_chain apply an explicit ordered list of effects in one call
detect_faces report detected face bounding boxes/confidence, read-only
crop_portrait detect a face and crop a headroom-including portrait around it
random_overlay layer <times> randomly chosen effect chains, like re-rolling
the CLI's batch onto the same accumulating image
undo drop the last <n> effects, replaying history from scratch
reset_image discard all effects, back to the original
get_history every effect applied so far, in order, with its name,
intensity, seed, and exact params
list_images every loaded session id, dimensions, effect count
save_image encode + write to disk; pass manifest:true to also write
<path>.manifest.json alongside it (default off)Every tool that changes the image returns a downscaled preview so the agent
can see the result. load_image/apply_effect/etc. all take an optional id
so several images can be worked on at once.
apply_effect and each step of apply_chain also accept cropDirection,
cropSizePercent, and cropAspectRatio — randomCrop-only overrides that pin
its direction/size/ratio instead of leaving them random (see
randomCrop above). Leave them unset for the default random
behavior. cropAspectRatio accepts a named format from the
crop format catalog (e.g. youtube_thumbnail) or a
raw W:H ratio — call list_crop_formats to see the full catalog.
crop_portrait accepts aspectRatio (a named format or raw W:H ratio,
same as cropAspectRatio above), margin, faceIndex (which detected face
to crop around, 0 = largest, default), and minConfidence — see
Portrait crop above. Call detect_faces first on a
multi-face photo to see how many faces there are and pick a faceIndex. If
no face clears minConfidence, crop_portrait leaves the image unchanged
and reports that in its response instead of erroring. crop_portrait pushes
a portraitCrop step onto the session's history like any other effect, so
undo/get_history/reset_image all account for it.
Adding an effect
Drop a file in src/effects/ and append it to src/effects/index.js.
export default {
name: 'myEffect',
weight: 1.0, // relative pick probability
async: false, // true if apply() returns a promise
params(rng, scale, ctx, overrides) { /* sample everything here */ },
apply(canvas, params) { /* mutate or return a new canvas */ },
};Sampling in params() rather than inside apply() is what makes the manifest
and --replay work. canvas is { data: Uint8ClampedArray /* RGBA */, width,
height }. scale is the intensity multiplier; ctx carries width, height
and percentile(p) — a luma-distribution lookup, so thresholds land on tones
the image actually contains instead of silently selecting nothing. overrides
is an optional, effect-defined object for pinning specific parameters instead
of leaving them random — see randomCrop for the pattern. The CLI's
--crop-* flags funnel into it through buildChain's name-keyed
overridesByName map; the MCP server's apply_effect/apply_chain pass
their crop* tool fields straight to effect.params(...) instead, since
each call only ever targets one effect at a time. random_overlay calls
buildChain without overrides, so any randomCrop steps it picks stay fully
random.
Notes
jpegDatabendflattens transparency (JPEG has no alpha channel).- Roughly 0.45s per variation at 1200x800;
--max-dimbounds the cost. - Face detection (
--portrait-crop,detect_faces,crop_portrait) loads the vendored blazeface model and the tfjs CPU backend lazily, on first use only — a plain glitch run or MCP session that never touches these never pays that cost. First detection in a process (model load + inference) is well under a second on CPU; later calls in the same process reuse the already-loaded model.
