1use std::fs;
13use std::path::Path;
14
15use shuyi_assets::font::FontAtlasDef;
16use shuyi_assets::image::Image;
17use shuyi_assets::palette::PaletteDef;
18
19use crate::codec::{decode_png, encode_png};
20use crate::project::GlyphTiles;
21
22pub const MAX_GLYPHS: usize = 3;
24
25pub fn tile_origin(rules: &GlyphTiles, index: usize) -> (u32, u32) {
27 let i = u32::try_from(index).unwrap_or(u32::MAX);
28 let columns = rules.columns.max(1);
29 (i % columns * rules.tile, i / columns * rules.tile)
30}
31
32pub fn render(
37 rules: &GlyphTiles,
38 atlas: &FontAtlasDef,
39 atlas_image: &Image,
40 palette: &PaletteDef,
41) -> Result<Image, String> {
42 if rules.tile == 0 || rules.columns == 0 {
43 return Err("tile and columns must be above 0".to_owned());
44 }
45 let count = u32::try_from(rules.tiles.len()).unwrap_or(u32::MAX);
46 let width = rules.columns * rules.tile;
47 let height = count.div_ceil(rules.columns).max(1) * rules.tile;
48 let mut out = Image::filled(width, height, [0; 4]);
49 let color = |name: &str| {
50 palette
51 .get(name)
52 .ok_or_else(|| format!("the palette has no color \"{name}\""))
53 };
54 for (index, tile) in rules.tiles.iter().enumerate() {
55 let glyphs: Vec<char> = tile.glyphs.chars().collect();
56 let wide = u32::try_from(glyphs.len()).unwrap_or(u32::MAX) * atlas.cell_w;
57 if glyphs.is_empty() || glyphs.len() > MAX_GLYPHS {
58 return Err(format!(
59 "tile {index}: {} glyphs, not 1 to {MAX_GLYPHS}",
60 glyphs.len()
61 ));
62 }
63 if wide > rules.tile || atlas.cell_h > rules.tile {
64 return Err(format!(
65 "tile {index}: \"{}\" doesn't fit a {} px tile",
66 tile.glyphs, rules.tile
67 ));
68 }
69 let (x, y) = tile_origin(rules, index);
70 if let Some(bg) = &tile.bg {
71 let [red, green, blue] = color(bg)?;
72 for py in y..y + rules.tile {
73 for px in x..x + rules.tile {
74 set(&mut out, px, py, [red, green, blue, 255]);
75 }
76 }
77 }
78 let fg = color(&tile.fg)?;
79 let left = x + (rules.tile - wide) / 2;
80 let top = y + (rules.tile - atlas.cell_h) / 2;
81 for (i, glyph) in (0..).zip(glyphs) {
82 let cell = atlas
83 .glyph_rect(glyph)
84 .ok_or_else(|| format!("tile {index}: the font has no '{glyph}'"))?;
85 for gy in 0..cell.h {
86 for gx in 0..cell.w {
87 let cover = atlas_image
88 .pixel(cell.x + gx, cell.y + gy)
89 .map_or(0, |[.., alpha]| alpha);
90 if cover > 0 {
91 stamp(&mut out, left + i * atlas.cell_w + gx, top + gy, fg, cover);
92 }
93 }
94 }
95 }
96 }
97 Ok(out)
98}
99
100fn index(image: &Image, x: u32, y: u32) -> Option<usize> {
101 (x < image.width && y < image.height)
102 .then(|| to_usize((u64::from(y) * u64::from(image.width) + u64::from(x)) * 4))
103}
104
105fn set(image: &mut Image, x: u32, y: u32, rgba: [u8; 4]) {
106 if let Some(at) = index(image, x, y) {
107 image.rgba[at..at + 4].copy_from_slice(&rgba);
108 }
109}
110
111fn stamp(image: &mut Image, x: u32, y: u32, color: [u8; 3], cover: u8) {
114 let under = image.pixel(x, y).unwrap_or([0; 4]);
115 let out = if under[3] == 0 {
116 [color[0], color[1], color[2], cover]
117 } else {
118 let mix = |c: usize| blend(under[c], color[c], cover);
119 [mix(0), mix(1), mix(2), under[3]]
120 };
121 set(image, x, y, out);
122}
123
124fn blend(under: u8, over: u8, cover: u8) -> u8 {
126 let cover = u32::from(cover);
127 let v = (u32::from(under) * (255 - cover) + u32::from(over) * cover + 127) / 255;
128 u8::try_from(v).unwrap_or(u8::MAX)
129}
130
131fn to_usize(v: u64) -> usize {
132 usize::try_from(v).unwrap_or(usize::MAX)
133}
134
135pub fn render_files(rules: &GlyphTiles, root: &Path) -> Result<Image, String> {
137 let read = |rel: &str| {
138 let path = root.join(rel);
139 fs::read(&path).map_err(|e| format!("reading {}: {e}", path.display()))
140 };
141 let text = |rel: &str| {
142 let bytes = read(rel)?;
143 String::from_utf8(bytes).map_err(|e| format!("{rel}: {e}"))
144 };
145 let atlas_ron = format!("{}/atlas.ron", rules.font);
146 let atlas_png = format!("{}/atlas.png", rules.font);
147 let png = read(&atlas_png)?;
148 let joined = |errors: Vec<shuyi_assets::ContentError>| {
149 let errors: Vec<String> = errors.iter().map(ToString::to_string).collect();
150 errors.join("\n")
151 };
152 let atlas =
153 FontAtlasDef::from_source(&atlas_ron, &text(&atlas_ron)?, &atlas_png, Some(&png), "")
154 .map_err(joined)?;
155 let (width, height, rgba) = decode_png(&png)?;
156 let atlas_image = Image {
157 width,
158 height,
159 rgba,
160 };
161 let palette =
162 PaletteDef::from_source(&rules.palette, &text(&rules.palette)?, &[]).map_err(joined)?;
163 render(rules, &atlas, &atlas_image, &palette)
164}
165
166pub fn run(rules: &GlyphTiles, root: &Path) -> Result<String, String> {
169 let tiles = render_files(rules, root)?;
170 let png = encode_png(tiles.width, tiles.height, &tiles.rgba)?;
171 let path = root.join(&rules.out);
172 if let Some(dir) = path.parent() {
173 fs::create_dir_all(dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
174 }
175 fs::write(&path, png).map_err(|e| format!("writing {}: {e}", path.display()))?;
176 Ok(format!(
177 "glyph-tiles: {} tiles, {}×{} px → {}",
178 rules.tiles.len(),
179 tiles.width,
180 tiles.height,
181 rules.out
182 ))
183}
184
185#[cfg(test)]
186mod tests;