1use std::sync::Arc;
15
16use otf_pixels_core::{
17 ChannelLayout, ImageDescriptor, Op, PixelFormat, PixelsError, Region, Result, Tile, TileMut,
18};
19
20use crate::convert::{from_rgba, to_rgba};
21
22fn luma([r, g, b, _]: [f32; 4]) -> f32 {
25 0.299 * r + 0.587 * g + 0.114 * b
26}
27
28const fn with_colour(format: PixelFormat) -> PixelFormat {
31 match format {
32 PixelFormat::Gray8 => PixelFormat::Rgb8,
33 PixelFormat::GrayA8 => PixelFormat::Rgba8,
34 PixelFormat::Gray16 => PixelFormat::Rgb16,
35 other => other,
36 }
37}
38
39trait PixelMap: Send + Sync + std::fmt::Debug {
42 fn name(&self) -> &'static str;
43
44 fn output(&self, input: PixelFormat) -> Result<PixelFormat> {
46 Ok(input)
47 }
48
49 fn map(&self, rgba: [f32; 4]) -> [f32; 4];
52}
53
54fn compute<M: PixelMap + ?Sized>(
56 map: &M,
57 inputs: &[Tile<'_>],
58 output: &mut TileMut<'_>,
59) -> Result<()> {
60 let input = inputs
61 .first()
62 .ok_or_else(|| PixelsError::graph(format!("`{}` takes one input tile", map.name())))?;
63 let (from, to) = (input.pixel(), output.pixel());
64 let (in_bytes, out_bytes) = (from.bytes_per_pixel(), to.bytes_per_pixel());
65 let region = output.region();
66 for y in region.y..region.y + region.height {
67 let (Some(source), Some(target)) = (input.row(y), output.row_mut(y)) else {
68 continue;
69 };
70 for (pixel, slot) in source
71 .chunks_exact(in_bytes)
72 .zip(target.chunks_exact_mut(out_bytes))
73 {
74 from_rgba(map.map(to_rgba(pixel, from)), slot, to);
75 }
76 }
77 Ok(())
78}
79
80macro_rules! pointwise_op {
83 ($type:ty) => {
84 impl Op for $type {
85 fn name(&self) -> &'static str {
86 PixelMap::name(self)
87 }
88
89 fn rescaled(&self) -> Option<Arc<dyn Op>> {
90 Some(Arc::new(self.clone()))
91 }
92
93 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
94 let [input] = inputs else {
95 return Err(PixelsError::graph(format!(
96 "`{}` takes exactly one input",
97 PixelMap::name(self)
98 )));
99 };
100 let mut out = *input;
101 out.pixel = self.output(input.pixel)?;
102 Ok(out)
103 }
104
105 fn input_regions(
106 &self,
107 output: Region,
108 _inputs: &[ImageDescriptor],
109 ) -> Result<Vec<Region>> {
110 Ok(vec![output])
111 }
112
113 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
114 compute(self, inputs, output)
115 }
116 }
117 };
118}
119
120fn finite(name: &'static str, value: f32) -> Result<f32> {
122 if value.is_finite() {
123 Ok(value)
124 } else {
125 Err(PixelsError::invalid_argument(
126 name,
127 format!("must be finite, got {value}"),
128 ))
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct Negate {
135 alpha: bool,
136}
137
138impl Negate {
139 #[must_use]
142 pub const fn new(alpha: bool) -> Self {
143 Self { alpha }
144 }
145}
146
147impl PixelMap for Negate {
148 fn name(&self) -> &'static str {
149 "negate"
150 }
151
152 fn map(&self, [r, g, b, a]: [f32; 4]) -> [f32; 4] {
153 [
154 1.0 - r,
155 1.0 - g,
156 1.0 - b,
157 if self.alpha { 1.0 - a } else { a },
158 ]
159 }
160}
161pointwise_op!(Negate);
162
163#[derive(Debug, Clone, Copy, PartialEq)]
170pub struct Gamma {
171 exponent: f32,
172}
173
174impl Gamma {
175 pub fn new(exponent: f32) -> Result<Self> {
182 if finite("exponent", exponent)? <= 0.0 {
183 return Err(PixelsError::invalid_argument(
184 "exponent",
185 format!("must be positive, got {exponent}"),
186 ));
187 }
188 Ok(Self { exponent })
189 }
190}
191
192impl PixelMap for Gamma {
193 fn name(&self) -> &'static str {
194 "gamma"
195 }
196
197 fn map(&self, [r, g, b, a]: [f32; 4]) -> [f32; 4] {
198 let power = 1.0 / self.exponent;
199 let apply = |v: f32| v.max(0.0).powf(power);
200 [apply(r), apply(g), apply(b), a]
201 }
202}
203pointwise_op!(Gamma);
204
205#[derive(Debug, Clone, Copy, PartialEq)]
209pub struct Linear {
210 multiply: [f32; 3],
211 add: [f32; 3],
212}
213
214impl Linear {
215 pub fn new(a: f32, b: f32) -> Result<Self> {
221 Self::per_channel([a; 3], [b; 3])
222 }
223
224 pub fn per_channel(a: [f32; 3], b: [f32; 3]) -> Result<Self> {
231 for value in a {
232 finite("a", value)?;
233 }
234 for value in b {
235 finite("b", value)?;
236 }
237 Ok(Self {
238 multiply: a,
239 add: b.map(|v| v / 255.0),
240 })
241 }
242}
243
244impl PixelMap for Linear {
245 fn name(&self) -> &'static str {
246 "linear"
247 }
248
249 fn map(&self, [r, g, b, a]: [f32; 4]) -> [f32; 4] {
250 let [mr, mg, mb] = self.multiply;
251 let [ar, ag, ab] = self.add;
252 [r * mr + ar, g * mg + ag, b * mb + ab, a]
253 }
254}
255pointwise_op!(Linear);
256
257#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct Threshold {
266 level: f32,
267 greyscale: bool,
268}
269
270impl Threshold {
271 #[must_use]
273 pub fn new(level: u8, greyscale: bool) -> Self {
274 Self {
275 level: f32::from(level) / 255.0,
276 greyscale,
277 }
278 }
279}
280
281impl PixelMap for Threshold {
282 fn name(&self) -> &'static str {
283 "threshold"
284 }
285
286 fn map(&self, rgba: [f32; 4]) -> [f32; 4] {
287 let step = |v: f32| {
290 if (v * 255.0).round() >= (self.level * 255.0).round() {
291 1.0
292 } else {
293 0.0
294 }
295 };
296 let [r, g, b, a] = rgba;
297 if self.greyscale {
298 let value = step(luma(rgba));
299 [value, value, value, a]
300 } else {
301 [step(r), step(g), step(b), a]
302 }
303 }
304}
305pointwise_op!(Threshold);
306
307#[derive(Debug, Clone, Copy, PartialEq)]
315pub struct Tint {
316 chroma: [f32; 2],
318}
319
320impl Tint {
321 #[must_use]
323 pub fn new(red: u8, green: u8, blue: u8) -> Self {
324 let rgba = [red, green, blue].map(|v| f32::from(v) / 255.0);
325 let [r, g, b] = rgba;
326 let y = luma([r, g, b, 1.0]);
327 Self {
328 chroma: [(b - y) * 0.564, (r - y) * 0.713],
329 }
330 }
331}
332
333impl PixelMap for Tint {
334 fn name(&self) -> &'static str {
335 "tint"
336 }
337
338 fn output(&self, input: PixelFormat) -> Result<PixelFormat> {
339 Ok(with_colour(input))
340 }
341
342 fn map(&self, rgba: [f32; 4]) -> [f32; 4] {
343 let y = luma(rgba);
344 let [cb, cr] = self.chroma;
345 [
346 y + 1.403 * cr,
347 y - 0.344 * cb - 0.714 * cr,
348 y + 1.773 * cb,
349 rgba[3],
350 ]
351 }
352}
353pointwise_op!(Tint);
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
359pub struct Greyscale;
360
361impl PixelMap for Greyscale {
362 fn name(&self) -> &'static str {
363 "greyscale"
364 }
365
366 fn map(&self, rgba: [f32; 4]) -> [f32; 4] {
367 let y = luma(rgba);
368 [y, y, y, rgba[3]]
369 }
370}
371pointwise_op!(Greyscale);
372
373#[derive(Debug, Clone, Copy, PartialEq)]
380pub struct Recomb {
381 matrix: [[f32; 4]; 4],
382 alpha: bool,
383}
384
385impl Recomb {
386 pub fn rgb(matrix: [[f32; 3]; 3]) -> Result<Self> {
392 let mut full = [[0.0; 4]; 4];
393 for (row, from) in full.iter_mut().zip(matrix) {
394 for (slot, value) in row.iter_mut().zip(from) {
395 *slot = finite("matrix", value)?;
396 }
397 }
398 full[3][3] = 1.0;
399 Ok(Self {
400 matrix: full,
401 alpha: false,
402 })
403 }
404
405 pub fn rgba(matrix: [[f32; 4]; 4]) -> Result<Self> {
411 for value in matrix.iter().flatten() {
412 finite("matrix", *value)?;
413 }
414 Ok(Self {
415 matrix,
416 alpha: true,
417 })
418 }
419}
420
421impl PixelMap for Recomb {
422 fn name(&self) -> &'static str {
423 "recomb"
424 }
425
426 fn output(&self, input: PixelFormat) -> Result<PixelFormat> {
427 let colour = with_colour(input);
428 Ok(if self.alpha {
429 with_alpha(colour)
430 } else {
431 colour
432 })
433 }
434
435 fn map(&self, rgba: [f32; 4]) -> [f32; 4] {
436 self.matrix.map(|row| {
437 row.iter()
438 .zip(rgba)
439 .map(|(weight, value)| weight * value)
440 .sum()
441 })
442 }
443}
444pointwise_op!(Recomb);
445
446const fn with_alpha(format: PixelFormat) -> PixelFormat {
448 match format {
449 PixelFormat::Gray8 => PixelFormat::GrayA8,
450 PixelFormat::Rgb8 => PixelFormat::Rgba8,
451 PixelFormat::Rgb16 => PixelFormat::Rgba16,
452 PixelFormat::RgbF32 => PixelFormat::RgbaF32,
453 PixelFormat::Gray16 => PixelFormat::Rgba16,
455 other => other,
456 }
457}
458
459#[derive(Debug, Clone, Copy, PartialEq)]
462pub struct EnsureAlpha {
463 level: f32,
464}
465
466impl EnsureAlpha {
467 pub fn new(level: f32) -> Result<Self> {
473 if !(0.0..=1.0).contains(&finite("level", level)?) {
474 return Err(PixelsError::invalid_argument(
475 "level",
476 format!("must be between 0 and 1, got {level}"),
477 ));
478 }
479 Ok(Self { level })
480 }
481}
482
483impl PixelMap for EnsureAlpha {
484 fn name(&self) -> &'static str {
485 "ensure_alpha"
486 }
487
488 fn output(&self, input: PixelFormat) -> Result<PixelFormat> {
489 Ok(with_alpha(input))
490 }
491
492 fn map(&self, rgba: [f32; 4]) -> [f32; 4] {
493 rgba
496 }
497}
498
499impl Op for EnsureAlpha {
500 fn name(&self) -> &'static str {
501 PixelMap::name(self)
502 }
503
504 fn rescaled(&self) -> Option<Arc<dyn Op>> {
505 Some(Arc::new(*self))
506 }
507
508 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
509 let [input] = inputs else {
510 return Err(PixelsError::graph("`ensure_alpha` takes exactly one input"));
511 };
512 let mut out = *input;
513 out.pixel = self.output(input.pixel)?;
514 Ok(out)
515 }
516
517 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
518 Ok(vec![output])
519 }
520
521 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
522 let had_alpha = inputs.first().is_some_and(|tile| {
523 matches!(
524 tile.pixel().layout(),
525 ChannelLayout::GrayAlpha | ChannelLayout::Rgba
526 )
527 });
528 if had_alpha {
529 return compute(self, inputs, output);
530 }
531 let level = self.level;
532 compute(&WithLevel(level), inputs, output)
533 }
534}
535
536#[derive(Debug)]
538struct WithLevel(f32);
539
540impl PixelMap for WithLevel {
541 fn name(&self) -> &'static str {
542 "ensure_alpha"
543 }
544
545 fn map(&self, [r, g, b, _]: [f32; 4]) -> [f32; 4] {
546 [r, g, b, self.0]
547 }
548}
549
550#[cfg(test)]
551#[allow(
552 clippy::unwrap_used,
553 clippy::indexing_slicing,
554 reason = "tests operate on known-good values and assert shapes directly"
555)]
556mod tests {
557 use super::*;
558 use otf_pixels_core::TileBuf;
559
560 fn run(op: &dyn Op, format: PixelFormat, width: u32, bytes: &[u8]) -> (PixelFormat, Vec<u8>) {
562 let height = (bytes.len() / format.bytes_per_pixel()) as u32 / width;
563 let input = ImageDescriptor::new(width, height, format).unwrap();
564 let out = op.output_descriptor(&[input]).unwrap();
565 let source = TileBuf::from_vec(input.region(), format, bytes.to_vec()).unwrap();
566 let mut target = TileBuf::for_image(&out).unwrap();
567 op.compute(
568 &[source.as_tile().unwrap()],
569 &mut target.as_tile_mut().unwrap(),
570 )
571 .unwrap();
572 (out.pixel, target.into_bytes())
573 }
574
575 #[test]
576 fn negate_inverts_colour_and_optionally_alpha() {
577 let (_, out) = run(
578 &Negate::new(true),
579 PixelFormat::Rgba8,
580 1,
581 &[0, 100, 255, 40],
582 );
583 assert_eq!(out, [255, 155, 0, 215]);
584 let (_, out) = run(
585 &Negate::new(false),
586 PixelFormat::Rgba8,
587 1,
588 &[0, 100, 255, 40],
589 );
590 assert_eq!(out, [255, 155, 0, 40]);
591 let (_, out) = run(
592 &Negate::new(true),
593 PixelFormat::Gray16,
594 1,
595 &1000_u16.to_ne_bytes(),
596 );
597 assert_eq!(u16::from_ne_bytes([out[0], out[1]]), 64535);
598 }
599
600 #[test]
601 fn gamma_brightens_above_one_and_leaves_the_ends_fixed() {
602 let op = Gamma::new(2.0).unwrap();
603 let (_, out) = run(&op, PixelFormat::Rgb8, 1, &[0, 64, 255]);
604 assert_eq!(out, [0, 128, 255]);
606 assert!(Gamma::new(0.0).is_err());
607 assert!(Gamma::new(f32::NAN).is_err());
608 }
609
610 #[test]
611 fn linear_multiplies_and_offsets_in_8_bit_units_and_clamps() {
612 let op = Linear::per_channel([2.0, 1.0, 0.5], [10.0, -20.0, 0.0]).unwrap();
613 let (_, out) = run(&op, PixelFormat::Rgba8, 1, &[100, 10, 200, 77]);
614 assert_eq!(out, [210, 0, 100, 77]);
615 let (_, out) = run(
616 &Linear::new(1.0, 255.0).unwrap(),
617 PixelFormat::Gray8,
618 1,
619 &[3],
620 );
621 assert_eq!(out, [255]);
622 }
623
624 #[test]
625 fn threshold_uses_luma_or_each_channel() {
626 let pixel = [200, 50, 50];
627 let (_, out) = run(&Threshold::new(128, true), PixelFormat::Rgb8, 1, &pixel);
629 assert_eq!(out, [0, 0, 0]);
630 let (_, out) = run(&Threshold::new(128, false), PixelFormat::Rgb8, 1, &pixel);
631 assert_eq!(out, [255, 0, 0]);
632 let (_, out) = run(
634 &Threshold::new(128, false),
635 PixelFormat::Gray8,
636 2,
637 &[127, 128],
638 );
639 assert_eq!(out, [0, 255]);
640 let half = 0x8080_u16.to_ne_bytes();
641 let (_, out) = run(&Threshold::new(128, false), PixelFormat::Gray16, 1, &half);
642 assert_eq!(out, [255, 255]);
643 }
644
645 #[test]
646 fn greyscale_keeps_three_equal_channels_and_alpha() {
647 let (format, out) = run(&Greyscale, PixelFormat::Rgba8, 1, &[200, 50, 50, 9]);
648 assert_eq!(format, PixelFormat::Rgba8);
649 assert_eq!(out, [95, 95, 95, 9]);
650 }
651
652 #[test]
653 fn tint_keeps_luma_and_takes_the_tints_hue() {
654 let (format, out) = run(&Tint::new(200, 100, 100), PixelFormat::Gray8, 1, &[128]);
657 assert_eq!(format, PixelFormat::Rgb8);
658 let [r, g, b] = [out[0], out[1], out[2]];
659 assert!(r > g && r > b, "a red tint should be red: {out:?}");
660 let y = 0.299 * f32::from(r) + 0.587 * f32::from(g) + 0.114 * f32::from(b);
661 assert!((y - 128.0).abs() <= 2.0, "luma moved to {y}");
662 let (_, out) = run(&Tint::new(90, 90, 90), PixelFormat::Rgb8, 1, &[70, 70, 70]);
664 assert_eq!(out, [70, 70, 70]);
665 }
666
667 #[test]
668 fn recomb_applies_the_matrix() {
669 let swap = Recomb::rgb([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]).unwrap();
670 let (_, out) = run(&swap, PixelFormat::Rgba8, 1, &[10, 20, 30, 40]);
671 assert_eq!(out, [30, 20, 10, 40]);
672 let alpha_from_red = Recomb::rgba([
673 [1.0, 0.0, 0.0, 0.0],
674 [0.0, 1.0, 0.0, 0.0],
675 [0.0, 0.0, 1.0, 0.0],
676 [1.0, 0.0, 0.0, 0.0],
677 ])
678 .unwrap();
679 let (format, out) = run(&alpha_from_red, PixelFormat::Rgb8, 1, &[10, 20, 30]);
680 assert_eq!(format, PixelFormat::Rgba8);
681 assert_eq!(out, [10, 20, 30, 10]);
682 assert!(Recomb::rgb([[f32::INFINITY; 3]; 3]).is_err());
683 }
684
685 #[test]
686 fn ensure_alpha_adds_a_level_only_when_missing() {
687 let op = EnsureAlpha::new(0.5).unwrap();
688 let (format, out) = run(&op, PixelFormat::Rgb8, 1, &[1, 2, 3]);
689 assert_eq!(format, PixelFormat::Rgba8);
690 assert_eq!(out, [1, 2, 3, 128]);
691 let (format, out) = run(&op, PixelFormat::Rgba8, 1, &[1, 2, 3, 4]);
692 assert_eq!(format, PixelFormat::Rgba8);
693 assert_eq!(out, [1, 2, 3, 4]);
694 let (format, _) = run(&op, PixelFormat::Gray16, 1, &[0, 0]);
695 assert_eq!(format, PixelFormat::Rgba16);
696 assert!(EnsureAlpha::new(1.5).is_err());
697 }
698}