Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PixelEncode

Encode arbitrary data into PNG images and decode it back out.

Requirements

  • Node.js 18+ or Bun 1+
  • sharp is 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).

Installing

npm install @airplanegobrr/pixelencode

Basic usage

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!

Multi-image & headers

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.

Reading a header without decoding

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.

EncodeOptions

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.

Streaming

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 chunk

Decoding 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, decodeIter must 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, use header: false or header: "first" instead and supply images in the correct order.

Auto-detecting encoding parameters

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: fromImage works 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.

Constructor

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.

Compression

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: 3 for majority-vote recovery

License

ISC

About

Encode/decode data into a picture

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages