1#![cfg_attr(feature = "raw", doc = "```")]
13#![cfg_attr(not(feature = "raw"), doc = "```ignore")]
14#![cfg_attr(all(feature = "png", feature = "webp"), doc = "```")]
72#![cfg_attr(not(all(feature = "png", feature = "webp")), doc = "```ignore")]
73use otf_pixels_core::{BufferSource, Op, Prefixed, Producer, TileBuf};
127use std::sync::Arc;
128
129pub use otf_pixels_core::{
130 AccessPattern, Animation, ChannelLayout, ChromaSubsampling, Codec, ColorModel, Decoder,
131 EncodeOptions, Encoder, ErrorCode, Format, ImageDescriptor, Limit, Limits, Metadata,
132 Orientation, PixelFormat, PixelsError, PlanOptions, Region, Result, RunStats, SampleKind,
133 Scheduler, SchedulerOptions, Sink, Source, TileShape, evaluate as evaluate_reference,
134};
135pub use otf_pixels_ops::{
136 Blend, Composite, Conversion, ConvertFormat, Convolve, Crop, EnsureAlpha, Extend, ExtendWith,
137 ExtractChannel, Filter, Fit, Flatten, Flip, Flop, Gamma, Greyscale, Kernel, Linear, Modulate,
138 Negate, Placement, Position, Quarter, Recomb, Resize, ResizeOptions, Rotate, Threshold, Tint,
139 ToSrgb, Trim, Unconvertible,
140};
141
142#[cfg(feature = "raw")]
143pub use otf_pixels_codec_raw::{RawCodec, RawDecoder, RawEncoder, RawFormat};
144
145#[cfg(feature = "png")]
146pub use otf_pixels_codec_png::{PngCodec, PngDecoder, PngEncoder};
147
148#[cfg(feature = "gif")]
149pub use otf_pixels_codec_gif::{GifCodec, GifDecoder, GifEncoder};
150
151#[cfg(feature = "jpeg")]
152pub use otf_pixels_codec_jpeg::{JpegCodec, JpegDecoder, JpegEncoder, Scale, Subsampling};
153
154#[cfg(feature = "tiff")]
155pub use otf_pixels_codec_tiff::{TiffCodec, TiffDecoder, TiffEncoder, TiffLayout};
156
157#[cfg(feature = "webp")]
158pub use otf_pixels_codec_webp::{WebPCodec, WebPDecoder, WebPEncoder};
159
160#[cfg(feature = "avif")]
161pub use otf_pixels_codec_avif::{AvifCodec, AvifDecoder, AvifEncoder};
162
163#[cfg(feature = "jxl")]
164pub use otf_pixels_codec_jxl::{JxlCodec, JxlDecoder, JxlEncoder};
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct OpenOptions {
170 pub auto_orient: bool,
178 pub to_srgb: bool,
187 pub animated: bool,
196 pub page: u32,
203 pub limits: Limits,
207}
208
209impl OpenOptions {
210 #[must_use]
215 pub const fn with_auto_orient(mut self, auto_orient: bool) -> Self {
216 self.auto_orient = auto_orient;
217 self
218 }
219
220 #[must_use]
222 pub const fn with_animated(mut self, animated: bool) -> Self {
223 self.animated = animated;
224 self
225 }
226
227 #[must_use]
230 pub const fn with_page(mut self, page: u32) -> Self {
231 self.page = page;
232 self
233 }
234
235 #[must_use]
237 pub const fn with_limits(mut self, limits: Limits) -> Self {
238 self.limits = limits;
239 self
240 }
241
242 #[must_use]
244 pub const fn with_to_srgb(mut self, to_srgb: bool) -> Self {
245 self.to_srgb = to_srgb;
246 self
247 }
248}
249
250impl Default for OpenOptions {
251 fn default() -> Self {
252 Self {
253 auto_orient: true,
254 to_srgb: true,
255 animated: false,
256 page: 0,
257 limits: Limits::default(),
258 }
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264#[non_exhaustive]
265pub enum Channel {
266 Red,
268 Green,
270 Blue,
272 Alpha,
274}
275
276#[derive(Debug, Clone)]
281pub struct Image {
282 inner: std::result::Result<otf_pixels_core::Image, Arc<PixelsError>>,
284 icc: Option<Arc<[u8]>>,
287 animation: Option<Arc<Animation>>,
289 pages: u32,
291 trim_offset: Option<(u32, u32)>,
293 exif: Option<Arc<[u8]>>,
295}
296
297impl Image {
298 pub fn from_raw(descriptor: ImageDescriptor, bytes: Vec<u8>) -> Result<Self> {
307 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes)?;
308 let source = BufferSource::new(descriptor, Arc::new(buffer))?;
309 Ok(Self::from_producer(Arc::new(source), Format::Raw))
310 }
311
312 #[cfg(feature = "raw")]
322 pub fn from_raw_stream(
323 layout: RawFormat,
324 source: impl Source + std::fmt::Debug + 'static,
325 ) -> Result<Self> {
326 let decoder = RawDecoder::new(layout, source)?;
327 Ok(Self::from_decoder(Box::new(decoder), Format::Raw))
328 }
329
330 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
344 Self::open_with(path, OpenOptions::default())
345 }
346
347 pub fn open_with(path: impl AsRef<std::path::Path>, options: OpenOptions) -> Result<Self> {
353 let path = path.as_ref();
354 let file = std::fs::File::open(path).map_err(|e| {
355 let kind = e.kind();
359 let detail = std::io::Error::new(kind, format!("{}: {e}", path.display()));
360 PixelsError::io("opening image file", detail)
361 })?;
362 Self::from_stream_with(std::io::BufReader::new(file), options)
363 }
364
365 pub fn from_stream(source: impl Source + std::fmt::Debug + 'static) -> Result<Self> {
381 Self::from_stream_with(source, OpenOptions::default())
382 }
383
384 pub fn from_stream_with(
390 mut source: impl Source + std::fmt::Debug + 'static,
391 options: OpenOptions,
392 ) -> Result<Self> {
393 let codecs = sniffing_codecs();
394 let longest = codecs.iter().map(|c| c.magic_len()).max().unwrap_or(0);
395
396 let mut prefix = Vec::with_capacity(longest);
400 let mut buffer = vec![0_u8; longest];
401 while prefix.len() < longest {
402 let Some(rest) = buffer.get_mut(prefix.len()..) else {
403 break;
404 };
405 match source.read(rest)? {
406 0 => break,
407 n => {
408 let Some(read) = rest.get(..n) else { break };
409 prefix.extend_from_slice(read);
410 }
411 }
412 }
413
414 let Some(codec) = codecs.iter().find(|codec| codec.probe(&prefix)) else {
415 return Err(PixelsError::unsupported(format!(
416 "no codec recognises this stream; its first {} bytes are {:02x?}",
417 prefix.len().min(8),
418 prefix.get(..prefix.len().min(8)).unwrap_or(&[])
419 )));
420 };
421 let stream = Prefixed::new(prefix, source);
422 if options.page > 0 && codec.format() != Format::Tiff {
424 return Err(PixelsError::invalid_argument(
425 "page",
426 format!(
427 "page {} was asked for, but a {} has only page 0",
428 options.page,
429 codec.format()
430 ),
431 ));
432 }
433 let _ = (&stream, options);
436
437 match codec.format() {
438 #[cfg(feature = "png")]
439 Format::Png => {
440 let decoder = PngDecoder::new(stream, options.limits)?;
441 Self::decoded(Box::new(decoder), Format::Png, options)
442 }
443 #[cfg(feature = "gif")]
444 Format::Gif => {
445 let decoder = GifDecoder::new(stream, options.limits)?;
446 Self::decoded(Box::new(decoder), Format::Gif, options)
447 }
448 #[cfg(feature = "jpeg")]
449 Format::Jpeg => {
450 let decoder = JpegDecoder::new(stream, options.limits)?;
451 Self::decoded(Box::new(decoder), Format::Jpeg, options)
452 }
453 #[cfg(feature = "tiff")]
454 Format::Tiff => {
455 let decoder = TiffDecoder::with_page(stream, options.limits, options.page)?;
456 Self::decoded(Box::new(decoder), Format::Tiff, options)
457 }
458 #[cfg(feature = "webp")]
459 Format::WebP => {
460 let decoder = WebPDecoder::new(stream, options.limits)?;
461 Self::decoded(Box::new(decoder), Format::WebP, options)
462 }
463 #[cfg(feature = "avif")]
464 Format::Avif => {
465 let decoder = AvifDecoder::new(stream, options.limits)?;
466 Self::decoded(Box::new(decoder), Format::Avif, options)
467 }
468 #[cfg(feature = "jxl")]
469 Format::Jxl => {
470 let decoder = JxlDecoder::new(stream, options.limits)?;
471 Self::decoded(Box::new(decoder), Format::Jxl, options)
472 }
473 other => Err(PixelsError::unsupported(format!(
474 "{other} was detected but no decoder for it is compiled in"
475 ))),
476 }
477 }
478
479 #[cfg(any(
484 feature = "png",
485 feature = "gif",
486 feature = "jpeg",
487 feature = "tiff",
488 feature = "webp",
489 feature = "avif",
490 feature = "jxl"
491 ))]
492 fn decoded(decoder: Box<dyn Decoder>, format: Format, options: OpenOptions) -> Result<Self> {
493 let orientation = decoder.orientation();
494 let icc = decoder.icc_profile().map(Vec::from);
495 let animation = decoder.animation().map(Arc::new);
496 let pages = decoder.pages();
497 let mut exif = decoder.exif().map(<[u8]>::to_vec);
498 if let Some(block) = exif.as_mut().filter(|_| options.auto_orient) {
501 Orientation::Normal.write_to_exif_block(block);
502 }
503 if options.animated && animation.is_some() {
504 return Err(PixelsError::unsupported(format!(
505 "{format}: multi-frame (animated) pipelines are not implemented yet; \
506 open with `animated` off to process the first frame"
507 )));
508 }
509 let mut image = Self::from_decoder(decoder, format).with_icc_profile(icc);
510 image.animation = animation;
511 image.pages = pages;
512 image.exif = exif.map(Arc::from);
513 if options.to_srgb {
514 image = image.to_srgb();
515 }
516 Ok(if options.auto_orient {
517 image.orient(orientation)
518 } else {
519 image
520 })
521 }
522
523 #[must_use]
529 pub fn from_decoder(decoder: Box<dyn Decoder>, format: Format) -> Self {
530 let source = otf_pixels_core::DecodedSource::new(decoder);
531 Self::from_producer(Arc::new(source), format)
532 }
533
534 #[must_use]
536 pub fn from_producer(producer: Arc<dyn Producer>, format: Format) -> Self {
537 Self {
538 icc: None,
539 animation: None,
540 pages: 1,
541 trim_offset: None,
542 exif: None,
543 inner: Ok(otf_pixels_core::Image::from_producer(producer, format)),
544 }
545 }
546
547 pub fn metadata(&self) -> Result<Metadata> {
556 self.graph()?.metadata()
557 }
558
559 pub fn descriptor(&self) -> Result<ImageDescriptor> {
565 Ok(self.graph()?.descriptor())
566 }
567
568 #[must_use]
572 pub fn crop(self, x: u32, y: u32, width: u32, height: u32) -> Self {
573 match Crop::at(x, y, width, height) {
574 Ok(op) => self.apply(Arc::new(op)),
575 Err(error) => Self::failed(error),
576 }
577 }
578
579 #[must_use]
581 pub fn flip(self) -> Self {
582 self.apply(Arc::new(Flip))
583 }
584
585 #[must_use]
587 pub fn flop(self) -> Self {
588 self.apply(Arc::new(Flop))
589 }
590
591 #[must_use]
593 pub fn resize(self, width: u32, height: u32) -> Self {
594 self.resize_with(width, height, ResizeOptions::default())
595 }
596
597 #[must_use]
599 pub fn resize_with(self, width: u32, height: u32, options: ResizeOptions) -> Self {
600 match Resize::new(width, height, options) {
601 Ok(op) => self.apply(Arc::new(op)),
602 Err(error) => Self::failed(error),
603 }
604 }
605
606 #[must_use]
608 pub fn thumbnail(self, width: u32, height: u32) -> Self {
609 let options = ResizeOptions::default()
610 .with_fit(Fit::Inside)
611 .without_enlargement(true);
612 self.resize_with(width, height, options)
613 }
614
615 #[must_use]
623 pub fn orient(self, orientation: Orientation) -> Self {
624 let turns = orientation.clockwise_turns();
625 let image = if turns == 0 {
626 self
627 } else {
628 self.rotate(90 * i32::from(turns))
629 };
630 if orientation.mirrored() {
631 image.flop()
632 } else {
633 image
634 }
635 }
636
637 #[must_use]
639 pub fn rotate(self, degrees: i32) -> Self {
640 match Rotate::degrees(degrees) {
641 Ok(op) => self.apply(Arc::new(op)),
642 Err(error) => Self::failed(error),
643 }
644 }
645
646 #[must_use]
648 pub fn modulate(self, options: Modulate) -> Self {
649 self.apply(Arc::new(options))
650 }
651
652 #[must_use]
654 pub fn convolve(self, kernel: Kernel) -> Self {
655 self.apply(Arc::new(Convolve::new(kernel)))
656 }
657
658 #[must_use]
660 pub fn blur(self, sigma: f32) -> Self {
661 match Kernel::gaussian(sigma) {
662 Ok(kernel) => self.convolve(kernel),
663 Err(error) => Self::failed(error),
664 }
665 }
666
667 #[must_use]
669 pub fn sharpen(self, amount: f32) -> Self {
670 match Kernel::sharpen(amount) {
671 Ok(kernel) => self.convolve(kernel),
672 Err(error) => Self::failed(error),
673 }
674 }
675
676 #[must_use]
678 pub fn extract_channel(self, index: usize) -> Self {
679 self.apply(Arc::new(ExtractChannel::new(index)))
680 }
681
682 #[must_use]
688 pub fn extract_channel_named(self, channel: Channel) -> Self {
689 let layout = match self.descriptor() {
690 Ok(descriptor) => descriptor.pixel.layout(),
691 Err(error) => return Self::failed(error),
692 };
693 let index = match (channel, layout) {
694 (Channel::Red, _) => Some(0),
695 (Channel::Green, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(1),
696 (Channel::Blue, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(2),
697 (Channel::Alpha, ChannelLayout::GrayAlpha) => Some(1),
698 (Channel::Alpha, ChannelLayout::Rgba) => Some(3),
699 _ => None,
700 };
701 match index {
702 Some(index) => self.extract_channel(index),
703 None => Self::failed(PixelsError::invalid_argument(
704 "channel",
705 format!("a {layout:?} image has no {channel:?} channel"),
706 )),
707 }
708 }
709
710 #[must_use]
713 pub fn negate(self, alpha: bool) -> Self {
714 self.apply(Arc::new(Negate::new(alpha)))
715 }
716
717 #[must_use]
723 pub fn gamma(self, exponent: f32) -> Self {
724 match Gamma::new(exponent) {
725 Ok(op) => self.apply(Arc::new(op)),
726 Err(error) => Self::failed(error),
727 }
728 }
729
730 #[must_use]
733 pub fn linear(self, a: f32, b: f32) -> Self {
734 self.linear_per_channel([a; 3], [b; 3])
735 }
736
737 #[must_use]
739 pub fn linear_per_channel(self, a: [f32; 3], b: [f32; 3]) -> Self {
740 match Linear::per_channel(a, b) {
741 Ok(op) => self.apply(Arc::new(op)),
742 Err(error) => Self::failed(error),
743 }
744 }
745
746 #[must_use]
751 pub fn threshold(self, level: u8, greyscale: bool) -> Self {
752 self.apply(Arc::new(Threshold::new(level, greyscale)))
753 }
754
755 #[must_use]
758 pub fn tint(self, red: u8, green: u8, blue: u8) -> Self {
759 self.apply(Arc::new(Tint::new(red, green, blue)))
760 }
761
762 #[must_use]
766 pub fn greyscale(self) -> Self {
767 self.apply(Arc::new(Greyscale))
768 }
769
770 #[must_use]
773 pub fn recomb(self, matrix: [[f32; 3]; 3]) -> Self {
774 match Recomb::rgb(matrix) {
775 Ok(op) => self.apply(Arc::new(op)),
776 Err(error) => Self::failed(error),
777 }
778 }
779
780 #[must_use]
783 pub fn recomb_rgba(self, matrix: [[f32; 4]; 4]) -> Self {
784 match Recomb::rgba(matrix) {
785 Ok(op) => self.apply(Arc::new(op)),
786 Err(error) => Self::failed(error),
787 }
788 }
789
790 #[must_use]
794 pub fn ensure_alpha(self, level: f32) -> Self {
795 match EnsureAlpha::new(level) {
796 Ok(op) => self.apply(Arc::new(op)),
797 Err(error) => Self::failed(error),
798 }
799 }
800
801 #[must_use]
805 pub fn remove_alpha(self) -> Self {
806 let pixel = match self.descriptor() {
807 Ok(descriptor) => descriptor.pixel,
808 Err(error) => return Self::failed(error),
809 };
810 let opaque = match pixel {
811 PixelFormat::GrayA8 => PixelFormat::Gray8,
812 PixelFormat::Rgba8 => PixelFormat::Rgb8,
813 PixelFormat::Rgba16 => PixelFormat::Rgb16,
814 PixelFormat::RgbaF32 => PixelFormat::RgbF32,
815 other => other,
816 };
817 self.to_pixel_format(opaque)
818 }
819
820 #[must_use]
822 pub fn flatten(self, red: u8, green: u8, blue: u8) -> Self {
823 self.apply(Arc::new(Flatten::onto(red, green, blue)))
824 }
825
826 #[must_use]
831 pub fn extend(self, options: Extend) -> Self {
832 if options.method() == ExtendWith::Background && options.background()[3] < 255 {
833 self.ensure_alpha(1.0).apply(Arc::new(options))
834 } else {
835 self.apply(Arc::new(options))
836 }
837 }
838
839 #[must_use]
853 pub fn trim(self, options: Trim) -> Self {
854 self.trimmed(options).unwrap_or_else(Self::failed)
855 }
856
857 fn trimmed(self, options: Trim) -> Result<Self> {
859 let format = self.metadata()?.format;
860 let (graph, _) = otf_pixels_core::shrink_on_load(self.graph()?)?;
863 let descriptor = graph.descriptor();
864 let mut pixels = TileBuf::for_image(&descriptor)?;
865 {
866 let mut view = pixels.as_tile_mut()?;
867 default_scheduler()?.run(&graph, |region, tile| {
868 otf_pixels_core::copy_region(tile, &mut view, region)
869 })?;
870 }
871
872 let (pixels, cut) = match options.find(&pixels.as_tile()?)? {
873 Some(kept) if kept != descriptor.region() => {
874 let mut part = TileBuf::zeroed(kept, descriptor.pixel)?;
875 otf_pixels_core::copy_region(&pixels.as_tile()?, &mut part.as_tile_mut()?, kept)?;
876 let size = Region::from_size(kept.width, kept.height);
877 (
878 TileBuf::from_vec(size, descriptor.pixel, part.into_bytes())?,
879 kept,
880 )
881 }
882 _ => (pixels, descriptor.region()),
883 };
884 let trimmed = descriptor.resized(cut.width, cut.height)?;
885 let source = BufferSource::new(trimmed, Arc::new(pixels))?;
886 Ok(Self {
887 inner: Ok(otf_pixels_core::Image::from_producer(
888 Arc::new(source),
889 format,
890 )),
891 icc: self.icc,
892 animation: self.animation,
893 pages: self.pages,
894 trim_offset: Some((cut.x, cut.y)),
895 exif: self.exif,
896 })
897 }
898
899 #[must_use]
903 pub const fn trim_offset(&self) -> Option<(u32, u32)> {
904 self.trim_offset
905 }
906
907 #[must_use]
912 pub fn composite(self, overlay: Self, x: i64, y: i64) -> Self {
913 self.composite_with(overlay, x, y, Blend::Over)
914 }
915
916 #[must_use]
918 pub fn composite_with(self, overlay: Self, x: i64, y: i64, blend: Blend) -> Self {
919 self.composite_by(overlay, Composite::at(x, y, blend))
920 }
921
922 #[must_use]
931 pub fn composite_overlays(self, overlays: impl IntoIterator<Item = Overlay>) -> Self {
932 overlays.into_iter().fold(self, |image, overlay| {
933 let op = Composite::new(overlay.placement(), overlay.blend);
934 image.composite_by(overlay.image, op)
935 })
936 }
937
938 fn composite_by(self, overlay: Self, op: Composite) -> Self {
940 let (icc, animation, pages, trim_offset, exif) = (
943 self.icc,
944 self.animation,
945 self.pages,
946 self.trim_offset,
947 self.exif,
948 );
949 let (base, over) = match (self.inner, overlay.inner) {
950 (Ok(base), Ok(over)) => (base, over),
951 (Err(error), _) | (Ok(_), Err(error)) => return Self::failed_shared(error),
953 };
954 let op: Arc<dyn Op> = Arc::new(op);
955 Self {
956 inner: otf_pixels_core::Image::combine(&[base, over], op).map_err(Arc::new),
957 icc,
958 animation,
959 pages,
960 trim_offset,
961 exif,
962 }
963 }
964
965 fn failed(error: PixelsError) -> Self {
967 Self::failed_shared(Arc::new(error))
968 }
969
970 const fn failed_shared(error: Arc<PixelsError>) -> Self {
972 Self {
973 inner: Err(error),
974 icc: None,
975 animation: None,
976 pages: 1,
977 trim_offset: None,
978 exif: None,
979 }
980 }
981
982 #[must_use]
989 pub fn animation(&self) -> Option<&Animation> {
990 self.animation.as_deref()
991 }
992
993 #[must_use]
1000 pub const fn pages(&self) -> u32 {
1001 self.pages
1002 }
1003
1004 #[must_use]
1010 pub fn icc_profile(&self) -> Option<&[u8]> {
1011 self.icc.as_deref()
1012 }
1013
1014 #[must_use]
1024 pub fn to_srgb(self) -> Self {
1025 let Some(profile) = self.icc.clone() else {
1026 return self;
1027 };
1028 let Ok(descriptor) = self.descriptor() else {
1029 return self;
1030 };
1031 match ToSrgb::from_profile(&profile) {
1032 Conversion::Convert(op) if op.applies_to(descriptor.pixel) => {
1033 self.apply(Arc::new(op)).with_icc_profile(None)
1034 }
1035 Conversion::AlreadySrgb => self.with_icc_profile(None),
1036 _ => self,
1037 }
1038 }
1039
1040 #[must_use]
1048 pub fn to_pixel_format(self, pixel: PixelFormat) -> Self {
1049 match self.descriptor() {
1050 Ok(descriptor) if descriptor.pixel == pixel => self,
1051 _ => self.apply(Arc::new(ConvertFormat::to(pixel))),
1052 }
1053 }
1054
1055 fn encodable_as(self, format: Format) -> Self {
1059 let Ok(descriptor) = self.descriptor() else {
1060 return self;
1061 };
1062 let pixel = descriptor.pixel;
1063 let kind = match (format, pixel.sample_kind()) {
1064 (
1065 Format::Jpeg | Format::WebP | Format::Avif | Format::Gif,
1066 SampleKind::U16 | SampleKind::F32,
1067 ) => SampleKind::U8,
1068 (Format::Png | Format::Tiff | Format::Jxl, SampleKind::F32) => SampleKind::U16,
1069 _ => return self,
1070 };
1071 match PixelFormat::from_parts(pixel.layout(), kind) {
1072 Some(target) => self.to_pixel_format(target),
1073 None => self,
1074 }
1075 }
1076
1077 #[must_use]
1083 pub fn exif(&self) -> Option<&[u8]> {
1084 self.exif.as_deref()
1085 }
1086
1087 #[must_use]
1091 pub fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
1092 self.exif = exif.map(Arc::from);
1093 self
1094 }
1095
1096 #[must_use]
1099 pub fn with_icc_profile(mut self, profile: Option<Vec<u8>>) -> Self {
1100 self.icc = profile.map(Arc::from);
1101 self
1102 }
1103
1104 #[must_use]
1109 pub fn apply(self, op: Arc<dyn Op>) -> Self {
1110 Self {
1111 icc: self.icc,
1112 animation: self.animation,
1113 pages: self.pages,
1114 trim_offset: self.trim_offset,
1115 exif: self.exif,
1116 inner: match self.inner {
1117 Ok(image) => image.apply(op).map_err(Arc::new),
1118 Err(error) => Err(error),
1120 },
1121 }
1122 }
1123
1124 #[must_use]
1130 pub fn output(self, format: Format, options: EncodeOptions) -> Output {
1131 Output {
1132 image: self,
1133 format,
1134 options,
1135 scheduler: None,
1136 shared: None,
1137 }
1138 }
1139
1140 fn graph(&self) -> Result<&otf_pixels_core::Image> {
1142 match &self.inner {
1143 Ok(image) => Ok(image),
1144 Err(error) => Err(rebuild(error)),
1146 }
1147 }
1148}
1149
1150fn rebuild(error: &Arc<PixelsError>) -> PixelsError {
1156 let detail = error.to_string();
1157 match error.code() {
1158 ErrorCode::Io => PixelsError::io("running the pipeline", std::io::Error::other(detail)),
1159 ErrorCode::Malformed => PixelsError::malformed("pipeline", detail),
1160 ErrorCode::Unsupported => PixelsError::unsupported(detail),
1161 ErrorCode::InvalidArgument => PixelsError::invalid_argument("pipeline", detail),
1162 ErrorCode::Graph => PixelsError::graph(detail),
1163 ErrorCode::LimitExceeded => match **error {
1164 PixelsError::LimitExceeded {
1165 limit,
1166 requested,
1167 allowed,
1168 } => PixelsError::limit_exceeded(limit, requested, allowed),
1169 _ => PixelsError::graph(detail),
1170 },
1171 _ => PixelsError::graph(detail),
1173 }
1174}
1175
1176#[derive(Debug, Clone)]
1179pub struct Overlay {
1180 image: Image,
1181 blend: Blend,
1182 gravity: Position,
1183 offset: Option<(i64, i64)>,
1184 tile: bool,
1185}
1186
1187impl Overlay {
1188 #[must_use]
1190 pub const fn new(image: Image) -> Self {
1191 Self {
1192 image,
1193 blend: Blend::Over,
1194 gravity: Position::Centre,
1195 offset: None,
1196 tile: false,
1197 }
1198 }
1199
1200 #[must_use]
1202 pub const fn with_blend(mut self, blend: Blend) -> Self {
1203 self.blend = blend;
1204 self
1205 }
1206
1207 #[must_use]
1210 pub const fn with_gravity(mut self, gravity: Position) -> Self {
1211 self.gravity = gravity;
1212 self
1213 }
1214
1215 #[must_use]
1218 pub const fn at(mut self, x: i64, y: i64) -> Self {
1219 self.offset = Some((x, y));
1220 self
1221 }
1222
1223 #[must_use]
1226 pub const fn tiled(mut self, tile: bool) -> Self {
1227 self.tile = tile;
1228 self
1229 }
1230
1231 const fn placement(&self) -> Placement {
1233 match (self.tile, self.offset) {
1234 (false, Some((x, y))) => Placement::At(x, y),
1235 (false, None) => Placement::Gravity(self.gravity),
1236 (true, Some((x, y))) => Placement::TiledFrom(x, y),
1237 (true, None) => Placement::Tiled(self.gravity),
1238 }
1239 }
1240}
1241
1242#[derive(Debug, Clone)]
1247pub struct Output {
1248 image: Image,
1249 format: Format,
1250 options: EncodeOptions,
1251 scheduler: Option<SchedulerOptions>,
1253 shared: Option<Arc<Scheduler>>,
1255}
1256
1257impl Output {
1258 fn exif(&self) -> Option<&[u8]> {
1261 self.image
1262 .exif
1263 .as_deref()
1264 .filter(|_| self.options.keep_exif)
1265 }
1266
1267 #[must_use]
1269 pub const fn format(&self) -> Format {
1270 self.format
1271 }
1272
1273 #[must_use]
1275 pub const fn options(&self) -> EncodeOptions {
1276 self.options
1277 }
1278
1279 #[must_use]
1291 pub fn threads(mut self, threads: usize) -> Self {
1292 self.scheduler = Some(self.scheduler.unwrap_or_default().with_threads(threads));
1293 self
1294 }
1295
1296 #[must_use]
1306 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
1307 self.shared = Some(scheduler);
1308 self
1309 }
1310
1311 #[must_use]
1317 pub const fn scheduler_options(mut self, options: SchedulerOptions) -> Self {
1318 self.scheduler = Some(options);
1319 self
1320 }
1321
1322 pub fn write(self, sink: impl Sink) -> Result<()> {
1335 self.write_with_stats(sink).map(|_| ())
1336 }
1337
1338 pub fn write_with_stats(self, mut sink: impl Sink) -> Result<RunStats> {
1350 let encodable = self.image.clone().encodable_as(self.format);
1353 let (image, reduction) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1354 let descriptor = image.descriptor();
1355 let mut encoder = encoder_for(self.format, self.options)?;
1356 encoder.set_icc_profile(self.image.icc.as_deref())?;
1357 encoder.set_exif(self.exif())?;
1358 encoder.write_header(&descriptor, &mut sink)?;
1359
1360 let scheduler = match (&self.shared, self.scheduler) {
1366 (Some(shared), _) => Arc::clone(shared),
1367 (None, Some(options)) => Arc::new(Scheduler::new(options)?),
1368 (None, None) => default_scheduler()?,
1369 };
1370 let mut rows = RowAssembler::new(descriptor);
1371 let mut stats = scheduler.run(&image, |region, tile| {
1372 rows.accept(region, tile, &mut |row| encoder.write_row(row, &mut sink))
1373 })?;
1374 rows.finish(&mut |row| encoder.write_row(row, &mut sink))?;
1375 encoder.finish(&mut sink)?;
1376 stats.reduction = reduction;
1377 Ok(stats)
1378 }
1379
1380 pub fn bytes_via_reference(self) -> Result<Vec<u8>> {
1397 let encodable = self.image.clone().encodable_as(self.format);
1401 let (image, _) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1402 let descriptor = image.descriptor();
1403 let mut encoder = encoder_for(self.format, self.options)?;
1404 encoder.set_icc_profile(self.image.icc.as_deref())?;
1405 encoder.set_exif(self.exif())?;
1406 let mut sink = Vec::with_capacity(descriptor.byte_len().unwrap_or_default());
1407 encoder.write_header(&descriptor, &mut sink)?;
1408 otf_pixels_core::evaluate_rows(&image, |_, row| encoder.write_row(row, &mut sink))?;
1409 encoder.finish(&mut sink)?;
1410 Ok(sink)
1411 }
1412
1413 pub fn bytes(self) -> Result<Vec<u8>> {
1419 let hint = self
1422 .image
1423 .descriptor()
1424 .ok()
1425 .and_then(|d| d.byte_len())
1426 .unwrap_or_default();
1427 let mut buffer = Vec::with_capacity(hint);
1428 self.write(&mut buffer)?;
1429 Ok(buffer)
1430 }
1431}
1432
1433fn default_scheduler() -> Result<Arc<Scheduler>> {
1438 if otf_pixels_core::ThreadPool::on_worker_thread() {
1439 Ok(Arc::new(Scheduler::with_defaults()?))
1440 } else {
1441 Scheduler::global()
1442 }
1443}
1444
1445#[derive(Debug)]
1453struct RowAssembler {
1454 descriptor: ImageDescriptor,
1455 band: Option<otf_pixels_core::TileBuf>,
1457 next_row: u32,
1459}
1460
1461impl RowAssembler {
1462 const fn new(descriptor: ImageDescriptor) -> Self {
1463 Self {
1464 descriptor,
1465 band: None,
1466 next_row: 0,
1467 }
1468 }
1469
1470 fn accept(
1472 &mut self,
1473 region: Region,
1474 tile: &otf_pixels_core::Tile<'_>,
1475 emit: &mut impl FnMut(&[u8]) -> Result<()>,
1476 ) -> Result<()> {
1477 if region.width == self.descriptor.width && self.band.is_none() {
1479 for y in region.y..region.y.saturating_add(region.height) {
1480 let row = tile
1481 .row(y)
1482 .ok_or_else(|| PixelsError::graph(format!("output tile is missing row {y}")))?;
1483 emit(row)?;
1484 self.next_row = y.saturating_add(1);
1485 }
1486 return Ok(());
1487 }
1488
1489 let band_region = Region::new(0, region.y, self.descriptor.width, region.height);
1492 let starts_new_band = self
1493 .band
1494 .as_ref()
1495 .is_none_or(|band| band.region().y != region.y);
1496 if starts_new_band {
1497 self.flush(emit)?;
1498 self.band = Some(otf_pixels_core::TileBuf::zeroed(
1499 band_region,
1500 self.descriptor.pixel,
1501 )?);
1502 }
1503 let Some(band) = self.band.as_mut() else {
1504 return Err(PixelsError::graph("row band vanished"));
1505 };
1506 otf_pixels_core::copy_region(tile, &mut band.as_tile_mut()?, region)?;
1507
1508 if region.right() >= u64::from(self.descriptor.width) {
1510 self.flush(emit)?;
1511 }
1512 Ok(())
1513 }
1514
1515 fn flush(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1517 let Some(band) = self.band.take() else {
1518 return Ok(());
1519 };
1520 let region = band.region();
1521 let view = band.as_tile()?;
1522 for y in region.y..region.y.saturating_add(region.height) {
1523 let row = view
1524 .row(y)
1525 .ok_or_else(|| PixelsError::graph(format!("row band is missing row {y}")))?;
1526 emit(row)?;
1527 self.next_row = y.saturating_add(1);
1528 }
1529 Ok(())
1530 }
1531
1532 fn finish(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1534 self.flush(emit)
1535 }
1536}
1537
1538fn sniffing_codecs() -> Vec<Box<dyn Codec>> {
1543 let codecs: Vec<Box<dyn Codec>> = vec![
1544 #[cfg(feature = "png")]
1545 Box::new(PngCodec),
1546 #[cfg(feature = "gif")]
1547 Box::new(GifCodec),
1548 #[cfg(feature = "jpeg")]
1549 Box::new(JpegCodec),
1550 #[cfg(feature = "tiff")]
1551 Box::new(TiffCodec),
1552 #[cfg(feature = "webp")]
1553 Box::new(WebPCodec),
1554 #[cfg(feature = "avif")]
1555 Box::new(AvifCodec),
1556 #[cfg(feature = "jxl")]
1557 Box::new(JxlCodec),
1558 ];
1559 codecs
1560}
1561
1562fn encoder_for(format: Format, options: EncodeOptions) -> Result<Box<dyn Encoder>> {
1564 let _ = &options;
1567 match format {
1568 #[cfg(feature = "raw")]
1569 Format::Raw => Ok(Box::new(RawEncoder::new())),
1570 #[cfg(feature = "png")]
1571 Format::Png => Ok(Box::new(PngEncoder::from_options(&options))),
1572 #[cfg(feature = "gif")]
1573 Format::Gif => Ok(Box::new(GifEncoder::from_options(&options))),
1574 #[cfg(feature = "jpeg")]
1575 Format::Jpeg => Ok(Box::new(JpegEncoder::from_options(&options))),
1576 #[cfg(feature = "tiff")]
1577 Format::Tiff => Ok(Box::new(TiffEncoder::from_options(&options))),
1578 #[cfg(feature = "webp")]
1579 Format::WebP => Ok(Box::new(WebPEncoder::from_options(&options))),
1580 #[cfg(feature = "avif")]
1581 Format::Avif => Ok(Box::new(AvifEncoder::from_options(&options))),
1582 #[cfg(feature = "jxl")]
1583 Format::Jxl => Ok(Box::new(JxlEncoder::from_options(&options))),
1584 #[cfg(not(feature = "raw"))]
1585 Format::Raw => Err(PixelsError::unsupported(
1586 "raw encoding requires the `raw` feature of otf-pixels",
1587 )),
1588 other => Err(PixelsError::unsupported(format!(
1589 "encoding {other} is not implemented yet; \
1590 see the ROADMAP for which milestone lands it"
1591 ))),
1592 }
1593}
1594
1595#[cfg(all(test, feature = "raw"))]
1600#[allow(
1601 clippy::unwrap_used,
1602 clippy::expect_used,
1603 clippy::indexing_slicing,
1604 clippy::panic,
1605 reason = "tests operate on known-good values and assert shapes directly"
1606)]
1607mod tests {
1608 use super::*;
1609
1610 fn ramp(width: u32, height: u32) -> Image {
1611 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
1612 let len = descriptor.byte_len().unwrap();
1613 Image::from_raw(descriptor, (0..len).map(|i| i as u8).collect()).unwrap()
1614 }
1615
1616 #[test]
1617 fn from_raw_requires_an_exactly_sized_buffer() {
1618 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1619 assert!(Image::from_raw(descriptor, vec![0; 4]).is_ok());
1620 assert_eq!(
1621 Image::from_raw(descriptor, vec![0; 3]).unwrap_err().code(),
1622 ErrorCode::InvalidArgument
1623 );
1624 assert_eq!(
1625 Image::from_raw(descriptor, vec![0; 5]).unwrap_err().code(),
1626 ErrorCode::InvalidArgument
1627 );
1628 }
1629
1630 #[test]
1631 fn a_chain_error_surfaces_at_the_terminal() {
1632 let result = ramp(4, 4)
1634 .crop(3, 3, 4, 4)
1635 .flip()
1636 .flop()
1637 .output(Format::Raw, EncodeOptions::default())
1638 .bytes();
1639 let err = result.unwrap_err();
1640 assert_eq!(err.code(), ErrorCode::InvalidArgument);
1641 }
1642
1643 #[test]
1644 fn an_error_short_circuits_later_ops() {
1645 let broken = ramp(4, 4).crop(0, 0, 0, 0);
1648 assert!(broken.metadata().is_err());
1649 assert!(broken.clone().flip().descriptor().is_err());
1650 }
1651
1652 #[test]
1653 fn metadata_is_free_and_reports_the_pipeline_shape() {
1654 let meta = ramp(8, 6).metadata().unwrap();
1655 assert_eq!((meta.width, meta.height), (8, 6));
1656 assert_eq!(meta.format, Format::Raw);
1657 assert_eq!(meta.pixel, PixelFormat::Gray8);
1658 let cropped = ramp(8, 6).crop(1, 1, 3, 2).metadata().unwrap();
1660 assert_eq!((cropped.width, cropped.height), (3, 2));
1661 }
1662
1663 #[cfg(not(feature = "avif"))]
1667 #[test]
1668 fn a_format_built_out_is_a_catchable_error() {
1669 let format = Format::Avif;
1670 let err = ramp(2, 2)
1671 .output(format, EncodeOptions::default())
1672 .bytes()
1673 .unwrap_err();
1674 assert_eq!(err.code(), ErrorCode::Unsupported, "{format}");
1675 assert!(err.to_string().contains(format.as_str()), "{err}");
1676 }
1677
1678 #[cfg(feature = "tiff")]
1683 #[test]
1684 fn tiff_round_trips_through_the_facade() {
1685 let bytes = ramp(12, 9)
1686 .output(Format::Tiff, EncodeOptions::default())
1687 .bytes()
1688 .unwrap();
1689 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1690 let metadata = image.metadata().unwrap();
1691 assert_eq!(metadata.format, Format::Tiff);
1692 assert_eq!((metadata.width, metadata.height), (12, 9));
1693
1694 let decoded = image
1696 .output(Format::Raw, EncodeOptions::default())
1697 .bytes()
1698 .unwrap();
1699 let expected: Vec<u8> = (0..12_u32 * 9).map(|i| i as u8).collect();
1700 assert_eq!(decoded, expected);
1701 }
1702
1703 #[cfg(feature = "jpeg")]
1706 #[test]
1707 fn jpeg_round_trips_through_the_facade() {
1708 let (width, height) = (32_u32, 24_u32);
1709 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1710 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1711 .map(|i| {
1712 let pixel = i / 3;
1715 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1716 match i % 3 {
1717 0 => (x * 255 / width) as u8,
1718 1 => (y * 255 / height) as u8,
1719 _ => 128,
1720 }
1721 })
1722 .collect();
1723
1724 let bytes = Image::from_raw(descriptor, pixels.clone())
1725 .unwrap()
1726 .output(Format::Jpeg, EncodeOptions::with_quality(95).unwrap())
1727 .bytes()
1728 .unwrap();
1729
1730 let image = Image::from_stream(std::io::Cursor::new(bytes.clone())).unwrap();
1731 let metadata = image.metadata().unwrap();
1732 assert_eq!(metadata.format, Format::Jpeg, "sniffing missed the JPEG");
1733 assert_eq!((metadata.width, metadata.height), (width, height));
1734 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1735
1736 let decoded = image
1737 .output(Format::Raw, EncodeOptions::default())
1738 .bytes()
1739 .unwrap();
1740 assert_eq!(decoded.len(), pixels.len());
1741 let worst = decoded
1742 .iter()
1743 .zip(&pixels)
1744 .map(|(&a, &b)| a.abs_diff(b))
1745 .max()
1746 .unwrap_or(0);
1747 assert!(worst <= 12, "worst sample differs by {worst}");
1748 }
1749
1750 #[cfg(feature = "jpeg")]
1761 #[test]
1762 fn a_streaming_resize_matches_the_reference_evaluator() {
1763 for &(width, height, target) in &[
1764 (512_u32, 512_u32, 300_u32),
1765 (512, 512, 256),
1766 (256, 256, 200),
1767 (320, 240, 129),
1768 (64, 64, 16),
1769 ] {
1770 let source = jpeg_source(width, height);
1771 let scheduled = Image::from_stream(std::io::Cursor::new(source.clone()))
1772 .unwrap()
1773 .resize(target, target)
1774 .output(Format::Raw, EncodeOptions::default())
1775 .bytes()
1776 .unwrap_or_else(|e| panic!("{width}x{height} -> {target}: {e}"));
1777 let reference = Image::from_stream(std::io::Cursor::new(source))
1778 .unwrap()
1779 .resize(target, target)
1780 .output(Format::Raw, EncodeOptions::default())
1781 .bytes_via_reference()
1782 .unwrap();
1783
1784 assert_eq!(
1785 scheduled.len(),
1786 (target * target * 3) as usize,
1787 "{width}x{height} -> {target}: size"
1788 );
1789 assert_eq!(
1790 scheduled, reference,
1791 "{width}x{height} -> {target}: the scheduler and the oracle disagree"
1792 );
1793 }
1794 }
1795
1796 #[cfg(feature = "webp")]
1802 #[test]
1803 fn webp_round_trips_through_the_facade() {
1804 let bytes = ramp(20, 12)
1805 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1806 .bytes()
1807 .unwrap();
1808 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1809 let metadata = image.metadata().unwrap();
1810 assert_eq!(metadata.format, Format::WebP, "sniffing missed the WebP");
1811 assert_eq!((metadata.width, metadata.height), (20, 12));
1812 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1814
1815 let decoded = image
1816 .output(Format::Raw, EncodeOptions::default())
1817 .bytes()
1818 .unwrap();
1819 let expected: Vec<u8> = (0..20_u32 * 12)
1820 .flat_map(|i| {
1821 let value = i as u8;
1822 [value, value, value]
1823 })
1824 .collect();
1825 assert_eq!(decoded, expected, "a lossless round trip lost pixels");
1826 }
1827
1828 #[cfg(feature = "webp")]
1831 #[test]
1832 fn webp_keeps_alpha_through_the_facade() {
1833 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::Rgba8).unwrap();
1834 let pixels: Vec<u8> = (0..(9 * 7))
1835 .flat_map(|i| [(i * 3) as u8, 40, 200, (i * 7) as u8])
1836 .collect();
1837 let bytes = Image::from_raw(descriptor, pixels.clone())
1838 .unwrap()
1839 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1840 .bytes()
1841 .unwrap();
1842
1843 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1844 assert_eq!(image.metadata().unwrap().pixel, PixelFormat::Rgba8);
1845 let decoded = image
1846 .output(Format::Raw, EncodeOptions::default())
1847 .bytes()
1848 .unwrap();
1849 assert_eq!(decoded, pixels);
1850 }
1851
1852 #[cfg(feature = "webp")]
1856 #[test]
1857 fn webp_is_lossy_by_default_and_keeps_alpha_exact() {
1858 let (w, h) = (48_u32, 32_u32);
1859 let descriptor = ImageDescriptor::new(w, h, PixelFormat::Rgba8).unwrap();
1860 let pixels: Vec<u8> = (0..w * h)
1861 .flat_map(|i| {
1862 let (x, y) = (i % w, i / w);
1863 [(x * 5) as u8, (y * 7) as u8, 120, (x * 3 + y) as u8]
1864 })
1865 .collect();
1866 let encode = |options: EncodeOptions| {
1867 Image::from_raw(descriptor, pixels.clone())
1868 .unwrap()
1869 .output(Format::WebP, options)
1870 .bytes()
1871 .unwrap()
1872 };
1873 let good = encode(EncodeOptions::default());
1874 let rough = encode(EncodeOptions::with_quality(10).unwrap());
1875 assert!(
1876 rough.len() < good.len(),
1877 "{} vs {}",
1878 rough.len(),
1879 good.len()
1880 );
1881
1882 let decoded = Image::from_stream(std::io::Cursor::new(good))
1883 .unwrap()
1884 .output(Format::Raw, EncodeOptions::default())
1885 .bytes()
1886 .unwrap();
1887 for (ours, source) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
1888 assert_eq!(ours[3], source[3], "alpha changed");
1889 for c in 0..3 {
1890 assert!(ours[c].abs_diff(source[c]) <= 12, "{ours:?} vs {source:?}");
1891 }
1892 }
1893 }
1894
1895 #[cfg(feature = "jpeg")]
1898 #[test]
1899 fn a_thumbnail_pipeline_shrinks_the_jpeg_on_load() {
1900 let source = jpeg_source(512, 512);
1902
1903 let image = Image::from_stream(std::io::Cursor::new(source)).unwrap();
1904 assert_eq!(
1905 image.metadata().unwrap().width,
1906 512,
1907 "metadata is full size"
1908 );
1909
1910 let mut bytes = Vec::new();
1913 let stats = image
1914 .resize(32, 32)
1915 .output(Format::Raw, EncodeOptions::default())
1916 .write_with_stats(&mut bytes)
1917 .unwrap();
1918
1919 let reduction = stats
1920 .reduction
1921 .expect("a 512 to 32 resize should shrink the source on load");
1922 assert_eq!(reduction.from, (512, 512));
1923 assert_eq!(
1924 reduction.to,
1925 (64, 64),
1926 "1/8 is the coarsest scale that still covers 32"
1927 );
1928 assert!((reduction.factor() - 64.0).abs() < 0.01);
1929 assert_eq!(bytes.len(), 32 * 32 * 3);
1931 }
1932
1933 #[cfg(feature = "jpeg")]
1936 #[test]
1937 fn a_crop_blocks_shrink_on_load() {
1938 let source = jpeg_source(512, 512);
1939 let stats = Image::from_stream(std::io::Cursor::new(source))
1940 .unwrap()
1941 .crop(256, 256, 128, 128)
1942 .resize(32, 32)
1943 .output(Format::Raw, EncodeOptions::default())
1944 .write_with_stats(&mut Vec::new())
1945 .unwrap();
1946 assert!(
1947 stats.reduction.is_none(),
1948 "a crop must block shrink-on-load: {:?}",
1949 stats.reduction
1950 );
1951 }
1952
1953 #[cfg(feature = "jpeg")]
1956 #[test]
1957 fn a_crop_after_the_resize_still_allows_shrinking() {
1958 let source = jpeg_source(512, 512);
1959 let stats = Image::from_stream(std::io::Cursor::new(source))
1960 .unwrap()
1961 .resize(64, 64)
1962 .crop(0, 0, 32, 32)
1963 .output(Format::Raw, EncodeOptions::default())
1964 .write_with_stats(&mut Vec::new())
1965 .unwrap();
1966 assert!(stats.reduction.is_none());
1970 }
1971
1972 #[cfg(feature = "jpeg")]
1975 #[test]
1976 fn a_mild_resize_shrinks_by_less_or_not_at_all() {
1977 let source = jpeg_source(512, 512);
1978 let stats = Image::from_stream(std::io::Cursor::new(source.clone()))
1980 .unwrap()
1981 .resize(300, 300)
1982 .output(Format::Raw, EncodeOptions::default())
1983 .write_with_stats(&mut Vec::new())
1984 .unwrap();
1985 assert!(stats.reduction.is_none(), "{:?}", stats.reduction);
1986
1987 let stats = Image::from_stream(std::io::Cursor::new(source))
1989 .unwrap()
1990 .resize(200, 200)
1991 .output(Format::Raw, EncodeOptions::default())
1992 .write_with_stats(&mut Vec::new())
1993 .unwrap();
1994 assert_eq!(stats.reduction.map(|r| r.to), Some((256, 256)));
1995 }
1996
1997 #[cfg(feature = "jpeg")]
1999 fn jpeg_source(width: u32, height: u32) -> Vec<u8> {
2000 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
2001 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
2002 .map(|i| {
2003 let pixel = i / 3;
2004 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
2005 match i % 3 {
2006 0 => (x * 255 / width) as u8,
2007 1 => (y * 255 / height) as u8,
2008 _ => 96,
2009 }
2010 })
2011 .collect();
2012 Image::from_raw(descriptor, pixels)
2013 .unwrap()
2014 .output(Format::Jpeg, EncodeOptions::with_quality(85).unwrap())
2015 .bytes()
2016 .unwrap()
2017 }
2018
2019 #[cfg(feature = "jpeg")]
2021 #[test]
2022 fn a_jpeg_pipeline_resizes_and_re_encodes() {
2023 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
2024 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
2025 .map(|i| ((i / 3) % 251) as u8)
2026 .collect();
2027 let source = Image::from_raw(descriptor, pixels)
2028 .unwrap()
2029 .output(Format::Jpeg, EncodeOptions::default())
2030 .bytes()
2031 .unwrap();
2032
2033 let thumbnail = Image::from_stream(std::io::Cursor::new(source.clone()))
2034 .unwrap()
2035 .resize(16, 16)
2036 .output(Format::Jpeg, EncodeOptions::with_quality(70).unwrap())
2037 .bytes()
2038 .unwrap();
2039
2040 let metadata = Image::from_stream(std::io::Cursor::new(thumbnail.clone()))
2041 .unwrap()
2042 .metadata()
2043 .unwrap();
2044 assert_eq!((metadata.width, metadata.height), (16, 16));
2045 assert!(
2046 thumbnail.len() < source.len(),
2047 "a 16x16 thumbnail is {} bytes against a 64x64 source's {}",
2048 thumbnail.len(),
2049 source.len()
2050 );
2051 }
2052
2053 #[cfg(feature = "jpeg")]
2056 #[test]
2057 fn grayscale_jpeg_keeps_one_channel_through_the_facade() {
2058 let bytes = ramp(24, 16)
2059 .output(Format::Jpeg, EncodeOptions::with_quality(90).unwrap())
2060 .bytes()
2061 .unwrap();
2062 let metadata = Image::from_stream(std::io::Cursor::new(bytes))
2063 .unwrap()
2064 .metadata()
2065 .unwrap();
2066 assert_eq!(metadata.pixel, PixelFormat::Gray8);
2067 assert_eq!((metadata.width, metadata.height), (24, 16));
2068 }
2069
2070 #[test]
2071 fn write_streams_into_any_sink() {
2072 let mut sink = Vec::new();
2073 ramp(2, 2)
2074 .output(Format::Raw, EncodeOptions::default())
2075 .write(&mut sink)
2076 .unwrap();
2077 assert_eq!(sink, [0, 1, 2, 3]);
2078 }
2079
2080 #[test]
2081 fn output_reports_its_settings() {
2082 let options = EncodeOptions::with_quality(55).unwrap();
2083 let output = ramp(2, 2).output(Format::Raw, options);
2084 assert_eq!(output.format(), Format::Raw);
2085 assert_eq!(output.options().quality, 55);
2086 }
2087
2088 #[cfg(feature = "raw")]
2089 #[test]
2090 fn from_raw_stream_defers_decoding_to_the_terminal() {
2091 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
2092 let layout = RawFormat::packed(descriptor);
2093 let cursor = std::io::Cursor::new(vec![1_u8, 2, 3, 4]);
2094 let image = Image::from_raw_stream(layout, cursor).unwrap();
2095 assert_eq!(image.metadata().unwrap().width, 2);
2097 let bytes = image
2098 .output(Format::Raw, EncodeOptions::default())
2099 .bytes()
2100 .unwrap();
2101 assert_eq!(bytes, [1, 2, 3, 4]);
2102 }
2103
2104 #[cfg(feature = "raw")]
2105 #[test]
2106 fn a_truncated_stream_fails_the_terminal_without_panicking() {
2107 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
2108 let layout = RawFormat::packed(descriptor);
2109 let cursor = std::io::Cursor::new(vec![1_u8; 5]);
2110 let image = Image::from_raw_stream(layout, cursor).unwrap();
2111 let err = image
2112 .output(Format::Raw, EncodeOptions::default())
2113 .bytes()
2114 .unwrap_err();
2115 assert_eq!(err.code(), ErrorCode::Malformed);
2116 }
2117
2118 #[test]
2119 fn images_are_send_sync_and_cheap_to_clone() {
2120 const fn assert_send_sync<T: Send + Sync>() {}
2121 assert_send_sync::<Image>();
2122 assert_send_sync::<Output>();
2123 let image = ramp(4, 4);
2124 let clone = image.clone();
2125 assert_eq!(
2126 image.metadata().unwrap().width,
2127 clone.metadata().unwrap().width
2128 );
2129 }
2130
2131 #[cfg(feature = "png")]
2132 #[test]
2133 fn a_png_round_trips_through_the_facade() {
2134 let png = ramp(37, 21)
2135 .output(Format::Png, EncodeOptions::default())
2136 .bytes()
2137 .unwrap();
2138 let image = Image::from_stream(std::io::Cursor::new(png)).unwrap();
2139 assert_eq!(image.descriptor().unwrap().width, 37);
2140 assert_eq!(image.metadata().unwrap().format, Format::Png);
2141
2142 let back = image
2143 .output(Format::Raw, EncodeOptions::default())
2144 .bytes()
2145 .unwrap();
2146 let expected = ramp(37, 21)
2147 .output(Format::Raw, EncodeOptions::default())
2148 .bytes()
2149 .unwrap();
2150 assert_eq!(back, expected, "pixels changed across a PNG round trip");
2151 }
2152
2153 #[cfg(feature = "png")]
2154 #[test]
2155 fn a_pipeline_survives_a_png_round_trip() {
2156 let png = ramp(16, 16)
2159 .output(Format::Png, EncodeOptions::default())
2160 .bytes()
2161 .unwrap();
2162 let cropped = Image::from_stream(std::io::Cursor::new(png))
2163 .unwrap()
2164 .crop(2, 3, 8, 5)
2165 .flip()
2166 .output(Format::Raw, EncodeOptions::default())
2167 .bytes()
2168 .unwrap();
2169 let direct = ramp(16, 16)
2170 .crop(2, 3, 8, 5)
2171 .flip()
2172 .output(Format::Raw, EncodeOptions::default())
2173 .bytes()
2174 .unwrap();
2175 assert_eq!(cropped, direct);
2176 }
2177
2178 #[cfg(feature = "png")]
2179 #[test]
2180 fn open_ignores_the_extension_and_reads_the_bytes() {
2181 let dir = std::env::temp_dir().join(format!("otf-pixels-open-{}", std::process::id()));
2182 std::fs::create_dir_all(&dir).unwrap();
2183 let path = dir.join("actually-a-png.jpg");
2184 let png = ramp(8, 8)
2185 .output(Format::Png, EncodeOptions::default())
2186 .bytes()
2187 .unwrap();
2188 std::fs::write(&path, &png).unwrap();
2189
2190 let image = Image::open(&path).unwrap();
2191 assert_eq!(
2192 image.metadata().unwrap().format,
2193 Format::Png,
2194 "extension won over content"
2195 );
2196 std::fs::remove_dir_all(&dir).ok();
2197 }
2198
2199 #[test]
2200 fn open_names_the_file_it_could_not_read() {
2201 let error = Image::open("/nonexistent/otf-pixels/missing.png").unwrap_err();
2202 assert_eq!(error.code(), ErrorCode::Io);
2203 assert!(error.to_string().contains("missing.png"), "{error}");
2204 }
2205
2206 #[test]
2207 fn an_unrecognised_stream_is_unsupported_not_a_guess() {
2208 for bytes in [&b""[..], &b"not an image"[..], &[0_u8; 64][..]] {
2209 let error = Image::from_stream(std::io::Cursor::new(bytes.to_vec())).unwrap_err();
2210 assert_eq!(error.code(), ErrorCode::Unsupported, "{bytes:02x?}");
2211 }
2212 }
2213
2214 #[cfg(feature = "png")]
2215 #[test]
2216 fn a_truncated_png_is_malformed_not_a_panic() {
2217 let png = ramp(8, 8)
2218 .output(Format::Png, EncodeOptions::default())
2219 .bytes()
2220 .unwrap();
2221 for cut in [9, 16, 24, 32, png.len() - 1] {
2224 let truncated = png[..cut].to_vec();
2225 let result = Image::from_stream(std::io::Cursor::new(truncated))
2226 .and_then(|i| i.output(Format::Raw, EncodeOptions::default()).bytes());
2227 assert!(result.is_err(), "truncating to {cut} bytes should fail");
2228 }
2229 }
2230
2231 #[cfg(feature = "png")]
2232 #[test]
2233 fn sniffing_reads_only_the_magic_bytes_before_deciding() {
2234 let error = Image::from_stream(std::io::Cursor::new(
2238 otf_pixels_codec_png::SIGNATURE.to_vec(),
2239 ))
2240 .unwrap_err();
2241 assert_eq!(error.code(), ErrorCode::Malformed, "{error}");
2242 }
2243}
2244
2245const _: () = {
2248 const fn shareable<T: Send + Sync>() {}
2249 shareable::<Image>();
2250 shareable::<Output>();
2251 shareable::<OpenOptions>();
2252 shareable::<PixelsError>();
2253 shareable::<Scheduler>();
2254};