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.
JavaScript CLI Rust
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
...
Extract a single page from a .bnl file.
$ bunle extract gallery.bnl 0 -o cover.webp
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