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
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 errorThe 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 errorThe one colour that best stands for the picture, as "#rrggbb": a background while it loads.
Get it
$ quillo install palette # palette.qwer into this folder$ quillo install palette --example # and its example, palette.qweOr 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.pngThen, in your program, all of it or only what you call:
use palette.qweruse palette, palette_dominant from palette // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
color-thief = "0.2"image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }Example
// 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
//! 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())}Related
gif.qwer · 1.0.0Animated GIFs made from a list of pictures with the delay and size you choose, and GIFs read back: frames, size and length.6 views · 7 downloadsASCII art ascii_art.qwer · 1.0.0Pictures turned into ASCII art: characters from light to dark, plain for terminals and emails, or coloured as HTML.6 views · 10 downloadsBarcodes barcode.qwer · 1.0.0EAN-13, EAN-8, UPC-A, Code 128 and Code 39 barcodes as SVG or PNG, with the digits underneath and check digits worked out.6 views · 5 downloadsBlurHash blurhash.qwer · 1.0.0A photo as a short BlurHash, shown blurred while the real one loads, and back as a small PNG data URL.7 views · 6 downloadsCaptions on images caption.qwer · 1.0.0Text drawn on pictures with a built-in font: captions at the top, middle or bottom, wrapped to fit, with an outline or a band behind them, meme-style top and bottom lines, and text measured before drawing.7 views · 7 downloadsCollage collage.qwer · 1.0.0Several pictures made into one: a grid of equal cells with gaps, or justified rows that keep each picture's shape, on your background.7 views · 7 downloadsThread 0 posts
On Palette: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.