Media

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

#images#maps#social#thumbnails

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 error

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.

image_tiles_grid(from: str, dir: str, rows: int, cols: int) → listor an error

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.

image_tiles_sheet(images: list, out: str, columns: int, thumb: int) → anyor an error

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.

Get it

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

Or 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.jpg

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

main.qwe
use image_tiles.qweruse image_tiles_cut, image_tiles_grid from image_tiles      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

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

Example

image_tiles.qwe
// 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

image_tiles.qwer
//! 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() }))}
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.