Media

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

#images#animation#gif

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 error

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.

gif_info(path: str) → anyor an error

What 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 error

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.

Get it

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

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

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

main.qwe
use gif.qweruse gif_make, gif_info from gif      // 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

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

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

Thread 0 posts

On Animated GIF: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.