Animated GIF
Animated GIFs made from a list of pictures with the delay and size you choose, and GIFs read back: frames, size and length.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use gif.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use gif_make, gif_info from gif (Quillo 0.6).
gif_make(frames: list, out: str, delay_ms: int, width: int) → anyor an errorAn animated GIF at out (folders made) from frames, paths of pictures shown in order, each for delay_ms milliseconds (20 to 60,000), looping for ever. Every frame is fitted to width pixels (0 for the first frame's width; up to 2,000) and the first frame's shape, centred on transparency. { frames, width, height, bytes }. GIFs have 256 colours a frame: photos look grainy, drawings and screenshots fine.
gif_info(path: str) → anyor an errorWhat a GIF holds: { frames, width, height, duration_ms, animated }, duration_ms one pass through all frames.
gif_frames(path: str, dir: str, max: int) → listor an errorThe GIF's frames saved as PNG files frame-1.png, frame-2.png... in the folder dir (made if needed), at most max of them (up to 500): the paths. For a thumbnail of an animation, take the first.
Get it
$ quillo install gif # gif.qwer into this folder$ quillo install gif --example # and its example, gif.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/gif.qwer$ curl -O https://quillolang.org/plugins/files/gif.qwe$ curl -O https://quillolang.org/plugins/files/gif_sample.pngThen, in your program, all of it or only what you call:
use gif.qweruse gif_make, gif_info from gif // 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 panning animation made from crops of a wide picture, served as a GIF, and read back.//// quillo run gif.qwe then open http://localhost:8080/pan.gifuse "gif.qwer"fn make() { // Six windows across the 640×160 picture, cut with Quillo's own image.convert. let frames = for i in range(6) => { let path = `data/gif/frame-${i}.png` image.convert("gif_sample.png", path, { crop: { x: i * 64, y: 0, w: 320, h: 160 }, format: "png" })? path } gif_make(frames, "data/gif/pan.gif", 250, 240)}route GET /pan.gif { make()? file("data/gif/pan.gif")}route GET /info { let made = make()? { made, read: gif_info("data/gif/pan.gif")?, first: gif_frames("data/gif/pan.gif", "data/gif/frames", 1)? }}$ quillo run gif.qwe$ curl 'localhost:8080/info'$ curl 'localhost:8080/pan.gif'Source
//! Animated GIFs made from a list of pictures, with the delay and size you choose, and GIFs read back: their frames, size and length.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use image::{AnimationDecoder, Delay, DynamicImage, Frame, ImageDecoder};use serde_json::{json, Value};const MAX_FRAMES: 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)), }}/// An animated GIF at `out` (folders made) from `frames`, paths of pictures shown in order, each for `delay_ms` milliseconds (20 to 60,000), looping for ever./// Every frame is fitted to `width` pixels (0 for the first frame's width; up to 2,000) and the first frame's shape, centred on transparency./// `{ frames, width, height, bytes }`. GIFs have 256 colours a frame: photos look grainy, drawings and screenshots fine.pub fn gif_make(frames: Vec<String>, out: &str, delay_ms: i64, width: i64) -> Result<Value, String> { if frames.is_empty() || frames.len() > MAX_FRAMES { return Err(format!("give 1 to {} frames", MAX_FRAMES)); } let first = open(&frames[0])?; let w = if width > 0 { width.min(2000) as u32 } else { first.width().min(2000) }; let h = ((first.height() as f64 * w as f64 / first.width().max(1) as f64).round() as u32).max(1); let delay = Delay::from_numer_denom_ms(delay_ms.clamp(20, 60_000) as u32, 1); if let Some(dir) = std::path::Path::new(out).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } let file = std::fs::File::create(out).map_err(|e| format!("{}: {}", out, e))?; let mut enc = image::codecs::gif::GifEncoder::new_with_speed(std::io::BufWriter::new(file), 10); enc.set_repeat(image::codecs::gif::Repeat::Infinite).map_err(|e| e.to_string())?; for (i, p) in frames.iter().enumerate() { let img = if i == 0 { first.clone() } else { open(p)? }; let fitted = img.resize(w, h, image::imageops::FilterType::Triangle).to_rgba8(); let mut canvas = image::RgbaImage::new(w, h); image::imageops::overlay(&mut canvas, &fitted, ((w - fitted.width()) / 2) as i64, ((h - fitted.height()) / 2) as i64); enc.encode_frame(Frame::from_parts(canvas, 0, 0, delay)).map_err(|e| format!("{}: {}", p, e))?; } drop(enc); Ok(json!({ "frames": frames.len(), "width": w, "height": h, "bytes": std::fs::metadata(out).map_err(|e| e.to_string())?.len() }))}fn decoder(path: &str) -> Result<image::codecs::gif::GifDecoder<std::io::BufReader<std::fs::File>>, String> { let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; let mut d = image::codecs::gif::GifDecoder::new(std::io::BufReader::new(f)).map_err(|e| format!("{}: {}", path, e))?; let mut l = image::Limits::default(); l.max_image_width = Some(8_000); l.max_image_height = Some(8_000); l.max_alloc = Some(512 * 1024 * 1024); d.set_limits(l).map_err(|e| e.to_string())?; Ok(d)}/// What a GIF holds: `{ frames, width, height, duration_ms, animated }`, `duration_ms` one pass through all frames.pub fn gif_info(path: &str) -> Result<Value, String> { let d = decoder(path)?; let (w, h) = d.dimensions(); let (mut n, mut ms) = (0u64, 0f64); for f in d.into_frames().take(10_000) { let f = f.map_err(|e| format!("{}: {}", path, e))?; let (num, den) = f.delay().numer_denom_ms(); ms += num as f64 / den.max(1) as f64; n += 1; } Ok(json!({ "frames": n, "width": w, "height": h, "duration_ms": ms.round() as i64, "animated": n > 1 }))}/// The GIF's frames saved as PNG files `frame-1.png`, `frame-2.png`... in the folder `dir` (made if needed), at most `max` of them (up to 500): the paths./// For a thumbnail of an animation, take the first.pub fn gif_frames(path: &str, dir: &str, max: i64) -> Result<Vec<String>, String> { let d = decoder(path)?; std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir, e))?; let mut out = vec![]; for (i, f) in d.into_frames().take(max.clamp(1, MAX_FRAMES as i64) as usize).enumerate() { let f = f.map_err(|e| format!("{}: {}", path, e))?; let p = format!("{}/frame-{}.png", dir.trim_end_matches('/'), i + 1); DynamicImage::ImageRgba8(f.into_buffer()).save_with_format(&p, image::ImageFormat::Png).map_err(|e| format!("{}: {}", p, e))?; out.push(p); } Ok(out)}Related
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 downloadsDithering dither.qwer · 1.0.0Pictures dithered to a few colours: black and white for thermal receipt and label printers, the 3 and 7 colours of e-ink, or any palette, by Floyd–Steinberg, Atkinson, Stucki, Bayer or threshold; and 1-bit rows packed for printers.7 views · 9 downloadsThread 0 posts
On Animated GIF: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.