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 pub const WHITE: Self = Self {
72 a: 255,
73 r: 255,
74 g: 255,
75 b: 255,
76 };
77
78 #[must_use]
80 pub const fn new(a: u8, r: u8, g: u8, b: u8) -> Self {
81 Self { a, r, g, b }
82 }
83
84 #[must_use]
86 pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
87 Self { a: 255, r, g, b }
88 }
89
90 #[must_use]
94 pub fn is_invisible(self) -> bool {
95 self.a == 0 && self.r == 0 && self.g == 0 && self.b == 0
96 }
97
98 #[must_use]
100 pub fn to_peniko(self) -> peniko::Color {
101 peniko::Color::from_rgba8(self.r, self.g, self.b, self.a)
102 }
103
104 #[must_use]
106 pub const fn with_alpha(self, a: u8) -> Self {
107 Self { a, ..self }
108 }
109}
110
111#[must_use]
117pub fn rgb_to_gray(r: u8, g: u8, b: u8) -> u8 {
118 #[expect(
121 clippy::cast_possible_truncation,
122 reason = "weights summing to 100 bound the quotient to 0..=255"
123 )]
124 let gray = ((u32::from(b) * 11 + u32::from(g) * 59 + u32::from(r) * 30) / 100) as u8;
125 gray
126}
127
128#[must_use]
130fn translate_color(mode: ColorMode, c: Argb) -> Argb {
131 match mode {
132 ColorMode::Normal | ColorMode::Alpha => c,
133 ColorMode::Gray | ColorMode::Forced(_) => {
134 let g = rgb_to_gray(c.r, c.g, c.b);
135 Argb {
136 a: c.a,
137 r: g,
138 g,
139 b: g,
140 }
141 }
142 }
143}
144
145#[must_use]
147fn translate_object_color(opts: &RenderOptions, c: Argb, kind: ObjectKind, stroking: bool) -> Argb {
148 let ColorMode::Forced(scheme) = opts.color_mode else {
149 return translate_color(opts.color_mode, c);
150 };
151 let replacement = match (kind, stroking) {
152 (ObjectKind::Path, false) => scheme.path_fill,
153 (ObjectKind::Path, true) => scheme.path_stroke,
154 (ObjectKind::Text, false) => scheme.text_fill,
155 (ObjectKind::Text, true) => scheme.text_stroke,
156 (ObjectKind::Other, _) => return c,
157 };
158 Argb {
159 a: c.a,
160 ..replacement
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq)]
176enum ColorRef {
177 Resolved(Rgb),
179 Invisible,
181 Missing,
183}
184
185#[must_use]
195fn color_ref(value: &ColorValue) -> ColorRef {
196 if let Some(rgb) = value.to_rgb() {
197 return ColorRef::Resolved(rgb);
198 }
199 let Some(pattern) = value.pattern.as_ref() else {
200 return ColorRef::Missing;
201 };
202 let colored_tiling = matches!(
203 pattern.loaded.as_deref(),
204 Some(pdfrum_page::Pattern::Tiling(t)) if t.colored
205 );
206 if colored_tiling {
207 ColorRef::Resolved(Rgb {
210 r: 191.0 / 255.0,
211 g: 191.0 / 255.0,
212 b: 191.0 / 255.0,
213 })
214 } else {
215 ColorRef::Invisible
216 }
217}
218
219#[must_use]
226pub fn resolve_argb(
227 value: &ColorValue,
228 alpha: f32,
229 transfer: Option<&TransferFunc>,
230 inherited: Option<Argb>,
231 opts: &RenderOptions,
232 kind: ObjectKind,
233 stroking: bool,
234) -> Argb {
235 let base = match color_ref(value) {
236 ColorRef::Resolved(rgb) => {
237 let [r, g, b] = rgb.to_bytes();
238 Argb { a: 255, r, g, b }
239 }
240 ColorRef::Invisible => return Argb::TRANSPARENT,
241 ColorRef::Missing => inherited.unwrap_or(Argb::BLACK),
244 };
245
246 let a = alpha_byte_truncating(alpha);
247 let transferred = match transfer {
248 Some(tf) if !tf.is_identity() => tf.translate(base),
249 _ => base,
250 };
251 translate_object_color(opts, transferred.with_alpha(a), kind, stroking)
252}
253
254#[cfg(test)]
255mod tests {
256 use std::sync::Arc;
257
258 use pdfrum_page::ColorSpace;
259 use smallvec::SmallVec;
260
261 use super::*;
262
263 fn gray(v: f32) -> ColorValue {
264 let mut c = ColorValue::default();
265 c.set_stock(ColorSpace::DeviceGray, &[v]);
266 c
267 }
268
269 #[test]
270 fn a_shading_pattern_colour_is_the_invisibility_sentinel() {
271 let mut c = ColorValue::default();
276 c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
277 c.set_pattern(pdfrum_object::Name::from("P0"), &[], None);
278 let argb = resolve_argb(
279 &c,
280 1.0,
281 None,
282 None,
283 &RenderOptions::default(),
284 ObjectKind::Text,
285 false,
286 );
287 assert!(argb.is_invisible(), "got {argb:?}");
288 }
289
290 #[test]
291 fn a_pattern_colour_that_resolves_keeps_its_colour() {
292 let mut c = ColorValue::default();
295 c.set_space(Arc::new(ColorSpace::Pattern(Box::new(
296 pdfrum_page::PatternSpace {
297 base: Some(Box::new(ColorSpace::DeviceRgb)),
298 },
299 ))));
300 c.set_pattern(pdfrum_object::Name::from("P0"), &[1.0, 0.0, 0.0], None);
301 let argb = resolve_argb(
302 &c,
303 1.0,
304 None,
305 None,
306 &RenderOptions::default(),
307 ObjectKind::Text,
308 false,
309 );
310 assert_eq!(argb, Argb::opaque(255, 0, 0));
311 }
312
313 #[test]
314 fn a_white_fill_is_white_and_not_the_invisibility_sentinel() {
315 let opts = RenderOptions::default();
324 let c = resolve_argb(&gray(1.0), 1.0, None, None, &opts, ObjectKind::Path, false);
325 assert_eq!(c, Argb::opaque(255, 255, 255));
326 assert!(!c.is_invisible());
327 }
328
329 #[test]
330 fn a_colour_that_will_not_resolve_inherits_rather_than_vanishing() {
331 let none = ColorValue {
335 space: Some(Arc::new(ColorSpace::Separation(Box::new(
336 pdfrum_page::Separation {
337 none: true,
338 alternate: None,
339 tint: None,
340 },
341 )))),
342 components: SmallVec::from_slice(&[1.0]),
343 pattern: None,
344 };
345 let opts = RenderOptions::default();
346 assert_eq!(
347 resolve_argb(&none, 1.0, None, None, &opts, ObjectKind::Path, false),
348 Argb::BLACK
349 );
350 assert_eq!(
351 resolve_argb(
352 &none,
353 1.0,
354 None,
355 Some(Argb::opaque(9, 8, 7)),
356 &opts,
357 ObjectKind::Path,
358 false
359 ),
360 Argb::opaque(9, 8, 7)
361 );
362 }
363
364 #[test]
365 fn alpha_truncates() {
366 let opts = RenderOptions::default();
368 let c = resolve_argb(&gray(0.0), 0.5, None, None, &opts, ObjectKind::Path, false);
369 assert_eq!(c.a, 127);
370 }
371
372 #[test]
373 fn missing_color_inherits_from_the_enclosing_state() {
374 let opts = RenderOptions::default();
375 let empty = ColorValue {
376 space: None,
377 components: SmallVec::new(),
378 ..Default::default()
379 };
380 let inherited = Argb::opaque(10, 20, 30);
381 let c = resolve_argb(
382 &empty,
383 1.0,
384 None,
385 Some(inherited),
386 &opts,
387 ObjectKind::Path,
388 false,
389 );
390 assert_eq!((c.r, c.g, c.b), (10, 20, 30));
391 }
392
393 #[test]
394 fn gray_mode_uses_ntsc_weights() {
395 let opts = RenderOptions {
396 color_mode: ColorMode::Gray,
397 ..RenderOptions::default()
398 };
399 let mut red = ColorValue::default();
400 red.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.0, 0.0]);
401 let c = resolve_argb(&red, 1.0, None, None, &opts, ObjectKind::Path, false);
402 assert_eq!((c.r, c.g, c.b), (76, 76, 76));
404 }
405
406 #[test]
407 fn forced_scheme_leaves_images_alone() {
408 let scheme = crate::options::ColorScheme {
409 path_fill: Argb::opaque(1, 2, 3),
410 path_stroke: Argb::opaque(4, 5, 6),
411 text_fill: Argb::opaque(7, 8, 9),
412 text_stroke: Argb::opaque(10, 11, 12),
413 };
414 let opts = RenderOptions {
415 color_mode: ColorMode::Forced(scheme),
416 ..RenderOptions::default()
417 };
418 let value = {
419 let mut c = ColorValue::default();
420 c.set_space(Arc::new(ColorSpace::DeviceRgb));
421 let _ = c.set_components(&[0.0, 0.0, 0.0]);
422 c
423 };
424 let path = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Path, false);
425 assert_eq!((path.r, path.g, path.b), (1, 2, 3));
426 let other = resolve_argb(&value, 1.0, None, None, &opts, ObjectKind::Other, false);
427 assert_eq!((other.r, other.g, other.b), (0, 0, 0));
428 }
429
430 #[test]
431 fn rgb_to_gray_is_the_oracle_formula() {
432 assert_eq!(rgb_to_gray(255, 255, 255), 255);
433 assert_eq!(rgb_to_gray(0, 0, 0), 0);
434 assert_eq!(rgb_to_gray(255, 0, 0), 76);
435 assert_eq!(rgb_to_gray(0, 255, 0), 150);
436 assert_eq!(rgb_to_gray(0, 0, 255), 28);
437 }
438}