BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / icon.rs

icon.rs

icon.rs 8.7 KiB Download Raw
1//! Application icon supplied by Tim Falken.
2//!
3//! `SOURCE_PNG` is his original file. The window and the package bitmaps are
4//! scaled from it; the drawing is not replaced.
5
6use std::io::Cursor;
7
8/// Tim Falken's original icon, unchanged.
9/// Window app id (Wayland xdg-shell app_id, X11 WM_CLASS). Must equal the
10/// `.desktop` file name and its `StartupWMClass` so the desktop maps the
11/// window to the entry and its icon (Tim t998u.2).
12pub const APP_ID: &str = "paperdoll-renderer";
13
14pub const SOURCE_PNG: &[u8] = include_bytes!("../assets/icons/paperdoll-renderer.png");
15
16/// Freedesktop hicolor sizes installed by the Linux packages.
17pub const HICOLOR_SIZES: [u32; 9] = [16, 22, 24, 32, 48, 64, 128, 256, 512];
18
19/// Square RGBA icon for the eframe window.
20///
21/// 128 is a multiple of 4. A 256 image plus the X11 width and height words
22/// exceeds the classic 64 KiB request limit, so the property would be empty.
23pub fn window_icon_rgba() -> (u32, u32, Vec<u8>) {
24 const SIZE: u32 = 128;
25 (SIZE, SIZE, hicolor_rgba(SIZE))
26}
27
28/// Square RGBA raster at `size`, scaled from Tim's PNG.
29pub fn hicolor_rgba(size: u32) -> Vec<u8> {
30 let (width, height, pixels) = decode_rgba(SOURCE_PNG).expect("application icon png");
31 let (side, square) = pad_square(&pixels, width, height);
32 resize_premultiplied(&square, side, side, size, size)
33}
34
35pub fn decode_rgba(bytes: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
36 let mut decoder = png::Decoder::new(Cursor::new(bytes));
37 decoder.set_transformations(png::Transformations::EXPAND);
38 let mut reader = decoder.read_info().map_err(|err| err.to_string())?;
39 let mut buffer = vec![0; reader.output_buffer_size().ok_or("png has no frame")?];
40 let info = reader
41 .next_frame(&mut buffer)
42 .map_err(|err| err.to_string())?;
43 if info.color_type != png::ColorType::Rgba || info.bit_depth != png::BitDepth::Eight {
44 return Err(format!(
45 "icon must decode to 8-bit rgba, got {:?} {:?}",
46 info.color_type, info.bit_depth
47 ));
48 }
49 buffer.truncate(info.buffer_size());
50 if buffer.len() != (info.width as usize) * (info.height as usize) * 4 {
51 return Err("icon frame size does not match rgba pixels".into());
52 }
53 Ok((info.width, info.height, buffer))
54}
55
56fn pad_square(pixels: &[u8], width: u32, height: u32) -> (u32, Vec<u8>) {
57 let side = width.max(height);
58 let mut canvas = vec![0; (side as usize) * (side as usize) * 4];
59 let ox = (side - width) / 2;
60 let oy = (side - height) / 2;
61 for y in 0..height {
62 for x in 0..width {
63 let src = ((y * width + x) * 4) as usize;
64 let dst = (((y + oy) * side + (x + ox)) * 4) as usize;
65 canvas[dst..dst + 4].copy_from_slice(&pixels[src..src + 4]);
66 }
67 }
68 (side, canvas)
69}
70
71/// Lanczos-3 resize of straight RGBA. Channels are resampled premultiplied
72/// so the soft edge of the source stays the same drawing.
73fn resize_premultiplied(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
74 let mut premul = vec![0.0; src.len()];
75 for pixel in src.chunks_exact(4).zip(premul.chunks_exact_mut(4)) {
76 let (rgba, out) = pixel;
77 let alpha = rgba[3] as f32 / 255.0;
78 out[0] = rgba[0] as f32 / 255.0 * alpha;
79 out[1] = rgba[1] as f32 / 255.0 * alpha;
80 out[2] = rgba[2] as f32 / 255.0 * alpha;
81 out[3] = alpha;
82 }
83 let horizontal = resample_x(&premul, sw, sh, dw);
84 let vertical = resample_y(&horizontal, dw, sh, dh);
85 let mut out = vec![0; (dw as usize) * (dh as usize) * 4];
86 for (pixel, dst) in vertical.chunks_exact(4).zip(out.chunks_exact_mut(4)) {
87 let alpha = pixel[3];
88 if alpha <= 1.0 / 512.0 {
89 continue;
90 }
91 dst[0] = to_byte(pixel[0] / alpha);
92 dst[1] = to_byte(pixel[1] / alpha);
93 dst[2] = to_byte(pixel[2] / alpha);
94 dst[3] = to_byte(alpha);
95 }
96 out
97}
98
99fn to_byte(value: f32) -> u8 {
100 (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
101}
102
103fn resample_x(src: &[f32], sw: u32, sh: u32, dw: u32) -> Vec<f32> {
104 let (filter_scale, support) = filter_scale(dw, sw);
105 let mut out = vec![0.0; (dw as usize) * (sh as usize) * 4];
106 for y in 0..sh {
107 for x in 0..dw {
108 let center = (x as f32 + 0.5) * (sw as f32) / (dw as f32) - 0.5;
109 let (acc, weight) = accumulate(center, support, filter_scale, sw, |index| {
110 let base = ((y * sw + index) * 4) as usize;
111 [src[base], src[base + 1], src[base + 2], src[base + 3]]
112 });
113 write_sample(&mut out, ((y * dw + x) * 4) as usize, acc, weight);
114 }
115 }
116 out
117}
118
119fn resample_y(src: &[f32], sw: u32, sh: u32, dh: u32) -> Vec<f32> {
120 let (filter_scale, support) = filter_scale(dh, sh);
121 let mut out = vec![0.0; (sw as usize) * (dh as usize) * 4];
122 for y in 0..dh {
123 let center = (y as f32 + 0.5) * (sh as f32) / (dh as f32) - 0.5;
124 for x in 0..sw {
125 let (acc, weight) = accumulate(center, support, filter_scale, sh, |index| {
126 let base = ((index * sw + x) * 4) as usize;
127 [src[base], src[base + 1], src[base + 2], src[base + 3]]
128 });
129 write_sample(&mut out, ((y * sw + x) * 4) as usize, acc, weight);
130 }
131 }
132 out
133}
134
135fn filter_scale(dst: u32, src: u32) -> (f32, f32) {
136 let scale = dst as f32 / src as f32;
137 if scale < 1.0 {
138 (scale, 3.0 / scale)
139 } else {
140 (1.0, 3.0)
141 }
142}
143
144fn accumulate(
145 center: f32,
146 support: f32,
147 filter_scale: f32,
148 limit: u32,
149 sample: impl Fn(u32) -> [f32; 4],
150) -> ([f32; 4], f32) {
151 let left = (center - support).floor() as i32;
152 let right = (center + support).ceil() as i32;
153 let mut acc = [0.0; 4];
154 let mut weight = 0.0;
155 for index in left..=right {
156 let distance = (index as f32 - center).abs();
157 let kernel = lanczos3(distance * filter_scale);
158 if kernel == 0.0 {
159 continue;
160 }
161 let clamped = index.clamp(0, limit as i32 - 1) as u32;
162 let pixel = sample(clamped);
163 for channel in 0..4 {
164 acc[channel] += pixel[channel] * kernel;
165 }
166 weight += kernel;
167 }
168 (acc, weight)
169}
170
171fn write_sample(out: &mut [f32], base: usize, acc: [f32; 4], weight: f32) {
172 if weight.abs() <= f32::EPSILON {
173 return;
174 }
175 for channel in 0..4 {
176 out[base + channel] = acc[channel] / weight;
177 }
178}
179
180fn lanczos3(x: f32) -> f32 {
181 let x = x.abs();
182 if x < 1.0e-6 {
183 return 1.0;
184 }
185 if x >= 3.0 {
186 return 0.0;
187 }
188 let pi_x = std::f32::consts::PI * x;
189 (pi_x.sin() / pi_x) * ((pi_x / 3.0).sin() / (pi_x / 3.0))
190}
191
192#[cfg(test)]
193mod tests {
194 use super::{
195 decode_rgba, hicolor_rgba, pad_square, resize_premultiplied, window_icon_rgba, SOURCE_PNG,
196 };
197
198 #[test]
199 fn source_png_is_the_57_by_60_original() {
200 let (width, height, pixels) = decode_rgba(SOURCE_PNG).unwrap();
201 assert_eq!((width, height), (57, 60));
202 assert_eq!(pixels.len(), 57 * 60 * 4);
203 assert_eq!(SOURCE_PNG.len(), 3050);
204 }
205
206 #[test]
207 fn pad_keeps_every_source_pixel() {
208 let (width, height, pixels) = decode_rgba(SOURCE_PNG).unwrap();
209 let (side, canvas) = pad_square(&pixels, width, height);
210 assert_eq!(side, 60);
211 let ox = (side - width) / 2;
212 for y in 0..height {
213 for x in 0..width {
214 let src = ((y * width + x) * 4) as usize;
215 let dst = ((y * side + x + ox) * 4) as usize;
216 assert_eq!(&canvas[dst..dst + 4], &pixels[src..src + 4]);
217 }
218 }
219 }
220
221 #[test]
222 fn flat_opaque_color_stays_flat() {
223 let src = vec![
224 10, 20, 30, 255, 10, 20, 30, 255, 10, 20, 30, 255, 10, 20, 30, 255,
225 ];
226 let out = resize_premultiplied(&src, 2, 2, 5, 4);
227 for pixel in out.chunks_exact(4) {
228 assert!((pixel[0] as i32 - 10).abs() <= 1, "{pixel:?}");
229 assert!((pixel[1] as i32 - 20).abs() <= 1, "{pixel:?}");
230 assert!((pixel[2] as i32 - 30).abs() <= 1, "{pixel:?}");
231 assert_eq!(pixel[3], 255);
232 }
233 }
234
235 #[test]
236 fn clear_pixels_stay_clear() {
237 let out = resize_premultiplied(&[0; 16], 2, 2, 8, 8);
238 assert!(out.iter().all(|byte| *byte == 0));
239 }
240
241 #[test]
242 fn window_size_is_square_and_still_a_light_figure() {
243 let pixels = hicolor_rgba(64);
244 assert_eq!(pixels.len(), 64 * 64 * 4);
245 let center = (32 * 64 + 32) * 4;
246 assert!(pixels[center + 3] > 200);
247 assert!(pixels[center] > 200);
248 let corner = pixels[3];
249 assert_eq!(corner, 0);
250 let (width, height, window) = window_icon_rgba();
251 assert_eq!((width, height, window.len()), (128, 128, 128 * 128 * 4));
252 }
253}