Collage
Several pictures made into one: a grid of equal cells with gaps, or justified rows that keep each picture's shape, on your background.
Version 1.0.0 · 2 functions · 1 crate
Functions
What use collage.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use collage_grid, collage_rows from collage (Quillo 0.6).
collage_grid(images: list, out: str, columns: int, width: int, ratio: float, options: any) → anyor an errorA grid collage at out: columns cells to a row (1 to 50), the whole width pixels wide (100 to 8,000), cells ratio wide to 1 high (1 for squares, 1.5 for 3:2). options (or null): gap between cells and around them in pixels (8), background "#rrggbb" (white, or "#rrggbbaa"), fit "cover" (fill each cell, cropping) or "contain" (show all). { width, height, count }.
collage_rows(images: list, out: str, width: int, row_height: int, options: any) → anyor an errorA collage of justified rows at out, as photo sites lay out albums: pictures keep their shape, each row is scaled to fill width exactly, and rows are about row_height pixels high (the last row too, unless it would be stretched). options: gap and background as for collage_grid. { width, height, count, rows }.
Get it
$ quillo install collage # collage.qwer into this folder$ quillo install collage --example # and its example, collage.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/collage.qwer$ curl -O https://quillolang.org/plugins/files/collage.qwe$ curl -O https://quillolang.org/plugins/files/collage_sample.jpgThen, in your program, all of it or only what you call:
use collage.qweruse collage_grid, collage_rows from collage // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }Example
// An album page made of a holiday's photos: a square grid for social posts, and justified rows for the website.//// quillo run collage.qwe then open http://localhost:8080/grid.jpguse "collage.qwer"// Five "photos" of different shapes, cut from one picture with Quillo's own image.convert.fn photos() = for c in [{ x: 0, y: 0, w: 300, h: 200 }, { x: 300, y: 0, w: 300, h: 400 }, { x: 0, y: 200, w: 300, h: 200 }, { x: 100, y: 50, w: 400, h: 200 }, { x: 200, y: 100, w: 200, h: 200 }] => { let path = `data/album/${c.x}-${c.y}-${c.w}.jpg` image.convert("collage_sample.jpg", path, { crop: c, format: "jpeg", quality: 85 })? path}route GET /grid.jpg { collage_grid(photos(), "data/album/grid.jpg", 3, 900, 1.0, { gap: 10 })? file("data/album/grid.jpg")}route GET /rows.jpg { collage_rows(photos(), "data/album/rows.jpg", 900, 220, { gap: 6, background: "#1b1630" })? file("data/album/rows.jpg")}route GET /made = { grid: collage_grid(photos(), "data/album/grid.jpg", 3, 900, 1.0, { gap: 10 })?, contain: collage_grid(photos(), "data/album/contain.png", 2, 600, 1.5, { fit: "contain", background: "#00000000" })?, rows: collage_rows(photos(), "data/album/rows.jpg", 900, 220, { gap: 6 })?,}$ quillo run collage.qwe$ curl 'localhost:8080/made'$ curl 'localhost:8080/grid.jpg'Source
//! Several pictures put together into one: a grid of equal cells with gaps, or justified rows that keep each picture's shape, on the background you choose.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use image::{DynamicImage, ImageDecoder, Rgba, RgbaImage};use serde_json::{json, Value};const MAX_IMAGES: usize = 500;fn reader(path: &str) -> Result<image::ImageReader<std::io::BufReader<std::fs::File>>, String> { let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?; // A small file claiming to be enormous is refused before anything is allocated. 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); Ok(r)}// The picture turned upright by its EXIF orientation, as phones expect.fn open(path: &str) -> Result<DynamicImage, String> { let mut dec = reader(path)?.into_decoder().map_err(|e| format!("{}: {}", path, e))?; let o = dec.orientation().unwrap_or(image::metadata::Orientation::NoTransforms); let mut img = DynamicImage::from_decoder(dec).map_err(|e| format!("{}: {}", path, e))?; img.apply_orientation(o); Ok(img)}// Saved in the format its extension names: .jpg (quality 88), .png, .webp (lossless) or .gif; folders are made.fn save(img: &DynamicImage, path: &str) -> Result<(), String> { if let Some(dir) = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } let ext = path.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default(); let err = |e: image::ImageError| format!("{}: {}", path, e); match ext.as_str() { "jpg" | "jpeg" => { let f = std::fs::File::create(path).map_err(|e| format!("{}: {}", path, e))?; image::codecs::jpeg::JpegEncoder::new_with_quality(std::io::BufWriter::new(f), 88).encode_image(&img.to_rgb8()).map_err(err) } "png" => img.save_with_format(path, image::ImageFormat::Png).map_err(err), "webp" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::WebP).map_err(err), "gif" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::Gif).map_err(err), _ => Err(format!("{}: save as .jpg, .png, .webp or .gif", path)), }}fn colour(o: &Value) -> Rgba<u8> { let h = o.get("background").and_then(Value::as_str).unwrap_or("#ffffff").trim_start_matches('#').to_string(); match (h.len(), u32::from_str_radix(&h, 16)) { (6, Ok(v)) => Rgba([(v >> 16) as u8, (v >> 8) as u8, v as u8, 255]), (8, Ok(v)) => Rgba([(v >> 24) as u8, (v >> 16) as u8, (v >> 8) as u8, v as u8]), _ => Rgba([255, 255, 255, 255]), }}fn load_all(images: &[String]) -> Result<Vec<DynamicImage>, String> { if images.is_empty() || images.len() > MAX_IMAGES { return Err(format!("give 1 to {} pictures", MAX_IMAGES)); } images.iter().map(|p| open(p)).collect()}/// A grid collage at `out`: `columns` cells to a row (1 to 50), the whole `width` pixels wide (100 to 8,000), cells `ratio` wide to 1 high (1 for squares, 1.5 for 3:2)./// `options` (or null): `gap` between cells and around them in pixels (8), `background` "#rrggbb" (white, or "#rrggbbaa"), `fit` "cover" (fill each cell, cropping) or "contain" (show all)./// `{ width, height, count }`.pub fn collage_grid(images: Vec<String>, out: &str, columns: i64, width: i64, ratio: f64, options: serde_json::Value) -> Result<Value, String> { let imgs = load_all(&images)?; let cols = columns.clamp(1, 50) as u32; let gap = options.get("gap").and_then(Value::as_u64).unwrap_or(8).min(200) as u32; let w = width.clamp(100, 8000) as u32; let cw = (w.saturating_sub(gap * (cols + 1))) / cols; let ch = ((cw as f64 / ratio.clamp(0.1, 10.0)).round() as u32).max(1); if cw == 0 { return Err("the gaps take the whole width: make it wider or the gaps smaller".into()); } let rows = (imgs.len() as u32).div_ceil(cols); let h = rows * ch + gap * (rows + 1); if h > 16_000 { return Err("the collage would be over 16,000 pixels high".into()); } let cover = options.get("fit").and_then(Value::as_str) != Some("contain"); let mut canvas = RgbaImage::from_pixel(w, h, colour(&options)); for (i, img) in imgs.iter().enumerate() { let (c, r) = (i as u32 % cols, i as u32 / cols); let (x, y) = (gap + c * (cw + gap), gap + r * (ch + gap)); let cell = if cover { img.resize_to_fill(cw, ch, image::imageops::FilterType::Lanczos3) } else { img.resize(cw, ch, image::imageops::FilterType::Lanczos3) }; let (dx, dy) = ((cw - cell.width()) / 2, (ch - cell.height()) / 2); image::imageops::overlay(&mut canvas, &cell.to_rgba8(), (x + dx) as i64, (y + dy) as i64); } save(&DynamicImage::ImageRgba8(canvas), out)?; Ok(json!({ "width": w, "height": h, "count": imgs.len() }))}/// A collage of justified rows at `out`, as photo sites lay out albums: pictures keep their shape, each row is scaled to fill `width` exactly,/// and rows are about `row_height` pixels high (the last row too, unless it would be stretched). `options`: `gap` and `background` as for `collage_grid`./// `{ width, height, count, rows }`.pub fn collage_rows(images: Vec<String>, out: &str, width: i64, row_height: i64, options: serde_json::Value) -> Result<Value, String> { let imgs = load_all(&images)?; let gap = options.get("gap").and_then(Value::as_u64).unwrap_or(8).min(200) as f64; let w = width.clamp(100, 8000) as f64; let target = row_height.clamp(20, 4000) as f64; let inner = w - 2.0 * gap; // Fill each row with pictures at the target height until it is full, then scale it to fit. let mut rows: Vec<(Vec<usize>, f64)> = vec![]; let mut cur: Vec<usize> = vec![]; let mut sum = 0.0; for (i, img) in imgs.iter().enumerate() { let aspect = img.width() as f64 / img.height().max(1) as f64; cur.push(i); sum += aspect; let row_w = sum * target + gap * (cur.len() as f64 - 1.0); if row_w >= inner { let hgt = (inner - gap * (cur.len() as f64 - 1.0)) / sum; rows.push((std::mem::take(&mut cur), hgt)); sum = 0.0; } } if !cur.is_empty() { let hgt = ((inner - gap * (cur.len() as f64 - 1.0)) / sum).min(target); rows.push((cur, hgt)); } let h = rows.iter().map(|(_, rh)| rh.round()).sum::<f64>() + gap * (rows.len() as f64 + 1.0); if h > 16_000.0 { return Err("the collage would be over 16,000 pixels high".into()); } let mut canvas = RgbaImage::from_pixel(w as u32, h as u32, colour(&options)); let mut y = gap; for (idx, rh) in &rows { let rh = rh.round().max(1.0); let mut x = gap; for &i in idx { let img = &imgs[i]; let iw = (img.width() as f64 * rh / img.height().max(1) as f64).round().max(1.0); let cell = img.resize_exact(iw as u32, rh as u32, image::imageops::FilterType::Lanczos3); image::imageops::overlay(&mut canvas, &cell.to_rgba8(), x as i64, y as i64); x += iw + gap; } y += rh + gap; } save(&DynamicImage::ImageRgba8(canvas), out)?; Ok(json!({ "width": w as i64, "height": h as i64, "count": imgs.len(), "rows": rows.len() }))}Related
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 downloadsSmart crop smart_crop.qwer · 1.0.0Thumbnails cropped to the interesting part of a picture, as smartcrop.js does: edges, skin tones and strong colours scored, the best window of the shape you ask found, and the crop resized to the exact size.7 views · 8 downloadsEXIF exif.qwer · 1.0.0A photo's EXIF: camera and lens, when it was taken, where (GPS), exposure and orientation, from JPEG, HEIC, PNG and WebP.7 views · 5 downloadsEXIF strip exif_strip.qwer · 1.0.0Photos made safe to publish: GPS, camera and other EXIF, XMP, IPTC and comments taken out of JPEG, PNG and WebP, without re-encoding.6 views · 7 downloadsImage edits image_ops.qwer · 1.0.0Crop, rotate, flip, grayscale, blur, and phone photos turned upright by their EXIF orientation, saved as JPEG, PNG, WebP or GIF.7 views · 7 downloadsImage tiles image_tiles.qwer · 1.0.0Pictures cut into tiles for zoomable viewers, puzzles or a grid of posts, and many pictures made into one contact sheet of thumbnails.8 views · 8 downloadsThread 0 posts
On Collage: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.