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};
129use std::sync::Arc;
130
131mod metadata;
132pub use metadata::{ChannelStats, SourceMetadata, Stats};
133
134pub use otf_pixels_core::{
135 AccessPattern, Animation, Background, ChannelLayout, ChromaSubsampling, Codec, ColorModel,
136 Comment, Decoder, Density, EncodeOptions, Encoder, ErrorCode, Format, ImageDescriptor, Limit,
137 Limits, Metadata, Orientation, PixelFormat, PixelsError, PlanOptions, Region, ResolutionUnit,
138 Result, RunStats, SampleKind, Scheduler, SchedulerOptions, Sink, Source, SourceInfo, TileShape,
139 evaluate as evaluate_reference,
140};
141pub use otf_pixels_ops::{
142 Blend, Composite, Conversion, ConvertFormat, Convolve, Crop, EnsureAlpha, Extend, ExtendWith,
143 ExtractChannel, Filter, Fit, Flatten, Flip, Flop, Gamma, Greyscale, Kernel, Linear, Modulate,
144 Negate, Placement, Position, Quarter, Recomb, Resize, ResizeOptions, Rotate, Threshold, Tint,
145 ToSrgb, Trim, Unconvertible,
146};
147
148#[cfg(feature = "raw")]
149pub use otf_pixels_codec_raw::{RawCodec, RawDecoder, RawEncoder, RawFormat};
150
151#[cfg(feature = "png")]
152pub use otf_pixels_codec_png::{PngCodec, PngDecoder, PngEncoder};
153
154#[cfg(feature = "gif")]
155pub use otf_pixels_codec_gif::{GifCodec, GifDecoder, GifEncoder};
156
157#[cfg(feature = "jpeg")]
158pub use otf_pixels_codec_jpeg::{JpegCodec, JpegDecoder, JpegEncoder, Scale, Subsampling};
159
160#[cfg(feature = "tiff")]
161pub use otf_pixels_codec_tiff::{TiffCodec, TiffDecoder, TiffEncoder, TiffLayout};
162
163#[cfg(feature = "webp")]
164pub use otf_pixels_codec_webp::{WebPCodec, WebPDecoder, WebPEncoder};
165
166#[cfg(feature = "avif")]
167pub use otf_pixels_codec_avif::{AvifCodec, AvifDecoder, AvifEncoder};
168
169#[cfg(feature = "jxl")]
170pub use otf_pixels_codec_jxl::{JxlCodec, JxlDecoder, JxlEncoder};
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[non_exhaustive]
175pub struct OpenOptions {
176 pub auto_orient: bool,
184 pub to_srgb: bool,
193 pub animated: bool,
202 pub page: u32,
209 pub limits: Limits,
213}
214
215impl OpenOptions {
216 #[must_use]
221 pub const fn with_auto_orient(mut self, auto_orient: bool) -> Self {
222 self.auto_orient = auto_orient;
223 self
224 }
225
226 #[must_use]
228 pub const fn with_animated(mut self, animated: bool) -> Self {
229 self.animated = animated;
230 self
231 }
232
233 #[must_use]
236 pub const fn with_page(mut self, page: u32) -> Self {
237 self.page = page;
238 self
239 }
240
241 #[must_use]
243 pub const fn with_limits(mut self, limits: Limits) -> Self {
244 self.limits = limits;
245 self
246 }
247
248 #[must_use]
250 pub const fn with_to_srgb(mut self, to_srgb: bool) -> Self {
251 self.to_srgb = to_srgb;
252 self
253 }
254}
255
256impl Default for OpenOptions {
257 fn default() -> Self {
258 Self {
259 auto_orient: true,
260 to_srgb: true,
261 animated: false,
262 page: 0,
263 limits: Limits::default(),
264 }
265 }
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
270#[non_exhaustive]
271pub enum Channel {
272 Red,
274 Green,
276 Blue,
278 Alpha,
280}
281
282#[derive(Debug, Clone)]
287pub struct Image {
288 inner: std::result::Result<otf_pixels_core::Image, Arc<PixelsError>>,
290 icc: Option<Arc<[u8]>>,
293 animation: Option<Arc<Animation>>,
295 pages: u32,
297 trim_offset: Option<(u32, u32)>,
299 exif: Option<Arc<[u8]>>,
301 xmp: Option<Arc<[u8]>>,
303 density: Option<Density>,
306 source: Option<Arc<SourceMetadata>>,
308}
309
310impl Image {
311 pub fn from_raw(descriptor: ImageDescriptor, bytes: Vec<u8>) -> Result<Self> {
320 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes)?;
321 let source = BufferSource::new(descriptor, Arc::new(buffer))?;
322 Ok(Self::from_producer(Arc::new(source), Format::Raw))
323 }
324
325 #[cfg(feature = "raw")]
335 pub fn from_raw_stream(
336 layout: RawFormat,
337 source: impl Source + std::fmt::Debug + 'static,
338 ) -> Result<Self> {
339 let decoder = RawDecoder::new(layout, source)?;
340 Ok(Self::from_decoder(Box::new(decoder), Format::Raw))
341 }
342
343 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
357 Self::open_with(path, OpenOptions::default())
358 }
359
360 pub fn open_with(path: impl AsRef<std::path::Path>, options: OpenOptions) -> Result<Self> {
366 let path = path.as_ref();
367 let file = std::fs::File::open(path).map_err(|e| {
368 let kind = e.kind();
372 let detail = std::io::Error::new(kind, format!("{}: {e}", path.display()));
373 PixelsError::io("opening image file", detail)
374 })?;
375 let size = file.metadata().ok().map(|m| m.len());
376 let mut image = Self::from_stream_with(std::io::BufReader::new(file), options)?;
377 if let Some(source) = image.source.as_mut() {
378 Arc::make_mut(source).size = size;
379 }
380 Ok(image)
381 }
382
383 pub fn from_stream(source: impl Source + std::fmt::Debug + 'static) -> Result<Self> {
399 Self::from_stream_with(source, OpenOptions::default())
400 }
401
402 pub fn from_stream_with(
408 mut source: impl Source + std::fmt::Debug + 'static,
409 options: OpenOptions,
410 ) -> Result<Self> {
411 let codecs = sniffing_codecs();
412 let longest = codecs.iter().map(|c| c.magic_len()).max().unwrap_or(0);
413
414 let mut prefix = Vec::with_capacity(longest);
418 let mut buffer = vec![0_u8; longest];
419 while prefix.len() < longest {
420 let Some(rest) = buffer.get_mut(prefix.len()..) else {
421 break;
422 };
423 match source.read(rest)? {
424 0 => break,
425 n => {
426 let Some(read) = rest.get(..n) else { break };
427 prefix.extend_from_slice(read);
428 }
429 }
430 }
431
432 let Some(codec) = codecs.iter().find(|codec| codec.probe(&prefix)) else {
433 return Err(PixelsError::unsupported(format!(
434 "no codec recognises this stream; its first {} bytes are {:02x?}",
435 prefix.len().min(8),
436 prefix.get(..prefix.len().min(8)).unwrap_or(&[])
437 )));
438 };
439 let stream = Prefixed::new(prefix, source);
440 if options.page > 0 && codec.format() != Format::Tiff {
442 return Err(PixelsError::invalid_argument(
443 "page",
444 format!(
445 "page {} was asked for, but a {} has only page 0",
446 options.page,
447 codec.format()
448 ),
449 ));
450 }
451 let _ = (&stream, options);
454
455 match codec.format() {
456 #[cfg(feature = "png")]
457 Format::Png => {
458 let decoder = PngDecoder::new(stream, options.limits)?;
459 Self::decoded(Box::new(decoder), Format::Png, options)
460 }
461 #[cfg(feature = "gif")]
462 Format::Gif => {
463 let decoder = GifDecoder::new(stream, options.limits)?;
464 Self::decoded(Box::new(decoder), Format::Gif, options)
465 }
466 #[cfg(feature = "jpeg")]
467 Format::Jpeg => {
468 let decoder = JpegDecoder::new(stream, options.limits)?;
469 Self::decoded(Box::new(decoder), Format::Jpeg, options)
470 }
471 #[cfg(feature = "tiff")]
472 Format::Tiff => {
473 let decoder = TiffDecoder::with_page(stream, options.limits, options.page)?;
474 Self::decoded(Box::new(decoder), Format::Tiff, options)
475 }
476 #[cfg(feature = "webp")]
477 Format::WebP => {
478 let decoder = WebPDecoder::new(stream, options.limits)?;
479 Self::decoded(Box::new(decoder), Format::WebP, options)
480 }
481 #[cfg(feature = "avif")]
482 Format::Avif => {
483 let decoder = AvifDecoder::new(stream, options.limits)?;
484 Self::decoded(Box::new(decoder), Format::Avif, options)
485 }
486 #[cfg(feature = "jxl")]
487 Format::Jxl => {
488 let decoder = JxlDecoder::new(stream, options.limits)?;
489 Self::decoded(Box::new(decoder), Format::Jxl, options)
490 }
491 other => Err(PixelsError::unsupported(format!(
492 "{other} was detected but no decoder for it is compiled in"
493 ))),
494 }
495 }
496
497 #[cfg(any(
502 feature = "png",
503 feature = "gif",
504 feature = "jpeg",
505 feature = "tiff",
506 feature = "webp",
507 feature = "avif",
508 feature = "jxl"
509 ))]
510 fn decoded(decoder: Box<dyn Decoder>, format: Format, options: OpenOptions) -> Result<Self> {
511 let orientation = decoder.orientation();
512 let icc = decoder.icc_profile().map(Vec::from);
513 let animation = decoder.animation().map(Arc::new);
514 let pages = decoder.pages();
515 let xmp = decoder.xmp().map(Arc::<[u8]>::from);
516 let info = decoder.source_info();
517 let stored = decoder.descriptor();
518 let mut exif = decoder.exif().map(<[u8]>::to_vec);
519 if let Some(block) = exif.as_mut().filter(|_| options.auto_orient) {
522 Orientation::Normal.write_to_exif_block(block);
523 }
524 if options.animated && animation.is_some() {
525 return Err(PixelsError::unsupported(format!(
526 "{format}: multi-frame (animated) pipelines are not implemented yet; \
527 open with `animated` off to process the first frame"
528 )));
529 }
530 let source = SourceMetadata::new(
531 format,
532 stored,
533 orientation,
534 pages,
535 animation.as_deref().cloned(),
536 icc.as_deref(),
537 decoder.exif(),
538 xmp.clone(),
539 info,
540 );
541 let mut image = Self::from_decoder(decoder, format).with_icc_profile(icc);
542 image.animation = animation;
543 image.pages = pages;
544 image.exif = exif.map(Arc::from);
545 image.xmp = xmp;
546 image.source = Some(Arc::new(source));
547 if options.to_srgb {
548 image = image.to_srgb();
549 }
550 Ok(if options.auto_orient {
551 image.orient(orientation)
552 } else {
553 image
554 })
555 }
556
557 #[must_use]
563 pub fn from_decoder(decoder: Box<dyn Decoder>, format: Format) -> Self {
564 let source = otf_pixels_core::DecodedSource::new(decoder);
565 Self::from_producer(Arc::new(source), format)
566 }
567
568 #[must_use]
570 pub fn from_producer(producer: Arc<dyn Producer>, format: Format) -> Self {
571 Self {
572 icc: None,
573 animation: None,
574 pages: 1,
575 trim_offset: None,
576 exif: None,
577 xmp: None,
578 density: None,
579 source: None,
580 inner: Ok(otf_pixels_core::Image::from_producer(producer, format)),
581 }
582 }
583
584 pub fn metadata(&self) -> Result<Metadata> {
593 self.graph()?.metadata()
594 }
595
596 pub fn descriptor(&self) -> Result<ImageDescriptor> {
602 Ok(self.graph()?.descriptor())
603 }
604
605 #[must_use]
609 pub fn crop(self, x: u32, y: u32, width: u32, height: u32) -> Self {
610 match Crop::at(x, y, width, height) {
611 Ok(op) => self.apply(Arc::new(op)),
612 Err(error) => Self::failed(error),
613 }
614 }
615
616 #[must_use]
618 pub fn flip(self) -> Self {
619 self.apply(Arc::new(Flip))
620 }
621
622 #[must_use]
624 pub fn flop(self) -> Self {
625 self.apply(Arc::new(Flop))
626 }
627
628 #[must_use]
630 pub fn resize(self, width: u32, height: u32) -> Self {
631 self.resize_with(width, height, ResizeOptions::default())
632 }
633
634 #[must_use]
636 pub fn resize_with(self, width: u32, height: u32, options: ResizeOptions) -> Self {
637 match Resize::new(width, height, options) {
638 Ok(op) => self.apply(Arc::new(op)),
639 Err(error) => Self::failed(error),
640 }
641 }
642
643 #[must_use]
645 pub fn thumbnail(self, width: u32, height: u32) -> Self {
646 let options = ResizeOptions::default()
647 .with_fit(Fit::Inside)
648 .without_enlargement(true);
649 self.resize_with(width, height, options)
650 }
651
652 #[must_use]
660 pub fn orient(self, orientation: Orientation) -> Self {
661 let turns = orientation.clockwise_turns();
662 let image = if turns == 0 {
663 self
664 } else {
665 self.rotate(90 * i32::from(turns))
666 };
667 if orientation.mirrored() {
668 image.flop()
669 } else {
670 image
671 }
672 }
673
674 #[must_use]
676 pub fn rotate(self, degrees: i32) -> Self {
677 match Rotate::degrees(degrees) {
678 Ok(op) => self.apply(Arc::new(op)),
679 Err(error) => Self::failed(error),
680 }
681 }
682
683 #[must_use]
685 pub fn modulate(self, options: Modulate) -> Self {
686 self.apply(Arc::new(options))
687 }
688
689 #[must_use]
691 pub fn convolve(self, kernel: Kernel) -> Self {
692 self.apply(Arc::new(Convolve::new(kernel)))
693 }
694
695 #[must_use]
697 pub fn blur(self, sigma: f32) -> Self {
698 match Kernel::gaussian(sigma) {
699 Ok(kernel) => self.convolve(kernel),
700 Err(error) => Self::failed(error),
701 }
702 }
703
704 #[must_use]
706 pub fn sharpen(self, amount: f32) -> Self {
707 match Kernel::sharpen(amount) {
708 Ok(kernel) => self.convolve(kernel),
709 Err(error) => Self::failed(error),
710 }
711 }
712
713 #[must_use]
715 pub fn extract_channel(self, index: usize) -> Self {
716 self.apply(Arc::new(ExtractChannel::new(index)))
717 }
718
719 #[must_use]
725 pub fn extract_channel_named(self, channel: Channel) -> Self {
726 let layout = match self.descriptor() {
727 Ok(descriptor) => descriptor.pixel.layout(),
728 Err(error) => return Self::failed(error),
729 };
730 let index = match (channel, layout) {
731 (Channel::Red, _) => Some(0),
732 (Channel::Green, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(1),
733 (Channel::Blue, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(2),
734 (Channel::Alpha, ChannelLayout::GrayAlpha) => Some(1),
735 (Channel::Alpha, ChannelLayout::Rgba) => Some(3),
736 _ => None,
737 };
738 match index {
739 Some(index) => self.extract_channel(index),
740 None => Self::failed(PixelsError::invalid_argument(
741 "channel",
742 format!("a {layout:?} image has no {channel:?} channel"),
743 )),
744 }
745 }
746
747 #[must_use]
750 pub fn negate(self, alpha: bool) -> Self {
751 self.apply(Arc::new(Negate::new(alpha)))
752 }
753
754 #[must_use]
760 pub fn gamma(self, exponent: f32) -> Self {
761 match Gamma::new(exponent) {
762 Ok(op) => self.apply(Arc::new(op)),
763 Err(error) => Self::failed(error),
764 }
765 }
766
767 #[must_use]
770 pub fn linear(self, a: f32, b: f32) -> Self {
771 self.linear_per_channel([a; 3], [b; 3])
772 }
773
774 #[must_use]
776 pub fn linear_per_channel(self, a: [f32; 3], b: [f32; 3]) -> Self {
777 match Linear::per_channel(a, b) {
778 Ok(op) => self.apply(Arc::new(op)),
779 Err(error) => Self::failed(error),
780 }
781 }
782
783 #[must_use]
788 pub fn threshold(self, level: u8, greyscale: bool) -> Self {
789 self.apply(Arc::new(Threshold::new(level, greyscale)))
790 }
791
792 #[must_use]
795 pub fn tint(self, red: u8, green: u8, blue: u8) -> Self {
796 self.apply(Arc::new(Tint::new(red, green, blue)))
797 }
798
799 #[must_use]
803 pub fn greyscale(self) -> Self {
804 self.apply(Arc::new(Greyscale))
805 }
806
807 #[must_use]
810 pub fn recomb(self, matrix: [[f32; 3]; 3]) -> Self {
811 match Recomb::rgb(matrix) {
812 Ok(op) => self.apply(Arc::new(op)),
813 Err(error) => Self::failed(error),
814 }
815 }
816
817 #[must_use]
820 pub fn recomb_rgba(self, matrix: [[f32; 4]; 4]) -> Self {
821 match Recomb::rgba(matrix) {
822 Ok(op) => self.apply(Arc::new(op)),
823 Err(error) => Self::failed(error),
824 }
825 }
826
827 #[must_use]
831 pub fn ensure_alpha(self, level: f32) -> Self {
832 match EnsureAlpha::new(level) {
833 Ok(op) => self.apply(Arc::new(op)),
834 Err(error) => Self::failed(error),
835 }
836 }
837
838 #[must_use]
842 pub fn remove_alpha(self) -> Self {
843 let pixel = match self.descriptor() {
844 Ok(descriptor) => descriptor.pixel,
845 Err(error) => return Self::failed(error),
846 };
847 let opaque = match pixel {
848 PixelFormat::GrayA8 => PixelFormat::Gray8,
849 PixelFormat::Rgba8 => PixelFormat::Rgb8,
850 PixelFormat::Rgba16 => PixelFormat::Rgb16,
851 PixelFormat::RgbaF32 => PixelFormat::RgbF32,
852 other => other,
853 };
854 self.to_pixel_format(opaque)
855 }
856
857 #[must_use]
859 pub fn flatten(self, red: u8, green: u8, blue: u8) -> Self {
860 self.apply(Arc::new(Flatten::onto(red, green, blue)))
861 }
862
863 #[must_use]
868 pub fn extend(self, options: Extend) -> Self {
869 if options.method() == ExtendWith::Background && options.background()[3] < 255 {
870 self.ensure_alpha(1.0).apply(Arc::new(options))
871 } else {
872 self.apply(Arc::new(options))
873 }
874 }
875
876 #[must_use]
890 pub fn trim(self, options: Trim) -> Self {
891 self.trimmed(options).unwrap_or_else(Self::failed)
892 }
893
894 fn trimmed(self, options: Trim) -> Result<Self> {
896 let format = self.metadata()?.format;
897 let (graph, _) = otf_pixels_core::shrink_on_load(self.graph()?)?;
900 let descriptor = graph.descriptor();
901 let mut pixels = TileBuf::for_image(&descriptor)?;
902 {
903 let mut view = pixels.as_tile_mut()?;
904 default_scheduler()?.run(&graph, |region, tile| {
905 otf_pixels_core::copy_region(tile, &mut view, region)
906 })?;
907 }
908
909 let (pixels, cut) = match options.find(&pixels.as_tile()?)? {
910 Some(kept) if kept != descriptor.region() => {
911 let mut part = TileBuf::zeroed(kept, descriptor.pixel)?;
912 otf_pixels_core::copy_region(&pixels.as_tile()?, &mut part.as_tile_mut()?, kept)?;
913 let size = Region::from_size(kept.width, kept.height);
914 (
915 TileBuf::from_vec(size, descriptor.pixel, part.into_bytes())?,
916 kept,
917 )
918 }
919 _ => (pixels, descriptor.region()),
920 };
921 let trimmed = descriptor.resized(cut.width, cut.height)?;
922 let buffer = BufferSource::new(trimmed, Arc::new(pixels))?;
923 Ok(Self {
924 inner: Ok(otf_pixels_core::Image::from_producer(
925 Arc::new(buffer),
926 format,
927 )),
928 icc: self.icc,
929 animation: self.animation,
930 pages: self.pages,
931 trim_offset: Some((cut.x, cut.y)),
932 exif: self.exif,
933 xmp: self.xmp,
934 density: self.density,
935 source: self.source,
936 })
937 }
938
939 #[must_use]
943 pub const fn trim_offset(&self) -> Option<(u32, u32)> {
944 self.trim_offset
945 }
946
947 #[must_use]
952 pub fn composite(self, overlay: Self, x: i64, y: i64) -> Self {
953 self.composite_with(overlay, x, y, Blend::Over)
954 }
955
956 #[must_use]
958 pub fn composite_with(self, overlay: Self, x: i64, y: i64, blend: Blend) -> Self {
959 self.composite_by(overlay, Composite::at(x, y, blend))
960 }
961
962 #[must_use]
971 pub fn composite_overlays(self, overlays: impl IntoIterator<Item = Overlay>) -> Self {
972 overlays.into_iter().fold(self, |image, overlay| {
973 let op = Composite::new(overlay.placement(), overlay.blend);
974 image.composite_by(overlay.image, op)
975 })
976 }
977
978 fn composite_by(self, overlay: Self, op: Composite) -> Self {
980 let (icc, animation, pages, trim_offset, exif) = (
983 self.icc,
984 self.animation,
985 self.pages,
986 self.trim_offset,
987 self.exif,
988 );
989 let (xmp, density, source) = (self.xmp, self.density, self.source);
990 let (base, over) = match (self.inner, overlay.inner) {
991 (Ok(base), Ok(over)) => (base, over),
992 (Err(error), _) | (Ok(_), Err(error)) => return Self::failed_shared(error),
994 };
995 let op: Arc<dyn Op> = Arc::new(op);
996 Self {
997 inner: otf_pixels_core::Image::combine(&[base, over], op).map_err(Arc::new),
998 icc,
999 animation,
1000 pages,
1001 trim_offset,
1002 exif,
1003 xmp,
1004 density,
1005 source,
1006 }
1007 }
1008
1009 fn failed(error: PixelsError) -> Self {
1011 Self::failed_shared(Arc::new(error))
1012 }
1013
1014 const fn failed_shared(error: Arc<PixelsError>) -> Self {
1016 Self {
1017 inner: Err(error),
1018 icc: None,
1019 animation: None,
1020 pages: 1,
1021 trim_offset: None,
1022 exif: None,
1023 xmp: None,
1024 density: None,
1025 source: None,
1026 }
1027 }
1028
1029 #[must_use]
1036 pub fn animation(&self) -> Option<&Animation> {
1037 self.animation.as_deref()
1038 }
1039
1040 #[must_use]
1047 pub const fn pages(&self) -> u32 {
1048 self.pages
1049 }
1050
1051 #[must_use]
1057 pub fn icc_profile(&self) -> Option<&[u8]> {
1058 self.icc.as_deref()
1059 }
1060
1061 #[must_use]
1071 pub fn to_srgb(self) -> Self {
1072 let Some(profile) = self.icc.clone() else {
1073 return self;
1074 };
1075 let Ok(descriptor) = self.descriptor() else {
1076 return self;
1077 };
1078 match ToSrgb::from_profile(&profile) {
1079 Conversion::Convert(op) if op.applies_to(descriptor.pixel) => {
1080 self.apply(Arc::new(op)).with_icc_profile(None)
1081 }
1082 Conversion::AlreadySrgb => self.with_icc_profile(None),
1083 _ => self,
1084 }
1085 }
1086
1087 #[must_use]
1095 pub fn to_pixel_format(self, pixel: PixelFormat) -> Self {
1096 match self.descriptor() {
1097 Ok(descriptor) if descriptor.pixel == pixel => self,
1098 _ => self.apply(Arc::new(ConvertFormat::to(pixel))),
1099 }
1100 }
1101
1102 fn encodable_as(self, format: Format) -> Self {
1106 let Ok(descriptor) = self.descriptor() else {
1107 return self;
1108 };
1109 let pixel = descriptor.pixel;
1110 let kind = match (format, pixel.sample_kind()) {
1111 (
1112 Format::Jpeg | Format::WebP | Format::Avif | Format::Gif,
1113 SampleKind::U16 | SampleKind::F32,
1114 ) => SampleKind::U8,
1115 (Format::Png | Format::Tiff | Format::Jxl, SampleKind::F32) => SampleKind::U16,
1116 _ => return self,
1117 };
1118 match PixelFormat::from_parts(pixel.layout(), kind) {
1119 Some(target) => self.to_pixel_format(target),
1120 None => self,
1121 }
1122 }
1123
1124 #[must_use]
1130 pub fn exif(&self) -> Option<&[u8]> {
1131 self.exif.as_deref()
1132 }
1133
1134 #[must_use]
1138 pub fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
1139 self.exif = exif.map(Arc::from);
1140 self
1141 }
1142
1143 #[must_use]
1148 pub fn xmp(&self) -> Option<&[u8]> {
1149 self.xmp.as_deref()
1150 }
1151
1152 #[must_use]
1155 pub fn with_xmp(mut self, xmp: Option<Vec<u8>>) -> Self {
1156 self.xmp = xmp.map(Arc::from);
1157 self
1158 }
1159
1160 #[must_use]
1163 pub const fn density(&self) -> Option<Density> {
1164 self.density
1165 }
1166
1167 #[must_use]
1172 pub const fn with_density(mut self, density: Option<Density>) -> Self {
1173 self.density = density;
1174 self
1175 }
1176
1177 #[must_use]
1183 pub fn source_metadata(&self) -> Option<&SourceMetadata> {
1184 self.source.as_deref()
1185 }
1186
1187 pub fn stats(&self) -> Result<Stats> {
1196 let descriptor = self.descriptor()?;
1197 let pixels = self
1198 .clone()
1199 .output(Format::Raw, EncodeOptions::default())
1200 .bytes()?;
1201 Ok(Stats::of(&descriptor, &pixels))
1202 }
1203
1204 #[must_use]
1207 pub fn with_icc_profile(mut self, profile: Option<Vec<u8>>) -> Self {
1208 self.icc = profile.map(Arc::from);
1209 self
1210 }
1211
1212 #[must_use]
1217 pub fn apply(self, op: Arc<dyn Op>) -> Self {
1218 Self {
1219 icc: self.icc,
1220 animation: self.animation,
1221 pages: self.pages,
1222 trim_offset: self.trim_offset,
1223 exif: self.exif,
1224 xmp: self.xmp,
1225 density: self.density,
1226 source: self.source,
1227 inner: match self.inner {
1228 Ok(image) => image.apply(op).map_err(Arc::new),
1229 Err(error) => Err(error),
1231 },
1232 }
1233 }
1234
1235 #[must_use]
1241 pub fn output(self, format: Format, options: EncodeOptions) -> Output {
1242 Output {
1243 image: self,
1244 format,
1245 options,
1246 scheduler: None,
1247 shared: None,
1248 }
1249 }
1250
1251 fn graph(&self) -> Result<&otf_pixels_core::Image> {
1253 match &self.inner {
1254 Ok(image) => Ok(image),
1255 Err(error) => Err(rebuild(error)),
1257 }
1258 }
1259}
1260
1261fn rebuild(error: &Arc<PixelsError>) -> PixelsError {
1267 let detail = error.to_string();
1268 match error.code() {
1269 ErrorCode::Io => PixelsError::io("running the pipeline", std::io::Error::other(detail)),
1270 ErrorCode::Malformed => PixelsError::malformed("pipeline", detail),
1271 ErrorCode::Unsupported => PixelsError::unsupported(detail),
1272 ErrorCode::InvalidArgument => PixelsError::invalid_argument("pipeline", detail),
1273 ErrorCode::Graph => PixelsError::graph(detail),
1274 ErrorCode::LimitExceeded => match **error {
1275 PixelsError::LimitExceeded {
1276 limit,
1277 requested,
1278 allowed,
1279 } => PixelsError::limit_exceeded(limit, requested, allowed),
1280 _ => PixelsError::graph(detail),
1281 },
1282 _ => PixelsError::graph(detail),
1284 }
1285}
1286
1287#[derive(Debug, Clone)]
1290pub struct Overlay {
1291 image: Image,
1292 blend: Blend,
1293 gravity: Position,
1294 offset: Option<(i64, i64)>,
1295 tile: bool,
1296}
1297
1298impl Overlay {
1299 #[must_use]
1301 pub const fn new(image: Image) -> Self {
1302 Self {
1303 image,
1304 blend: Blend::Over,
1305 gravity: Position::Centre,
1306 offset: None,
1307 tile: false,
1308 }
1309 }
1310
1311 #[must_use]
1313 pub const fn with_blend(mut self, blend: Blend) -> Self {
1314 self.blend = blend;
1315 self
1316 }
1317
1318 #[must_use]
1321 pub const fn with_gravity(mut self, gravity: Position) -> Self {
1322 self.gravity = gravity;
1323 self
1324 }
1325
1326 #[must_use]
1329 pub const fn at(mut self, x: i64, y: i64) -> Self {
1330 self.offset = Some((x, y));
1331 self
1332 }
1333
1334 #[must_use]
1337 pub const fn tiled(mut self, tile: bool) -> Self {
1338 self.tile = tile;
1339 self
1340 }
1341
1342 const fn placement(&self) -> Placement {
1344 match (self.tile, self.offset) {
1345 (false, Some((x, y))) => Placement::At(x, y),
1346 (false, None) => Placement::Gravity(self.gravity),
1347 (true, Some((x, y))) => Placement::TiledFrom(x, y),
1348 (true, None) => Placement::Tiled(self.gravity),
1349 }
1350 }
1351}
1352
1353#[derive(Debug, Clone)]
1358pub struct Output {
1359 image: Image,
1360 format: Format,
1361 options: EncodeOptions,
1362 scheduler: Option<SchedulerOptions>,
1364 shared: Option<Arc<Scheduler>>,
1366}
1367
1368impl Output {
1369 fn describe(&self, encoder: &mut dyn Encoder) -> Result<()> {
1373 let options = self.options;
1374 encoder.set_icc_profile(self.image.icc.as_deref())?;
1375 encoder.set_exif(self.image.exif.as_deref().filter(|_| options.keep_exif))?;
1376 encoder.set_xmp(self.image.xmp.as_deref().filter(|_| options.keep_xmp))?;
1377 let kept = (options.keep_exif && options.keep_xmp)
1378 .then(|| self.image.source.as_ref().and_then(|s| s.density))
1379 .flatten();
1380 encoder.set_density(self.image.density.or(kept))
1381 }
1382
1383 #[must_use]
1385 pub const fn format(&self) -> Format {
1386 self.format
1387 }
1388
1389 #[must_use]
1391 pub const fn options(&self) -> EncodeOptions {
1392 self.options
1393 }
1394
1395 #[must_use]
1407 pub fn threads(mut self, threads: usize) -> Self {
1408 self.scheduler = Some(self.scheduler.unwrap_or_default().with_threads(threads));
1409 self
1410 }
1411
1412 #[must_use]
1422 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
1423 self.shared = Some(scheduler);
1424 self
1425 }
1426
1427 #[must_use]
1433 pub const fn scheduler_options(mut self, options: SchedulerOptions) -> Self {
1434 self.scheduler = Some(options);
1435 self
1436 }
1437
1438 pub fn write(self, sink: impl Sink) -> Result<()> {
1451 self.write_with_stats(sink).map(|_| ())
1452 }
1453
1454 pub fn write_with_stats(self, mut sink: impl Sink) -> Result<RunStats> {
1466 let encodable = self.image.clone().encodable_as(self.format);
1469 let (image, reduction) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1470 let descriptor = image.descriptor();
1471 let mut encoder = encoder_for(self.format, self.options)?;
1472 self.describe(encoder.as_mut())?;
1473 encoder.write_header(&descriptor, &mut sink)?;
1474
1475 let scheduler = match (&self.shared, self.scheduler) {
1481 (Some(shared), _) => Arc::clone(shared),
1482 (None, Some(options)) => Arc::new(Scheduler::new(options)?),
1483 (None, None) => default_scheduler()?,
1484 };
1485 let mut rows = RowAssembler::new(descriptor);
1486 let mut stats = scheduler.run(&image, |region, tile| {
1487 rows.accept(region, tile, &mut |row| encoder.write_row(row, &mut sink))
1488 })?;
1489 rows.finish(&mut |row| encoder.write_row(row, &mut sink))?;
1490 encoder.finish(&mut sink)?;
1491 stats.reduction = reduction;
1492 Ok(stats)
1493 }
1494
1495 pub fn bytes_via_reference(self) -> Result<Vec<u8>> {
1512 let encodable = self.image.clone().encodable_as(self.format);
1516 let (image, _) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1517 let descriptor = image.descriptor();
1518 let mut encoder = encoder_for(self.format, self.options)?;
1519 self.describe(encoder.as_mut())?;
1520 let mut sink = Vec::with_capacity(descriptor.byte_len().unwrap_or_default());
1521 encoder.write_header(&descriptor, &mut sink)?;
1522 otf_pixels_core::evaluate_rows(&image, |_, row| encoder.write_row(row, &mut sink))?;
1523 encoder.finish(&mut sink)?;
1524 Ok(sink)
1525 }
1526
1527 pub fn bytes(self) -> Result<Vec<u8>> {
1533 let hint = self
1536 .image
1537 .descriptor()
1538 .ok()
1539 .and_then(|d| d.byte_len())
1540 .unwrap_or_default();
1541 let mut buffer = Vec::with_capacity(hint);
1542 self.write(&mut buffer)?;
1543 Ok(buffer)
1544 }
1545}
1546
1547fn default_scheduler() -> Result<Arc<Scheduler>> {
1552 if otf_pixels_core::ThreadPool::on_worker_thread() {
1553 Ok(Arc::new(Scheduler::with_defaults()?))
1554 } else {
1555 Scheduler::global()
1556 }
1557}
1558
1559#[derive(Debug)]
1567struct RowAssembler {
1568 descriptor: ImageDescriptor,
1569 band: Option<otf_pixels_core::TileBuf>,
1571 next_row: u32,
1573}
1574
1575impl RowAssembler {
1576 const fn new(descriptor: ImageDescriptor) -> Self {
1577 Self {
1578 descriptor,
1579 band: None,
1580 next_row: 0,
1581 }
1582 }
1583
1584 fn accept(
1586 &mut self,
1587 region: Region,
1588 tile: &otf_pixels_core::Tile<'_>,
1589 emit: &mut impl FnMut(&[u8]) -> Result<()>,
1590 ) -> Result<()> {
1591 if region.width == self.descriptor.width && self.band.is_none() {
1593 for y in region.y..region.y.saturating_add(region.height) {
1594 let row = tile
1595 .row(y)
1596 .ok_or_else(|| PixelsError::graph(format!("output tile is missing row {y}")))?;
1597 emit(row)?;
1598 self.next_row = y.saturating_add(1);
1599 }
1600 return Ok(());
1601 }
1602
1603 let band_region = Region::new(0, region.y, self.descriptor.width, region.height);
1606 let starts_new_band = self
1607 .band
1608 .as_ref()
1609 .is_none_or(|band| band.region().y != region.y);
1610 if starts_new_band {
1611 self.flush(emit)?;
1612 self.band = Some(otf_pixels_core::TileBuf::zeroed(
1613 band_region,
1614 self.descriptor.pixel,
1615 )?);
1616 }
1617 let Some(band) = self.band.as_mut() else {
1618 return Err(PixelsError::graph("row band vanished"));
1619 };
1620 otf_pixels_core::copy_region(tile, &mut band.as_tile_mut()?, region)?;
1621
1622 if region.right() >= u64::from(self.descriptor.width) {
1624 self.flush(emit)?;
1625 }
1626 Ok(())
1627 }
1628
1629 fn flush(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1631 let Some(band) = self.band.take() else {
1632 return Ok(());
1633 };
1634 let region = band.region();
1635 let view = band.as_tile()?;
1636 for y in region.y..region.y.saturating_add(region.height) {
1637 let row = view
1638 .row(y)
1639 .ok_or_else(|| PixelsError::graph(format!("row band is missing row {y}")))?;
1640 emit(row)?;
1641 self.next_row = y.saturating_add(1);
1642 }
1643 Ok(())
1644 }
1645
1646 fn finish(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1648 self.flush(emit)
1649 }
1650}
1651
1652fn sniffing_codecs() -> Vec<Box<dyn Codec>> {
1657 let codecs: Vec<Box<dyn Codec>> = vec![
1658 #[cfg(feature = "png")]
1659 Box::new(PngCodec),
1660 #[cfg(feature = "gif")]
1661 Box::new(GifCodec),
1662 #[cfg(feature = "jpeg")]
1663 Box::new(JpegCodec),
1664 #[cfg(feature = "tiff")]
1665 Box::new(TiffCodec),
1666 #[cfg(feature = "webp")]
1667 Box::new(WebPCodec),
1668 #[cfg(feature = "avif")]
1669 Box::new(AvifCodec),
1670 #[cfg(feature = "jxl")]
1671 Box::new(JxlCodec),
1672 ];
1673 codecs
1674}
1675
1676fn encoder_for(format: Format, options: EncodeOptions) -> Result<Box<dyn Encoder>> {
1678 let _ = &options;
1681 match format {
1682 #[cfg(feature = "raw")]
1683 Format::Raw => Ok(Box::new(RawEncoder::new())),
1684 #[cfg(feature = "png")]
1685 Format::Png => Ok(Box::new(PngEncoder::from_options(&options))),
1686 #[cfg(feature = "gif")]
1687 Format::Gif => Ok(Box::new(GifEncoder::from_options(&options))),
1688 #[cfg(feature = "jpeg")]
1689 Format::Jpeg => Ok(Box::new(JpegEncoder::from_options(&options))),
1690 #[cfg(feature = "tiff")]
1691 Format::Tiff => Ok(Box::new(TiffEncoder::from_options(&options))),
1692 #[cfg(feature = "webp")]
1693 Format::WebP => Ok(Box::new(WebPEncoder::from_options(&options))),
1694 #[cfg(feature = "avif")]
1695 Format::Avif => Ok(Box::new(AvifEncoder::from_options(&options))),
1696 #[cfg(feature = "jxl")]
1697 Format::Jxl => Ok(Box::new(JxlEncoder::from_options(&options))),
1698 #[cfg(not(feature = "raw"))]
1699 Format::Raw => Err(PixelsError::unsupported(
1700 "raw encoding requires the `raw` feature of otf-pixels",
1701 )),
1702 other => Err(PixelsError::unsupported(format!(
1703 "encoding {other} is not implemented yet; \
1704 see the ROADMAP for which milestone lands it"
1705 ))),
1706 }
1707}
1708
1709#[cfg(all(test, feature = "raw"))]
1714#[allow(
1715 clippy::unwrap_used,
1716 clippy::expect_used,
1717 clippy::indexing_slicing,
1718 clippy::panic,
1719 reason = "tests operate on known-good values and assert shapes directly"
1720)]
1721mod tests {
1722 use super::*;
1723
1724 fn ramp(width: u32, height: u32) -> Image {
1725 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
1726 let len = descriptor.byte_len().unwrap();
1727 Image::from_raw(descriptor, (0..len).map(|i| i as u8).collect()).unwrap()
1728 }
1729
1730 #[test]
1731 fn from_raw_requires_an_exactly_sized_buffer() {
1732 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1733 assert!(Image::from_raw(descriptor, vec![0; 4]).is_ok());
1734 assert_eq!(
1735 Image::from_raw(descriptor, vec![0; 3]).unwrap_err().code(),
1736 ErrorCode::InvalidArgument
1737 );
1738 assert_eq!(
1739 Image::from_raw(descriptor, vec![0; 5]).unwrap_err().code(),
1740 ErrorCode::InvalidArgument
1741 );
1742 }
1743
1744 #[test]
1745 fn a_chain_error_surfaces_at_the_terminal() {
1746 let result = ramp(4, 4)
1748 .crop(3, 3, 4, 4)
1749 .flip()
1750 .flop()
1751 .output(Format::Raw, EncodeOptions::default())
1752 .bytes();
1753 let err = result.unwrap_err();
1754 assert_eq!(err.code(), ErrorCode::InvalidArgument);
1755 }
1756
1757 #[test]
1758 fn an_error_short_circuits_later_ops() {
1759 let broken = ramp(4, 4).crop(0, 0, 0, 0);
1762 assert!(broken.metadata().is_err());
1763 assert!(broken.clone().flip().descriptor().is_err());
1764 }
1765
1766 #[test]
1767 fn metadata_is_free_and_reports_the_pipeline_shape() {
1768 let meta = ramp(8, 6).metadata().unwrap();
1769 assert_eq!((meta.width, meta.height), (8, 6));
1770 assert_eq!(meta.format, Format::Raw);
1771 assert_eq!(meta.pixel, PixelFormat::Gray8);
1772 let cropped = ramp(8, 6).crop(1, 1, 3, 2).metadata().unwrap();
1774 assert_eq!((cropped.width, cropped.height), (3, 2));
1775 }
1776
1777 #[cfg(not(feature = "avif"))]
1781 #[test]
1782 fn a_format_built_out_is_a_catchable_error() {
1783 let format = Format::Avif;
1784 let err = ramp(2, 2)
1785 .output(format, EncodeOptions::default())
1786 .bytes()
1787 .unwrap_err();
1788 assert_eq!(err.code(), ErrorCode::Unsupported, "{format}");
1789 assert!(err.to_string().contains(format.as_str()), "{err}");
1790 }
1791
1792 #[cfg(feature = "tiff")]
1797 #[test]
1798 fn tiff_round_trips_through_the_facade() {
1799 let bytes = ramp(12, 9)
1800 .output(Format::Tiff, EncodeOptions::default())
1801 .bytes()
1802 .unwrap();
1803 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1804 let metadata = image.metadata().unwrap();
1805 assert_eq!(metadata.format, Format::Tiff);
1806 assert_eq!((metadata.width, metadata.height), (12, 9));
1807
1808 let decoded = image
1810 .output(Format::Raw, EncodeOptions::default())
1811 .bytes()
1812 .unwrap();
1813 let expected: Vec<u8> = (0..12_u32 * 9).map(|i| i as u8).collect();
1814 assert_eq!(decoded, expected);
1815 }
1816
1817 #[cfg(feature = "jpeg")]
1820 #[test]
1821 fn jpeg_round_trips_through_the_facade() {
1822 let (width, height) = (32_u32, 24_u32);
1823 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1824 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1825 .map(|i| {
1826 let pixel = i / 3;
1829 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1830 match i % 3 {
1831 0 => (x * 255 / width) as u8,
1832 1 => (y * 255 / height) as u8,
1833 _ => 128,
1834 }
1835 })
1836 .collect();
1837
1838 let bytes = Image::from_raw(descriptor, pixels.clone())
1839 .unwrap()
1840 .output(Format::Jpeg, EncodeOptions::with_quality(95).unwrap())
1841 .bytes()
1842 .unwrap();
1843
1844 let image = Image::from_stream(std::io::Cursor::new(bytes.clone())).unwrap();
1845 let metadata = image.metadata().unwrap();
1846 assert_eq!(metadata.format, Format::Jpeg, "sniffing missed the JPEG");
1847 assert_eq!((metadata.width, metadata.height), (width, height));
1848 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1849
1850 let decoded = image
1851 .output(Format::Raw, EncodeOptions::default())
1852 .bytes()
1853 .unwrap();
1854 assert_eq!(decoded.len(), pixels.len());
1855 let worst = decoded
1856 .iter()
1857 .zip(&pixels)
1858 .map(|(&a, &b)| a.abs_diff(b))
1859 .max()
1860 .unwrap_or(0);
1861 assert!(worst <= 12, "worst sample differs by {worst}");
1862 }
1863
1864 #[cfg(feature = "jpeg")]
1875 #[test]
1876 fn a_streaming_resize_matches_the_reference_evaluator() {
1877 for &(width, height, target) in &[
1878 (512_u32, 512_u32, 300_u32),
1879 (512, 512, 256),
1880 (256, 256, 200),
1881 (320, 240, 129),
1882 (64, 64, 16),
1883 ] {
1884 let source = jpeg_source(width, height);
1885 let scheduled = Image::from_stream(std::io::Cursor::new(source.clone()))
1886 .unwrap()
1887 .resize(target, target)
1888 .output(Format::Raw, EncodeOptions::default())
1889 .bytes()
1890 .unwrap_or_else(|e| panic!("{width}x{height} -> {target}: {e}"));
1891 let reference = Image::from_stream(std::io::Cursor::new(source))
1892 .unwrap()
1893 .resize(target, target)
1894 .output(Format::Raw, EncodeOptions::default())
1895 .bytes_via_reference()
1896 .unwrap();
1897
1898 assert_eq!(
1899 scheduled.len(),
1900 (target * target * 3) as usize,
1901 "{width}x{height} -> {target}: size"
1902 );
1903 assert_eq!(
1904 scheduled, reference,
1905 "{width}x{height} -> {target}: the scheduler and the oracle disagree"
1906 );
1907 }
1908 }
1909
1910 #[cfg(feature = "webp")]
1916 #[test]
1917 fn webp_round_trips_through_the_facade() {
1918 let bytes = ramp(20, 12)
1919 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1920 .bytes()
1921 .unwrap();
1922 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1923 let metadata = image.metadata().unwrap();
1924 assert_eq!(metadata.format, Format::WebP, "sniffing missed the WebP");
1925 assert_eq!((metadata.width, metadata.height), (20, 12));
1926 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1928
1929 let decoded = image
1930 .output(Format::Raw, EncodeOptions::default())
1931 .bytes()
1932 .unwrap();
1933 let expected: Vec<u8> = (0..20_u32 * 12)
1934 .flat_map(|i| {
1935 let value = i as u8;
1936 [value, value, value]
1937 })
1938 .collect();
1939 assert_eq!(decoded, expected, "a lossless round trip lost pixels");
1940 }
1941
1942 #[cfg(feature = "webp")]
1945 #[test]
1946 fn webp_keeps_alpha_through_the_facade() {
1947 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::Rgba8).unwrap();
1948 let pixels: Vec<u8> = (0..(9 * 7))
1949 .flat_map(|i| [(i * 3) as u8, 40, 200, (i * 7) as u8])
1950 .collect();
1951 let bytes = Image::from_raw(descriptor, pixels.clone())
1952 .unwrap()
1953 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1954 .bytes()
1955 .unwrap();
1956
1957 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1958 assert_eq!(image.metadata().unwrap().pixel, PixelFormat::Rgba8);
1959 let decoded = image
1960 .output(Format::Raw, EncodeOptions::default())
1961 .bytes()
1962 .unwrap();
1963 assert_eq!(decoded, pixels);
1964 }
1965
1966 #[cfg(feature = "webp")]
1970 #[test]
1971 fn webp_is_lossy_by_default_and_keeps_alpha_exact() {
1972 let (w, h) = (48_u32, 32_u32);
1973 let descriptor = ImageDescriptor::new(w, h, PixelFormat::Rgba8).unwrap();
1974 let pixels: Vec<u8> = (0..w * h)
1975 .flat_map(|i| {
1976 let (x, y) = (i % w, i / w);
1977 [(x * 5) as u8, (y * 7) as u8, 120, (x * 3 + y) as u8]
1978 })
1979 .collect();
1980 let encode = |options: EncodeOptions| {
1981 Image::from_raw(descriptor, pixels.clone())
1982 .unwrap()
1983 .output(Format::WebP, options)
1984 .bytes()
1985 .unwrap()
1986 };
1987 let good = encode(EncodeOptions::default());
1988 let rough = encode(EncodeOptions::with_quality(10).unwrap());
1989 assert!(
1990 rough.len() < good.len(),
1991 "{} vs {}",
1992 rough.len(),
1993 good.len()
1994 );
1995
1996 let decoded = Image::from_stream(std::io::Cursor::new(good))
1997 .unwrap()
1998 .output(Format::Raw, EncodeOptions::default())
1999 .bytes()
2000 .unwrap();
2001 for (ours, source) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
2002 assert_eq!(ours[3], source[3], "alpha changed");
2003 for c in 0..3 {
2004 assert!(ours[c].abs_diff(source[c]) <= 12, "{ours:?} vs {source:?}");
2005 }
2006 }
2007 }
2008
2009 #[cfg(feature = "jpeg")]
2012 #[test]
2013 fn a_thumbnail_pipeline_shrinks_the_jpeg_on_load() {
2014 let source = jpeg_source(512, 512);
2016
2017 let image = Image::from_stream(std::io::Cursor::new(source)).unwrap();
2018 assert_eq!(
2019 image.metadata().unwrap().width,
2020 512,
2021 "metadata is full size"
2022 );
2023
2024 let mut bytes = Vec::new();
2027 let stats = image
2028 .resize(32, 32)
2029 .output(Format::Raw, EncodeOptions::default())
2030 .write_with_stats(&mut bytes)
2031 .unwrap();
2032
2033 let reduction = stats
2034 .reduction
2035 .expect("a 512 to 32 resize should shrink the source on load");
2036 assert_eq!(reduction.from, (512, 512));
2037 assert_eq!(
2038 reduction.to,
2039 (64, 64),
2040 "1/8 is the coarsest scale that still covers 32"
2041 );
2042 assert!((reduction.factor() - 64.0).abs() < 0.01);
2043 assert_eq!(bytes.len(), 32 * 32 * 3);
2045 }
2046
2047 #[cfg(feature = "jpeg")]
2050 #[test]
2051 fn a_crop_blocks_shrink_on_load() {
2052 let source = jpeg_source(512, 512);
2053 let stats = Image::from_stream(std::io::Cursor::new(source))
2054 .unwrap()
2055 .crop(256, 256, 128, 128)
2056 .resize(32, 32)
2057 .output(Format::Raw, EncodeOptions::default())
2058 .write_with_stats(&mut Vec::new())
2059 .unwrap();
2060 assert!(
2061 stats.reduction.is_none(),
2062 "a crop must block shrink-on-load: {:?}",
2063 stats.reduction
2064 );
2065 }
2066
2067 #[cfg(feature = "jpeg")]
2070 #[test]
2071 fn a_crop_after_the_resize_still_allows_shrinking() {
2072 let source = jpeg_source(512, 512);
2073 let stats = Image::from_stream(std::io::Cursor::new(source))
2074 .unwrap()
2075 .resize(64, 64)
2076 .crop(0, 0, 32, 32)
2077 .output(Format::Raw, EncodeOptions::default())
2078 .write_with_stats(&mut Vec::new())
2079 .unwrap();
2080 assert!(stats.reduction.is_none());
2084 }
2085
2086 #[cfg(feature = "jpeg")]
2089 #[test]
2090 fn a_mild_resize_shrinks_by_less_or_not_at_all() {
2091 let source = jpeg_source(512, 512);
2092 let stats = Image::from_stream(std::io::Cursor::new(source.clone()))
2094 .unwrap()
2095 .resize(300, 300)
2096 .output(Format::Raw, EncodeOptions::default())
2097 .write_with_stats(&mut Vec::new())
2098 .unwrap();
2099 assert!(stats.reduction.is_none(), "{:?}", stats.reduction);
2100
2101 let stats = Image::from_stream(std::io::Cursor::new(source))
2103 .unwrap()
2104 .resize(200, 200)
2105 .output(Format::Raw, EncodeOptions::default())
2106 .write_with_stats(&mut Vec::new())
2107 .unwrap();
2108 assert_eq!(stats.reduction.map(|r| r.to), Some((256, 256)));
2109 }
2110
2111 #[cfg(feature = "jpeg")]
2113 fn jpeg_source(width: u32, height: u32) -> Vec<u8> {
2114 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
2115 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
2116 .map(|i| {
2117 let pixel = i / 3;
2118 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
2119 match i % 3 {
2120 0 => (x * 255 / width) as u8,
2121 1 => (y * 255 / height) as u8,
2122 _ => 96,
2123 }
2124 })
2125 .collect();
2126 Image::from_raw(descriptor, pixels)
2127 .unwrap()
2128 .output(Format::Jpeg, EncodeOptions::with_quality(85).unwrap())
2129 .bytes()
2130 .unwrap()
2131 }
2132
2133 #[cfg(feature = "jpeg")]
2135 #[test]
2136 fn a_jpeg_pipeline_resizes_and_re_encodes() {
2137 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
2138 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
2139 .map(|i| ((i / 3) % 251) as u8)
2140 .collect();
2141 let source = Image::from_raw(descriptor, pixels)
2142 .unwrap()
2143 .output(Format::Jpeg, EncodeOptions::default())
2144 .bytes()
2145 .unwrap();
2146
2147 let thumbnail = Image::from_stream(std::io::Cursor::new(source.clone()))
2148 .unwrap()
2149 .resize(16, 16)
2150 .output(Format::Jpeg, EncodeOptions::with_quality(70).unwrap())
2151 .bytes()
2152 .unwrap();
2153
2154 let metadata = Image::from_stream(std::io::Cursor::new(thumbnail.clone()))
2155 .unwrap()
2156 .metadata()
2157 .unwrap();
2158 assert_eq!((metadata.width, metadata.height), (16, 16));
2159 assert!(
2160 thumbnail.len() < source.len(),
2161 "a 16x16 thumbnail is {} bytes against a 64x64 source's {}",
2162 thumbnail.len(),
2163 source.len()
2164 );
2165 }
2166
2167 #[cfg(feature = "jpeg")]
2170 #[test]
2171 fn grayscale_jpeg_keeps_one_channel_through_the_facade() {
2172 let bytes = ramp(24, 16)
2173 .output(Format::Jpeg, EncodeOptions::with_quality(90).unwrap())
2174 .bytes()
2175 .unwrap();
2176 let metadata = Image::from_stream(std::io::Cursor::new(bytes))
2177 .unwrap()
2178 .metadata()
2179 .unwrap();
2180 assert_eq!(metadata.pixel, PixelFormat::Gray8);
2181 assert_eq!((metadata.width, metadata.height), (24, 16));
2182 }
2183
2184 #[test]
2185 fn write_streams_into_any_sink() {
2186 let mut sink = Vec::new();
2187 ramp(2, 2)
2188 .output(Format::Raw, EncodeOptions::default())
2189 .write(&mut sink)
2190 .unwrap();
2191 assert_eq!(sink, [0, 1, 2, 3]);
2192 }
2193
2194 #[test]
2195 fn output_reports_its_settings() {
2196 let options = EncodeOptions::with_quality(55).unwrap();
2197 let output = ramp(2, 2).output(Format::Raw, options);
2198 assert_eq!(output.format(), Format::Raw);
2199 assert_eq!(output.options().quality, 55);
2200 }
2201
2202 #[cfg(feature = "raw")]
2203 #[test]
2204 fn from_raw_stream_defers_decoding_to_the_terminal() {
2205 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
2206 let layout = RawFormat::packed(descriptor);
2207 let cursor = std::io::Cursor::new(vec![1_u8, 2, 3, 4]);
2208 let image = Image::from_raw_stream(layout, cursor).unwrap();
2209 assert_eq!(image.metadata().unwrap().width, 2);
2211 let bytes = image
2212 .output(Format::Raw, EncodeOptions::default())
2213 .bytes()
2214 .unwrap();
2215 assert_eq!(bytes, [1, 2, 3, 4]);
2216 }
2217
2218 #[cfg(feature = "raw")]
2219 #[test]
2220 fn a_truncated_stream_fails_the_terminal_without_panicking() {
2221 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
2222 let layout = RawFormat::packed(descriptor);
2223 let cursor = std::io::Cursor::new(vec![1_u8; 5]);
2224 let image = Image::from_raw_stream(layout, cursor).unwrap();
2225 let err = image
2226 .output(Format::Raw, EncodeOptions::default())
2227 .bytes()
2228 .unwrap_err();
2229 assert_eq!(err.code(), ErrorCode::Malformed);
2230 }
2231
2232 #[test]
2233 fn images_are_send_sync_and_cheap_to_clone() {
2234 const fn assert_send_sync<T: Send + Sync>() {}
2235 assert_send_sync::<Image>();
2236 assert_send_sync::<Output>();
2237 let image = ramp(4, 4);
2238 let clone = image.clone();
2239 assert_eq!(
2240 image.metadata().unwrap().width,
2241 clone.metadata().unwrap().width
2242 );
2243 }
2244
2245 #[cfg(feature = "png")]
2246 #[test]
2247 fn a_png_round_trips_through_the_facade() {
2248 let png = ramp(37, 21)
2249 .output(Format::Png, EncodeOptions::default())
2250 .bytes()
2251 .unwrap();
2252 let image = Image::from_stream(std::io::Cursor::new(png)).unwrap();
2253 assert_eq!(image.descriptor().unwrap().width, 37);
2254 assert_eq!(image.metadata().unwrap().format, Format::Png);
2255
2256 let back = image
2257 .output(Format::Raw, EncodeOptions::default())
2258 .bytes()
2259 .unwrap();
2260 let expected = ramp(37, 21)
2261 .output(Format::Raw, EncodeOptions::default())
2262 .bytes()
2263 .unwrap();
2264 assert_eq!(back, expected, "pixels changed across a PNG round trip");
2265 }
2266
2267 #[cfg(feature = "png")]
2268 #[test]
2269 fn a_pipeline_survives_a_png_round_trip() {
2270 let png = ramp(16, 16)
2273 .output(Format::Png, EncodeOptions::default())
2274 .bytes()
2275 .unwrap();
2276 let cropped = Image::from_stream(std::io::Cursor::new(png))
2277 .unwrap()
2278 .crop(2, 3, 8, 5)
2279 .flip()
2280 .output(Format::Raw, EncodeOptions::default())
2281 .bytes()
2282 .unwrap();
2283 let direct = ramp(16, 16)
2284 .crop(2, 3, 8, 5)
2285 .flip()
2286 .output(Format::Raw, EncodeOptions::default())
2287 .bytes()
2288 .unwrap();
2289 assert_eq!(cropped, direct);
2290 }
2291
2292 #[cfg(feature = "png")]
2293 #[test]
2294 fn open_ignores_the_extension_and_reads_the_bytes() {
2295 let dir = std::env::temp_dir().join(format!("otf-pixels-open-{}", std::process::id()));
2296 std::fs::create_dir_all(&dir).unwrap();
2297 let path = dir.join("actually-a-png.jpg");
2298 let png = ramp(8, 8)
2299 .output(Format::Png, EncodeOptions::default())
2300 .bytes()
2301 .unwrap();
2302 std::fs::write(&path, &png).unwrap();
2303
2304 let image = Image::open(&path).unwrap();
2305 assert_eq!(
2306 image.metadata().unwrap().format,
2307 Format::Png,
2308 "extension won over content"
2309 );
2310 std::fs::remove_dir_all(&dir).ok();
2311 }
2312
2313 #[test]
2314 fn open_names_the_file_it_could_not_read() {
2315 let error = Image::open("/nonexistent/otf-pixels/missing.png").unwrap_err();
2316 assert_eq!(error.code(), ErrorCode::Io);
2317 assert!(error.to_string().contains("missing.png"), "{error}");
2318 }
2319
2320 #[test]
2321 fn an_unrecognised_stream_is_unsupported_not_a_guess() {
2322 for bytes in [&b""[..], &b"not an image"[..], &[0_u8; 64][..]] {
2323 let error = Image::from_stream(std::io::Cursor::new(bytes.to_vec())).unwrap_err();
2324 assert_eq!(error.code(), ErrorCode::Unsupported, "{bytes:02x?}");
2325 }
2326 }
2327
2328 #[cfg(feature = "png")]
2329 #[test]
2330 fn a_truncated_png_is_malformed_not_a_panic() {
2331 let png = ramp(8, 8)
2332 .output(Format::Png, EncodeOptions::default())
2333 .bytes()
2334 .unwrap();
2335 for cut in [9, 16, 24, 32, png.len() - 1] {
2338 let truncated = png[..cut].to_vec();
2339 let result = Image::from_stream(std::io::Cursor::new(truncated))
2340 .and_then(|i| i.output(Format::Raw, EncodeOptions::default()).bytes());
2341 assert!(result.is_err(), "truncating to {cut} bytes should fail");
2342 }
2343 }
2344
2345 #[cfg(feature = "png")]
2346 #[test]
2347 fn sniffing_reads_only_the_magic_bytes_before_deciding() {
2348 let error = Image::from_stream(std::io::Cursor::new(
2352 otf_pixels_codec_png::SIGNATURE.to_vec(),
2353 ))
2354 .unwrap_err();
2355 assert_eq!(error.code(), ErrorCode::Malformed, "{error}");
2356 }
2357}
2358
2359const _: () = {
2362 const fn shareable<T: Send + Sync>() {}
2363 shareable::<Image>();
2364 shareable::<Output>();
2365 shareable::<OpenOptions>();
2366 shareable::<PixelsError>();
2367 shareable::<Scheduler>();
2368};