BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / icon.rs

icon.rs

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