#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
use pdfrum_object::Array;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Color {
#[default]
Transparent,
Gray(f32),
Rgb(f32, f32, f32),
Cmyk(f32, f32, f32, f32),
}
impl Color {
#[must_use]
pub fn from_array(array: &Array) -> Color {
let at = |i: usize| array.number_at_or_zero(i);
match array.len() {
1 => Color::Gray(at(0)),
3 => Color::Rgb(at(0), at(1), at(2)),
4 => Color::Cmyk(at(0), at(1), at(2), at(3)),
_ => Color::Transparent,
}
}
#[must_use]
pub fn annot_rgb_bytes(self) -> (u32, u32, u32) {
let byte = |v: f32| {
let scaled = v * 255.0;
if scaled <= 0.0 {
0
} else if !scaled.is_finite() || scaled >= 4_294_967_296.0 {
u32::MAX
} else {
scaled as u32
}
};
match self {
Color::Transparent => (0, 0, 0),
Color::Gray(g) => (byte(g), byte(g), byte(g)),
Color::Rgb(r, g, b) => (byte(r), byte(g), byte(b)),
Color::Cmyk(c, m, y, k) => (
byte((1.0 - c) * (1.0 - k)),
byte((1.0 - m) * (1.0 - k)),
byte((1.0 - y) * (1.0 - k)),
),
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn mk_rgb_bytes(self) -> (u8, u8, u8) {
let byte = |v: f32| {
let scaled = v * 255.0 + 0.5;
if scaled <= 0.0 {
0
} else if scaled >= 255.0 {
255
} else {
scaled as u8
}
};
match self {
Color::Transparent => (0, 0, 0),
Color::Gray(g) => (byte(g), byte(g), byte(g)),
Color::Rgb(r, g, b) => (byte(r), byte(g), byte(b)),
Color::Cmyk(c, m, y, k) => (
byte(1.0 - (c + k).min(1.0)),
byte(1.0 - (m + k).min(1.0)),
byte(1.0 - (y + k).min(1.0)),
),
}
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn rgb_bytes(r: u8, g: u8, b: u8) -> Color {
Color::Rgb(
f32::from(r) / 255.0,
f32::from(g) / 255.0,
f32::from(b) / 255.0,
)
}
#[cfg(test)]
mod tests {
use super::{Color, rgb_bytes};
use pdfrum_object::{Array, Object};
fn array(values: &[f32]) -> Array {
Array::of(values.iter().copied().map(Object::from))
}
#[test]
fn length_alone_chooses_the_space() {
assert_eq!(Color::from_array(&array(&[])), Color::Transparent);
assert_eq!(Color::from_array(&array(&[0.5])), Color::Gray(0.5));
assert_eq!(Color::from_array(&array(&[0.5, 0.25])), Color::Transparent);
assert_eq!(
Color::from_array(&array(&[0.0, 0.5, 1.0])),
Color::Rgb(0.0, 0.5, 1.0)
);
assert_eq!(
Color::from_array(&array(&[0.0, 0.1, 0.2, 0.3])),
Color::Cmyk(0.0, 0.1, 0.2, 0.3)
);
assert_eq!(
Color::from_array(&array(&[0.0, 0.1, 0.2, 0.3, 0.4])),
Color::Transparent
);
}
#[test]
fn a_mistyped_component_reads_as_zero() {
let mixed = Array::of([
Object::Name(pdfrum_object::Name::from("Oops")),
Object::from(0.5_f32),
Object::from(1.0_f32),
]);
assert_eq!(Color::from_array(&mixed), Color::Rgb(0.0, 0.5, 1.0));
}
#[test]
fn the_two_cmyk_formulas_differ() {
let c = Color::Cmyk(0.5, 0.0, 0.0, 0.5);
assert_eq!(c.annot_rgb_bytes().0, 63);
assert_eq!(c.mk_rgb_bytes().0, 0);
}
#[test]
fn negative_components_saturate_rather_than_wrap() {
assert_eq!(Color::Rgb(-1.0, 0.0, 1.0).annot_rgb_bytes(), (0, 0, 255));
}
#[test]
fn byte_constructor_round_trips_through_the_annot_formula() {
assert_eq!(rgb_bytes(220, 220, 220).annot_rgb_bytes(), (220, 220, 220));
assert_eq!(rgb_bytes(0, 51, 113).annot_rgb_bytes(), (0, 51, 113));
}
}