Media

Palette

A picture's main colours as hex, how much of it each covers, and a readable text colour on each, for themes and matching cards.

Version 1.0.0 · 3 functions · 2 crates

#images#colors#themes

Functions

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

palette(path: str, count: int) → listor an error

The picture's count main colours (2 to 16) as "#rrggbb", the most representative first. A few may be missing from a picture with fewer colours.

palette_dominant(path: str) → stror an error

The one colour that best stands for the picture, as "#rrggbb": a background while it loads.

palette_shares(path: str, count: int) → listor an error

The main colours with how much of the picture each covers: { hex, share, text }, share from 0 to 1 (they add up to 1), text "#000000" or "#ffffff" to write on it. Largest share first.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/palette.qwer$ curl -O https://quillolang.org/plugins/files/palette.qwe$ curl -O https://quillolang.org/plugins/files/palette_sample.png

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

main.qwe
use palette.qweruse palette, palette_dominant from palette      // Quillo 0.6: only these

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

[dependencies]
color-thief = "0.2"image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }

Example

palette.qwe
// A product card coloured from its photo: the background, the text on it, and a strip of the photo's colours.////   quillo run palette.qwe      then open http://localhost:8080/use "palette.qwer"let PHOTO = "palette_sample.png"route GET /colors = { palette: palette(PHOTO, 6)?, dominant: palette_dominant(PHOTO)?, shares: palette_shares(PHOTO, 4)? }route GET /photo.png = file(PHOTO)route GET / {    let main = palette_shares(PHOTO, 5)?[0]    <html><body style={`margin: 0; font-family: sans-serif; background: ${main.hex}; color: ${main.text}`}>        <div style="padding: 24px">            <img src="/photo.png" width="300" alt="Sunset over the hills"/>            <h1>Sunset blend</h1>            <div style="display: flex">{for c in palette(PHOTO, 6)? => <div style={`width: 40px; height: 24px; background: ${c}`}></div>}</div>        </div>    </body></html>}
$ quillo run palette.qwe$ curl 'localhost:8080/colors'$ curl 'localhost:8080/'

Source

palette.qwer
//! A picture's main colours as hex, how much of it each covers, and a readable text colour on each: for themes, placeholders and cards that match a photo.//! version: 1.0.0//!//! [dependencies]//! color-thief = "0.2"//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use serde_json::{json, Value};fn open(path: &str) -> Result<image::RgbImage, String> {    let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?;    let mut l = image::Limits::default();    l.max_image_width = Some(12_000);    l.max_image_height = Some(12_000);    l.max_alloc = Some(512 * 1024 * 1024);    r.limits(l);    let img = r.decode().map_err(|e| format!("{}: {}", path, e))?;    // The colours of a 256 px copy are the picture's colours, found in a fraction of the time.    Ok(img.thumbnail(256, 256).to_rgb8())}fn colours(img: &image::RgbImage, count: i64) -> Result<Vec<[u8; 3]>, String> {    let n = count.clamp(2, 16) as u8;    let pixels = img.as_raw();    if pixels.len() < 3 {        return Err("the picture is empty".into());    }    let p = std::panic::catch_unwind(|| color_thief::get_palette(pixels, color_thief::ColorFormat::Rgb, 5, n)).map_err(|_| "the picture could not be read".to_string())?;    let p = p.map_err(|e| e.to_string())?;    Ok(p.into_iter().map(|c| [c.r, c.g, c.b]).collect())}fn hex(c: [u8; 3]) -> String {    format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2])}// Black or white, whichever reads better on `c` (WCAG relative luminance).fn text_on(c: [u8; 3]) -> &'static str {    let lin = |v: u8| {        let s = v as f64 / 255.0;        if s <= 0.03928 { s / 12.92 } else { ((s + 0.055) / 1.055).powf(2.4) }    };    let l = 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);    if (l + 0.05) / 0.05 > 1.05 / (l + 0.05) { "#000000" } else { "#ffffff" }}/// The picture's `count` main colours (2 to 16) as "#rrggbb", the most representative first. A few may be missing from a picture with fewer colours.pub fn palette(path: &str, count: i64) -> Result<Vec<String>, String> {    Ok(colours(&open(path)?, count)?.into_iter().map(hex).collect())}/// The one colour that best stands for the picture, as "#rrggbb": a background while it loads.pub fn palette_dominant(path: &str) -> Result<String, String> {    palette_shares(path, 5).map(|v| v.first().and_then(|c| c["hex"].as_str()).unwrap_or("#808080").to_string())}/// The main colours with how much of the picture each covers: `{ hex, share, text }`, `share` from 0 to 1 (they add up to 1), `text` "#000000" or "#ffffff" to write on it./// Largest share first.pub fn palette_shares(path: &str, count: i64) -> Result<Vec<Value>, String> {    let img = open(path)?;    let cs = colours(&img, count)?;    let mut counts = vec![0u64; cs.len()];    for p in img.pixels() {        let best = cs.iter().enumerate().min_by_key(|(_, c)| (0..3).map(|i| (p.0[i] as i32 - c[i] as i32).pow(2)).sum::<i32>()).map(|(i, _)| i).unwrap_or(0);        counts[best] += 1;    }    let total = counts.iter().sum::<u64>().max(1) as f64;    let mut out: Vec<(u64, Value)> = cs.iter().zip(&counts).map(|(c, &n)| (n, json!({ "hex": hex(*c), "share": (n as f64 / total * 1000.0).round() / 1000.0, "text": text_on(*c) }))).collect();    out.sort_by(|a, b| b.0.cmp(&a.0));    Ok(out.into_iter().map(|(_, v)| v).collect())}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.