Tiny AVIF image placeholders. Add an avash="..." attribute to your images and
the avash library will render placeholder image while the bigger image
downloads. Like BlurHash, but with AVIF.
An avash is a short string holding one AV1 still picture with the container and header data stripped off (the container, OBU framing, sequence header and uncompressed frame header). This means with a small JS library, it's possible to read the avash string and re-convert it back into an AVIF image for decoding in the browsers native decoder.
AVIF images tend to have more recognisable features than a BlurHash at the equivalent size, and the quality is fine-tunable depending on exactly how long you want the hash to be, making it a scalable replacement for BlurHash or equivalent hash methods.
The bytes are a bitstream, big-endian:
2 version = 1
2 mode
mode 0, canonical: both headers regenerated, only tile data stored
6 width / 2 - 1 (even, 2..=128)
6 height / 2 - 1
6 quantiser index / 4 (63 means 255)
1 tx_mode_select
1 loop filter present
6 loop filter level (only if present)
.. pad to a byte, then the tile group bytes verbatim
mode 1, canonical sequence header, frame stored whole (odd or large frames)
4 padding, 8 width - 1, 8 height - 1, then the OBU_FRAME payload
mode 2, foreign encoder: sequence header stored too
4 padding, 8 sequence header length, the header, then the OBU_FRAME payload
Mode 0 keeps a three byte prefix (four when the frame keeps deblocking) where a raw stream carries a 9 byte sequence header and a 5 to 7 byte frame header. Stripping the frame header alone takes 6 to 7 bytes off every hash: 45% of a 16px placeholder, 30% of a 24 px one, 12% at 48 px and 5% at 64 px. The regenerated sequence header is profile 0, reduced still picture, 128x128 superblocks, filter-intra and intra-edge on, superres/CDEF/restoration off, 8-bit 4:2:0, sRGB/BT.601 full range; the regenerated frame header is a key frame with one tile, no screen-content tools, no segmentation and no delta quantisers. The four loop filter levels collapse to one value, which is why the reconstruction is not bit-identical to the encoder's own stream (mean SSIM difference over Kodak 24: 0.0003).
The bytes are written as one big-endian integer in base 88 over printable ASCII
minus space, ", &, ', <, > and \, so the string drops into HTML
attributes and JSON unescaped at 6.46 bits per character (base64 is 6,
BlurHash's base83 is 6.38; the largest attribute-safe ASCII alphabet would only
buy another 0.5%). Any AV1 encoder producing a reduced still-picture header and
a single frame OBU can emit an avash (avash::pack takes a raw OBU stream), but
only libaom's tool set matches the canonical headers. rav1e signals 64x64
superblocks, no filter-intra and a separate UV delta q, so the wasm encoder
falls back to mode 2 and its strings run about 11 characters longer. Decoding
rebuilds the OBU stream or a minimal AVIF; browsers decode the AVIF natively.
// size 2..=128, quality 0..=63
let hash = avash::encode(&rgba, width, height, &avash::Options::default())?;
// e.g. (48, 32); both always even
let (w, h) = avash::dimensions(&hash)?;
// RGBA8 at native size, via libaom
let img = avash::decode(&hash)?;
// standalone .avif bytes
let avif = avash::to_avif(&hash)?;
// and avash::to_base88 / from_base88
let bytes = avash::pack(&raw_obu_stream)?;
Features: aom (default, libaom encode + decode, built from source), rav1e
(pure Rust encode, mode 2, ~9 bytes longer), wasm (rav1e + wasm-bindgen
exports encode and toAvif), cli.
avash encode photo.jpg [--size 48] [--quality 23]
avash decode <hash> -o out.png [--width 384]
avash avif <hash> -o out.avif
avash info <hash>
npm install avashjs
<script type="module" src="https://unpkg.com/avashjs"></script>
<img
avash="#/BXnHmUmisJm0-cON6z#@7ewu]gV|naU9FTdh8gG(8l|2?6O,xyX9|4zo_*mA%u!:h?PtIkd3(]ty!kc{S]$F4MwV"
src="photo.avif"
width="1200"
height="800"
alt="..."
/>
js/avash.js (no dependencies, no wasm, 2.4 kB minified / 1.4 kB gzipped)
paints the placeholder as the image's background until load, and watches for
img[avash] added later. Importing the package does the same, and also exports
toAvif(hash), toObjectURL(hash), decode(hash) and dimensions(hash).
Browser-side encoding uses the wasm build in js/pkg (wasm-pack build --target web --no-default-features --features wasm, not published to npm); see
index.html.
2 commits
Rust
63.4%
JavaScript
22.2%
HTML
14.4%
Tiny AVIF image placeholders. Add an avash="..." attribute to your images and
the avash library will render placeholder image while the bigger image
downloads. Like BlurHash, but with AVIF.
An avash is a short string holding one AV1 still picture with the container and header data stripped off (the container, OBU framing, sequence header and uncompressed frame header). This means with a small JS library, it's possible to read the avash string and re-convert it back into an AVIF image for decoding in the browsers native decoder.
AVIF images tend to have more recognisable features than a BlurHash at the equivalent size, and the quality is fine-tunable depending on exactly how long you want the hash to be, making it a scalable replacement for BlurHash or equivalent hash methods.
The bytes are a bitstream, big-endian:
2 version = 1
2 mode
mode 0, canonical: both headers regenerated, only tile data stored
6 width / 2 - 1 (even, 2..=128)
6 height / 2 - 1
6 quantiser index / 4 (63 means 255)
1 tx_mode_select
1 loop filter present
6 loop filter level (only if present)
.. pad to a byte, then the tile group bytes verbatim
mode 1, canonical sequence header, frame stored whole (odd or large frames)
4 padding, 8 width - 1, 8 height - 1, then the OBU_FRAME payload
mode 2, foreign encoder: sequence header stored too
4 padding, 8 sequence header length, the header, then the OBU_FRAME payload
Mode 0 keeps a three byte prefix (four when the frame keeps deblocking) where a raw stream carries a 9 byte sequence header and a 5 to 7 byte frame header. Stripping the frame header alone takes 6 to 7 bytes off every hash: 45% of a 16px placeholder, 30% of a 24 px one, 12% at 48 px and 5% at 64 px. The regenerated sequence header is profile 0, reduced still picture, 128x128 superblocks, filter-intra and intra-edge on, superres/CDEF/restoration off, 8-bit 4:2:0, sRGB/BT.601 full range; the regenerated frame header is a key frame with one tile, no screen-content tools, no segmentation and no delta quantisers. The four loop filter levels collapse to one value, which is why the reconstruction is not bit-identical to the encoder's own stream (mean SSIM difference over Kodak 24: 0.0003).
The bytes are written as one big-endian integer in base 88 over printable ASCII
minus space, ", &, ', <, > and \, so the string drops into HTML
attributes and JSON unescaped at 6.46 bits per character (base64 is 6,
BlurHash's base83 is 6.38; the largest attribute-safe ASCII alphabet would only
buy another 0.5%). Any AV1 encoder producing a reduced still-picture header and
a single frame OBU can emit an avash (avash::pack takes a raw OBU stream), but
only libaom's tool set matches the canonical headers. rav1e signals 64x64
superblocks, no filter-intra and a separate UV delta q, so the wasm encoder
falls back to mode 2 and its strings run about 11 characters longer. Decoding
rebuilds the OBU stream or a minimal AVIF; browsers decode the AVIF natively.
// size 2..=128, quality 0..=63
let hash = avash::encode(&rgba, width, height, &avash::Options::default())?;
// e.g. (48, 32); both always even
let (w, h) = avash::dimensions(&hash)?;
// RGBA8 at native size, via libaom
let img = avash::decode(&hash)?;
// standalone .avif bytes
let avif = avash::to_avif(&hash)?;
// and avash::to_base88 / from_base88
let bytes = avash::pack(&raw_obu_stream)?;
Features: aom (default, libaom encode + decode, built from source), rav1e
(pure Rust encode, mode 2, ~9 bytes longer), wasm (rav1e + wasm-bindgen
exports encode and toAvif), cli.
avash encode photo.jpg [--size 48] [--quality 23]
avash decode <hash> -o out.png [--width 384]
avash avif <hash> -o out.avif
avash info <hash>
npm install avashjs
<script type="module" src="https://unpkg.com/avashjs"></script>
<img
avash="#/BXnHmUmisJm0-cON6z#@7ewu]gV|naU9FTdh8gG(8l|2?6O,xyX9|4zo_*mA%u!:h?PtIkd3(]ty!kc{S]$F4MwV"
src="photo.avif"
width="1200"
height="800"
alt="..."
/>
js/avash.js (no dependencies, no wasm, 2.4 kB minified / 1.4 kB gzipped)
paints the placeholder as the image's background until load, and watches for
img[avash] added later. Importing the package does the same, and also exports
toAvif(hash), toObjectURL(hash), decode(hash) and dimensions(hash).
Browser-side encoding uses the wasm build in js/pkg (wasm-pack build --target web --no-default-features --features wasm, not published to npm); see
index.html.
2 commits
Rust
63.4%
JavaScript
22.2%
HTML
14.4%