DitherBox
Adjustable dithering for photos, in two interfaces sharing one engine: a widget that runs in your browser (the photo is never uploaded anywhere) and a full-screen terminal application with boxed panels, themes and the preview drawn inside the terminal itself. Nineteen algorithms, twenty-six palettes, fifteen presets, five languages.
What it is
DitherBox applies dithering to a photograph while letting you turn the knobs and watch the result. It exists as two interfaces sharing one engine: a widget that runs in the browser, the one in the Tool tab above, and a full-screen terminal application with boxed panels, six-colour themes and the preview drawn inside the terminal itself.
In the widget everything happens on a canvas in your own browser. The photo is never uploaded anywhere, there is no server receiving it, and closing the tab is all it takes to make it gone.
What dithering is, and why it was needed
When the available colours are fewer than the photograph’s, every pixel has to move to the nearest one it is allowed to use. Done without care, a sky that fades turns into a single block of colour with a hard step where the tone crossed the halfway mark. That is banding, and it is ugly in a recognisable way.
Dithering fixes it by spreading the error instead of swallowing it. If a tone sits halfway between the two available colours, rather than picking one for the whole area the two get alternated, and at a distance the eye averages them back into that half tone. The gradient returns, paid for with a texture of dots.
It was a technical necessity in 1984, with one-bit screens and printers that only knew how to put ink down or not. Today it is an aesthetic choice, and that is why this program exists: that texture has a character perfect gradients do not.
Try it
The Tool tab above holds the real widget, with a photo already loaded. Drop your own onto the frame, turn the knobs, and download the PNG when you like it.
It is worth starting from one of the fifteen presets (Macintosh 1984, Game Boy, Newsprint, Photocopied zine, 8-bit console, 16-bit console, VGA 256 colours, Amiga Workbench) and then moving the individual parameters, rather than facing twenty sliders from a standing start.
What you can adjust
| Parameter | Range | What it does |
|---|---|---|
| Palette | 26 palettes | The colours the result is allowed to use |
| Algorithm | 19 algorithms | How the dots get distributed |
| Pixel | 1 to 16 | Reduce before dithering: 1 is full detail, 8 is chunky 8-bit pixels |
| Strength | 0 to 200% | How much of the error (or of the ordered noise) is applied |
| Threshold | -100 to 100 | Moves the cut-off point: negative darkens, positive lightens |
| Grain | 0 to 100% | Random noise before the threshold, to break up textures that are too regular |
| Serpentine | on/off | Alternating scan row by row, which removes the diagonal streaks |
| Brightness, Contrast, Gamma, Saturation | Tone adjustments, applied before the dithering | |
| Sharpen | 0 to 200% | Unsharp mask, to recover the detail the dithering eats |
| Aspect | 10 ratios | Frames the result to a fixed ratio (1:1, 4:3, 16:9, 9:16…), cropping or adding bars |
| Fit | crop / bars | What to do with whatever does not fit the ratio |
| Zoom | 10 to 100% | Size of the crop rectangle: 100 is the largest that fits, less zooms in |
| Offset X, Offset Y | 0 to 100 | Moves the crop rectangle, when there is room to move it |
| Width, Height | in pixels, 0 = automatic | Size of the file in pixels, keeping the photo’s ratio; at 0 the megapixels decide |
| Lock ratio | on/off | Writing one side fills in the other, keeping the ratio |
| Megapixels | 0.01 up to the photo’s own resolution | Resolution of the result: lower it to ruin the photo on purpose |
| Upscale | on/off | Brings the result back to the original size with crisp pixels |
The algorithms
They fall into three families, and the choice changes the character of the result more than any other parameter.
Error diffusion, where each pixel’s error is spread over its neighbours. Irregular texture, very faithful tone: floydSteinberg, atkinson, jarvis, stucki, burkes, sierra and their variants.
atkinson is the 1984 Macintosh one. It diffuses only six eighths of the error rather than all of it, and the bit it throws away is exactly where the marked contrast that makes it recognisable at a glance comes from.
Ordered matrix, where each pixel is compared against a threshold that depends on where it sits. Regular texture, old-video-game air: bayer2 through bayer16, cluster4 and cluster8 (the magazine halftone), lines4 (engraving).
No texture: none (hard threshold) and random (pure noise).
The palettes, and the counterintuitive part
Twenty-six palettes, from one-bit black and white to the Game Boy, from CGA to the C64 to the colours of Marathon, by way of the NES, the Mega Drive, the MSX, EGA 16, the Workbench, the Virtual Boy and Teletext, plus any hand-written list of hex colours.
Here sits the technical choice that actually matters. The tonal ramps (black and white, greys, Game Boy, phosphor, blueprint) are mapped on luminance, not on the nearest colour in RGB space. It sounds like a detail and it is the difference between a legible result and an unwatchable one: a saturated red, matched by colour proximity, lands on whatever light green happens to sit next to it in RGB, so a red shirt comes out brighter than the sky. Mapped on luminance it goes to the dark step, which is where the eye expects to find it.
The same goes for marathon, which takes the colours of the 2025 game (hyper-saturated pinks and yellows over cold steel blues and deep blacks) and treats them as a luminance ramp. That is the difference between the flat block of colour the game actually uses and a snowfall of confetti.
The terminal interface
It follows cliamp: boxed panels, six-colour themes, sliders where the equaliser would be. The keys are its keys too, so anyone already using that one is at home.
A terminal has no pixels, it has characters, and a cell is twice as tall as it is wide. Each preview mode uses the character differently: halfblock (one character, two pixels), braille (the most detailed, two by four), quadrant (two by two) and ascii.
The default is halfblock and not braille for a practical reason: ▀ is a block, exactly one cell wide in any font. Braille glyphs are missing from plenty of monospaced fonts, the terminal falls back to another font with a different advance, the columns drift apart and the frame looks broken. If your font handles braille, one key more than doubles the detail.
The preview is dithered directly at the terminal’s resolution, not shrunk afterwards. Dithering large and then reducing means averaging the pixels, and averaging closes the dots back into greys: the texture disappears and a blurred photo is what remains. What you see is real dithering.
From the command line
Without an interface, for doing things in bulk:
ditherbox photo.jpg -p macintosh -o result.png
ditherbox photo.jpg --palette "#0a0c10,#c2fe0b" --megapixels 0.3 -o poster.png
ditherbox ~/Pictures --preset fanzine --out-dir ./results
With only two tints the result is a duotone: the light and the dark of the photo land on the two chosen colours, and the dithering manufactures the midtones in between.
Every engine parameter has its own option, and the permanent settings live in ~/.config/ditherbox/config.toml. Personal themes use the same six-colour schema as cliamp, so a theme written for that one works here without changes.
How it is built
The engine is JavaScript with no dependencies and not a single reference to the DOM, so the same file runs identically in Node and in a browser. That is why the three interfaces do not drift apart: the parameter schema lives in one place, and adding one makes it appear by itself in the terminal, on the command line and in the widget.
The tests cover the engine, the terminal primitives, image reading and writing, the interface, the command line and the translations. The layout ones really do open the widget in Chromium and check it; if Chromium is not there they skip instead of failing the suite.
Installing
All it needs is Node 18 or newer, nothing to compile and no native modules:
npm install -g github:Pricesswg/DitherBox
To try it without installing anything at all:
npx github:Pricesswg/DitherBox ~/Pictures
Licence
MIT: you can use it, modify it and redistribute it, including in commercial projects, with the only obligation being to keep the copyright notice. The engine and the widget have no dependencies; the terminal application has two, jpeg-js (BSD-3-Clause) and pngjs (MIT), both permissive licences that ask nobody to open their own code.
Exact width and height in pixels (v0.3.3), with a lock that fills in the other side keeping the ratio. Megapixels say how big, not how wide: when the file has to fit a precise space, a wallpaper, a cover, a zine page, what you need is the two real numbers rather than an area. Leave the fields at zero and the megapixels still decide, so anyone without a size constraint never notices.
Framing and cropping (v0.3.x). The result can be taken to a fixed ratio, from the square to 9:16, choosing whether to crop what is left over or add bars, and the crop is not an automatic box: it is a rectangle you size with Zoom and place with Offset X and Y, drawn as a guide over the preview while you move it. The preview shows the photo as it will be seen rather than as it sits in the file, and a 1:1 view crops instead of resampling, so the dots can be judged at their real size instead of shrunk.
Eight console palettes and seven era presets: the NES, the Mega Drive, the MSX, EGA, the Amiga Workbench, the Virtual Boy and Teletext join the previous eighteen, and the presets that pose them bring the total to fifteen. These are the choices of real machines rather than invented tints, which is exactly why they earn their place: the NES palette is not an arbitrary set of colours, it is the constraint that console drew everything within.
The megapixel control no longer lies about small photos. The reduction never upscales, which is correct, since asking for more megapixels than the photo has cannot invent detail; but the slider ran to 24 MP regardless, so on the sample photo on this page the top positions all produced the same file. Drag it, watch nothing happen, hit save, get the original size: half the travel was dead and nothing on screen said so. The slider now stops at the first step that covers the photo, so every position changes the output, and the number beside it shows what will actually come out rather than what was asked for. Loading a bigger photo reopens the range, and the terminal app gets the same treatment.
A sample photo ships with the program and loads at startup, so there is something to turn the knobs on straight away instead of an empty frame. It is not an arbitrary photo: the stuccoed wall is a long continuous gradient and the wire fence is a high-frequency pattern, and those are exactly the two things dithering handles differently.
The interface speaks five languages (English, Italian, Spanish, French, German) with a live picker. No human-readable text lives in the engine: labels and messages all come from a translator keyed on canonical English strings, and a key missing from a translation falls back to English instead of vanishing into an empty string.
Site colours as the widget's starting point, and a scheme that can be imposed through data-theme. A widget that follows system preferences flips to light on whoever keeps their system in light mode, which on a black page means a bright box in the middle of the dark: the attribute hands the choice back to whoever writes the page.
The header is gone from the terminal, the status line becomes the progress bar of whatever is running, and the preview got bigger. The bar follows the real phases (reading, processing, writing) instead of inventing a countdown, so while a phase works it stays put where it is, and on a narrow terminal the line drops the least important information instead of truncating the file name.
Fixed the terminal preview with Pixel greater than one, and added a syntax check over every file to the screenshot script. The README images are generated rather than taken by hand, so they cannot advertise a version that no longer exists.
Widget layout, file field, control over the output megapixels, and hand-written palettes as a list of hex colours, accepted anywhere a palette name can be written.
First version: the dithering engine (plain JavaScript, no dependencies and no reference to the DOM), the browser widget and the terminal application, all three reading the parameter schema from the same file.