| 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 | |
| 6 | use std::io::Cursor; |
| 7 | |
| 8 | /// Tim Falken's original icon, unchanged. |
| 9 | pub const SOURCE_PNG: &[u8] = include_bytes!("../assets/icons/paperdoll-renderer.png"); |
| 10 | |
| 11 | /// Freedesktop hicolor sizes installed by the Linux packages. |
| 12 | pub 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. |
| 18 | pub 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. |
| 24 | pub 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 | |
| 30 | pub 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 | |
| 51 | fn 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. |
| 68 | fn 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 | |
| 94 | fn to_byte(value: f32) -> u8 { |
| 95 | (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8 |
| 96 | } |
| 97 | |
| 98 | fn 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 | |
| 114 | fn 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 | |
| 130 | fn 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 | |
| 139 | fn 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 | |
| 166 | fn 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 | |
| 175 | fn 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)] |
| 188 | mod 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 | } |