authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 22:49:14-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 22:49:20-07:00
log229f12f5ff836ffa36d21023589fe3955df3f21d
treeaf99cc50a44ed7e8e8d9134a47f2b870fd12daa6
parent519c225b1dd09a293f258f6422dbb315f1473302
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: preserve colors when copying translucent pictures

Reverse the renderer’s linear-light premultiplication before encoding non-PNG pictures for the clipboard. Cover translucent TIFF colors alongside exact PNG alpha. Refs #80 Assisted-by: gpt-6.1-sol

1 files changed, 20 insertions(+), 6 deletions(-)

crates/snowbound/src/paste.rs+20-6
...@@ -53,12 +53,10 @@ pub(crate) fn pixels(encoded: &[u8]) -> Result<([u32; 2], Vec<u8>), Box<dyn Erro...@@ -53,12 +53,10 @@ pub(crate) fn pixels(encoded: &[u8]) -> Result<([u32; 2], Vec<u8>), Box<dyn Erro
53 .map_err(|error| format!("{error:?}"))?;53 .map_err(|error| format!("{error:?}"))?;
54 let mut rgba = image.pixels().to_vec();54 let mut rgba = image.pixels().to_vec();
55 for pixel in rgba.chunks_exact_mut(4) {55 for pixel in rgba.chunks_exact_mut(4) {
56 let alpha = u32::from(pixel[3]);56 if pixel[3] != 255 {
57 for channel in &mut pixel[..3] {57 let scale = 255.0 / f32::from(pixel[3].max(1));
58 *channel = (u32::from(*channel) * 255 + alpha / 2)58 let linear = draw::srgb(pixel[0], pixel[1], pixel[2]).map(|v| v * scale);
59 .checked_div(alpha)59 pixel[..3].copy_from_slice(&draw::srgb_bytes(linear));
60 .unwrap_or(0)
61 .min(255) as u8;
62 }60 }
63 }61 }
64 return Ok((image.size(), rgba));62 return Ok((image.size(), rgba));
...@@ -487,6 +485,22 @@ mod tests {...@@ -487,6 +485,22 @@ mod tests {
487 assert_eq!(pixels(&encoded).unwrap(), ([2, 1], rgba.to_vec()));485 assert_eq!(pixels(&encoded).unwrap(), ([2, 1], rgba.to_vec()));
488 }486 }
489487
488 #[test]
489 fn clipboard_pixels_keep_translucent_tiff_colors() {
490 use image::ImageEncoder;
491 let rgba = [220, 120, 80, 128];
492 let mut encoded = Vec::new();
493 image::codecs::tiff::TiffEncoder::new(std::io::Cursor::new(&mut encoded))
494 .write_image(&rgba, 1, 1, image::ExtendedColorType::Rgba8)
495 .unwrap();
496 let (size, decoded) = pixels(&encoded).unwrap();
497 assert_eq!(size, [1, 1]);
498 assert_eq!(decoded[3], rgba[3]);
499 for (actual, expected) in decoded[..3].iter().zip(&rgba[..3]) {
500 assert!(actual.abs_diff(*expected) <= 2, "{decoded:?}");
501 }
502 }
503
490 fn png_at(pixels_per_metre: Option<u32>) -> Vec<u8> {504 fn png_at(pixels_per_metre: Option<u32>) -> Vec<u8> {
491 let mut bytes = Vec::new();505 let mut bytes = Vec::new();
492 let mut encoder = png::Encoder::new(&mut bytes, 200, 100);506 let mut encoder = png::Encoder::new(&mut bytes, 200, 100);