Encode arbitrary data into PNG images and decode it back out.
- Node.js 18+ or Bun 1+
sharpis a native dependency — it ships prebuilt binaries for most platforms. If you're on an unusual platform or architecture you may need to build it from source (see the sharp docs).
npm install @airplanegobrr/pixelencode
CommonJS (wrap await in an async function):
const { PixelEncode } = require("@airplanegobrr/pixelencode")
async function main() {
const ed = new PixelEncode()
const data = Buffer.from("Hello, World!")
const images = await ed.encode(data)
// images is an array of PNG Buffers
const { data: decoded } = await ed.decode(images)
console.log(decoded.toString()) // Hello, World!
}
main()ESM / top-level await:
import { PixelEncode } from "@airplanegobrr/pixelencode"
const ed = new PixelEncode()
const data = Buffer.from("Hello, World!")
const images = await ed.encode(data)
const { data: decoded } = await ed.decode(images)
console.log(decoded.toString()) // Hello, World!Large data is automatically split across multiple images. Use header to embed metadata so images can be reassembled even if they arrive out of order:
const images = await ed.encode(data, { header: "all", filename: "archive.zip" })
const { data: decoded, header } = await ed.decode(images, { header: "all" })
// header → { index, total, filename }decode always returns { data, header? }. The header field is populated whenever a header mode other than false is used.
const header = await ed.readHeader(images[0])
// → { index: 1, total: 3, filename: "archive.zip" }header value |
Behavior |
|---|---|
false (default) |
No metadata. Caller must keep images in order. |
"all" |
Every image carries its 1-based index and total. decode sorts them automatically. |
"first" |
Only the first image has a header; the rest are pure data. |
"meta-image" |
A dedicated metadata PNG is prepended as images[0]; data images have no per-image overhead. |
| Option | Default | Description |
|---|---|---|
header |
false |
Header mode (see table above). |
filename |
— | Filename or extension hint to embed (e.g. "photo.png" or "png"). |
embedTotal |
true |
Whether to embed the total image count in the header. |
For large files, use encodeIter / decodeIter instead of encode / decode. Each method processes one image at a time so you never hold the full encoded or decoded result in memory at once.
Encoding a large file to disk:
import fs from "fs/promises"
import { PixelEncode } from "@airplanegobrr/pixelencode"
const ed = new PixelEncode()
const data = await fs.readFile("video.mp4")
let i = 0
for await (const img of ed.encodeIter(data, { header: "all", filename: "video.mp4" })) {
await fs.writeFile(`frame_${i++}.png`, img)
}
// Produces frame_0.png, frame_1.png, ... — each a self-contained chunkDecoding those frames back to a file:
import fs from "fs/promises"
import { createWriteStream } from "fs"
import { PixelEncode } from "@airplanegobrr/pixelencode"
const ed = new PixelEncode()
// Load frames in any order — "all" mode sorts by embedded index automatically
const files = await fs.readdir(".")
const frames = await Promise.all(
files.filter(f => f.startsWith("frame_")).sort().map(f => fs.readFile(f))
)
const out = createWriteStream("recovered.mp4")
for await (const chunk of ed.decodeIter(frames, { header: "all" })) {
out.write(chunk)
}
out.end()Streaming trade-off with
header: "all": Because images may arrive out of order,decodeItermust buffer every image internally before it can sort by index and begin yielding chunks. If you need true one-at-a-time memory usage during decoding, useheader: falseorheader: "first"instead and supply images in the correct order.
If you receive an encoded image without knowing how it was produced, use the static PixelEncode.fromImage factory to inspect it and get back a ready-to-use instance:
const encoder = await PixelEncode.fromImage(png)
const { data } = await encoder.decode([png])fromImage auto-detects three parameters by analysing the pixel structure of the image:
| Detected | How |
|---|---|
blockSize |
Within each block every pixel shares an identical hue, so consecutive same-hue pixel runs in sampled rows/columns have lengths that are integer multiples of blockSize; their GCD is the block size. |
mode |
Each encoding alphabet has a fixed symbol count (binary=2, hex=16, base32=32, base64=64); counting distinct hue values in the data area maps directly to the mode. |
redundancy |
With redundancy=N, every character is written as N consecutive identical-hue blocks; the GCD of those block-run lengths gives the redundancy factor. |
The returned instance uses the image's actual pixel dimensions. maxHSL and saturation default to 330 / 85 (the library's own defaults); decoding is robust to small deviations because hue values are snapped to the nearest table entry.
Lossy images:
fromImageworks best with the lossless PNG files the library produces. Images that have been lossy-compressed (JPEG, WebP) cause pixel drift that can confuse the run-length block-size detection.
new PixelEncode(mode, blockSize, maxHSL, width, height, saturation, redundancy)| Parameter | Default | Description |
|---|---|---|
mode |
"base64" |
Encoding alphabet: "base64", "base32", "hex", or "binary". Lower-alphabet modes use wider hue spacing, improving compression resistance at the cost of capacity. |
blockSize |
3 |
Side length of the square pixel block per character. Larger blocks survive lossy compression better. |
maxHSL |
330 |
Upper bound of the hue range (0–360). Avoids wrapping back near red (hue 0), which is the background sentinel. |
width |
3840 |
Output image width in pixels. |
height |
2160 |
Output image height in pixels. |
saturation |
85 |
HSL saturation (0–100). Higher values preserve hue through JPEG/WebP compression. |
redundancy |
1 |
Consecutive blocks written per character. Set to 3 for majority-vote error recovery; capacity is divided by this value. |
Platforms like Discord and social media re-encode uploaded images, which can corrupt pixel data. To improve resilience:
- Use a larger
blockSize(e.g.10–20) - Use
"hex"or"binary"mode (fewer distinct hues, wider spacing) - Increase
saturation(vivid colors survive JPEG chroma subsampling better) - Use
redundancy: 3for majority-vote recovery
ISC