Media

ThumbHash

ThumbHash placeholders: a photo as about 25 bytes to keep with it, shown as a soft preview while it loads, finer than BlurHash and with the photo's aspect ratio, average colour and transparency inside.

Version 1.0.0 · 3 functions · 4 crates

#images#placeholders#performance#photos

Functions

What use thumbhash.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use thumbhash_of, thumbhash_data_url from thumbhash (Quillo 0.6).

thumbhash_of(path: str) → stror an error

The ThumbHash of a picture (JPEG, PNG, WebP or GIF), in base64: about 33 characters to store with it.

thumbhash_data_url(hash: str) → stror an error

A ThumbHash as a small PNG data: URL, for an <img src> or a CSS background while the photo loads (about 32 pixels on its long side, scale it up in CSS).

thumbhash_info(hash: str) → anyor an error

What a ThumbHash holds without drawing it: { width, height, ratio, color, alpha }; width and height of the placeholder, ratio the photo's width over its height (about), color its average as #rrggbb (a background for the box before anything loads), alpha whether it has transparency.

Get it

$ quillo install thumbhash               # thumbhash.qwer into this folder$ quillo install thumbhash --example     # and its example, thumbhash.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/thumbhash.qwer$ curl -O https://quillolang.org/plugins/files/thumbhash.qwe$ curl -O https://quillolang.org/plugins/files/thumbhash_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use thumbhash.qweruse thumbhash_of, thumbhash_data_url from thumbhash      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
thumbhash = "=0.1.0"image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "gif"] }base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }

Example

thumbhash.qwe
// A photo gallery: each photo's ThumbHash made on upload and kept with it, then shown as a soft placeholder until the photo arrives.////   quillo run thumbhash.qwe      then open http://localhost:8080/use "thumbhash.qwer"let PHOTO = "thumbhash_sample.png"route GET /hash {    let hash = thumbhash_of(PHOTO)?    { hash: hash, length: len(hash), info: thumbhash_info(hash)?, preview_starts: slice(thumbhash_data_url(hash)?, 0, 22) }}route GET / {    let hash = thumbhash_of(PHOTO)?    let info = thumbhash_info(hash)?    <html><body>        <div style={`width: 480px; aspect-ratio: ${info.ratio}; background: ${info.color} url(${thumbhash_data_url(hash)?}) center / cover`}>            <img src="/photo.png" width="480" loading="lazy" alt="Hills, the sun, a bowl of apples"/>        </div>    </body></html>}route GET /photo.png = file(PHOTO)
$ quillo run thumbhash.qwe$ curl 'localhost:8080/hash'$ curl 'localhost:8080/'

Source

thumbhash.qwer
//! ThumbHash placeholders: a photo as about 25 bytes (base64 text kept beside it in the database) that a page shows as a soft preview while the real one loads; finer than BlurHash, with the photo's aspect ratio, average colour and transparency inside.//! version: 1.0.0//!//! [dependencies]//! thumbhash = "=0.1.0"//! image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "gif"] }//! base64 = "0.22"//! serde_json = { version = "1", features = ["preserve_order"] }use base64::engine::general_purpose::STANDARD;use base64::Engine;use serde_json::{json, Value};fn decode(hash: &str) -> Result<Vec<u8>, String> {    let b = STANDARD.decode(hash.trim()).map_err(|_| format!("`{}` is not a ThumbHash in base64", hash))?;    if b.len() < 5 { return Err("a ThumbHash is at least 5 bytes".into()) }    Ok(b)}/// The ThumbHash of a picture (JPEG, PNG, WebP or GIF), in base64: about 33 characters to store with it.pub fn thumbhash_of(path: &str) -> Result<String, String> {    let img = image::open(path).map_err(|e| format!("{}: {}", path, e))?;    // ThumbHash takes at most 100 × 100 pixels.    let small = img.thumbnail(100, 100).to_rgba8();    let (w, h) = small.dimensions();    Ok(STANDARD.encode(thumbhash::rgba_to_thumb_hash(w as usize, h as usize, small.as_raw())))}/// A ThumbHash as a small PNG `data:` URL, for an `<img src>` or a CSS background while the photo loads (about 32 pixels on its long side, scale it up in CSS).pub fn thumbhash_data_url(hash: &str) -> Result<String, String> {    let (w, h, rgba) = thumbhash::thumb_hash_to_rgba(&decode(hash)?).map_err(|_| "the ThumbHash is damaged".to_string())?;    let img = image::RgbaImage::from_raw(w as u32, h as u32, rgba).ok_or("the ThumbHash is damaged")?;    let mut png = Vec::new();    img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).map_err(|e| e.to_string())?;    Ok(format!("data:image/png;base64,{}", STANDARD.encode(png)))}/// What a ThumbHash holds without drawing it: `{ width, height, ratio, color, alpha }`; `width` and `height` of the placeholder, `ratio` the photo's width over its height (about), `color` its average as `#rrggbb` (a background for the box before anything loads), `alpha` whether it has transparency.pub fn thumbhash_info(hash: &str) -> Result<Value, String> {    let b = decode(hash)?;    let (w, h, _) = thumbhash::thumb_hash_to_rgba(&b).map_err(|_| "the ThumbHash is damaged".to_string())?;    let (r, g, bl, a) = thumbhash::thumb_hash_to_average_rgba(&b).map_err(|_| "the ThumbHash is damaged".to_string())?;    let ratio = thumbhash::thumb_hash_to_approximate_aspect_ratio(&b).map_err(|_| "the ThumbHash is damaged".to_string())?;    let c = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;    Ok(json!({        "width": w, "height": h,        "ratio": (ratio as f64 * 100.0).round() / 100.0,        "color": format!("#{:02x}{:02x}{:02x}", c(r), c(g), c(bl)),        "alpha": a < 0.999 || (b[2] & 0x80) != 0,    }))}
Thread
79Useful? Like it, or add to its thread below.

Thread 0 posts

On ThumbHash: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.