npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pranjalpanging/png-parser

v0.3.0

Published

Hide any file inside PNG/BMP/TIFF/WebP — compress, encrypt, embed. Rust-powered steganography engine.

Readme

png-parser

License: MIT Rust WASM Python npm PyPI

Hide any file inside an image — compress, encrypt, embed.

A high-performance steganography engine written in Rust, available for Python and JavaScript (WebAssembly). Hide any file — PDF, video, ZIP, binary — inside a PNG, BMP, TIFF, or WebP image. The carrier image looks identical to the original. No pixels are changed in chunk mode.


Table of Contents


How it works

hide:
  secret file
    → flate2 compress      (rust_backend, pure Rust, WASM-safe)
    → AES-256-GCM encrypt
    → embed into image
    → save image

reveal:
  stego image
    → extract payload
    → AES-256-GCM decrypt
    → flate2 decompress
    → restore original file

The original filename, file size, expiry, and embedding mode are stored inside the ciphertext — not in plaintext. An observer with the stego image but without the password sees none of this metadata.


Packages

| Platform | Install | Status | |---|---|---| | Python | pip install png-parser | ✅ Published | | JS / WASM | npm i @pranjalpanging/png-parser | ✅ Published |


What's new in v0.3.0

| Feature | v0.2 | v0.3 | |---|---|---| | Payload type | Text strings only | Any file (PDF, ZIP, video, …) | | Key derivation | PBKDF2-SHA256 (100k iter) | Argon2id (memory-hard, GPU-resistant) | | Salt size | 16 bytes | 32 bytes | | Compression | None | flate2 rust_backend (pure Rust, WASM-safe) | | Image formats | PNG only | PNG, BMP, TIFF, WebP | | Embed modes | Chunk only | Chunk + Pixel (adaptive LSB) | | Expiry granularity | Hours only | Days + hours + minutes + seconds | | Delete protection | Unprotected | Password-required for encrypted payloads | | Secure erase | No | Payload zeroed before removal | | New functions | — | info, verify, reencrypt, capacity, fingerprint, split, merge | | Filename preservation | No | Original filename stored in header | | Sharding | No | Split file across multiple images |


Python

Install

pip install png-parser>=0.3.0

Hide a file

import png_parser

# No password — plain compression only
png_parser.hide("photo.png", "out.png", "document.pdf")

# With encryption
png_parser.hide("photo.png", "out.png", "document.pdf",
    password="my-password")

# With encryption + flexible expiry (all units are additive)
png_parser.hide("photo.png", "out.png", "document.pdf",
    password="my-password",
    expires_days=1,
    expires_hours=6,
    expires_minutes=30,
    expires_seconds=0)

# Pixel mode — embeds in image pixels instead of a chunk
png_parser.hide("photo.png", "out.png", "document.pdf",
    password="my-password",
    mode="pixel")

Reveal a file

# output_path can be a directory — original filename is restored automatically
result = png_parser.reveal("out.png", "./extracted/",
    password="my-password")
print(result)
# ./extracted/document.pdf

Inspect without extracting

import json

raw  = png_parser.info("out.png", password="my-password")
meta = json.loads(raw)

print(meta["filename"])    # document.pdf
print(meta["file_size"])   # 204800
print(meta["encrypted"])   # True
print(meta["expires_at"])  # unix:1742000000 or permanent
print(meta["fingerprint"]) # a3f9c1d2...
print(meta["mode"])        # chunk or pixel

# Without password — only fingerprint and encrypted flag visible
raw = png_parser.info("out.png")
# {"encrypted": true, "fingerprint": "a3f9c1d2...", ...}

Verify a password

# Check without extracting anything
ok = png_parser.verify("out.png", "my-password")
print(ok)  # True or False

Delete a payload

# Password required if payload is encrypted
# Data is zeroed in place before removal — not just stripped
png_parser.delete("out.png", "clean.png", password="my-password")

Change password

# Re-encrypts without writing plaintext to disk at any point
png_parser.reencrypt("out.png", "new.png",
    old_password="old-password",
    new_password="new-password")

Check capacity

# How many bytes can this image hold?
bytes_available = png_parser.capacity("photo.png", mode="chunk")
print(f"{bytes_available:,} bytes available")

bytes_available = png_parser.capacity("photo.png", mode="pixel")
print(f"{bytes_available:,} bytes available in pixel mode")

Fingerprint

# SHA-256 of the raw envelope — no password needed
# Useful to verify two images carry the same payload
fp = png_parser.fingerprint("out.png")
print(fp)  # a3f9c1d2ee4b...

Split a large file across multiple images

# Each carrier image gets one shard
shards = png_parser.split(
    "bigfile.zip",
    ["photo1.png", "photo2.png", "photo3.png"],
    "./shards/",
    password="my-password",
    expires_days=7,
)
print(shards)
# ['./shards/shard_0_photo1.png',
#  './shards/shard_1_photo2.png',
#  './shards/shard_2_photo3.png']

Merge shards

# Shards can be passed in any order — sorted internally by index
# SHA-256 of reassembled file is verified against stored hash
output = png_parser.merge(
    ["./shards/shard_2_photo3.png",
     "./shards/shard_0_photo1.png",
     "./shards/shard_1_photo2.png"],
    "./output/",
    password="my-password",
)
print(output)  # ./output/bigfile.zip

JavaScript / WASM

Install

npm i @pranjalpanging/png-parser

Initialise

import init, {
    hide_js,
    reveal_js,
    info_js,
    verify_js,
    delete_js,
    reencrypt_js,
    fingerprint_js,
    capacity_js,
} from '@pranjalpanging/png-parser';

await init();

Hide a file

// imageBytes and fileBytes are Uint8Array
const stego = hide_js(
    imageBytes,    // carrier image bytes
    fileBytes,     // file to hide
    "secret.pdf",  // original filename (stored in header)
    "password",    // encryption password — pass null for no encryption
    "chunk",       // mode: "chunk" or "pixel"
    null,          // expires_days
    null,          // expires_hours
    null,          // expires_minutes
    null           // expires_seconds
);
// stego is Uint8Array — the modified image bytes

Reveal a file

// Returns Uint8Array of the hidden file bytes
const fileBytes = reveal_js(stegoBytes, "password");

// Download in browser
const blob = new Blob([fileBytes]);
const url  = URL.createObjectURL(blob);
const a    = document.createElement("a");
a.href     = url;
a.download = "secret.pdf";
a.click();
URL.revokeObjectURL(url);

Inspect without extracting

// Returns JSON string
const meta = JSON.parse(info_js(stegoBytes, "password"));
console.log(meta.filename);    // "secret.pdf"
console.log(meta.file_size);   // 204800
console.log(meta.encrypted);   // true
console.log(meta.expires_at);  // "unix:1742000000" or "permanent"
console.log(meta.fingerprint); // "a3f9c1d2..."

Other functions

// Verify password
const ok = verify_js(stegoBytes, "password"); // true / false

// Delete payload (password required if encrypted)
const cleanBytes = delete_js(stegoBytes, "password");

// Change password
const newBytes = reencrypt_js(stegoBytes, "old-pass", "new-pass");

// Fingerprint (no password needed)
const fp = fingerprint_js(stegoBytes);

// Capacity
const bytes = capacity_js(imageBytes, "chunk");

Full browser example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>png-parser demo</title>
</head>
<body>
  <h2>Hide a file inside an image</h2>

  <label>Carrier image (PNG/BMP/TIFF/WebP)</label><br>
  <input type="file" id="carrier" accept="image/*"><br><br>

  <label>Secret file (any format)</label><br>
  <input type="file" id="secret"><br><br>

  <label>Password (optional)</label><br>
  <input type="password" id="password" placeholder="Leave blank for no encryption"><br><br>

  <label>Expiry (optional)</label><br>
  <input type="number" id="days"    placeholder="Days"   style="width:80px">
  <input type="number" id="hours"   placeholder="Hours"  style="width:80px">
  <input type="number" id="minutes" placeholder="Minutes" style="width:80px"><br><br>

  <button onclick="hideFile()">Hide and Download</button>
  <button onclick="revealFile()">Reveal from image</button>

  <p id="status"></p>

  <script type="module">
    import init, { hide_js, reveal_js } from './pkg/png_parser.js';
    await init();

    function status(msg) {
      document.getElementById("status").textContent = msg;
    }

    function download(bytes, name, type = "image/png") {
      const blob = new Blob([bytes], { type });
      const a    = document.createElement("a");
      a.href     = URL.createObjectURL(blob);
      a.download = name;
      a.click();
      URL.revokeObjectURL(a.href);
    }

    window.hideFile = async () => {
      const carrier  = document.getElementById("carrier").files[0];
      const secret   = document.getElementById("secret").files[0];
      const password = document.getElementById("password").value || null;
      const days     = parseInt(document.getElementById("days").value)    || null;
      const hours    = parseInt(document.getElementById("hours").value)   || null;
      const minutes  = parseInt(document.getElementById("minutes").value) || null;

      if (!carrier || !secret) {
        return status("Please select both files.");
      }

      const carrierBytes = new Uint8Array(await carrier.arrayBuffer());
      const secretBytes  = new Uint8Array(await secret.arrayBuffer());

      try {
        const result = hide_js(
            carrierBytes, secretBytes, secret.name,
            password, "chunk",
            days, hours, minutes, null
        );
        download(result, "stego_" + carrier.name);
        status("Done — file hidden successfully.");
      } catch (e) {
        status("Error: " + e);
      }
    };

    window.revealFile = async () => {
      const carrier  = document.getElementById("carrier").files[0];
      const password = document.getElementById("password").value || null;

      if (!carrier) return status("Please select a stego image.");

      const carrierBytes = new Uint8Array(await carrier.arrayBuffer());

      try {
        const fileBytes = reveal_js(carrierBytes, password);
        download(fileBytes, "revealed_file", "application/octet-stream");
        status("Done — file revealed successfully.");
      } catch (e) {
        status("Error: " + e);
      }
    };
  </script>
</body>
</html>

Supported formats

| Format | Chunk mode | Pixel mode | Method | |---|---|---|---| | PNG | ✅ | ✅ | Custom stEg ancillary chunk / adaptive LSB | | BMP | ✅ | ✅ | Append after EOF / adaptive LSB | | TIFF | ✅ | ✅ | Append after EOF / adaptive LSB | | WebP | ✅ | ❌ | Unknown RIFF chunk | | JPEG | ❌ | ❌ | Rejected — lossy format destroys data |


Security

| Component | Detail | |---|---| | Cipher | AES-256-GCM — authenticated encryption | | KDF | Argon2id — memory-hard, resistant to GPU/ASIC attacks | | Salt | 32 bytes, randomly generated per operation | | Nonce | 12 bytes, randomly generated per operation | | Compression | flate2 rust_backend — pure Rust, zero C dependencies | | Metadata | Filename, expiry, mode stored inside ciphertext — tamper-proof | | Integrity | GCM authentication tag detects any modification | | Secure delete | Payload bytes zeroed in place before chunk removal | | Password verify | Decrypt attempted — plaintext never returned on failure |

Two encryptions of the same file with the same password produce different ciphertext because salt and nonce are randomised every time.


Embedding modes

Chunk mode (default)

Inserts a custom ancillary PNG chunk named stEg just before IEND. Per the PNG specification, unknown ancillary chunks are silently skipped by all standard image viewers. The image pixels are never modified — the file is visually and statistically identical to the original.

For BMP and TIFF, data is appended after the image EOF with a magic marker. For WebP, a proper RIFF chunk is inserted.

Pixel mode

Embeds data into the least-significant bit of the R, G, B channels of high-texture pixels only. Flat, uniform regions are skipped entirely. This defeats simple statistical detectors such as chi-square analysis and RS analysis which look for LSB changes in smooth areas.

Use capacity() to check how many bytes are available before embedding.


Expiry system

Expiry is stored as a Unix timestamp inside the ciphertext. It is tamper-proof — an attacker cannot extend or remove the expiry without the password.

All time units are optional and additive:

# Expires in exactly 1 day, 6 hours, 30 minutes from now
png_parser.hide("photo.png", "out.png", "file.pdf",
    password="x",
    expires_days=1,
    expires_hours=6,
    expires_minutes=30)

When reveal() is called after expiry, a PermissionError is raised. The file bytes are never written to disk.


Sharding

Split a file too large for a single carrier across multiple images:

file.zip (300 MB)
  → shard 0 → photo1.png  (100 MB payload)
  → shard 1 → photo2.png  (100 MB payload)
  → shard 2 → photo3.png  (100 MB payload)

Each shard header stores the shard index, total count, and SHA-256 of the full original file. merge() sorts shards by index, assembles them, and verifies the SHA-256 before writing output. Any missing or mismatched shard causes an error.


CLI

# Hide a file
png-parser-cli hide -i photo.png -f secret.pdf -o out.png \
    -p password --days 1 --hours 6 --minutes 30 --mode chunk

# Reveal
png-parser-cli reveal -i out.png -o ./extracted/ -p password

# Inspect metadata
png-parser-cli info -i out.png -p password

# Verify password
png-parser-cli verify -i out.png -p password

# Delete payload (password required if encrypted)
png-parser-cli delete -i out.png -o clean.png -p password

# Change password
png-parser-cli reencrypt -i out.png -o new.png \
    --old-password old --new-password new

# Check capacity
png-parser-cli capacity -i photo.png --mode chunk
png-parser-cli capacity -i photo.png --mode pixel

# Fingerprint
png-parser-cli fingerprint -i out.png

# Inspect PNG chunk structure
png-parser-cli inspect -i photo.png

# Strip metadata chunks (tEXt, zTXt, iTXt, eXIf)
png-parser-cli strip -i photo.png -o clean.png

# Split a file across multiple carriers
png-parser-cli split -f bigfile.zip \
    -c photo1.png photo2.png photo3.png \
    -o ./shards/ -p password --days 7

# Merge shards (any order)
png-parser-cli merge \
    -i shards/shard_0_photo1.png \
       shards/shard_1_photo2.png \
       shards/shard_2_photo3.png \
    -o ./output/ -p password

Build from source

Requirements:

  • Rust stable (rustup install stable)
  • Python 3.8+ with pip install maturin (for Python build)
  • npm install -g wasm-pack (for WASM build)
# Clone
git clone https://github.com/PranjalPanging/png-parser
cd png-parser

# Python — installs into current virtualenv
maturin develop --release

# WASM — outputs to pkg/
wasm-pack build --target web --release \
    --no-default-features --features js

# CLI binary
cargo build --release --no-default-features

Architecture

src/
├── lib.rs          Python + WASM API surface
├── main.rs         CLI entry point
├── error.rs        All error variants + Python exception mapping
├── chunk_type.rs   4-byte PNG chunk name + spec rules
├── chunk.rs        PNG chunk: parse, CRC verify, secure erase
├── png.rs          Full PNG file: parse, build, insert, remove
├── header.rs       PayloadHeader — inside ciphertext, tamper-proof
├── crypto.rs       Argon2id KDF + AES-256-GCM + zlib
├── format.rs       Format detection + routing (PNG/BMP/TIFF/WebP)
├── pixel.rs        Adaptive LSB engine
└── commands/
    ├── mod.rs
    └── mode.rs     All operations: hide, reveal, info, verify,
                    delete, reencrypt, capacity, fingerprint,
                    split, merge, inspect, strip

Versioning

This project follows Semantic Versioning.

| Package | Current | Registry | |---|---|---| | Python | 0.3.0 | PyPI | | JavaScript | 0.3.0 | npm |

Python and JavaScript versions are kept in sync from v0.3.0 onwards.

| Version | Notes | |---|---| | 0.3.0 | Full rewrite — any file type, Argon2id, multi-format, WASM, sharding | | 0.2.0 | AES-256-GCM encryption, expiry system | | 0.1.0 | Initial release |

Versions below 1.0.0 may have breaking changes between minor releases.


License

MIT — see LICENSE


Built with ❤️ by Pranjal Panging

GitHub