npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thiagocolen/corruptatron

v1.0.0

Published

Generate random algorithmic glitch variations of an image

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 heavy

Or 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 heavy

Output 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 heavy

Everything 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 text

Effects

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:

  1. Only touch bytes after the SOS marker — damage the header and there is nothing left to decode.
  2. Never write 0xFF. A stray FFD9 reads as end-of-image and truncates everything after it.
  3. Never overwrite an 0xFF or 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 raw W:H string like 16: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-bottom

It'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.2

Every 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/corruptatron

Point an MCP client at the corrup-mcp command (stdio transport). For Claude Code:

claude mcp add corrup -- corrup-mcp

Or, 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

  • jpegDatabend flattens transparency (JPEG has no alpha channel).
  • Roughly 0.45s per variation at 1200x800; --max-dim bounds 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.