use crate::color::{ColorSpace, Rgb};
use pdfrum_object::Array;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ColorKey {
pub ranges: Box<[(u32, u32)]>,
}
impl ColorKey {
#[must_use]
pub fn from_array(array: &Array, components: usize, max_data: u32) -> Self {
let complete = array.len() >= components * 2;
let ranges = (0..components)
.map(|i| {
if !complete {
return (0u32, 0u32);
}
let lo = array.int_at(i * 2).unwrap_or(0).max(0);
let hi = array
.int_at(i * 2 + 1)
.unwrap_or(0)
.clamp(0, i64::from(max_data));
(
u32::try_from(lo).unwrap_or(0),
u32::try_from(hi).unwrap_or(0),
)
})
.collect();
Self { ranges }
}
#[must_use]
pub fn is_complete(array: &Array, components: usize) -> bool {
array.len() >= components * 2
}
#[must_use]
pub fn is_transparent(&self, samples: &[u32]) -> bool {
if self.ranges.is_empty() {
return false;
}
self.ranges.iter().enumerate().all(|(i, (lo, hi))| {
let v = samples.get(i).copied().unwrap_or(0);
v >= *lo && v <= *hi
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ImageMask {
ColorKey(ColorKey),
Alpha {
width: u32,
height: u32,
alpha: Box<[u8]>,
stencil: bool,
},
}
impl ImageMask {
#[must_use]
pub fn alpha_at(&self, x: u32, y: u32) -> u8 {
match self {
Self::ColorKey(_) => 255,
Self::Alpha {
width,
height,
alpha,
stencil,
} => {
if x >= *width || y >= *height {
return 255;
}
let index = usize::try_from(y)
.ok()
.and_then(|row| row.checked_mul(usize::try_from(*width).ok()?))
.and_then(|base| base.checked_add(usize::try_from(x).ok()?));
let v = index.and_then(|i| alpha.get(i)).copied().unwrap_or(255);
if *stencil { 255 - v } else { v }
}
}
}
}
#[must_use]
pub fn matte_color(
matte: Option<&Array>,
space: Option<&ColorSpace>,
components: usize,
) -> Option<Rgb> {
let matte = matte?;
let space = space?;
if matches!(space, ColorSpace::Pattern(_)) {
return None;
}
if matte.len() != components {
return None;
}
if space.n_components() > components {
return None;
}
let comps: Vec<f32> = (0..components)
.map(|i| matte.number_at_or_zero(i))
.collect();
Some(space.to_rgb(&comps))
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{ColorKey, ImageMask, matte_color};
use crate::color::ColorSpace;
use pdfrum_object::{Array, Object};
fn array(values: &[i64]) -> Array {
Array::of(values.iter().copied().map(Object::Int))
}
#[test]
fn a_colour_key_needs_every_component_inside_its_range() {
let key = ColorKey::from_array(&array(&[10, 20, 30, 40, 50, 60]), 3, 255);
assert!(key.is_transparent(&[15, 35, 55]));
assert!(!key.is_transparent(&[15, 35, 61]));
assert!(!key.is_transparent(&[9, 35, 55]));
assert!(key.is_transparent(&[10, 30, 50]));
assert!(key.is_transparent(&[20, 40, 60]));
}
#[test]
fn a_short_array_still_masks_but_only_exactly_zero_samples() {
let short = array(&[10, 20]);
assert!(!ColorKey::is_complete(&short, 3));
let key = ColorKey::from_array(&short, 3, 255);
assert!(key.is_transparent(&[0, 0, 0]));
assert!(!key.is_transparent(&[15, 0, 0]));
}
#[test]
fn colour_key_bounds_are_clamped_to_the_sample_range() {
let key = ColorKey::from_array(&array(&[-5, 999]), 1, 255);
assert_eq!(key.ranges.first(), Some(&(0, 255)));
}
#[test]
fn an_alpha_mask_reads_out_of_bounds_as_opaque() {
let mask = ImageMask::Alpha {
width: 2,
height: 2,
alpha: Box::from(&[0u8, 64, 128, 255][..]),
stencil: false,
};
assert_eq!(mask.alpha_at(0, 0), 0);
assert_eq!(mask.alpha_at(1, 1), 255);
assert_eq!(mask.alpha_at(5, 5), 255);
}
#[test]
fn a_stencil_mask_inverts_its_sense() {
let mask = ImageMask::Alpha {
width: 1,
height: 1,
alpha: Box::from(&[0u8][..]),
stencil: true,
};
assert_eq!(mask.alpha_at(0, 0), 255);
}
#[test]
fn matte_needs_all_four_preconditions() {
let rgb = ColorSpace::DeviceRgb;
let three = array(&[0, 0, 0]);
assert!(matte_color(Some(&three), Some(&rgb), 3).is_some());
assert!(matte_color(Some(&array(&[0, 0])), Some(&rgb), 3).is_none());
assert!(matte_color(Some(&array(&[0, 0, 0, 0])), Some(&rgb), 3).is_none());
assert!(matte_color(Some(&three), None, 3).is_none());
let pattern = ColorSpace::Pattern(Box::default());
assert!(matte_color(Some(&three), Some(&pattern), 3).is_none());
assert!(matte_color(Some(&array(&[0])), Some(&rgb), 1).is_none());
assert!(matte_color(None, Some(&rgb), 3).is_none());
}
}