1use otf_pixels_core::{
16 ChannelLayout, ImageDescriptor, Op, PixelFormat, PixelsError, Region, Result, SampleKind, Tile,
17 TileMut,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq)]
28#[non_exhaustive]
29pub struct Modulate {
30 pub brightness: f32,
32 pub saturation: f32,
34 pub hue: f32,
36}
37
38impl Default for Modulate {
39 fn default() -> Self {
40 Self {
41 brightness: 1.0,
42 saturation: 1.0,
43 hue: 0.0,
44 }
45 }
46}
47
48impl Modulate {
49 #[must_use]
51 pub fn identity() -> Self {
52 Self::default()
53 }
54
55 pub fn with_brightness(mut self, factor: f32) -> Result<Self> {
62 self.brightness = check_factor("brightness", factor)?;
63 Ok(self)
64 }
65
66 pub fn with_saturation(mut self, factor: f32) -> Result<Self> {
72 self.saturation = check_factor("saturation", factor)?;
73 Ok(self)
74 }
75
76 pub fn with_hue(mut self, degrees: f32) -> Result<Self> {
82 if !degrees.is_finite() {
83 return Err(PixelsError::invalid_argument(
84 "hue",
85 format!("rotation must be finite, got {degrees}"),
86 ));
87 }
88 self.hue = degrees;
89 Ok(self)
90 }
91
92 #[must_use]
94 pub fn is_identity(&self) -> bool {
95 self.brightness == 1.0 && self.saturation == 1.0 && self.hue % 360.0 == 0.0
96 }
97}
98
99fn check_factor(name: &'static str, factor: f32) -> Result<f32> {
101 if !factor.is_finite() || factor < 0.0 {
102 return Err(PixelsError::invalid_argument(
103 name,
104 format!("must be finite and non-negative, got {factor}"),
105 ));
106 }
107 Ok(factor)
108}
109
110fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
112 let max = r.max(g).max(b);
113 let min = r.min(g).min(b);
114 let delta = max - min;
115
116 let hue = if delta <= f32::EPSILON {
117 0.0
118 } else if max == r {
119 60.0 * (((g - b) / delta) % 6.0)
120 } else if max == g {
121 60.0 * ((b - r) / delta + 2.0)
122 } else {
123 60.0 * ((r - g) / delta + 4.0)
124 };
125 let saturation = if max <= f32::EPSILON {
126 0.0
127 } else {
128 delta / max
129 };
130 (hue, saturation, max)
131}
132
133fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> (f32, f32, f32) {
135 let hue = hue.rem_euclid(360.0);
136 let c = value * saturation;
137 let x = c * (1.0 - (((hue / 60.0) % 2.0) - 1.0).abs());
138 let m = value - c;
139 let (r, g, b) = match (hue / 60.0) as u32 {
140 0 => (c, x, 0.0),
141 1 => (x, c, 0.0),
142 2 => (0.0, c, x),
143 3 => (0.0, x, c),
144 4 => (x, 0.0, c),
145 _ => (c, 0.0, x),
146 };
147 (r + m, g + m, b + m)
148}
149
150fn modulate_rgb(m: &Modulate, r: f32, g: f32, b: f32) -> (f32, f32, f32) {
152 let (hue, saturation, value) = rgb_to_hsv(r, g, b);
153 hsv_to_rgb(
154 hue + m.hue,
155 (saturation * m.saturation).clamp(0.0, 1.0),
156 (value * m.brightness).clamp(0.0, 1.0),
157 )
158}
159
160impl Op for Modulate {
161 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
165 Some(std::sync::Arc::new(*self))
166 }
167 fn name(&self) -> &'static str {
168 "modulate"
169 }
170
171 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
172 let input = sole(self.name(), inputs)?;
173 Ok(*input)
174 }
175
176 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
177 Ok(vec![output])
178 }
179
180 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
181 let input = sole_tile(self.name(), inputs)?;
182 let format = output.pixel();
183 let region = output.region();
184 let channels = format.channels();
185 let colour = format.layout();
186
187 for y in region.y..region.y + region.height {
188 let Some(source) = input.row(y) else { continue };
189 let Some(target) = output.row_mut(y) else {
190 continue;
191 };
192 match format.sample_kind() {
193 SampleKind::U8 => modulate_row_u8(self, source, target, channels, colour),
194 SampleKind::U16 => modulate_row_u16(self, source, target, channels, colour),
195 SampleKind::F32 => modulate_row_f32(self, source, target, channels, colour),
196 }
197 }
198 Ok(())
199 }
200}
201
202const fn has_alpha(layout: ChannelLayout) -> bool {
204 matches!(layout, ChannelLayout::GrayAlpha | ChannelLayout::Rgba)
205}
206
207const fn is_gray(layout: ChannelLayout) -> bool {
209 matches!(layout, ChannelLayout::Gray | ChannelLayout::GrayAlpha)
210}
211
212fn modulate_row_u8(
213 m: &Modulate,
214 source: &[u8],
215 target: &mut [u8],
216 channels: usize,
217 layout: ChannelLayout,
218) {
219 let colour_channels = if has_alpha(layout) {
220 channels - 1
221 } else {
222 channels
223 };
224 for (from, to) in source
225 .chunks_exact(channels)
226 .zip(target.chunks_exact_mut(channels))
227 {
228 if is_gray(layout) {
229 let value = f32::from(from.first().copied().unwrap_or(0)) / 255.0;
232 let scaled = (value * m.brightness).clamp(0.0, 1.0);
233 if let Some(slot) = to.first_mut() {
234 *slot = (scaled * 255.0 + 0.5) as u8;
235 }
236 } else {
237 let r = f32::from(from.first().copied().unwrap_or(0)) / 255.0;
238 let g = f32::from(from.get(1).copied().unwrap_or(0)) / 255.0;
239 let b = f32::from(from.get(2).copied().unwrap_or(0)) / 255.0;
240 let (r, g, b) = modulate_rgb(m, r, g, b);
241 for (index, value) in [r, g, b].into_iter().enumerate().take(colour_channels) {
242 if let Some(slot) = to.get_mut(index) {
243 *slot = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
244 }
245 }
246 }
247 if has_alpha(layout) {
249 if let (Some(alpha), Some(slot)) = (from.get(channels - 1), to.get_mut(channels - 1)) {
250 *slot = *alpha;
251 }
252 }
253 }
254}
255
256fn modulate_row_u16(
257 m: &Modulate,
258 source: &[u8],
259 target: &mut [u8],
260 channels: usize,
261 layout: ChannelLayout,
262) {
263 let pixel_bytes = channels * 2;
264 for (from, to) in source
265 .chunks_exact(pixel_bytes)
266 .zip(target.chunks_exact_mut(pixel_bytes))
267 {
268 let read = |index: usize| -> f32 {
269 let at = index * 2;
270 let value = u16::from_ne_bytes([
271 from.get(at).copied().unwrap_or(0),
272 from.get(at + 1).copied().unwrap_or(0),
273 ]);
274 f32::from(value) / 65535.0
275 };
276 let mut write = |index: usize, value: f32| {
277 let scaled = (value.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
278 let at = index * 2;
279 for (offset, byte) in scaled.to_ne_bytes().iter().enumerate() {
280 if let Some(slot) = to.get_mut(at + offset) {
281 *slot = *byte;
282 }
283 }
284 };
285
286 if is_gray(layout) {
287 write(0, read(0) * m.brightness);
288 } else {
289 let (r, g, b) = modulate_rgb(m, read(0), read(1), read(2));
290 write(0, r);
291 write(1, g);
292 write(2, b);
293 }
294 if has_alpha(layout) {
295 let alpha = channels - 1;
296 let at = alpha * 2;
297 for offset in 0..2 {
298 if let (Some(byte), Some(slot)) = (from.get(at + offset), to.get_mut(at + offset)) {
299 *slot = *byte;
300 }
301 }
302 }
303 }
304}
305
306fn modulate_row_f32(
307 m: &Modulate,
308 source: &[u8],
309 target: &mut [u8],
310 channels: usize,
311 layout: ChannelLayout,
312) {
313 let pixel_bytes = channels * 4;
314 for (from, to) in source
315 .chunks_exact(pixel_bytes)
316 .zip(target.chunks_exact_mut(pixel_bytes))
317 {
318 let read = |index: usize| -> f32 {
319 let at = index * 4;
320 let mut bytes = [0_u8; 4];
321 for (slot, offset) in bytes.iter_mut().zip(0..4) {
322 *slot = from.get(at + offset).copied().unwrap_or(0);
323 }
324 f32::from_ne_bytes(bytes)
325 };
326 let mut write = |index: usize, value: f32| {
327 let at = index * 4;
328 for (offset, byte) in value.to_ne_bytes().iter().enumerate() {
329 if let Some(slot) = to.get_mut(at + offset) {
330 *slot = *byte;
331 }
332 }
333 };
334
335 if is_gray(layout) {
336 write(0, read(0) * m.brightness);
337 } else {
338 let (r, g, b) = modulate_rgb(m, read(0), read(1), read(2));
339 write(0, r);
340 write(1, g);
341 write(2, b);
342 }
343 if has_alpha(layout) {
344 let at = (channels - 1) * 4;
345 for offset in 0..4 {
346 if let (Some(byte), Some(slot)) = (from.get(at + offset), to.get_mut(at + offset)) {
347 *slot = *byte;
348 }
349 }
350 }
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
356pub struct ExtractChannel {
357 index: usize,
358}
359
360impl ExtractChannel {
361 #[must_use]
366 pub const fn new(index: usize) -> Self {
367 Self { index }
368 }
369
370 #[must_use]
372 pub const fn index(&self) -> usize {
373 self.index
374 }
375}
376
377impl Op for ExtractChannel {
378 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
382 Some(std::sync::Arc::new(*self))
383 }
384 fn name(&self) -> &'static str {
385 "extract_channel"
386 }
387
388 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
389 let input = sole(self.name(), inputs)?;
390 if self.index >= input.pixel.channels() {
391 return Err(PixelsError::invalid_argument(
392 "index",
393 format!(
394 "channel {} does not exist in {} ({} channels)",
395 self.index,
396 input.pixel,
397 input.pixel.channels()
398 ),
399 ));
400 }
401 let gray = match input.pixel.sample_kind() {
402 SampleKind::U8 => PixelFormat::Gray8,
403 SampleKind::U16 => PixelFormat::Gray16,
404 SampleKind::F32 => {
407 return Err(PixelsError::unsupported(format!(
408 "cannot extract a channel from {}: v1 has no float greyscale format",
409 input.pixel
410 )));
411 }
412 };
413 let mut out = *input;
414 out.pixel = gray;
415 Ok(out)
416 }
417
418 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
419 Ok(vec![output])
420 }
421
422 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
423 let input = sole_tile(self.name(), inputs)?;
424 let region = output.region();
425 let in_channels = input.pixel().channels();
426 let sample = input.pixel().sample_kind().size();
427
428 for y in region.y..region.y + region.height {
429 let Some(source) = input.row(y) else { continue };
430 let Some(target) = output.row_mut(y) else {
431 continue;
432 };
433 for (pixel, slot) in source
434 .chunks_exact(in_channels * sample)
435 .zip(target.chunks_exact_mut(sample))
436 {
437 let at = self.index * sample;
438 let Some(from) = pixel.get(at..at + sample) else {
439 continue;
440 };
441 slot.copy_from_slice(from);
442 }
443 }
444 Ok(())
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
450pub struct Flatten {
451 background: [u8; 3],
452}
453
454impl Flatten {
455 #[must_use]
457 pub const fn onto(red: u8, green: u8, blue: u8) -> Self {
458 Self {
459 background: [red, green, blue],
460 }
461 }
462
463 #[must_use]
465 pub const fn black() -> Self {
466 Self::onto(0, 0, 0)
467 }
468
469 #[must_use]
471 pub const fn background(&self) -> [u8; 3] {
472 self.background
473 }
474}
475
476impl Op for Flatten {
477 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
481 Some(std::sync::Arc::new(*self))
482 }
483 fn name(&self) -> &'static str {
484 "flatten"
485 }
486
487 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
488 let input = sole(self.name(), inputs)?;
489 let opaque = match input.pixel {
490 PixelFormat::GrayA8 => PixelFormat::Gray8,
491 PixelFormat::Rgba8 => PixelFormat::Rgb8,
492 PixelFormat::Rgba16 => PixelFormat::Rgb16,
493 PixelFormat::RgbaF32 => PixelFormat::RgbF32,
494 other => other,
497 };
498 let mut out = *input;
499 out.pixel = opaque;
500 Ok(out)
501 }
502
503 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
504 Ok(vec![output])
505 }
506
507 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
508 let input = sole_tile(self.name(), inputs)?;
509 let region = output.region();
510 let in_format = input.pixel();
511 let out_format = output.pixel();
512
513 if in_format == out_format {
514 for y in region.y..region.y + region.height {
516 let (Some(source), Some(target)) = (input.row(y), output.row_mut(y)) else {
517 continue;
518 };
519 let len = target.len().min(source.len());
520 if let (Some(from), Some(to)) = (source.get(..len), target.get_mut(..len)) {
521 to.copy_from_slice(from);
522 }
523 }
524 return Ok(());
525 }
526
527 if in_format.sample_kind() != SampleKind::U8 {
528 return Err(PixelsError::unsupported(format!(
529 "flatten is implemented for 8-bit input; got {in_format}"
530 )));
531 }
532
533 let in_channels = in_format.channels();
534 let out_channels = out_format.channels();
535 for y in region.y..region.y + region.height {
536 let (Some(source), Some(target)) = (input.row(y), output.row_mut(y)) else {
537 continue;
538 };
539 for (pixel, slot) in source
540 .chunks_exact(in_channels)
541 .zip(target.chunks_exact_mut(out_channels))
542 {
543 let alpha = u32::from(pixel.get(in_channels - 1).copied().unwrap_or(255));
544 for channel in 0..out_channels {
545 let foreground = u32::from(pixel.get(channel).copied().unwrap_or(0));
546 let background = u32::from(self.background.get(channel).copied().unwrap_or(0));
547 let blended = (foreground * alpha + background * (255 - alpha) + 127) / 255;
550 if let Some(out) = slot.get_mut(channel) {
551 *out = blended as u8;
552 }
553 }
554 }
555 }
556 Ok(())
557 }
558}
559
560fn sole<'a>(op: &str, inputs: &'a [ImageDescriptor]) -> Result<&'a ImageDescriptor> {
562 inputs
563 .first()
564 .ok_or_else(|| PixelsError::graph(format!("`{op}` takes one input, got none")))
565}
566
567fn sole_tile<'a, 'b>(op: &str, inputs: &'a [Tile<'b>]) -> Result<&'a Tile<'b>> {
569 inputs
570 .first()
571 .ok_or_else(|| PixelsError::graph(format!("`{op}` takes one input tile, got none")))
572}
573
574#[cfg(test)]
575#[allow(
576 clippy::unwrap_used,
577 clippy::expect_used,
578 clippy::indexing_slicing,
579 clippy::panic,
580 reason = "tests operate on known-good values and assert shapes directly"
581)]
582mod tests {
583 use super::*;
584 use otf_pixels_core::TileBuf;
585
586 fn apply(
588 op: &dyn Op,
589 input: &ImageDescriptor,
590 bytes: &[u8],
591 ) -> Result<(ImageDescriptor, Vec<u8>)> {
592 let out_desc = op.output_descriptor(std::slice::from_ref(input))?;
593 let source = TileBuf::from_vec(input.region(), input.pixel, bytes.to_vec())?;
594 let mut target = TileBuf::for_image(&out_desc)?;
595 op.compute(&[source.as_tile()?], &mut target.as_tile_mut()?)?;
596 Ok((out_desc, target.into_bytes()))
597 }
598
599 fn image(
600 width: u32,
601 height: u32,
602 format: PixelFormat,
603 fill: &[u8],
604 ) -> (ImageDescriptor, Vec<u8>) {
605 let descriptor = ImageDescriptor::new(width, height, format).unwrap();
606 let len = descriptor.byte_len().unwrap();
607 let bytes = (0..len).map(|i| fill[i % fill.len()]).collect();
608 (descriptor, bytes)
609 }
610
611 #[test]
616 fn the_identity_modulation_changes_nothing() {
617 for format in [
620 PixelFormat::Gray8,
621 PixelFormat::Rgb8,
622 PixelFormat::Rgba8,
623 PixelFormat::Rgb16,
624 PixelFormat::Rgba16,
625 ] {
626 let (desc, bytes) = image(8, 8, format, &[13, 200, 77, 255, 4, 91]);
627 let (_, out) = apply(&Modulate::identity(), &desc, &bytes).unwrap();
628 assert_eq!(out, bytes, "{format} changed under the identity");
629 }
630 }
631
632 #[test]
633 fn hsv_round_trips_for_every_colour() {
634 for r in 0..8 {
637 for g in 0..8 {
638 for b in 0..8 {
639 let (rf, gf, bf) = (r as f32 / 7.0, g as f32 / 7.0, b as f32 / 7.0);
640 let (h, s, v) = rgb_to_hsv(rf, gf, bf);
641 let (r2, g2, b2) = hsv_to_rgb(h, s, v);
642 assert!(
643 (rf - r2).abs() < 1e-4 && (gf - g2).abs() < 1e-4 && (bf - b2).abs() < 1e-4,
644 "({rf},{gf},{bf}) -> ({h},{s},{v}) -> ({r2},{g2},{b2})"
645 );
646 }
647 }
648 }
649 }
650
651 #[test]
652 fn zero_saturation_produces_grey() {
653 let (desc, bytes) = image(4, 4, PixelFormat::Rgb8, &[200, 50, 30]);
654 let m = Modulate::identity().with_saturation(0.0).unwrap();
655 let (_, out) = apply(&m, &desc, &bytes).unwrap();
656 for pixel in out.chunks_exact(3) {
657 assert_eq!(pixel[0], pixel[1], "not grey: {pixel:?}");
658 assert_eq!(pixel[1], pixel[2], "not grey: {pixel:?}");
659 }
660 }
661
662 #[test]
663 fn brightness_scales_and_saturates_rather_than_wrapping() {
664 let (desc, bytes) = image(4, 4, PixelFormat::Rgb8, &[200, 200, 200]);
666 let m = Modulate::identity().with_brightness(2.0).unwrap();
667 let (_, out) = apply(&m, &desc, &bytes).unwrap();
668 assert!(
669 out.iter().all(|&v| v == 255),
670 "expected white, got {:?}",
671 &out[..3]
672 );
673
674 let m = Modulate::identity().with_brightness(0.5).unwrap();
675 let (_, dim) = apply(&m, &desc, &bytes).unwrap();
676 assert!(
677 dim.iter().all(|&v| v == 100),
678 "expected 100, got {:?}",
679 &dim[..3]
680 );
681 }
682
683 #[test]
684 fn a_full_hue_rotation_is_the_identity() {
685 let (desc, bytes) = image(4, 4, PixelFormat::Rgb8, &[200, 50, 30]);
686 let m = Modulate::identity().with_hue(360.0).unwrap();
687 let (_, out) = apply(&m, &desc, &bytes).unwrap();
688 for (a, b) in out.iter().zip(&bytes) {
689 assert!(a.abs_diff(*b) <= 1, "360 degrees changed {b} to {a}");
690 }
691 }
692
693 #[test]
694 fn alpha_passes_through_untouched() {
695 let (desc, bytes) = image(4, 4, PixelFormat::Rgba8, &[200, 50, 30, 137]);
698 let m = Modulate::identity()
699 .with_brightness(0.25)
700 .unwrap()
701 .with_saturation(2.0)
702 .unwrap();
703 let (_, out) = apply(&m, &desc, &bytes).unwrap();
704 for pixel in out.chunks_exact(4) {
705 assert_eq!(pixel[3], 137, "alpha was modulated");
706 }
707 }
708
709 #[test]
710 fn greyscale_takes_brightness_but_not_hue() {
711 let (desc, bytes) = image(4, 4, PixelFormat::Gray8, &[100]);
712 let m = Modulate::identity()
713 .with_brightness(0.5)
714 .unwrap()
715 .with_hue(180.0)
716 .unwrap();
717 let (_, out) = apply(&m, &desc, &bytes).unwrap();
718 assert!(out.iter().all(|&v| v == 50), "got {:?}", &out[..4]);
719 }
720
721 #[test]
722 fn a_non_finite_factor_is_an_error_not_a_poisoned_image() {
723 assert!(Modulate::identity().with_brightness(f32::NAN).is_err());
724 assert!(Modulate::identity().with_brightness(f32::INFINITY).is_err());
725 assert!(Modulate::identity().with_brightness(-1.0).is_err());
726 assert!(Modulate::identity().with_saturation(f32::NAN).is_err());
727 assert!(Modulate::identity().with_hue(f32::NAN).is_err());
728 }
729
730 #[test]
735 fn extracting_a_channel_gives_that_channel() {
736 let (desc, bytes) = image(4, 4, PixelFormat::Rgba8, &[10, 20, 30, 40]);
737 for index in 0..4 {
738 let (out_desc, out) = apply(&ExtractChannel::new(index), &desc, &bytes).unwrap();
739 assert_eq!(out_desc.pixel, PixelFormat::Gray8);
740 let expected = (index as u8 + 1) * 10;
741 assert!(
742 out.iter().all(|&v| v == expected),
743 "channel {index} gave {:?}",
744 &out[..4]
745 );
746 }
747 }
748
749 #[test]
750 fn extracting_sixteen_bit_channels_preserves_precision() {
751 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Rgb16).unwrap();
752 let mut bytes = Vec::new();
753 for _ in 0..16 {
754 for value in [1000_u16, 30000, 65535] {
755 bytes.extend_from_slice(&value.to_ne_bytes());
756 }
757 }
758 let (out_desc, out) = apply(&ExtractChannel::new(1), &descriptor, &bytes).unwrap();
759 assert_eq!(out_desc.pixel, PixelFormat::Gray16);
760 for pair in out.chunks_exact(2) {
761 assert_eq!(u16::from_ne_bytes([pair[0], pair[1]]), 30000);
762 }
763 }
764
765 #[test]
766 fn extracting_a_channel_that_does_not_exist_is_an_error() {
767 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
768 let error = ExtractChannel::new(3)
769 .output_descriptor(std::slice::from_ref(&descriptor))
770 .unwrap_err();
771 assert!(error.to_string().contains("does not exist"), "{error}");
772 }
773
774 #[test]
775 fn extracting_from_a_float_image_is_unsupported_not_wrong() {
776 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::RgbF32).unwrap();
779 assert!(
780 ExtractChannel::new(0)
781 .output_descriptor(std::slice::from_ref(&descriptor))
782 .is_err()
783 );
784 }
785
786 #[test]
791 fn flattening_an_opaque_pixel_keeps_its_colour() {
792 let (desc, bytes) = image(4, 4, PixelFormat::Rgba8, &[200, 100, 50, 255]);
793 let (out_desc, out) = apply(&Flatten::black(), &desc, &bytes).unwrap();
794 assert_eq!(out_desc.pixel, PixelFormat::Rgb8);
795 for pixel in out.chunks_exact(3) {
796 assert_eq!(pixel, [200, 100, 50], "opaque pixel changed");
797 }
798 }
799
800 #[test]
801 fn flattening_a_transparent_pixel_gives_the_background() {
802 let (desc, bytes) = image(4, 4, PixelFormat::Rgba8, &[200, 100, 50, 0]);
803 let (_, out) = apply(&Flatten::onto(9, 8, 7), &desc, &bytes).unwrap();
804 for pixel in out.chunks_exact(3) {
805 assert_eq!(
806 pixel,
807 [9, 8, 7],
808 "transparent pixel did not take the background"
809 );
810 }
811 }
812
813 #[test]
814 fn flattening_at_half_alpha_is_the_midpoint() {
815 let (desc, bytes) = image(4, 4, PixelFormat::Rgba8, &[255, 255, 255, 128]);
817 let (_, out) = apply(&Flatten::black(), &desc, &bytes).unwrap();
818 for pixel in out.chunks_exact(3) {
819 assert_eq!(pixel, [128, 128, 128], "blend is not the midpoint");
820 }
821 }
822
823 #[test]
824 fn flattening_an_image_without_alpha_is_a_pass_through() {
825 let (desc, bytes) = image(4, 4, PixelFormat::Rgb8, &[1, 2, 3]);
827 let (out_desc, out) = apply(&Flatten::onto(200, 200, 200), &desc, &bytes).unwrap();
828 assert_eq!(out_desc.pixel, PixelFormat::Rgb8);
829 assert_eq!(out, bytes);
830 }
831
832 #[test]
833 fn flattening_greyscale_alpha_gives_greyscale() {
834 let (desc, bytes) = image(4, 4, PixelFormat::GrayA8, &[255, 0]);
835 let (out_desc, out) = apply(&Flatten::onto(30, 0, 0), &desc, &bytes).unwrap();
836 assert_eq!(out_desc.pixel, PixelFormat::Gray8);
837 assert!(out.iter().all(|&v| v == 30), "got {:?}", &out[..4]);
838 }
839}