Indexed Color: no panic when forced colours fill the palette (#17)

Image > Mode > Indexed Color with Colors 2 + Forced Black and White
(or 8 + Primaries, or <=216 + Web) leaves no room for computed entries.
median_cut(.., 0) returns no centres and kmeans then indexed into the
empty list ("index out of bounds: the len is 0 but the index is 0"),
taking the command down with a panic. kmeans now returns early when
there are no centres, so the palette is just the forced colours.
1 test (algo quantize).

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
storybored379
2026-10-04 00:57:38 -04:00
committed by GitHub
co-authored by storybored379 Claude Opus 5.5
parent bf111b524e
commit b6a95b6c8d
+22
View File
@@ -247,6 +247,10 @@ fn median_cut(pts: &[([f32; 3], u32)], n: usize) -> Vec<[f32; 3]> {
/// Lloyd refinement of `centers` over weighted points.
fn kmeans(pts: &[([f32; 3], u32)], centers: &mut [[f32; 3]], iters: usize) {
if centers.is_empty() {
// No room left (the forced colours fill the palette): nothing to refine.
return;
}
for _ in 0..iters {
let mut sum = vec![[0.0f64; 3]; centers.len()];
let mut cnt = vec![0.0f64; centers.len()];
@@ -690,6 +694,24 @@ mod tests {
}
}
#[test]
fn forced_colours_filling_the_count_do_not_panic() {
// 2 colours + Black and White (or 8 + Primaries) leaves no room for
// computed entries; the median cut used to return no centres and
// k-means indexed into the empty list.
let px = vec![[1.0, 1.0, 1.0, 1.0]; 16];
for kind in [
PaletteKind::Adaptive,
PaletteKind::Perceptual,
PaletteKind::Selective,
] {
let pal = build_palette(&px, kind, 2, Forced::BlackWhite).unwrap();
assert_eq!(pal, vec![[0, 0, 0], [255, 255, 255]], "{kind:?}");
let pal = build_palette(&gradient(8, 8), kind, 8, Forced::Primaries).unwrap();
assert_eq!(pal.len(), 8, "{kind:?}");
}
}
#[test]
fn quantize_maps_to_palette_with_each_dither() {
let pal = vec![[0, 0, 0], [255, 255, 255]];