PuttyPNG

Press your data into a PNG. Hand it to anyone.

How it works

From "what is this?" to "it is in my project" in a few minutes.

1. What a PuttyPNG is

A PuttyPNG is an ordinary PNG image with your data pressed invisibly into its pixels. Because PNGs are lossless, you can copy, paste, download, and message the image around, and the data rides along untouched. Hand someone the picture; they read the data back. No server, no link, no account.

2. Make your first one

Type a message and press it into a PNG right here, then read it straight back to see the round-trip.

3. A whole page, or one zone

A drop target can be the entire page or a single bounded area. The box on the left is a bounded one: drop a PuttyPNG on it, paste one, or click to browse, and the result appears right here rather than anywhere else. The code on the right is what you write to get a zone of your own, in about twenty lines.

Drop a PNG here
or paste with Ctrl/Cmd+V · or click to browse [...]
Nothing read yet.
The Importer
<!-- PuttyPNG importer: drop, paste, or browse a PuttyPNG -->
<div id="drop" tabindex="0" style="border:2px dashed #ccc;padding:24px;text-align:center;cursor:pointer">
  Drop a PuttyPNG here, paste it, or click to browse
  <input id="file" type="file" accept="image/png" hidden>
</div>
<script src="puttypng.js"></script>
<script>
  // Your app decides what to do with the decoded data.
  async function onData(result) {
    if (result.type === "binary") PuttyPNG.download(result);   // save the file
    else console.log("Got:", result.text);                     // text or JSON
  }

  const drop = document.getElementById("drop");
  const file = document.getElementById("file");

  async function read(source) {
    try { onData(await PuttyPNG.decode(source)); }             // prompts if encrypted
    catch (err) { alert(err.message); }                        // err.code is the E## code
  }

  drop.addEventListener("click", () => file.click());
  file.addEventListener("change", () => file.files[0] && read(file.files[0]));
  drop.addEventListener("dragover", e => e.preventDefault());
  drop.addEventListener("drop", e => {
    e.preventDefault();
    if (e.dataTransfer.files[0]) read(e.dataTransfer.files[0]);
  });
  drop.addEventListener("paste", e => {
    for (const item of e.clipboardData.items)
      if (item.type.startsWith("image/")) read(item.getAsFile());
  });
</script>
A drop / paste / browse zone. Everything decoded is handed to your onData callback - your form, game, or app decides what to do with it.
Under the hood No server and no magic. Clever use of an ordinary lossless PNG.

A PNG stores its pixels losslessly - every color value survives a copy, a paste, even a trip through many chat apps. PuttyPNG hides your data in the lowest, least-visible bits of the red, green, and blue channels of the image's fully opaque pixels. Nudging those bits shifts each color by an amount your eye cannot see, but the bytes are perfectly recoverable.

The protocol

A tiny header (PPNG + version + a CRC32 checksum) marks the image and guarantees the data came out exactly as it went in. Descriptive details ride in a small JSON block; the version byte lets the format grow without ever misreading an old PuttyPNG.

Compression & encryption

Data is gzip-compressed automatically whenever that helps. Add a password and it is sealed with AES-256-GCM (a key stretched from your password with PBKDF2) - the filename and type are hidden too, so an encrypted PuttyPNG reveals nothing until it is opened.

4. Drop it into your own project Copy-paste steps. No build tools, no dependencies, works offline.
  1. The Engine: save puttypng.js and include it.
  2. The Exporter: a box and a button that make a PuttyPNG.

The Importer is the third, and it is further up this page, beside the live zone it builds.

1 The Engine
Loading engine source...
Save as puttypng.js and include it, or paste the whole thing into a <script> tag. This is the entire protocol - the only file you truly need. Download puttypng.js
2 The Exporter
<!-- PuttyPNG exporter: type something, get a PuttyPNG -->
<textarea id="text">Hello!</textarea>
<button id="make">Make PuttyPNG</button>
<a id="save" download="my.png">Save</a>
<img id="out" alt="your PuttyPNG">
<script src="puttypng.js"></script>
<script>
  document.getElementById("make").addEventListener("click", async () => {
    const text = document.getElementById("text").value;
    const png = await PuttyPNG.encode(text);   // add { password, depth, cover }, etc.
    document.getElementById("out").src = png.dataUrl;
    document.getElementById("save").href = png.dataUrl;
  });
</script>
A box and a button that turns whatever the user types into a downloadable PuttyPNG. See the Docs page for encryption, custom covers, and calling the engine directly.

Going further

When you are ready for more: call the engine directly, add a styled password prompt, use your own cover art, or route your app's own PuttyPNGs with the developer tag. It is all on the Docs page.