Quick start

Share a gallery in 3 steps: pack images into a .bnl file, upload it, and share the link. Recipients see photos streaming instantly.

import { Bunle } from "@nghyane/bunle" // Open a .bnl file from URL const bnl = await Bunle.open(url) // Stream all pages progressively for await (const { index, blob } of bnl.stream()) { images[index].src = URL.createObjectURL(blob) }

Installation

JavaScript / TypeScript

$ npm i @nghyane/bunle

3 KB, zero dependencies. Works in browsers and Node.js.

CLI (macOS / Linux)

$ curl -fsSL https://raw.githubusercontent.com/nghyane/mcz/main/install.sh | sh

CLI (Windows)

$ irm https://raw.githubusercontent.com/nghyane/mcz/main/install.ps1 | iex

Rust crate

$ cargo install bunle

Bunle.open(url)

Open a .bnl file from a URL. Uses a single HTTP Range request to fetch the index, then streams pages on demand.

const bnl = await Bunle.open("https://cdn.example.com/gallery.bnl") bnl.pageCount // number of pages bnl.pages // PageInfo[] with index, width, height, format

Returns a Bunle instance. Call .close() when done to revoke cached ObjectURLs.

bnl.stream(options?)

Stream all pages progressively as an async generator. Yields { index, blob } as data arrives.

for await (const { index, blob } of bnl.stream({ onProgress(received, total) { bar.style.width = Math.round(received / total * 100) + "%" } })) { // Each page arrives as a Blob img[index].src = URL.createObjectURL(blob) }

bnl.blob(index)

Fetch a single page by index. Uses an HTTP Range request — no need to download the full file.

const blob = await bnl.blob(0) document.querySelector("img").src = URL.createObjectURL(blob)

Bunle.pack(inputs)

Pack images into a .bnl ArrayBuffer in the browser. Each input needs data, width, height, and format.

const packed = await Bunle.pack([ { data: file1, width: 1920, height: 1080, format: "webp" }, { data: file2, width: 800, height: 600, format: "jpeg" }, ]) // Upload the ArrayBuffer await fetch("/upload", { method: "POST", body: packed })

bunle pack

Pack a directory of images into a single .bnl file.

$ bunle pack <dir> -o <output> [-q quality] Options: -o, --output Output file path -q, --quality WebP quality 1-100 (default: 80) --no-cover Skip WebP cover (disables polyglot detection)

bunle info

Display metadata for a .bnl file: page count, dimensions, formats, offsets.

$ bunle info gallery.bnl BNL v1 — 12 pages, 4821504 bytes 0: 1920×1080 webp offset=200 size=402312 1: 1920×1080 webp offset=402512 size=389104 ...

bunle extract

Extract a single page from a .bnl file.

$ bunle extract gallery.bnl 0 -o cover.webp

Format spec

The BNL format is a simple binary container optimized for HTTP Range requests and progressive streaming.

  • Magic: MCZ\x01 (4 bytes LE)
  • Header: 8 bytes — magic + version + flags + page count
  • Index: 16 bytes per page — offset, size, width, height, format
  • Data: concatenated image blobs (WebP, JPEG, JXL)

Images pass through untouched — no re-encoding. The index is at the start of the file, enabling instant layout before any image data downloads.

Full spec: SPEC.md on GitHub

REST API

Available on Business plan. Upload and manage galleries programmatically.

Upload a gallery

POST https://api.bunle.cloud/v1/galleries Content-Type: application/octet-stream Authorization: Bearer <api_key> Body: raw .bnl file bytes Response: { "id": "g_a3f8k2", "url": "https://bunle.cloud/s/a3f8k2", "pages": 42, "size": 4821504, "expires": null }

List galleries

GET https://api.bunle.cloud/v1/galleries Authorization: Bearer <api_key>

Delete a gallery

DELETE https://api.bunle.cloud/v1/galleries/:id Authorization: Bearer <api_key>

Questions?

Open an issue on GitHub or email hi@bunle.cloud