//! Application icon supplied by Tim Falken. //! //! `SOURCE_PNG` is his original file. The window and the package bitmaps are //! scaled from it; the drawing is not replaced. use std::io::Cursor; /// Tim Falken's original icon, unchanged. /// Window app id (Wayland xdg-shell app_id, X11 WM_CLASS). Must equal the /// `.desktop` file name and its `StartupWMClass` so the desktop maps the /// window to the entry and its icon (Tim t998u.2). pub const APP_ID: &str = "paperdoll-renderer"; pub const SOURCE_PNG: &[u8] = include_bytes!("../assets/icons/paperdoll-renderer.png"); /// Freedesktop hicolor sizes installed by the Linux packages. pub const HICOLOR_SIZES: [u32; 9] = [16, 22, 24, 32, 48, 64, 128, 256, 512]; /// Square RGBA icon for the eframe window. /// /// 128 is a multiple of 4. A 256 image plus the X11 width and height words /// exceeds the classic 64 KiB request limit, so the property would be empty. pub fn window_icon_rgba() -> (u32, u32, Vec) { const SIZE: u32 = 128; (SIZE, SIZE, hicolor_rgba(SIZE)) } /// Square RGBA raster at `size`, scaled from Tim's PNG. pub fn hicolor_rgba(size: u32) -> Vec { let (width, height, pixels) = decode_rgba(SOURCE_PNG).expect("application icon png"); let (side, square) = pad_square(&pixels, width, height); resize_premultiplied(&square, side, side, size, size) } pub fn decode_rgba(bytes: &[u8]) -> Result<(u32, u32, Vec), String> { let mut decoder = png::Decoder::new(Cursor::new(bytes)); decoder.set_transformations(png::Transformations::EXPAND); let mut reader = decoder.read_info().map_err(|err| err.to_string())?; let mut buffer = vec![0; reader.output_buffer_size().ok_or("png has no frame")?]; let info = reader .next_frame(&mut buffer) .map_err(|err| err.to_string())?; if info.color_type != png::ColorType::Rgba || info.bit_depth != png::BitDepth::Eight { return Err(format!( "icon must decode to 8-bit rgba, got {:?} {:?}", info.color_type, info.bit_depth )); } buffer.truncate(info.buffer_size()); if buffer.len() != (info.width as usize) * (info.height as usize) * 4 { return Err("icon frame size does not match rgba pixels".into()); } Ok((info.width, info.height, buffer)) } fn pad_square(pixels: &[u8], width: u32, height: u32) -> (u32, Vec) { let side = width.max(height); let mut canvas = vec![0; (side as usize) * (side as usize) * 4]; let ox = (side - width) / 2; let oy = (side - height) / 2; for y in 0..height { for x in 0..width { let src = ((y * width + x) * 4) as usize; let dst = (((y + oy) * side + (x + ox)) * 4) as usize; canvas[dst..dst + 4].copy_from_slice(&pixels[src..src + 4]); } } (side, canvas) } /// Lanczos-3 resize of straight RGBA. Channels are resampled premultiplied /// so the soft edge of the source stays the same drawing. fn resize_premultiplied(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec { let mut premul = vec![0.0; src.len()]; for pixel in src.chunks_exact(4).zip(premul.chunks_exact_mut(4)) { let (rgba, out) = pixel; let alpha = rgba[3] as f32 / 255.0; out[0] = rgba[0] as f32 / 255.0 * alpha; out[1] = rgba[1] as f32 / 255.0 * alpha; out[2] = rgba[2] as f32 / 255.0 * alpha; out[3] = alpha; } let horizontal = resample_x(&premul, sw, sh, dw); let vertical = resample_y(&horizontal, dw, sh, dh); let mut out = vec![0; (dw as usize) * (dh as usize) * 4]; for (pixel, dst) in vertical.chunks_exact(4).zip(out.chunks_exact_mut(4)) { let alpha = pixel[3]; if alpha <= 1.0 / 512.0 { continue; } dst[0] = to_byte(pixel[0] / alpha); dst[1] = to_byte(pixel[1] / alpha); dst[2] = to_byte(pixel[2] / alpha); dst[3] = to_byte(alpha); } out } fn to_byte(value: f32) -> u8 { (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8 } fn resample_x(src: &[f32], sw: u32, sh: u32, dw: u32) -> Vec { let (filter_scale, support) = filter_scale(dw, sw); let mut out = vec![0.0; (dw as usize) * (sh as usize) * 4]; for y in 0..sh { for x in 0..dw { let center = (x as f32 + 0.5) * (sw as f32) / (dw as f32) - 0.5; let (acc, weight) = accumulate(center, support, filter_scale, sw, |index| { let base = ((y * sw + index) * 4) as usize; [src[base], src[base + 1], src[base + 2], src[base + 3]] }); write_sample(&mut out, ((y * dw + x) * 4) as usize, acc, weight); } } out } fn resample_y(src: &[f32], sw: u32, sh: u32, dh: u32) -> Vec { let (filter_scale, support) = filter_scale(dh, sh); let mut out = vec![0.0; (sw as usize) * (dh as usize) * 4]; for y in 0..dh { let center = (y as f32 + 0.5) * (sh as f32) / (dh as f32) - 0.5; for x in 0..sw { let (acc, weight) = accumulate(center, support, filter_scale, sh, |index| { let base = ((index * sw + x) * 4) as usize; [src[base], src[base + 1], src[base + 2], src[base + 3]] }); write_sample(&mut out, ((y * sw + x) * 4) as usize, acc, weight); } } out } fn filter_scale(dst: u32, src: u32) -> (f32, f32) { let scale = dst as f32 / src as f32; if scale < 1.0 { (scale, 3.0 / scale) } else { (1.0, 3.0) } } fn accumulate( center: f32, support: f32, filter_scale: f32, limit: u32, sample: impl Fn(u32) -> [f32; 4], ) -> ([f32; 4], f32) { let left = (center - support).floor() as i32; let right = (center + support).ceil() as i32; let mut acc = [0.0; 4]; let mut weight = 0.0; for index in left..=right { let distance = (index as f32 - center).abs(); let kernel = lanczos3(distance * filter_scale); if kernel == 0.0 { continue; } let clamped = index.clamp(0, limit as i32 - 1) as u32; let pixel = sample(clamped); for channel in 0..4 { acc[channel] += pixel[channel] * kernel; } weight += kernel; } (acc, weight) } fn write_sample(out: &mut [f32], base: usize, acc: [f32; 4], weight: f32) { if weight.abs() <= f32::EPSILON { return; } for channel in 0..4 { out[base + channel] = acc[channel] / weight; } } fn lanczos3(x: f32) -> f32 { let x = x.abs(); if x < 1.0e-6 { return 1.0; } if x >= 3.0 { return 0.0; } let pi_x = std::f32::consts::PI * x; (pi_x.sin() / pi_x) * ((pi_x / 3.0).sin() / (pi_x / 3.0)) } #[cfg(test)] mod tests { use super::{ decode_rgba, hicolor_rgba, pad_square, resize_premultiplied, window_icon_rgba, SOURCE_PNG, }; #[test] fn source_png_is_the_57_by_60_original() { let (width, height, pixels) = decode_rgba(SOURCE_PNG).unwrap(); assert_eq!((width, height), (57, 60)); assert_eq!(pixels.len(), 57 * 60 * 4); assert_eq!(SOURCE_PNG.len(), 3050); } #[test] fn pad_keeps_every_source_pixel() { let (width, height, pixels) = decode_rgba(SOURCE_PNG).unwrap(); let (side, canvas) = pad_square(&pixels, width, height); assert_eq!(side, 60); let ox = (side - width) / 2; for y in 0..height { for x in 0..width { let src = ((y * width + x) * 4) as usize; let dst = ((y * side + x + ox) * 4) as usize; assert_eq!(&canvas[dst..dst + 4], &pixels[src..src + 4]); } } } #[test] fn flat_opaque_color_stays_flat() { let src = vec![ 10, 20, 30, 255, 10, 20, 30, 255, 10, 20, 30, 255, 10, 20, 30, 255, ]; let out = resize_premultiplied(&src, 2, 2, 5, 4); for pixel in out.chunks_exact(4) { assert!((pixel[0] as i32 - 10).abs() <= 1, "{pixel:?}"); assert!((pixel[1] as i32 - 20).abs() <= 1, "{pixel:?}"); assert!((pixel[2] as i32 - 30).abs() <= 1, "{pixel:?}"); assert_eq!(pixel[3], 255); } } #[test] fn clear_pixels_stay_clear() { let out = resize_premultiplied(&[0; 16], 2, 2, 8, 8); assert!(out.iter().all(|byte| *byte == 0)); } #[test] fn window_size_is_square_and_still_a_light_figure() { let pixels = hicolor_rgba(64); assert_eq!(pixels.len(), 64 * 64 * 4); let center = (32 * 64 + 32) * 4; assert!(pixels[center + 3] > 200); assert!(pixels[center] > 200); let corner = pixels[3]; assert_eq!(corner, 0); let (width, height, window) = window_icon_rgba(); assert_eq!((width, height, window.len()), (128, 128, 128 * 128 * 4)); } }