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
5353 .map_err(|error| format!("{error:?}"))?;
5454 let mut rgba = image.pixels().to_vec();
5555 for pixel in rgba.chunks_exact_mut(4) {
56 let alpha = u32::from(pixel[3]);
57 for channel in &mut pixel[..3] {
58 *channel = (u32::from(*channel) * 255 + alpha / 2)
59 .checked_div(alpha)
60 .unwrap_or(0)
61 .min(255) as u8;
56 if pixel[3] != 255 {
57 let scale = 255.0 / f32::from(pixel[3].max(1));
58 let linear = draw::srgb(pixel[0], pixel[1], pixel[2]).map(|v| v * scale);
59 pixel[..3].copy_from_slice(&draw::srgb_bytes(linear));
6260 }
6361 }
6462 return Ok((image.size(), rgba));
......@@ -487,6 +485,22 @@ mod tests {
487485 assert_eq!(pixels(&encoded).unwrap(), ([2, 1], rgba.to_vec()));
488486 }
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
490504 fn png_at(pixels_per_metre: Option<u32>) -> Vec<u8> {
491505 let mut bytes = Vec::new();
492506 let mut encoder = png::Encoder::new(&mut bytes, 200, 100);