1use pdfrum_page::{ColorValue, Rgb};
18
19use crate::options::{ColorMode, RenderOptions};
20use crate::pixmap::alpha_byte_truncating;
21use crate::transfer::TransferFunc;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ObjectKind {
28 Path,
30 Text,
32 Other,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct Argb {
43 pub a: u8,
45 pub r: u8,
47 pub g: u8,
49 pub b: u8,
51}
52
53impl Argb {
54 pub const TRANSPARENT: Self = Self {
56 a: 0,
57 r: 0,
58 g: 0,
59 b: 0,
60 };
61
62 pub const BLACK: Self = Self {
64 a: 255,
65 r: 0,
66 g: 0,
67 b: 0,
68 };
69
70 #[must_use]
72 pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
73 Self { a: 255, r, g, b }
74 }
75
76 #[must_use]
80 pub fn is_invisible(self) -> bool {
81 self.a == 0 && self.r == 0 && self.g == 0 && self.b == 0
82 }
83
84 #[must_use]
86 pub fn to_peniko(self) -> peniko::Color {
87 peniko::Color::from_rgba8(self.r, self.g, self.b, self.a)
88 }
89
90 #[must_use]
92 pub const fn with_alpha(self, a: u8) -> Self {
93 Self { a, ..self }
94 }
95}
96
97#[must_use]
103pub fn rgb_to_gray(r: u8, g: u8, b: u8) -> u8 {
104 #[expect(
107 clippy::cast_possible_truncation,
108 reason = "weights summing to 100 bound the quotient to 0..=255"
109 )]
110 let gray = ((u32::from(b) * 11 + u32::from(g) * 59 + u32::from(r) * 30) / 100) as u8;
111 gray
112}
113
114#[must_use]
116fn translate_color(mode: ColorMode, c: Argb) -> Argb {
117 match mode {
118 ColorMode::Normal | ColorMode::Alpha => c,
119 ColorMode::Gray | ColorMode::Forced(_) => {
120 let g = rgb_to_gray(c.r, c.g, c.b);
121 Argb {
122 a: c.a,
123 r: g,
124 g,
125 b: g,
126 }
127 }
128 }
129}
130
131#[must_use]
133fn translate_object_color(opts: &RenderOptions, c: Argb, kind: ObjectKind, stroking: bool) -> Argb {
134 let ColorMode::Forced(scheme) = opts.color_mode else {
135 return translate_color(opts.color_mode, c);
136 };
137 let replacement = match (kind, stroking) {
138 (ObjectKind::Path, false) => scheme.path_fill,
139 (ObjectKind::Path, true) => scheme.path_stroke,
140 (ObjectKind::Text, false) => scheme.text_fill,
141 (ObjectKind::Text, true) => scheme.text_stroke,
142 (ObjectKind::Other, _) => return c,
143 };
144 Argb {
145 a: c.a,
146 ..replacement
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
162enum ColorRef {
163 Resolved(Rgb),
165 Invisible,
167 Missing,
169}
170
171#[must_use]
181fn color_ref(value: &ColorValue) -> ColorRef {
182 if let Some(rgb) = value.to_rgb() {
183 return ColorRef::Resolved(rgb);
184 }
185 let Some(pattern) = value.pattern.as_ref() else {
186 return ColorRef::Missing;
187 };
188 let colored_tiling = matches!(
189 pattern.loaded.as_deref(),
190 Some(pdfrum_page::Pattern::Tiling(t)) if t.colored
191 );
192 if colored_tiling {
193 ColorRef::Resolved(Rgb {
196 r: 191.0 / 255.0,
197 g: 191.0 / 255.0,
198 b: 191.0 / 255.0,
199 })
200 } else {
201 ColorRef::Invisible
202 }
203}
204
205#[must_use]
212pub fn resolve_argb(
213 value: &ColorValue,
214 alpha: f32,
215 transfer: Option<&TransferFunc>,
216 inherited: Option<Argb>,
217 opts: &RenderOptions,
218 kind: ObjectKind,
219 stroking: bool,
220) -> Argb {
221 let base = match color_ref(value) {
222 ColorRef::Resolved(rgb) => {
223 let [r, g, b] = rgb.to_bytes();
224 Argb { a: 255, r, g, b }
225 }
226 ColorRef::Invisible => return Argb::TRANSPARENT,
227 ColorRef::Missing => inherited.unwrap_or(Argb::BLACK),
230 };
231
232 let a = alpha_byte_truncating(alpha);
233 let transferred = match transfer {
234 Some(tf) if !tf.is_identity() => tf.translate(base),
235 _ => base,
236 };
237 translate_object_color(opts, transferred.with_alpha(a), kind, stroking)
238}
239
240#[cfg(test)]
241mod tests {
242 use std::sync::Arc;
243
244 use pdfrum_page::ColorSpace;
245 use smallvec::SmallVec;
246
247 use super::*;
248
249 fn gray(v: f32) -> ColorValue {
250 let mut c = ColorValue::default();
251 c.set_stock(ColorSpace::DeviceGray, &[v]);
252 c
253 }
254
255 #[test]
256 fn a_shading_pattern_colour_is_the_invisibility_sentinel() {
257 let mut c = ColorValue::default();
262 c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
263 c.set_pattern(pdfrum_object::Name::from("P0"), &[], None);
264 let argb = resolve_argb(
265 &c,
266 1.0,
267 None,
268 None,
269 &RenderOptions::default(),
270 ObjectKind::Text,
271 false,
272 );
273 assert!(argb.is_invisible(), "got {argb:?}");
274 }
275
276 #[test]
277 fn a_pattern_colour_that_resolves_keeps_its_colour() {
278 let mut c = ColorValue::default();
281 c.set_space(Arc::new(ColorSpace::Pattern(Box::new(
282 pdfrum_page::PatternSpace {
283 base: Some(Box::new(ColorSpace::DeviceRgb)),
284 },
285 ))));
286 c.set_pattern(pdfrum_object::Name::from("P0"), &[1.0, 0.0, 0.0], None);
287 let argb = resolve_argb(
288 &c,
289 1.0,
290 None,
291 None,
292 &RenderOptions::default(),
293 ObjectKind::Text,
294 false,
295 );
296 assert_eq!(argb, Argb::opaque(255, 0, 0));
297 }
298
299 #[test]
300 fn a_white_fill_is_white_and_not_the_invisibility_sentinel() {
301 let opts = RenderOptions::default();
310 let c = resolve_argb(&gray(1.0), 1.0, None, None, &opts, ObjectKind::Path, false);
311 assert_eq!(c, Argb::opaque(255, 255, 255));
312 assert!(!c.is_invisible());
313 }
314
315 #[test]
316 fn a_colour_that_will_not_resolve_inherits_rather_than_vanishing() {
317 let none = ColorValue {
321 space: Some(Arc::new(ColorSpace::Separation(Box::new(
322 pdfrum_page::Separation {
323 none: true,
324 alternate: None,
325 tint: None,
326 },
327 )))),
328 components: SmallVec::from_slice(&[1.0]),
329 pattern: None,
330 };
331 let opts = RenderOptions::default();
332 assert_eq!(
333 resolve_argb(&none, 1.0, None, None, &opts, ObjectKind::Path, false),
334 Argb::BLACK
335 );
336 assert_eq!(
337 resolve_argb(
338 &none,
339 1.0,
340 None,
341 Some(Argb::opaque(9, 8, 7)),
342 &opts,
343 ObjectKind::Path,
344 false
345 ),
346 Argb::opaque(9, 8, 7)
347 );
348 }
349
350 #[test]
351 fn alpha_truncates() {
352 let opts = RenderOptions::default();
354 let c = resolve_argb(&gray(0.0), 0.5, None, None, &opts, ObjectKind::Path, false);
355 assert_eq!(c.a, 127);
356 }
357
358 #[test]
359 fn missing_color_inherits_from_the_enclosing_state() {
360 let opts = RenderOptions::default();
361 let empty = ColorValue {
362 space: None,
363 components: SmallVec::new(),
364 ..Default::default()
365 };
366 let inherited = Argb::opaque(10, 20, 30);
367 let c = resolve_argb(
368 &empty,
369 1.0,
370 None,
371 Some(inherited),
372 &opts,
373 ObjectKind::Path,
374 false,
375 );
376 assert_eq!((c.r, c.g, c.b), (10, 20, 30));
377 }
378
379 #[test]
380 fn gray_mode_uses_ntsc_weights() {
381 let opts = RenderOptions {
382 color_mode: ColorMode::Gray,
383 ..RenderOptions::default()
384 };
385 let mut red = ColorValue::default();
386 red.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.0, 0.0]);
387 let c = resolve_argb(&red, 1.0, None, None, &opts, ObjectKind::Path, false);
388 assert_eq!((c.r, c.g, c.b), (76, 76, 76));
390 }
391
392 #[test]
393 fn forced_scheme_leaves_images_alone() {
394 let scheme = crate::options::ColorScheme {
395 path_fill: Argb::opaque(1, 2, 3),
396 path_stroke: Argb::opaque(4, 5, 6),
397 text_fill: Argb::opaque(7, 8, 9),
398 text_stroke: Argb::opaque(10, 11, 12),
399 };
400 let opts = RenderOptions {
401 color_mode: ColorMode::Forced(scheme),
402 ..RenderOptions::default()
403 };
404 let value = {
405 let mut c = ColorValue::default();
406 c.set_space(Arc::new(ColorSpace::DeviceRgb));
407 let _ = c.set_components(&[0.0, 0.0, 0.0]);
408 c
409 };
410 let path = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Path, false);
411 assert_eq!((path.r, path.g, path.b), (1, 2, 3));
412 let other = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Other, false);
413 assert_eq!((other.r, other.g, other.b), (0, 0, 0));
414 }
415
416 #[test]
417 fn rgb_to_gray_is_the_oracle_formula() {
418 assert_eq!(rgb_to_gray(255, 255, 255), 255);
419 assert_eq!(rgb_to_gray(0, 0, 0), 0);
420 assert_eq!(rgb_to_gray(255, 0, 0), 76);
421 assert_eq!(rgb_to_gray(0, 255, 0), 150);
422 assert_eq!(rgb_to_gray(0, 0, 255), 28);
423 }
424}