From 06dcdc12674f4f9e63858ea25c77df0ce4a962da Mon Sep 17 00:00:00 2001 From: Sir Starch Date: Sun, 11 Oct 2026 22:50:58 +0200 Subject: [PATCH] fix(mode): restore indexed pixels after clearing transparency (#3080) (#3153) --- crates/engine/src/mode_cmds.rs | 22 ++++++++++++++++----- crates/engine/src/mode_cmds/tests.rs | 29 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/mode_cmds.rs b/crates/engine/src/mode_cmds.rs index 2db342890d..96e6e0cb5a 100644 --- a/crates/engine/src/mode_cmds.rs +++ b/crates/engine/src/mode_cmds.rs @@ -325,11 +325,23 @@ fn color_table(s: &mut Session, p: &Value) -> Result { let mut data = surf.read_region(r); for q in data.chunks_exact_mut(n) { let c = photocraft_raster::to_rgba(&fmt, q); - if c[3] <= 0.0 { - continue; - } - let i = old.nearest([c[0], c[1], c[2]]); - let alpha = if table.transparent == Some(i as u8) { 0.0 } else { c[3] }; + let rgb = [c[0], c[1], c[2]]; + let i = if c[3] <= 0.0 { + let Some(i) = old.transparent.map(usize::from) else { continue }; + if i >= old.colors.len() || old.rgb(i) != rgb { + continue; + } + i + } else { + old.nearest(rgb) + }; + let alpha = if table.transparent == Some(i as u8) { + 0.0 + } else if c[3] <= 0.0 { + 1.0 + } else { + c[3] + }; let nc = if i < table.colors.len() { table.rgb(i) } else { table.rgb(table.nearest([c[0], c[1], c[2]])) }; let mut enc = [0.0f32; 8]; let m = photocraft_raster::from_rgba_into(&fmt, [nc[0], nc[1], nc[2], alpha], &mut enc); diff --git a/crates/engine/src/mode_cmds/tests.rs b/crates/engine/src/mode_cmds/tests.rs index b7b7068fd4..4aaf396af7 100644 --- a/crates/engine/src/mode_cmds/tests.rs +++ b/crates/engine/src/mode_cmds/tests.rs @@ -192,6 +192,35 @@ fn color_table_presets_and_entries() { assert!(s.execute("image.mode.colorTable", json!({"entries": {"999": "#000000"}})).is_err()); } +#[test] +fn clearing_color_table_transparency_restores_indexed_pixels() { + let pixel = |s: &mut Session| -> [f32; 4] { serde_json::from_value(s.execute("document.pixel", json!({"x": 5, "y": 8})).unwrap()).unwrap() }; + let mut s = Session::new(); + s.execute("file.new", json!({"width": 16, "height": 16, "background": "#ff0000"})).unwrap(); + s.execute("image.mode.indexedColor", json!({"palette": "exact", "colors": 2, "forced": "none", "dither": "none"})).unwrap(); + assert_eq!(doc(&s).color_table.as_ref().unwrap().colors, vec![[255, 0, 0]]); + assert_eq!(pixel(&mut s), [1.0, 0.0, 0.0, 1.0]); + + s.execute("image.mode.colorTable", json!({"transparent": 0})).unwrap(); + assert_eq!(pixel(&mut s), [0.0; 4]); + s.execute("image.mode.colorTable", json!({"transparent": null})).unwrap(); + assert_eq!(doc(&s).layers[0].surface().unwrap().rgba(5, 8), [1.0, 0.0, 0.0, 1.0], "clearing the transparent palette entry should restore the stored pixel"); + assert_eq!(pixel(&mut s), [1.0, 0.0, 0.0, 1.0]); + s.execute("image.mode.rgb", json!({})).unwrap(); + assert_eq!(pixel(&mut s), [1.0, 0.0, 0.0, 1.0]); + + let mut control = Session::new(); + control.execute("file.new", json!({"width": 16, "height": 16, "background": "#ff0000"})).unwrap(); + control.execute("image.mode.indexedColor", json!({"palette": "exact", "colors": 2, "forced": "none", "dither": "none"})).unwrap(); + control.execute("image.mode.colorTable", json!({"entries": {"0": "#0000ff"}})).unwrap(); + let blue: [f32; 4] = serde_json::from_value(control.execute("document.pixel", json!({"x": 5, "y": 8})).unwrap()).unwrap(); + assert_eq!(blue, [0.0, 0.0, 1.0, 1.0]); + control.execute("image.mode.colorTable", json!({"transparent": 0})).unwrap(); + control.execute("edit.undo", json!({})).unwrap(); + let restored: [f32; 4] = serde_json::from_value(control.execute("document.pixel", json!({"x": 5, "y": 8})).unwrap()).unwrap(); + assert_eq!(restored, [0.0, 0.0, 1.0, 1.0]); +} + #[test] fn bitmap_methods_need_grayscale() { let mut s = session(32, 32, 16, "rgb");