Stegassette encodes payloads — text, audio, or
arbitrary bytes — into image pixels using the STGC format. Every
payload pixel needs a paired key pixel to decode it. The image
describes its own encoding, too: combine op, keymap, traversal, and
border width are all written into the border, so
decode needs nothing but the image. Each option is
documented in the library's types.
STGC needs pixels back exactly as written, so it does not survive a JPEG re-save. Stegaprint is a prototype format for JPEG, with about 160× less capacity.
Stegassette
Encoding a basic text entry.
Stegassette.encode({ source: HTMLImageElement | HTMLCanvasElement; entries: Entry[]; combine?: CombineName; keymap?: KeymapName; traversal?: TraversalName; params?: TraversalParams; fit?: FitMode; border?: number; aspectRatio?: number; }): HTMLCanvasElement
Stegassette.decode({ source: HTMLImageElement | HTMLCanvasElement | StegaImageData }): { entries: DecodedEntry[]; opts: StgcOpts }
const { entries, opts } = Stegassette.decode({ source: encoded });
const message = new TextDecoder().decode(entries[0].data);
The STGC header lives in the border ring, so a small image is widened until the header fits, not grown all over to make room.
Audio entries
buildAudioEntry turns decoded PCM channels into an entry.
The mimetype carries sample rate, bit depth and channel layout, so a
decoded image can rebuild the buffer on its own.
Stegassette.buildAudioEntry({ channels: Float32Array[]; sampleRate: number; bitsPerSample?: 8 | 16 | 24; layout?: "planar" | "interleaved" | "block"; blockSize?: number; name?: string; }): Entry
Stegassette.parseAudioEntry(entry: DecodedEntry | Entry): { channels: Float32Array[]; sampleRate: number; bitsPerSample: 8 | 16 | 24; layout: "planar" | "interleaved" | "block"; blockSize: number; }
const { entries, opts } = Stegassette.decode({ source: encoded });
// opts: { combine, keymap, traversal } recovered from STGC header
const { channels, sampleRate } = Stegassette.parseAudioEntry(entries[0]);
const recon = Stegassette.reconstructCover(encoded, opts);
// recon develops on the canvas in sync with audio playback
A keyed keymap reserves the pixels that let
reconstructCover bring the picture back; with
keymap: "none" the reveal depends entirely on which
channels the plan leaves untouched. The line under the encode
reports what each choice actually cost.
Audio in pixels
This encode is a demonstration, but the pixels
are the actual payload: a black cover under
xor leaves every key channel at zero, and
a ^ 0 === a, so each channel holds one raw PCM byte
verbatim.
Stegaprint — prototype
Stegaprint encodes payloads that survive JPEG compression. Payload bytes are written into the low-frequency DCT coefficients of each 8×8 block. The header is a visible border of black and white blocks, since JPEG has no alpha channel to carry it.
The image below is encoded, passed through
canvas.toBlob("image/jpeg") at the selected quality,
decoded, and read back. The border carries the 40-byte header. The
interior carries the payload. Capacity is about 19 KB per megapixel,
roughly 160× less than STGC.
Stegaprint.encode({ source: HTMLImageElement | HTMLCanvasElement; entries: PrintEntry[]; modulate?: "qim" | "pair"; keymap?: KeymapName; traversal?: TraversalName; ecc?: "none" | "light" | "full"; repeat?: number | "auto"; width?: number; height?: number; }): HTMLCanvasElement
Stegaprint.jpegRoundTrip(canvas: HTMLCanvasElement, quality?: number, passes?: number): Promise<{ canvas: HTMLCanvasElement; bytes: number[] }>
const { canvas } = await Stegaprint.jpegRoundTrip(
encoded,
/ 100,
,
);
const { entries, header, registered } = Stegaprint.decode({ source: canvas });
header
Audio through a JPEG
The same example.mp3 as the Stegassette demo above,
resampled and encoded as an audio entry, then passed through
canvas.toBlob("image/jpeg") and decoded back. What plays
is what came out of the JPEG.
Capacity sets the length. One second of 8-bit 8 kHz mono costs 8 KB and the canvas grows with the clip, so 30 seconds is a question of what fidelity you spend it at: 4 kHz 4-bit fits in 3.4 megapixels, 8 kHz 8-bit needs 13.7. Past 12 megapixels this page declines to encode and says so rather than freezing — the arithmetic, not a limit of the format.
Stegaprint.buildAudioEntry({ channels: Float32Array[]; sampleRate: number; bitsPerSample?: 4 | 8 | 16 | 24; name?: string; }): PrintEntry
StegaAnimator
Animate a steganographic image. The source below is the encoded canvas from the first example.
new StegaAnimator({ resolution: number; source: HTMLImageElement | HTMLCanvasElement; fadeAmount?: number; rotationMode?: "2d" | "3d"; shape?: "circle" | "square" | "implicit"; })
Helpers
Loading, playback, and file I/O around the codec. None of them are required to encode or decode — they are the plumbing every example on this page ends up needing.
loadImageFromImageUrl
Load an image from a url string
async loadImageFromImageUrl({ url: string }): Promise<HTMLImageElement>
const source = await loadImageFromImageUrl({
url: "./example.jpg"
});
loadAudioBuffersFromAudioUrl
Load, downmix and resample an audio file into planar Float32 channels
async loadAudioBuffersFromAudioUrl({ url: string; audioContext: AudioContext; channels: 1 | 2; sampleRate?: number; }): Promise<Float32Array[]>
const audioContext = new AudioContext();
const audioBuffers = await loadAudioBuffersFromAudioUrl({
url: "./example.mp3",
audioContext,
channels: 2,
sampleRate: audioContext.sampleRate,
});
playDecodedAudioBuffers
Play decoded audio buffers
async playDecodedAudioBuffers({ audioBuffers: Float32Array[]; audioContext: AudioContext; sampleRate?: number; }): Promise<AudioBufferSourceNode>
const source = await playDecodedAudioBuffers({
audioBuffers,
audioContext,
sampleRate: audioContext.sampleRate,
});
source.stop();
createDropReader
Turn an HTML element into a file drop area
createDropReader({ element: HTMLElement; onSuccess: (element: HTMLImageElement | HTMLAudioElement) => void; onFailure?: (message: string) => void; onDragEnter?: () => void; onDragLeave?: () => void; onDrop?: () => void; types?: (AudioType | ImageType)[]; }): void
const element = document.body;
createDropReader({
element,
onSuccess: (image) => element.appendChild(image),
onFailure: (message) => console.error(message),
onDragEnter: () => element.classList.add("droppable"),
onDragLeave: () => element.classList.remove("droppable"),
onDrop: () => element.classList.remove("droppable"),
types: ["image/*"]
});
createFileReader
Turn an HTML input into a file input
createFileReader({ element: HTMLInputElement; onSuccess: (element: HTMLImageElement | HTMLAudioElement) => void; onBinarySuccess?: (result: { data: Uint8Array; mimeType: string; fileName: string }) => void; onFailure?: (message: string) => void; types?: (AudioType | ImageType | VideoType | "*/*")[]; }): void
const element = document.createElement("input");
createFileReader({
element,
onSuccess: (image) => document.body.appendChild(image),
onFailure: (message) => console.error(message),
types: ["image/*"]
});
// Or for binary data (any file type)
createFileReader({
element,
onBinarySuccess: ({ data, mimeType, fileName }) => {
console.log(`Loaded ${fileName} (${mimeType}): ${data.length} bytes`);
},
types: ["*/*"]
});
readFileAsBytes
Read a file as binary bytes
readFileAsBytes({ file: File }): Promise<{ data: Uint8Array; mimeType: string; fileName: string }>
const input = document.createElement("input");
input.type = "file";
input.onchange = async () => {
const file = input.files[0];
const { data, mimeType, fileName } = await readFileAsBytes({ file });
console.log(`Read ${fileName} (${mimeType}): ${data.length} bytes`);
};
bytesToBlobUrl
Convert binary data to a Blob URL
bytesToBlobUrl({ data: Uint8Array; mimeType: string }): string
const url = bytesToBlobUrl({ data: myBytes, mimeType: "image/png" });
const img = document.createElement("img");
img.src = url;
document.body.appendChild(img);
// Remember to revoke when done
URL.revokeObjectURL(url);
downloadBytes
Download binary data as a file
downloadBytes({ data: Uint8Array; mimeType: string; fileName: string }): void
downloadBytes({
data: myBytes,
mimeType: "application/pdf",
fileName: "document.pdf"
});