Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .cargo/config.toml

This file was deleted.

17 changes: 15 additions & 2 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build and Release
on:
push:
tags:
- 'v*'
- "v*"
workflow_dispatch:

permissions:
Expand All @@ -22,22 +22,35 @@ jobs:
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Validate tag matches crate version
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
VERSION=$(grep '^version = ' Cargo.toml | head -n1 | cut -d '"' -f2)
TAG_VERSION="${GITHUB_REF_NAME#v}"
if [ "$VERSION" != "$TAG_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match crate version ($VERSION)"
exit 1
fi

- name: Build release binary
run: cargo build --release
run: cargo build --release --locked

- name: Package binary (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p dist
cp target/release/maple dist/maple
tar -czf dist/maple-${{ runner.os }}.tar.gz -C dist maple
rm dist/maple

- name: Package binary (Windows)
if: runner.os == 'Windows'
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
Copy-Item target\\release\\maple.exe dist\\maple.exe
Compress-Archive -Path dist\\maple.exe -DestinationPath dist\\maple-Windows.zip
Remove-Item dist\\maple.exe
shell: pwsh

- name: Create GitHub Release
Expand Down
55 changes: 54 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
- main
tags:
- "core-v*"
pull_request:
branches:
- trunk
- main
workflow_dispatch:
inputs:
publish:
Expand Down Expand Up @@ -59,13 +63,62 @@ jobs:
- name: Test maple-render-core standalone crate
run: cargo test --manifest-path crates/maple-render-core/Cargo.toml

- name: Check maple-render-core without default features
run: cargo check --manifest-path crates/maple-render-core/Cargo.toml --no-default-features --all-targets

- name: Test maple-render-core without default features
run: cargo test --manifest-path crates/maple-render-core/Cargo.toml --no-default-features

- name: Verify maple-render-core publish dry-run
run: cargo publish --dry-run --allow-dirty --manifest-path crates/maple-render-core/Cargo.toml

native:
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2

- name: Check native path
run: cargo check --locked

- name: Test native WebP path
run: cargo test --manifest-path crates/maple-render-core/Cargo.toml

msrv:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Rust 1.88
uses: dtolnay/rust-toolchain@1.88.0

- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2

- name: Check root crate at minimum supported Rust version
run: cargo check --locked --all-targets

- name: Check standalone core at minimum supported Rust version
run: cargo check --manifest-path crates/maple-render-core/Cargo.toml --all-targets

- name: Check standalone core without default features at minimum supported Rust version
run: cargo check --manifest-path crates/maple-render-core/Cargo.toml --no-default-features --all-targets

publish-maple-render-core:
name: Publish maple-render-core
runs-on: ubuntu-latest
needs: rust
needs: [rust, native, msrv]
if: startsWith(github.ref, 'refs/tags/core-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')

steps:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**/target
fuzz/artifacts
fuzz/corpus
*.gif
*.png
46 changes: 7 additions & 39 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "maple"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
rust-version = "1.80"
rust-version = "1.88"

[lib]
crate-type = ["cdylib", "rlib"]
Expand All @@ -14,7 +14,7 @@ eyre = "0.6"
color-eyre = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
maple-render-core = { path = "crates/maple-render-core", version = "0.2.0" }
maple-render-core = { path = "crates/maple-render-core", version = "0.3.0" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
Expand Down Expand Up @@ -61,4 +61,3 @@ panic = "unwind"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.mimalloc]
version = "0.1"
default-features = false

9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Core rendering logic now lives in the publishable [`maple-render-core`](crates/m
cargo install --path .
```

Requires Rust 1.80+. Video output requires ffmpeg.
Requires Rust 1.88+. Video output requires ffmpeg.

## Core crate (publishable)

Expand All @@ -35,10 +35,14 @@ maple --zip <template.zip> --in <image.png> --gif out.gif
| Flag | Description |
| ------------------------- | ----------------------- |
| `--gif <path>` | Animated GIF |
| `--webp <path>` | Animated WebP |
| `--webp-single <path>` | Single-frame WebP |
| `--vid <path>` | Video (ffmpeg required) |
| `--save "frame_%06d.jpg"` | Individual frames |
| `--single` | Single frame only |

WebP output defaults to lossy quality 95 and compression method 4. Use `--webp-quality <0..100>`, `--webp-method <0..6>`, or `--webp-lossless` to override it.

### Transform

| Flag | Description |
Expand Down Expand Up @@ -76,6 +80,9 @@ maple --json config.json
# Photo to toaster GIF
maple --zip templates/toaster.zip --in photo.jpg --gif out.gif

# Full-color animated WebP
maple --zip templates/toaster.zip --in photo.jpg --webp out.webp

# Text billboard
maple --zip templates/billboard-cityscape.zip --in "text:SALE" --gif ad.gif

Expand Down
52 changes: 51 additions & 1 deletion benches/webp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod common;

use divan::Bencher;
use maple::render::RenderQuality;
use maple_render_core::webp_anim::{WebpAnim, WebpOptions};
use maple_render_core::webp_anim::{WebpAnim, WebpFrame, WebpOptions, encode_webp_animation};

fn main() {
common::init();
Expand All @@ -25,6 +25,10 @@ fn lossless() -> WebpOptions {
WebpOptions { lossless: true, ..WebpOptions::default() }
}

fn fast_lossy() -> WebpOptions {
WebpOptions { quality: 85.0, method: 0, ..WebpOptions::default() }
}

fn anim(template: &str, image: &str, options: WebpOptions) -> WebpAnim {
let mut anim = WebpAnim::new(common::renders(template, image, RenderQuality::Sampled));
let repo = common::repository(template);
Expand All @@ -43,6 +47,20 @@ fn warm_anim(template: &str, image: &str, options: WebpOptions) -> WebpAnim {
anim
}

struct BatchAnimation {
dimensions: (u32, u32),
frames: Vec<Vec<u8>>,
}

fn batch_anim(template: &str, image: &str) -> BatchAnimation {
let mut renders = common::warm_renders(template, image, RenderQuality::Sampled);
let dimensions = renders.get_render(0).expect("first frame").get().dimensions();
let frames = (0..renders.length() as i32)
.map(|index| renders.get_render(index).expect("composited frame").get().as_raw().clone())
.collect();
BatchAnimation { dimensions, frames }
}

/// Full animated WebP: every frame is composited and handed to libwebp. This is
/// what `maple --zip ... --webp out.webp` runs.
#[divan::bench(sample_count = 3, sample_size = 1)]
Expand All @@ -61,6 +79,38 @@ fn webp_serialize(bencher: Bencher) {
.bench_local_refs(|anim| anim.encode().expect("webp encode"));
}

/// Bounded-memory method-0 serialization, matching the fast settings used by
/// latency-sensitive consumers.
#[divan::bench(sample_count = 3, sample_size = 1)]
fn webp_serialize_fast(bencher: Bencher) {
bencher
.with_inputs(|| warm_anim("book", "frog.jpg", fast_lossy()))
.bench_local_refs(|anim| anim.encode().expect("webp encode"));
}

/// Batch method-0 serialization through the public high-throughput API. The
/// benchmark runner fixes Rayon to one thread for deterministic CodSpeed
/// results; local multicore runs can override `RAYON_NUM_THREADS`.
#[divan::bench(sample_count = 3, sample_size = 1)]
fn webp_batch_serialize_fast(bencher: Bencher) {
bencher.with_inputs(|| batch_anim("book", "frog.jpg")).bench_local_refs(|animation| {
let frames: Vec<_> = animation
.frames
.iter()
.enumerate()
.map(|(index, rgba)| WebpFrame::new(rgba, index as i32 * 40))
.collect();
encode_webp_animation(
animation.dimensions,
&frames,
frames.len() as i32 * 40,
fast_lossy(),
0,
)
.expect("batch WebP encode")
});
}

/// A single still frame through libwebp, the `--webp_single` path.
#[divan::bench]
fn webp_single_frame(bencher: Bencher) {
Expand Down
Loading
Loading