From 33b768622eb587de8217672e98210b6ee67f25a5 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 9 Jul 2026 22:55:29 +0300 Subject: [PATCH] pdf-color-space: clamp indexed color operands Clamp finite Indexed color operands into the palette range after rounding so negative values map to zero and high values map to hival. Keep non-finite operands rejected and add direct tests for negative, high, rounded, and non-finite index handling. Co-authored-by: Codex --- .../src/indexed_color_space.rs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/pdf-color-space/src/indexed_color_space.rs b/crates/pdf-color-space/src/indexed_color_space.rs index c9372ce6..7488a91b 100644 --- a/crates/pdf-color-space/src/indexed_color_space.rs +++ b/crates/pdf-color-space/src/indexed_color_space.rs @@ -122,13 +122,13 @@ impl IndexedColorSpace { .copied() .ok_or(ColorSpaceError::InsufficientComponents(1, components.len()))?; let rounded = index.round(); - if !rounded.is_finite() || rounded < 0.0 { + if !rounded.is_finite() { return Err(ColorSpaceError::Unsupported(format!( "Indexed color index out of range: {rounded}" ))); } - let bounded = rounded.min(f32::from(self.hival)); + let bounded = rounded.clamp(0.0, f32::from(self.hival)); let idx = (0..=self.hival) .find(|candidate| f32::from(*candidate) == bounded) .map(usize::from) @@ -148,3 +148,62 @@ impl IndexedColorSpace { indexed_entry_to_color(&self.base, entry) } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + use super::*; + + fn indexed_rgb() -> IndexedColorSpace { + IndexedColorSpace { + base: Box::new(ColorSpace::DeviceRGB), + hival: 7, + lookup: vec![ + 0, 128, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, + 243, 128, 255, + ], + } + } + + #[test] + fn apply_clamps_negative_indices_to_zero() { + let color = indexed_rgb() + .apply(&[-17.0]) + .expect("negative indexed color should clamp to zero"); + + assert_eq!(color, Color::from_rgb(0.0, 128.0 / 255.0, 0.0)); + } + + #[test] + fn apply_clamps_high_indices_to_hival() { + let color = indexed_rgb() + .apply(&[17.0]) + .expect("high indexed color should clamp to hival"); + + assert_eq!(color, Color::from_rgb(243.0 / 255.0, 128.0 / 255.0, 1.0)); + } + + #[test] + fn apply_rounds_before_clamping_to_hival() { + let color = indexed_rgb() + .apply(&[6.5]) + .expect("fractional indexed color should round before clamping"); + + assert_eq!(color, Color::from_rgb(243.0 / 255.0, 128.0 / 255.0, 1.0)); + } + + #[test] + fn apply_rejects_non_finite_indices() { + let err = indexed_rgb() + .apply(&[f32::NAN]) + .expect_err("nan indexed color should fail"); + + assert!(matches!(err, ColorSpaceError::Unsupported(_))); + + let err = indexed_rgb() + .apply(&[f32::INFINITY]) + .expect_err("infinite indexed color should fail"); + + assert!(matches!(err, ColorSpaceError::Unsupported(_))); + } +}