use pdfrum_page::{ColorValue, Rgb};
use crate::options::{ColorMode, RenderOptions};
use crate::pixmap::alpha_byte_truncating;
use crate::transfer::TransferFunc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectKind {
Path,
Text,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Argb {
pub a: u8,
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Argb {
pub const TRANSPARENT: Self = Self {
a: 0,
r: 0,
g: 0,
b: 0,
};
pub const BLACK: Self = Self {
a: 255,
r: 0,
g: 0,
b: 0,
};
#[must_use]
pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
Self { a: 255, r, g, b }
}
#[must_use]
pub fn is_invisible(self) -> bool {
self.a == 0 && self.r == 0 && self.g == 0 && self.b == 0
}
#[must_use]
pub fn to_peniko(self) -> peniko::Color {
peniko::Color::from_rgba8(self.r, self.g, self.b, self.a)
}
#[must_use]
pub const fn with_alpha(self, a: u8) -> Self {
Self { a, ..self }
}
}
#[must_use]
pub fn rgb_to_gray(r: u8, g: u8, b: u8) -> u8 {
#[expect(
clippy::cast_possible_truncation,
reason = "weights summing to 100 bound the quotient to 0..=255"
)]
let gray = ((u32::from(b) * 11 + u32::from(g) * 59 + u32::from(r) * 30) / 100) as u8;
gray
}
#[must_use]
fn translate_color(mode: ColorMode, c: Argb) -> Argb {
match mode {
ColorMode::Normal | ColorMode::Alpha => c,
ColorMode::Gray | ColorMode::Forced(_) => {
let g = rgb_to_gray(c.r, c.g, c.b);
Argb {
a: c.a,
r: g,
g,
b: g,
}
}
}
}
#[must_use]
fn translate_object_color(opts: &RenderOptions, c: Argb, kind: ObjectKind, stroking: bool) -> Argb {
let ColorMode::Forced(scheme) = opts.color_mode else {
return translate_color(opts.color_mode, c);
};
let replacement = match (kind, stroking) {
(ObjectKind::Path, false) => scheme.path_fill,
(ObjectKind::Path, true) => scheme.path_stroke,
(ObjectKind::Text, false) => scheme.text_fill,
(ObjectKind::Text, true) => scheme.text_stroke,
(ObjectKind::Other, _) => return c,
};
Argb {
a: c.a,
..replacement
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ColorRef {
Resolved(Rgb),
Invisible,
Missing,
}
#[must_use]
fn color_ref(value: &ColorValue) -> ColorRef {
if let Some(rgb) = value.to_rgb() {
return ColorRef::Resolved(rgb);
}
let Some(pattern) = value.pattern.as_ref() else {
return ColorRef::Missing;
};
let colored_tiling = matches!(
pattern.loaded.as_deref(),
Some(pdfrum_page::Pattern::Tiling(t)) if t.colored
);
if colored_tiling {
ColorRef::Resolved(Rgb {
r: 191.0 / 255.0,
g: 191.0 / 255.0,
b: 191.0 / 255.0,
})
} else {
ColorRef::Invisible
}
}
#[must_use]
pub fn resolve_argb(
value: &ColorValue,
alpha: f32,
transfer: Option<&TransferFunc>,
inherited: Option<Argb>,
opts: &RenderOptions,
kind: ObjectKind,
stroking: bool,
) -> Argb {
let base = match color_ref(value) {
ColorRef::Resolved(rgb) => {
let [r, g, b] = rgb.to_bytes();
Argb { a: 255, r, g, b }
}
ColorRef::Invisible => return Argb::TRANSPARENT,
ColorRef::Missing => inherited.unwrap_or(Argb::BLACK),
};
let a = alpha_byte_truncating(alpha);
let transferred = match transfer {
Some(tf) if !tf.is_identity() => tf.translate(base),
_ => base,
};
translate_object_color(opts, transferred.with_alpha(a), kind, stroking)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use pdfrum_page::ColorSpace;
use smallvec::SmallVec;
use super::*;
fn gray(v: f32) -> ColorValue {
let mut c = ColorValue::default();
c.set_stock(ColorSpace::DeviceGray, &[v]);
c
}
#[test]
fn a_shading_pattern_colour_is_the_invisibility_sentinel() {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
c.set_pattern(pdfrum_object::Name::from("P0"), &[], None);
let argb = resolve_argb(
&c,
1.0,
None,
None,
&RenderOptions::default(),
ObjectKind::Text,
false,
);
assert!(argb.is_invisible(), "got {argb:?}");
}
#[test]
fn a_pattern_colour_that_resolves_keeps_its_colour() {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::Pattern(Box::new(
pdfrum_page::PatternSpace {
base: Some(Box::new(ColorSpace::DeviceRgb)),
},
))));
c.set_pattern(pdfrum_object::Name::from("P0"), &[1.0, 0.0, 0.0], None);
let argb = resolve_argb(
&c,
1.0,
None,
None,
&RenderOptions::default(),
ObjectKind::Text,
false,
);
assert_eq!(argb, Argb::opaque(255, 0, 0));
}
#[test]
fn a_white_fill_is_white_and_not_the_invisibility_sentinel() {
let opts = RenderOptions::default();
let c = resolve_argb(&gray(1.0), 1.0, None, None, &opts, ObjectKind::Path, false);
assert_eq!(c, Argb::opaque(255, 255, 255));
assert!(!c.is_invisible());
}
#[test]
fn a_colour_that_will_not_resolve_inherits_rather_than_vanishing() {
let none = ColorValue {
space: Some(Arc::new(ColorSpace::Separation(Box::new(
pdfrum_page::Separation {
none: true,
alternate: None,
tint: None,
},
)))),
components: SmallVec::from_slice(&[1.0]),
pattern: None,
};
let opts = RenderOptions::default();
assert_eq!(
resolve_argb(&none, 1.0, None, None, &opts, ObjectKind::Path, false),
Argb::BLACK
);
assert_eq!(
resolve_argb(
&none,
1.0,
None,
Some(Argb::opaque(9, 8, 7)),
&opts,
ObjectKind::Path,
false
),
Argb::opaque(9, 8, 7)
);
}
#[test]
fn alpha_truncates() {
let opts = RenderOptions::default();
let c = resolve_argb(&gray(0.0), 0.5, None, None, &opts, ObjectKind::Path, false);
assert_eq!(c.a, 127);
}
#[test]
fn missing_color_inherits_from_the_enclosing_state() {
let opts = RenderOptions::default();
let empty = ColorValue {
space: None,
components: SmallVec::new(),
..Default::default()
};
let inherited = Argb::opaque(10, 20, 30);
let c = resolve_argb(
&empty,
1.0,
None,
Some(inherited),
&opts,
ObjectKind::Path,
false,
);
assert_eq!((c.r, c.g, c.b), (10, 20, 30));
}
#[test]
fn gray_mode_uses_ntsc_weights() {
let opts = RenderOptions {
color_mode: ColorMode::Gray,
..RenderOptions::default()
};
let mut red = ColorValue::default();
red.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.0, 0.0]);
let c = resolve_argb(&red, 1.0, None, None, &opts, ObjectKind::Path, false);
assert_eq!((c.r, c.g, c.b), (76, 76, 76));
}
#[test]
fn forced_scheme_leaves_images_alone() {
let scheme = crate::options::ColorScheme {
path_fill: Argb::opaque(1, 2, 3),
path_stroke: Argb::opaque(4, 5, 6),
text_fill: Argb::opaque(7, 8, 9),
text_stroke: Argb::opaque(10, 11, 12),
};
let opts = RenderOptions {
color_mode: ColorMode::Forced(scheme),
..RenderOptions::default()
};
let value = {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::DeviceRgb));
let _ = c.set_components(&[0.0, 0.0, 0.0]);
c
};
let path = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Path, false);
assert_eq!((path.r, path.g, path.b), (1, 2, 3));
let other = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Other, false);
assert_eq!((other.r, other.g, other.b), (0, 0, 0));
}
#[test]
fn rgb_to_gray_is_the_oracle_formula() {
assert_eq!(rgb_to_gray(255, 255, 255), 255);
assert_eq!(rgb_to_gray(0, 0, 0), 0);
assert_eq!(rgb_to_gray(255, 0, 0), 76);
assert_eq!(rgb_to_gray(0, 255, 0), 150);
assert_eq!(rgb_to_gray(0, 0, 255), 28);
}
}