Image tiles
Pictures cut into tiles for zoomable viewers, puzzles or a grid of posts, and many pictures made into one contact sheet of thumbnails.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use image_tiles.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use image_tiles_cut, image_tiles_grid from image_tiles (Quillo 0.6).
image_tiles_cut(from: str, dir: str, tile_w: int, tile_h: int, ext: str) → listor an errorCuts the picture into tiles tile_w × tile_h pixels, saved in the folder dir as r{row}-c{col}.{ext} (ext jpg, png or webp): the tiles as { path, row, col, x, y, width, height }, rows and columns from 0. Tiles on the right and bottom edges are smaller when the picture does not divide evenly. At most 10,000 tiles.
image_tiles_grid(from: str, dir: str, rows: int, cols: int) → listor an errorThe picture cut into a rows × cols grid (1 to 20 each) of equal tiles, saved as dir/1.jpg, dir/2.jpg... reading left to right, top to bottom: the paths. For a three-by-three Instagram grid, or a puzzle.
image_tiles_sheet(images: list, out: str, columns: int, thumb: int) → anyor an errorA contact sheet: images as square thumbnails thumb pixels a side (16 to 1,000), cropped to their centre, columns to a row (1 to 50), saved at out. { width, height, count }. Up to 1,000 pictures; the last row may be short, on white.
Get it
$ quillo install image_tiles # image_tiles.qwer into this folder$ quillo install image_tiles --example # and its example, image_tiles.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/image_tiles.qwer$ curl -O https://quillolang.org/plugins/files/image_tiles.qwe$ curl -O https://quillolang.org/plugins/files/image_tiles_sample.jpgThen, in your program, all of it or only what you call:
use image_tiles.qweruse image_tiles_cut, image_tiles_grid from image_tiles // 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
// A big picture cut into tiles for a zoomable viewer, a 3×3 grid for social posts, and a contact sheet of them.//// quillo run image_tiles.qwe then open http://localhost:8080/tilesuse "image_tiles.qwer"route GET /tiles { let tiles = image_tiles_cut("image_tiles_sample.jpg", "data/tiles/zoom", 200, 200, "jpg")? let grid = image_tiles_grid("image_tiles_sample.jpg", "data/tiles/grid", 3, 3)? { tiles: len(tiles), last: last(tiles), grid, sheet: image_tiles_sheet(grid, "data/tiles/sheet.jpg", 3, 120)? }}route GET /sheet.jpg { image_tiles_sheet(image_tiles_grid("image_tiles_sample.jpg", "data/tiles/grid", 3, 3)?, "data/tiles/sheet.jpg", 3, 120)? file("data/tiles/sheet.jpg")}$ quillo run image_tiles.qwe$ curl 'localhost:8080/tiles'Source
//! Pictures cut into tiles (for zoomable maps, puzzles or a grid of posts), and many pictures put together into one contact sheet of square thumbnails.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use image::{DynamicImage, ImageDecoder};use serde_json::{json, Value};const MAX_TILES: u64 = 10_000;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)), }}/// Cuts the picture into tiles `tile_w` × `tile_h` pixels, saved in the folder `dir` as `r{row}-c{col}.{ext}` (`ext` jpg, png or webp): the tiles as `{ path, row, col, x, y, width, height }`,/// rows and columns from 0. Tiles on the right and bottom edges are smaller when the picture does not divide evenly. At most 10,000 tiles.pub fn image_tiles_cut(from: &str, dir: &str, tile_w: i64, tile_h: i64, ext: &str) -> Result<Vec<Value>, String> { let img = open(from)?; let (tw, th) = (tile_w.clamp(1, 12_000) as u32, tile_h.clamp(1, 12_000) as u32); let (cols, rows) = (img.width().div_ceil(tw), img.height().div_ceil(th)); if cols as u64 * rows as u64 > MAX_TILES { return Err(format!("{} × {} tiles is more than {}: make them larger", cols, rows, MAX_TILES)); } let ext = match ext.trim_start_matches('.').to_lowercase().as_str() { "" | "jpg" | "jpeg" => "jpg", "png" => "png", "webp" => "webp", e => return Err(format!("tiles are jpg, png or webp, not `{}`", e)), }; let mut out = vec![]; for r in 0..rows { for c in 0..cols { let (x, y) = (c * tw, r * th); let (w, h) = (tw.min(img.width() - x), th.min(img.height() - y)); let path = format!("{}/r{}-c{}.{}", dir.trim_end_matches('/'), r, c, ext); save(&img.crop_imm(x, y, w, h), &path)?; out.push(json!({ "path": path, "row": r, "col": c, "x": x, "y": y, "width": w, "height": h })); } } Ok(out)}/// The picture cut into a `rows` × `cols` grid (1 to 20 each) of equal tiles, saved as `dir/1.jpg`, `dir/2.jpg`... reading left to right, top to bottom: the paths./// For a three-by-three Instagram grid, or a puzzle.pub fn image_tiles_grid(from: &str, dir: &str, rows: i64, cols: i64) -> Result<Vec<String>, String> { let img = open(from)?; let (rows, cols) = (rows.clamp(1, 20) as u32, cols.clamp(1, 20) as u32); let (tw, th) = (img.width() / cols, img.height() / rows); if tw == 0 || th == 0 { return Err("the picture is too small for so many tiles".into()); } let ext = from.rsplit_once('.').map(|(_, e)| e.to_lowercase()).filter(|e| ["jpg", "jpeg", "png", "webp"].contains(&e.as_str())).unwrap_or_else(|| "png".into()); let mut out = vec![]; for r in 0..rows { for c in 0..cols { let path = format!("{}/{}.{}", dir.trim_end_matches('/'), r * cols + c + 1, ext); save(&img.crop_imm(c * tw, r * th, tw, th), &path)?; out.push(path); } } Ok(out)}/// A contact sheet: `images` as square thumbnails `thumb` pixels a side (16 to 1,000), cropped to their centre, `columns` to a row (1 to 50), saved at `out`./// `{ width, height, count }`. Up to 1,000 pictures; the last row may be short, on white.pub fn image_tiles_sheet(images: Vec<String>, out: &str, columns: i64, thumb: i64) -> Result<Value, String> { if images.is_empty() || images.len() > 1000 { return Err("give 1 to 1,000 pictures".into()); } let (cols, t) = (columns.clamp(1, 50) as u32, thumb.clamp(16, 1000) as u32); let rows = (images.len() as u32).div_ceil(cols); let mut sheet = image::RgbaImage::from_pixel(cols.min(images.len() as u32) * t, rows * t, image::Rgba([255, 255, 255, 255])); for (i, p) in images.iter().enumerate() { let img = open(p)?; let side = img.width().min(img.height()); let sq = img.crop_imm((img.width() - side) / 2, (img.height() - side) / 2, side, side).resize_exact(t, t, image::imageops::FilterType::Lanczos3); image::imageops::overlay(&mut sheet, &sq.to_rgba8(), ((i as u32 % cols) * t) as i64, ((i as u32 / cols) * t) as i64); } let (w, h) = sheet.dimensions(); save(&DynamicImage::ImageRgba8(sheet), out)?; Ok(json!({ "width": w, "height": h, "count": images.len() }))}Related
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 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 downloadsHeatmaps heatmap.qwer · 1.0.0Heatmaps as PNG: clicks or sightings blurred into a density map over a screenshot or a plain background, and tables of numbers as coloured grids.9 views · 9 downloadsLink preview images og_image.qwer · 1.0.0Link-preview pictures (1200 × 630 PNG) with a title and subtitle, wrapped and sized to fit, in your colours, with a font built in.7 views · 9 downloadsAnimated GIF 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 downloadsThread 0 posts
On Image tiles: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.