maple-render-core 0.3.0

Core rendering and animation logic for maple templates
Documentation

maple-render-core

maple's renderer and compositing core.

This crate contains:

  • Template repository loading (Repository)
  • Frame compositing and mapping (Render, Renders)
  • GIF/video helpers (GifAnim, VidAnim)
  • Quantization pipeline
  • Input handling for images and text

It intentionally does not bundle templates or font assets. For text rendering, you must pass font bytes into Input::from_text.

Usage

The documentation in http://docs.rs/maple-render-core is your best bet for most of your usage of this crate.

You generally want to work with the respective [Format]Anim structs like GifAnim or WebpAnim for rendering.

Start by using Repository's either of load which can take a filesystem path or load_from_bytes which can take a Vec<u8> and is generally recommended to do so, as it avoids the need to read from the filesystem. You can just copy the templates and fonts from this repo's templates directory into your project, and include_bytes! each template into your code, like so, assuming you keep them in your project's assets/templates directory and assets/fonts directory:

const BACK_TATTOO_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/back-tattoo.zip"));
const BILLBOARD_CITYSCAPE_TEMPLATE: &[u8] = include_bytes!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/assets/templates/billboard-cityscape.zip"
));
const BOOK_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/book.zip"));
const CIRCUITBOARD_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/circuitboard.zip"));
const FLAG_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/flag.zip"));
const FLAG2_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/flag2.zip"));
const FORTUNE_COOKIE_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/fortune-cookie.zip"));
const HEART_LOCKET_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/heart-locket.zip"));
const RUBIKS_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/rubiks.zip"));
const TOASTER_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/toaster.zip"));
const VALENTINE_TEMPLATE: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/templates/valentine.zip"));

fn template_bytes(template: &str) -> Option<&'static [u8]> {
    match template {
        "back-tattoo.zip" => Some(BACK_TATTOO_TEMPLATE),
        "billboard-cityscape.zip" => Some(BILLBOARD_CITYSCAPE_TEMPLATE),
        "book.zip" => Some(BOOK_TEMPLATE),
        "circuitboard.zip" => Some(CIRCUITBOARD_TEMPLATE),
        "flag.zip" => Some(FLAG_TEMPLATE),
        "flag2.zip" => Some(FLAG2_TEMPLATE),
        "fortune-cookie.zip" => Some(FORTUNE_COOKIE_TEMPLATE),
        "heart-locket.zip" => Some(HEART_LOCKET_TEMPLATE),
        "rubiks.zip" => Some(RUBIKS_TEMPLATE),
        "toaster.zip" => Some(TOASTER_TEMPLATE),
        "valentine.zip" => Some(VALENTINE_TEMPLATE),
        _ => None,
    }
}

const FONT: &[u8] =
    include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/fonts/DejaVuSans-Bold.ttf"));

Inputs

An Input is a single image or piece of text composited onto one template layer. Layers map to different surfaces in the template (the screen, the book cover, the tattoo, ...), and they start at 1:

use maple_render_core::{Input, Inputs};

let mut inputs = Inputs::new();

inputs.push(Input::load("front.png")?); // layer 1
inputs.push(Input::load("back.png")?); // layer 2

// Two images stacked on the same layer? Just set it
let mut stamp = Input::load("stamp.png")?;
stamp.layer = 2;
inputs.push(stamp);

Text is an input too, and it needs a font, which is where that FONT const up there comes in. Loading the font bytes are on you either way:

use maple_render_core::TextOptions;

let sale = Input::from_text(
    "SALE", 3, &TextOptions { font_size: 96.0, ..Default::default() }, FONT,
)?;
inputs.push(sale);

TextOptions::default() is 72px black text on a white background with 40px of padding. color and background are image::Rgba<u8> values (so you'll want the image crate installed), and padding grows the canvas around the text.

Rendering

Everything goes through Renders struct, which uses the repository plus your inputs and renders frames on demand:

use maple_render_core::{Renders, render::RenderQuality};

let mut renders = Renders::new(repo, inputs, RenderQuality::Sampled);

renders.set_size(400, 300); // optional; content is fit inside, aspect kept

let frame = renders.get_render(0)?;
frame.save("frame0.png")?;

RenderQuality is Sampled (the default, 9-tap antialiased sampling), Simple (fast, nearest-sample), or None (just the template's neutral frame). Generally, just use Sampled unless you're benchmarking.

Good to know: renders.auto_zoom() scales inputs up to fill the mapped area (returns true if anything changed), and renders.length() tells you how many frames the animation has.

GIF

GifAnim turns a Renders into an animated GIF. GIFs only have 256 colors, so the frames get quantized against a palette. Which frames feed that palette is up to the template, frame 0 by default, whatever Repository::get_palette() says if you set it:

use maple_render_core::GifAnim;

let mut gif = GifAnim::new(renders);
gif.set_palette_frames(vec![0]); // quantize against frame 0
gif.set_timing(0.1, 5.0); // seconds per frame, extra hold on the last
gif.set_dither(true); // slower but smoother gradients
gif.apply()?;
// save to filesystem
gif.save("out.gif")?;
// or grab the bytes (`Vec<u8>`) instead:
let data = gif.encode()?;

The template's own timings are available on the repository too via repo.get_period() and repo.get_hold().

WebP

WebpAnim is the same idea with full color and no banding. It's backed by libwebp and defaults to lossy quality 95 with method 4:

use maple_render_core::{WebpAnim, WebpOptions};

let mut webp = WebpAnim::new(renders);
webp.set_timing(0.1, 5.0);
webp.set_options(WebpOptions { quality: 90.0, lossless: false, method: 4 });
webp.save("out.webp")?;

Need just one still image? WebpAnim::encode_single(&image, &options). If you want the finer-grained knobs, streaming frames one at a time with WebpEncoder, or handing encode_webp_animation a pile of RGBA frames you already have in memory and letting it encode them in parallel, that's all in the crate docs for WebpAnim. WebP is native-only and gated behind the default webp feature.

Video

VidAnim renders every frame and pipes them into ffmpeg at 30fps (H.264), so ffmpeg has to be on your PATH:

use maple_render_core::VidAnim;

let mut vid = VidAnim::new(renders);
vid.set_timing(0.1, 5.0);
vid.set_first_frame(3); // start the loop partway through
vid.apply("out.mp4")?;

Putting it together

use maple_render_core::{
    GifAnim, Input, Inputs, Renders, Repository, TextOptions, render::RenderQuality,
};

// Template code from above...

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let repo =
        Repository::load_from_bytes(template_bytes("toaster.zip").expect("template").to_vec())?;

    let mut inputs = Inputs::new();
    inputs.push(Input::load("monkey.jpg")?);
    inputs.push(Input::from_text("Helloooooooo", 2, &TextOptions::default(), FONT)?);

    let mut renders = Renders::new(repo, inputs, RenderQuality::Sampled);
    renders.auto_zoom()?;

    let mut gif = GifAnim::new(renders);
    gif.set_timing(0.1, 5.0);
    gif.apply()?;
    gif.save("out.gif")?;

    Ok(())
}

Animated WebP

WebpAnim and WebpEncoder use libwebp's animation encoder sequentially. This is the default path because it keeps memory bounded and performs animation-wide compression.

encode_webp_animation is an opt-in batch API for callers that already hold complete RGBA frames. It detects dirty rectangles and encodes them concurrently with rayon. This can reduce encoding time by roughly an order of magnitude on multicore machines, but it retains all source frames and can produce materially larger files because each rectangle is compressed independently. You could benchmark both output size and latency for your usecase before selecting it.

The native libwebp-sys dependency is pinned exactly at 0.14.4. Its bundled libwebp 1.6.0 synchronizes lazy DSP initialization on Unix; maple performs a one-time serialized warm-up on Windows, where that release uses an unsynchronized fallback. Upgrading the binding requires re-reviewing that initialization work.

WebP APIs are native-only and are excluded from wasm32 builds.