Skip to content

Commit 04a9f02

Browse files
committed
feat(core): load template zips from bytes and add GIF dither flag
Session-Id: 79d370e4-6659-40e5-94e4-321776e55484
1 parent 7ca0ba5 commit 04a9f02

4 files changed

Lines changed: 68 additions & 36 deletions

File tree

crates/maple-render-core/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4141
Ok(())
4242
}
4343
```
44+
45+
## Load templates from bytes (embedded at build time)
46+
47+
You can bundle your template ZIP into your binary and load it without touching the filesystem at runtime.
48+
49+
`build.rs`:
50+
51+
```rust
52+
use std::{env, fs, path::PathBuf};
53+
54+
fn main() {
55+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
56+
fs::copy("templates/toaster.zip", out_dir.join("template.zip")).unwrap();
57+
println!("cargo:rerun-if-changed=templates/toaster.zip");
58+
}
59+
```
60+
61+
`src/main.rs`:
62+
63+
```rust
64+
use maple_render_core::Repository;
65+
66+
const TEMPLATE_ZIP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/template.zip"));
67+
68+
fn main() -> Result<(), Box<dyn std::error::Error>> {
69+
let repo = Repository::load_from_bytes(TEMPLATE_ZIP.to_vec())?;
70+
// ...
71+
Ok(())
72+
}
73+
```

crates/maple-render-core/src/gif_anim.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub struct GifAnim {
2121
first_frame: i32,
2222
blocks: Vec<GifBlock>,
2323
palette: Option<Palette>,
24+
dither: bool,
2425
}
2526

2627
struct GifBlock {
@@ -40,9 +41,14 @@ impl GifAnim {
4041
first_frame: -1,
4142
blocks: Vec::new(),
4243
palette: None,
44+
dither: false,
4345
}
4446
}
4547

48+
pub fn set_dither(&mut self, dither: bool) {
49+
self.dither = dither;
50+
}
51+
4652
pub fn set_palette(&mut self, index: i32) {
4753
self.palette_frames = vec![index];
4854
}
@@ -73,19 +79,20 @@ impl GifAnim {
7379
let mut pal_index: i32 = -1;
7480

7581
for (i, &idx) in self.palette_frames.iter().enumerate() {
76-
// Get the rendered image first
77-
let img = {
78-
let render = self.renders.get_render(idx)?;
79-
render.get().clone()
80-
};
81-
self.renders.remove_mapping(idx);
82-
8382
if pals == 1 {
84-
pal_image = Some(img);
83+
let render = self.renders.get_render(idx)?;
84+
pal_image = Some(render.get().clone());
8585
pal_index = idx;
86-
} else {
86+
self.renders.remove_mapping(idx);
87+
continue;
88+
}
89+
90+
{
91+
let render = self.renders.get_render(idx)?;
92+
let img = render.get();
8793
let w = img.width() as usize;
8894
let h = img.height() as usize;
95+
8996
if pal_image.is_none() {
9097
pal_image = Some(img.clone());
9198
} else {
@@ -98,6 +105,8 @@ impl GifAnim {
98105
}
99106
}
100107
}
108+
109+
self.renders.remove_mapping(idx);
101110
}
102111

103112
let pal_image = pal_image.ok_or(Error::MissingData("No palette image".to_string()))?;
@@ -107,8 +116,7 @@ impl GifAnim {
107116
let mut quantizer = Quantizer::new(&pal_image);
108117
self.palette = Some(quantizer.palette().clone());
109118

110-
let mut prev_indices = quantizer.quantize(&pal_image, true);
111-
let _pre_prev_indices = prev_indices.clone();
119+
let mut prev_indices = quantizer.quantize(&pal_image, self.dither);
112120

113121
let frames = self.renders.length() as i32;
114122

@@ -122,12 +130,12 @@ impl GifAnim {
122130
step_pending = step_pending.saturating_add(step);
123131

124132
let curr_indices = if i != pal_index {
125-
let img = {
133+
let quantized = {
126134
let render = self.renders.get_render(i)?;
127-
render.get().clone()
135+
quantizer.quantize(render.get(), self.dither)
128136
};
129137
self.renders.remove_mapping(i);
130-
quantizer.quantize(&img, true)
138+
quantized
131139
} else {
132140
prev_indices.clone()
133141
};

crates/maple-render-core/src/repository.rs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use std::{
22
collections::HashMap,
3-
io::{Read, Seek},
3+
io::{Cursor, Read, Seek},
44
};
55

66
#[cfg(not(target_arch = "wasm32"))]
77
use std::{fs::File, io::BufReader, path::Path};
88

9-
#[cfg(target_arch = "wasm32")]
10-
use std::io::Cursor;
11-
129
use image::RgbaImage;
1310
use zip::ZipArchive;
1411

@@ -18,17 +15,11 @@ use crate::{
1815
template::Template,
1916
};
2017

21-
#[cfg(not(target_arch = "wasm32"))]
22-
pub struct Repository {
23-
zip: ZipArchive<BufReader<File>>,
24-
pub template: Template,
25-
mappings: HashMap<i32, Mapping>,
26-
peak_cache_count: usize,
27-
}
18+
trait ReadSeek: Read + Seek {}
19+
impl<T: Read + Seek> ReadSeek for T {}
2820

29-
#[cfg(target_arch = "wasm32")]
3021
pub struct Repository {
31-
zip: ZipArchive<Cursor<Vec<u8>>>,
22+
zip: ZipArchive<Box<dyn ReadSeek>>,
3223
pub template: Template,
3324
mappings: HashMap<i32, Mapping>,
3425
peak_cache_count: usize,
@@ -44,18 +35,16 @@ impl Repository {
4435
}
4536

4637
let file = File::open(path)?;
47-
let reader = BufReader::new(file);
48-
let mut zip = ZipArchive::new(reader)?;
49-
50-
let template_json = Self::load_text_from_zip(&mut zip, "template.json")?;
51-
let template: Template = serde_json::from_str(&template_json)?;
52-
53-
Ok(Repository { zip, template, mappings: HashMap::new(), peak_cache_count: 0 })
38+
let reader: Box<dyn ReadSeek> = Box::new(BufReader::new(file));
39+
Self::load_from_reader(reader)
5440
}
5541

56-
#[cfg(target_arch = "wasm32")]
5742
pub fn load_from_bytes(bytes: Vec<u8>) -> Result<Self> {
58-
let reader = Cursor::new(bytes);
43+
let reader: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
44+
Self::load_from_reader(reader)
45+
}
46+
47+
fn load_from_reader(reader: Box<dyn ReadSeek>) -> Result<Self> {
5948
let mut zip = ZipArchive::new(reader)?;
6049

6150
let template_json = Self::load_text_from_zip(&mut zip, "template.json")?;

src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ struct Cli {
117117
/// Background color for text inputs as hex (default: "FFFFFF" white)
118118
#[arg(long)]
119119
text_bg: Option<String>,
120+
121+
/// Enable Floyd-Steinberg dithering during GIF quantization (slower, can improve gradients)
122+
#[arg(long)]
123+
dither: bool,
120124
}
121125

122126
#[derive(Deserialize)]
@@ -265,6 +269,7 @@ fn main() -> Result<()> {
265269
let repo_ref = Repository::load(zip_path)?;
266270
anim.set_palette_frames(repo_ref.get_palette());
267271
anim.set_timing(repo_ref.get_period(), repo_ref.get_hold());
272+
anim.set_dither(cli.dither);
268273

269274
if let Some(start) = cli.start {
270275
anim.set_first_frame(start);

0 commit comments

Comments
 (0)