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};
126use std::sync::Arc;
127
128pub use otf_pixels_core::{
129 AccessPattern, Animation, ChannelLayout, Codec, ColorModel, Decoder, EncodeOptions, Encoder,
130 ErrorCode, Format, ImageDescriptor, Limit, Limits, Metadata, Orientation, PixelFormat,
131 PixelsError, PlanOptions, Region, Result, RunStats, SampleKind, Scheduler, SchedulerOptions,
132 Sink, Source, TileShape, evaluate as evaluate_reference,
133};
134pub use otf_pixels_ops::{
135 Blend, Composite, Conversion, ConvertFormat, Convolve, Crop, EnsureAlpha, ExtractChannel,
136 Filter, Fit, Flatten, Flip, Flop, Gamma, Greyscale, Kernel, Linear, Modulate, Negate, Position,
137 Quarter, Recomb, Resize, ResizeOptions, Rotate, Threshold, Tint, ToSrgb, Unconvertible,
138};
139
140#[cfg(feature = "raw")]
141pub use otf_pixels_codec_raw::{RawCodec, RawDecoder, RawEncoder, RawFormat};
142
143#[cfg(feature = "png")]
144pub use otf_pixels_codec_png::{PngCodec, PngDecoder, PngEncoder};
145
146#[cfg(feature = "gif")]
147pub use otf_pixels_codec_gif::{GifCodec, GifDecoder, GifEncoder};
148
149#[cfg(feature = "jpeg")]
150pub use otf_pixels_codec_jpeg::{JpegCodec, JpegDecoder, JpegEncoder, Scale, Subsampling};
151
152#[cfg(feature = "tiff")]
153pub use otf_pixels_codec_tiff::{TiffCodec, TiffDecoder, TiffEncoder, TiffLayout};
154
155#[cfg(feature = "webp")]
156pub use otf_pixels_codec_webp::{WebPCodec, WebPDecoder, WebPEncoder};
157
158#[cfg(feature = "avif")]
159pub use otf_pixels_codec_avif::{AvifCodec, AvifDecoder, AvifEncoder};
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[non_exhaustive]
164pub struct OpenOptions {
165 pub auto_orient: bool,
173 pub to_srgb: bool,
182 pub animated: bool,
191 pub page: u32,
198 pub limits: Limits,
202}
203
204impl OpenOptions {
205 #[must_use]
210 pub const fn with_auto_orient(mut self, auto_orient: bool) -> Self {
211 self.auto_orient = auto_orient;
212 self
213 }
214
215 #[must_use]
217 pub const fn with_animated(mut self, animated: bool) -> Self {
218 self.animated = animated;
219 self
220 }
221
222 #[must_use]
225 pub const fn with_page(mut self, page: u32) -> Self {
226 self.page = page;
227 self
228 }
229
230 #[must_use]
232 pub const fn with_limits(mut self, limits: Limits) -> Self {
233 self.limits = limits;
234 self
235 }
236
237 #[must_use]
239 pub const fn with_to_srgb(mut self, to_srgb: bool) -> Self {
240 self.to_srgb = to_srgb;
241 self
242 }
243}
244
245impl Default for OpenOptions {
246 fn default() -> Self {
247 Self {
248 auto_orient: true,
249 to_srgb: true,
250 animated: false,
251 page: 0,
252 limits: Limits::default(),
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
259#[non_exhaustive]
260pub enum Channel {
261 Red,
263 Green,
265 Blue,
267 Alpha,
269}
270
271#[derive(Debug, Clone)]
276pub struct Image {
277 inner: std::result::Result<otf_pixels_core::Image, Arc<PixelsError>>,
279 icc: Option<Arc<[u8]>>,
282 animation: Option<Arc<Animation>>,
284 pages: u32,
286}
287
288impl Image {
289 pub fn from_raw(descriptor: ImageDescriptor, bytes: Vec<u8>) -> Result<Self> {
298 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes)?;
299 let source = BufferSource::new(descriptor, Arc::new(buffer))?;
300 Ok(Self::from_producer(Arc::new(source), Format::Raw))
301 }
302
303 #[cfg(feature = "raw")]
313 pub fn from_raw_stream(
314 layout: RawFormat,
315 source: impl Source + std::fmt::Debug + 'static,
316 ) -> Result<Self> {
317 let decoder = RawDecoder::new(layout, source)?;
318 Ok(Self::from_decoder(Box::new(decoder), Format::Raw))
319 }
320
321 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
335 Self::open_with(path, OpenOptions::default())
336 }
337
338 pub fn open_with(path: impl AsRef<std::path::Path>, options: OpenOptions) -> Result<Self> {
344 let path = path.as_ref();
345 let file = std::fs::File::open(path).map_err(|e| {
346 let kind = e.kind();
350 let detail = std::io::Error::new(kind, format!("{}: {e}", path.display()));
351 PixelsError::io("opening image file", detail)
352 })?;
353 Self::from_stream_with(std::io::BufReader::new(file), options)
354 }
355
356 pub fn from_stream(source: impl Source + std::fmt::Debug + 'static) -> Result<Self> {
372 Self::from_stream_with(source, OpenOptions::default())
373 }
374
375 pub fn from_stream_with(
381 mut source: impl Source + std::fmt::Debug + 'static,
382 options: OpenOptions,
383 ) -> Result<Self> {
384 let codecs = sniffing_codecs();
385 let longest = codecs.iter().map(|c| c.magic_len()).max().unwrap_or(0);
386
387 let mut prefix = Vec::with_capacity(longest);
391 let mut buffer = vec![0_u8; longest];
392 while prefix.len() < longest {
393 let Some(rest) = buffer.get_mut(prefix.len()..) else {
394 break;
395 };
396 match source.read(rest)? {
397 0 => break,
398 n => {
399 let Some(read) = rest.get(..n) else { break };
400 prefix.extend_from_slice(read);
401 }
402 }
403 }
404
405 let Some(codec) = codecs.iter().find(|codec| codec.probe(&prefix)) else {
406 return Err(PixelsError::unsupported(format!(
407 "no codec recognises this stream; its first {} bytes are {:02x?}",
408 prefix.len().min(8),
409 prefix.get(..prefix.len().min(8)).unwrap_or(&[])
410 )));
411 };
412 let stream = Prefixed::new(prefix, source);
413 if options.page > 0 && codec.format() != Format::Tiff {
415 return Err(PixelsError::invalid_argument(
416 "page",
417 format!(
418 "page {} was asked for, but a {} has only page 0",
419 options.page,
420 codec.format()
421 ),
422 ));
423 }
424 let _ = (&stream, options);
427
428 match codec.format() {
429 #[cfg(feature = "png")]
430 Format::Png => {
431 let decoder = PngDecoder::new(stream, options.limits)?;
432 Self::decoded(Box::new(decoder), Format::Png, options)
433 }
434 #[cfg(feature = "gif")]
435 Format::Gif => {
436 let decoder = GifDecoder::new(stream, options.limits)?;
437 Self::decoded(Box::new(decoder), Format::Gif, options)
438 }
439 #[cfg(feature = "jpeg")]
440 Format::Jpeg => {
441 let decoder = JpegDecoder::new(stream, options.limits)?;
442 Self::decoded(Box::new(decoder), Format::Jpeg, options)
443 }
444 #[cfg(feature = "tiff")]
445 Format::Tiff => {
446 let decoder = TiffDecoder::with_page(stream, options.limits, options.page)?;
447 Self::decoded(Box::new(decoder), Format::Tiff, options)
448 }
449 #[cfg(feature = "webp")]
450 Format::WebP => {
451 let decoder = WebPDecoder::new(stream, options.limits)?;
452 Self::decoded(Box::new(decoder), Format::WebP, options)
453 }
454 #[cfg(feature = "avif")]
455 Format::Avif => {
456 let decoder = AvifDecoder::new(stream, options.limits)?;
457 Self::decoded(Box::new(decoder), Format::Avif, options)
458 }
459 other => Err(PixelsError::unsupported(format!(
460 "{other} was detected but no decoder for it is compiled in"
461 ))),
462 }
463 }
464
465 #[cfg(any(
470 feature = "png",
471 feature = "gif",
472 feature = "jpeg",
473 feature = "tiff",
474 feature = "webp",
475 feature = "avif"
476 ))]
477 fn decoded(decoder: Box<dyn Decoder>, format: Format, options: OpenOptions) -> Result<Self> {
478 let orientation = decoder.orientation();
479 let icc = decoder.icc_profile().map(Vec::from);
480 let animation = decoder.animation().map(Arc::new);
481 let pages = decoder.pages();
482 if options.animated && animation.is_some() {
483 return Err(PixelsError::unsupported(format!(
484 "{format}: multi-frame (animated) pipelines are not implemented yet; \
485 open with `animated` off to process the first frame"
486 )));
487 }
488 let mut image = Self::from_decoder(decoder, format).with_icc_profile(icc);
489 image.animation = animation;
490 image.pages = pages;
491 if options.to_srgb {
492 image = image.to_srgb();
493 }
494 Ok(if options.auto_orient {
495 image.orient(orientation)
496 } else {
497 image
498 })
499 }
500
501 #[must_use]
507 pub fn from_decoder(decoder: Box<dyn Decoder>, format: Format) -> Self {
508 let source = otf_pixels_core::DecodedSource::new(decoder);
509 Self::from_producer(Arc::new(source), format)
510 }
511
512 #[must_use]
514 pub fn from_producer(producer: Arc<dyn Producer>, format: Format) -> Self {
515 Self {
516 icc: None,
517 animation: None,
518 pages: 1,
519 inner: Ok(otf_pixels_core::Image::from_producer(producer, format)),
520 }
521 }
522
523 pub fn metadata(&self) -> Result<Metadata> {
532 self.graph()?.metadata()
533 }
534
535 pub fn descriptor(&self) -> Result<ImageDescriptor> {
541 Ok(self.graph()?.descriptor())
542 }
543
544 #[must_use]
548 pub fn crop(self, x: u32, y: u32, width: u32, height: u32) -> Self {
549 match Crop::at(x, y, width, height) {
550 Ok(op) => self.apply(Arc::new(op)),
551 Err(error) => Self::failed(error),
552 }
553 }
554
555 #[must_use]
557 pub fn flip(self) -> Self {
558 self.apply(Arc::new(Flip))
559 }
560
561 #[must_use]
563 pub fn flop(self) -> Self {
564 self.apply(Arc::new(Flop))
565 }
566
567 #[must_use]
569 pub fn resize(self, width: u32, height: u32) -> Self {
570 self.resize_with(width, height, ResizeOptions::default())
571 }
572
573 #[must_use]
575 pub fn resize_with(self, width: u32, height: u32, options: ResizeOptions) -> Self {
576 match Resize::new(width, height, options) {
577 Ok(op) => self.apply(Arc::new(op)),
578 Err(error) => Self::failed(error),
579 }
580 }
581
582 #[must_use]
584 pub fn thumbnail(self, width: u32, height: u32) -> Self {
585 let options = ResizeOptions::default()
586 .with_fit(Fit::Inside)
587 .without_enlargement(true);
588 self.resize_with(width, height, options)
589 }
590
591 #[must_use]
599 pub fn orient(self, orientation: Orientation) -> Self {
600 let turns = orientation.clockwise_turns();
601 let image = if turns == 0 {
602 self
603 } else {
604 self.rotate(90 * i32::from(turns))
605 };
606 if orientation.mirrored() {
607 image.flop()
608 } else {
609 image
610 }
611 }
612
613 #[must_use]
615 pub fn rotate(self, degrees: i32) -> Self {
616 match Rotate::degrees(degrees) {
617 Ok(op) => self.apply(Arc::new(op)),
618 Err(error) => Self::failed(error),
619 }
620 }
621
622 #[must_use]
624 pub fn modulate(self, options: Modulate) -> Self {
625 self.apply(Arc::new(options))
626 }
627
628 #[must_use]
630 pub fn convolve(self, kernel: Kernel) -> Self {
631 self.apply(Arc::new(Convolve::new(kernel)))
632 }
633
634 #[must_use]
636 pub fn blur(self, sigma: f32) -> Self {
637 match Kernel::gaussian(sigma) {
638 Ok(kernel) => self.convolve(kernel),
639 Err(error) => Self::failed(error),
640 }
641 }
642
643 #[must_use]
645 pub fn sharpen(self, amount: f32) -> Self {
646 match Kernel::sharpen(amount) {
647 Ok(kernel) => self.convolve(kernel),
648 Err(error) => Self::failed(error),
649 }
650 }
651
652 #[must_use]
654 pub fn extract_channel(self, index: usize) -> Self {
655 self.apply(Arc::new(ExtractChannel::new(index)))
656 }
657
658 #[must_use]
664 pub fn extract_channel_named(self, channel: Channel) -> Self {
665 let layout = match self.descriptor() {
666 Ok(descriptor) => descriptor.pixel.layout(),
667 Err(error) => return Self::failed(error),
668 };
669 let index = match (channel, layout) {
670 (Channel::Red, _) => Some(0),
671 (Channel::Green, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(1),
672 (Channel::Blue, ChannelLayout::Rgb | ChannelLayout::Rgba) => Some(2),
673 (Channel::Alpha, ChannelLayout::GrayAlpha) => Some(1),
674 (Channel::Alpha, ChannelLayout::Rgba) => Some(3),
675 _ => None,
676 };
677 match index {
678 Some(index) => self.extract_channel(index),
679 None => Self::failed(PixelsError::invalid_argument(
680 "channel",
681 format!("a {layout:?} image has no {channel:?} channel"),
682 )),
683 }
684 }
685
686 #[must_use]
689 pub fn negate(self, alpha: bool) -> Self {
690 self.apply(Arc::new(Negate::new(alpha)))
691 }
692
693 #[must_use]
699 pub fn gamma(self, exponent: f32) -> Self {
700 match Gamma::new(exponent) {
701 Ok(op) => self.apply(Arc::new(op)),
702 Err(error) => Self::failed(error),
703 }
704 }
705
706 #[must_use]
709 pub fn linear(self, a: f32, b: f32) -> Self {
710 self.linear_per_channel([a; 3], [b; 3])
711 }
712
713 #[must_use]
715 pub fn linear_per_channel(self, a: [f32; 3], b: [f32; 3]) -> Self {
716 match Linear::per_channel(a, b) {
717 Ok(op) => self.apply(Arc::new(op)),
718 Err(error) => Self::failed(error),
719 }
720 }
721
722 #[must_use]
727 pub fn threshold(self, level: u8, greyscale: bool) -> Self {
728 self.apply(Arc::new(Threshold::new(level, greyscale)))
729 }
730
731 #[must_use]
734 pub fn tint(self, red: u8, green: u8, blue: u8) -> Self {
735 self.apply(Arc::new(Tint::new(red, green, blue)))
736 }
737
738 #[must_use]
742 pub fn greyscale(self) -> Self {
743 self.apply(Arc::new(Greyscale))
744 }
745
746 #[must_use]
749 pub fn recomb(self, matrix: [[f32; 3]; 3]) -> Self {
750 match Recomb::rgb(matrix) {
751 Ok(op) => self.apply(Arc::new(op)),
752 Err(error) => Self::failed(error),
753 }
754 }
755
756 #[must_use]
759 pub fn recomb_rgba(self, matrix: [[f32; 4]; 4]) -> Self {
760 match Recomb::rgba(matrix) {
761 Ok(op) => self.apply(Arc::new(op)),
762 Err(error) => Self::failed(error),
763 }
764 }
765
766 #[must_use]
770 pub fn ensure_alpha(self, level: f32) -> Self {
771 match EnsureAlpha::new(level) {
772 Ok(op) => self.apply(Arc::new(op)),
773 Err(error) => Self::failed(error),
774 }
775 }
776
777 #[must_use]
781 pub fn remove_alpha(self) -> Self {
782 let pixel = match self.descriptor() {
783 Ok(descriptor) => descriptor.pixel,
784 Err(error) => return Self::failed(error),
785 };
786 let opaque = match pixel {
787 PixelFormat::GrayA8 => PixelFormat::Gray8,
788 PixelFormat::Rgba8 => PixelFormat::Rgb8,
789 PixelFormat::Rgba16 => PixelFormat::Rgb16,
790 PixelFormat::RgbaF32 => PixelFormat::RgbF32,
791 other => other,
792 };
793 self.to_pixel_format(opaque)
794 }
795
796 #[must_use]
798 pub fn flatten(self, red: u8, green: u8, blue: u8) -> Self {
799 self.apply(Arc::new(Flatten::onto(red, green, blue)))
800 }
801
802 #[must_use]
807 pub fn composite(self, overlay: Self, x: i64, y: i64) -> Self {
808 self.composite_with(overlay, x, y, Blend::Over)
809 }
810
811 #[must_use]
813 pub fn composite_with(self, overlay: Self, x: i64, y: i64, blend: Blend) -> Self {
814 let (icc, animation, pages) = (self.icc, self.animation, self.pages);
817 let (base, over) = match (self.inner, overlay.inner) {
818 (Ok(base), Ok(over)) => (base, over),
819 (Err(error), _) | (Ok(_), Err(error)) => return Self::failed_shared(error),
821 };
822 let op: Arc<dyn Op> = Arc::new(Composite::at(x, y, blend));
823 Self {
824 inner: otf_pixels_core::Image::combine(&[base, over], op).map_err(Arc::new),
825 icc,
826 animation,
827 pages,
828 }
829 }
830
831 fn failed(error: PixelsError) -> Self {
833 Self::failed_shared(Arc::new(error))
834 }
835
836 const fn failed_shared(error: Arc<PixelsError>) -> Self {
838 Self {
839 inner: Err(error),
840 icc: None,
841 animation: None,
842 pages: 1,
843 }
844 }
845
846 #[must_use]
853 pub fn animation(&self) -> Option<&Animation> {
854 self.animation.as_deref()
855 }
856
857 #[must_use]
864 pub const fn pages(&self) -> u32 {
865 self.pages
866 }
867
868 #[must_use]
874 pub fn icc_profile(&self) -> Option<&[u8]> {
875 self.icc.as_deref()
876 }
877
878 #[must_use]
888 pub fn to_srgb(self) -> Self {
889 let Some(profile) = self.icc.clone() else {
890 return self;
891 };
892 let Ok(descriptor) = self.descriptor() else {
893 return self;
894 };
895 match ToSrgb::from_profile(&profile) {
896 Conversion::Convert(op) if op.applies_to(descriptor.pixel) => {
897 self.apply(Arc::new(op)).with_icc_profile(None)
898 }
899 Conversion::AlreadySrgb => self.with_icc_profile(None),
900 _ => self,
901 }
902 }
903
904 #[must_use]
912 pub fn to_pixel_format(self, pixel: PixelFormat) -> Self {
913 match self.descriptor() {
914 Ok(descriptor) if descriptor.pixel == pixel => self,
915 _ => self.apply(Arc::new(ConvertFormat::to(pixel))),
916 }
917 }
918
919 fn encodable_as(self, format: Format) -> Self {
923 let Ok(descriptor) = self.descriptor() else {
924 return self;
925 };
926 let pixel = descriptor.pixel;
927 let kind = match (format, pixel.sample_kind()) {
928 (
929 Format::Jpeg | Format::WebP | Format::Avif | Format::Gif,
930 SampleKind::U16 | SampleKind::F32,
931 ) => SampleKind::U8,
932 (Format::Png | Format::Tiff, SampleKind::F32) => SampleKind::U16,
933 _ => return self,
934 };
935 match PixelFormat::from_parts(pixel.layout(), kind) {
936 Some(target) => self.to_pixel_format(target),
937 None => self,
938 }
939 }
940
941 #[must_use]
944 pub fn with_icc_profile(mut self, profile: Option<Vec<u8>>) -> Self {
945 self.icc = profile.map(Arc::from);
946 self
947 }
948
949 #[must_use]
954 pub fn apply(self, op: Arc<dyn Op>) -> Self {
955 Self {
956 icc: self.icc,
957 animation: self.animation,
958 pages: self.pages,
959 inner: match self.inner {
960 Ok(image) => image.apply(op).map_err(Arc::new),
961 Err(error) => Err(error),
963 },
964 }
965 }
966
967 #[must_use]
973 pub fn output(self, format: Format, options: EncodeOptions) -> Output {
974 Output {
975 image: self,
976 format,
977 options,
978 scheduler: None,
979 shared: None,
980 }
981 }
982
983 fn graph(&self) -> Result<&otf_pixels_core::Image> {
985 match &self.inner {
986 Ok(image) => Ok(image),
987 Err(error) => Err(rebuild(error)),
989 }
990 }
991}
992
993fn rebuild(error: &Arc<PixelsError>) -> PixelsError {
999 let detail = error.to_string();
1000 match error.code() {
1001 ErrorCode::Io => PixelsError::io("running the pipeline", std::io::Error::other(detail)),
1002 ErrorCode::Malformed => PixelsError::malformed("pipeline", detail),
1003 ErrorCode::Unsupported => PixelsError::unsupported(detail),
1004 ErrorCode::InvalidArgument => PixelsError::invalid_argument("pipeline", detail),
1005 ErrorCode::Graph => PixelsError::graph(detail),
1006 ErrorCode::LimitExceeded => match **error {
1007 PixelsError::LimitExceeded {
1008 limit,
1009 requested,
1010 allowed,
1011 } => PixelsError::limit_exceeded(limit, requested, allowed),
1012 _ => PixelsError::graph(detail),
1013 },
1014 _ => PixelsError::graph(detail),
1016 }
1017}
1018
1019#[derive(Debug, Clone)]
1024pub struct Output {
1025 image: Image,
1026 format: Format,
1027 options: EncodeOptions,
1028 scheduler: Option<SchedulerOptions>,
1030 shared: Option<Arc<Scheduler>>,
1032}
1033
1034impl Output {
1035 #[must_use]
1037 pub const fn format(&self) -> Format {
1038 self.format
1039 }
1040
1041 #[must_use]
1043 pub const fn options(&self) -> EncodeOptions {
1044 self.options
1045 }
1046
1047 #[must_use]
1059 pub fn threads(mut self, threads: usize) -> Self {
1060 self.scheduler = Some(self.scheduler.unwrap_or_default().with_threads(threads));
1061 self
1062 }
1063
1064 #[must_use]
1074 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
1075 self.shared = Some(scheduler);
1076 self
1077 }
1078
1079 #[must_use]
1085 pub const fn scheduler_options(mut self, options: SchedulerOptions) -> Self {
1086 self.scheduler = Some(options);
1087 self
1088 }
1089
1090 pub fn write(self, sink: impl Sink) -> Result<()> {
1103 self.write_with_stats(sink).map(|_| ())
1104 }
1105
1106 pub fn write_with_stats(self, mut sink: impl Sink) -> Result<RunStats> {
1118 let encodable = self.image.clone().encodable_as(self.format);
1121 let (image, reduction) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1122 let descriptor = image.descriptor();
1123 let mut encoder = encoder_for(self.format, self.options)?;
1124 encoder.set_icc_profile(self.image.icc.as_deref())?;
1125 encoder.write_header(&descriptor, &mut sink)?;
1126
1127 let scheduler = match (&self.shared, self.scheduler) {
1133 (Some(shared), _) => Arc::clone(shared),
1134 (None, Some(options)) => Arc::new(Scheduler::new(options)?),
1135 (None, None) if otf_pixels_core::ThreadPool::on_worker_thread() => {
1136 Arc::new(Scheduler::with_defaults()?)
1137 }
1138 (None, None) => Scheduler::global()?,
1139 };
1140 let mut rows = RowAssembler::new(descriptor);
1141 let mut stats = scheduler.run(&image, |region, tile| {
1142 rows.accept(region, tile, &mut |row| encoder.write_row(row, &mut sink))
1143 })?;
1144 rows.finish(&mut |row| encoder.write_row(row, &mut sink))?;
1145 encoder.finish(&mut sink)?;
1146 stats.reduction = reduction;
1147 Ok(stats)
1148 }
1149
1150 pub fn bytes_via_reference(self) -> Result<Vec<u8>> {
1167 let encodable = self.image.clone().encodable_as(self.format);
1171 let (image, _) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
1172 let descriptor = image.descriptor();
1173 let mut encoder = encoder_for(self.format, self.options)?;
1174 encoder.set_icc_profile(self.image.icc.as_deref())?;
1175 let mut sink = Vec::with_capacity(descriptor.byte_len().unwrap_or_default());
1176 encoder.write_header(&descriptor, &mut sink)?;
1177 otf_pixels_core::evaluate_rows(&image, |_, row| encoder.write_row(row, &mut sink))?;
1178 encoder.finish(&mut sink)?;
1179 Ok(sink)
1180 }
1181
1182 pub fn bytes(self) -> Result<Vec<u8>> {
1188 let hint = self
1191 .image
1192 .descriptor()
1193 .ok()
1194 .and_then(|d| d.byte_len())
1195 .unwrap_or_default();
1196 let mut buffer = Vec::with_capacity(hint);
1197 self.write(&mut buffer)?;
1198 Ok(buffer)
1199 }
1200}
1201
1202#[derive(Debug)]
1210struct RowAssembler {
1211 descriptor: ImageDescriptor,
1212 band: Option<otf_pixels_core::TileBuf>,
1214 next_row: u32,
1216}
1217
1218impl RowAssembler {
1219 const fn new(descriptor: ImageDescriptor) -> Self {
1220 Self {
1221 descriptor,
1222 band: None,
1223 next_row: 0,
1224 }
1225 }
1226
1227 fn accept(
1229 &mut self,
1230 region: Region,
1231 tile: &otf_pixels_core::Tile<'_>,
1232 emit: &mut impl FnMut(&[u8]) -> Result<()>,
1233 ) -> Result<()> {
1234 if region.width == self.descriptor.width && self.band.is_none() {
1236 for y in region.y..region.y.saturating_add(region.height) {
1237 let row = tile
1238 .row(y)
1239 .ok_or_else(|| PixelsError::graph(format!("output tile is missing row {y}")))?;
1240 emit(row)?;
1241 self.next_row = y.saturating_add(1);
1242 }
1243 return Ok(());
1244 }
1245
1246 let band_region = Region::new(0, region.y, self.descriptor.width, region.height);
1249 let starts_new_band = self
1250 .band
1251 .as_ref()
1252 .is_none_or(|band| band.region().y != region.y);
1253 if starts_new_band {
1254 self.flush(emit)?;
1255 self.band = Some(otf_pixels_core::TileBuf::zeroed(
1256 band_region,
1257 self.descriptor.pixel,
1258 )?);
1259 }
1260 let Some(band) = self.band.as_mut() else {
1261 return Err(PixelsError::graph("row band vanished"));
1262 };
1263 otf_pixels_core::copy_region(tile, &mut band.as_tile_mut()?, region)?;
1264
1265 if region.right() >= u64::from(self.descriptor.width) {
1267 self.flush(emit)?;
1268 }
1269 Ok(())
1270 }
1271
1272 fn flush(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1274 let Some(band) = self.band.take() else {
1275 return Ok(());
1276 };
1277 let region = band.region();
1278 let view = band.as_tile()?;
1279 for y in region.y..region.y.saturating_add(region.height) {
1280 let row = view
1281 .row(y)
1282 .ok_or_else(|| PixelsError::graph(format!("row band is missing row {y}")))?;
1283 emit(row)?;
1284 self.next_row = y.saturating_add(1);
1285 }
1286 Ok(())
1287 }
1288
1289 fn finish(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1291 self.flush(emit)
1292 }
1293}
1294
1295fn sniffing_codecs() -> Vec<Box<dyn Codec>> {
1300 let codecs: Vec<Box<dyn Codec>> = vec![
1301 #[cfg(feature = "png")]
1302 Box::new(PngCodec),
1303 #[cfg(feature = "gif")]
1304 Box::new(GifCodec),
1305 #[cfg(feature = "jpeg")]
1306 Box::new(JpegCodec),
1307 #[cfg(feature = "tiff")]
1308 Box::new(TiffCodec),
1309 #[cfg(feature = "webp")]
1310 Box::new(WebPCodec),
1311 #[cfg(feature = "avif")]
1312 Box::new(AvifCodec),
1313 ];
1314 codecs
1315}
1316
1317fn encoder_for(format: Format, options: EncodeOptions) -> Result<Box<dyn Encoder>> {
1319 let _ = &options;
1322 match format {
1323 #[cfg(feature = "raw")]
1324 Format::Raw => Ok(Box::new(RawEncoder::new())),
1325 #[cfg(feature = "png")]
1326 Format::Png => Ok(Box::new(PngEncoder::from_options(&options))),
1327 #[cfg(feature = "gif")]
1328 Format::Gif => Ok(Box::new(GifEncoder::from_options(&options))),
1329 #[cfg(feature = "jpeg")]
1330 Format::Jpeg => Ok(Box::new(JpegEncoder::from_options(&options))),
1331 #[cfg(feature = "tiff")]
1332 Format::Tiff => Ok(Box::new(TiffEncoder::from_options(&options))),
1333 #[cfg(feature = "webp")]
1334 Format::WebP => Ok(Box::new(WebPEncoder::from_options(&options))),
1335 #[cfg(feature = "avif")]
1336 Format::Avif => Ok(Box::new(AvifEncoder::from_options(&options))),
1337 #[cfg(not(feature = "raw"))]
1338 Format::Raw => Err(PixelsError::unsupported(
1339 "raw encoding requires the `raw` feature of otf-pixels",
1340 )),
1341 other => Err(PixelsError::unsupported(format!(
1342 "encoding {other} is not implemented yet; \
1343 see the ROADMAP for which milestone lands it"
1344 ))),
1345 }
1346}
1347
1348#[cfg(all(test, feature = "raw"))]
1353#[allow(
1354 clippy::unwrap_used,
1355 clippy::expect_used,
1356 clippy::indexing_slicing,
1357 clippy::panic,
1358 reason = "tests operate on known-good values and assert shapes directly"
1359)]
1360mod tests {
1361 use super::*;
1362
1363 fn ramp(width: u32, height: u32) -> Image {
1364 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
1365 let len = descriptor.byte_len().unwrap();
1366 Image::from_raw(descriptor, (0..len).map(|i| i as u8).collect()).unwrap()
1367 }
1368
1369 #[test]
1370 fn from_raw_requires_an_exactly_sized_buffer() {
1371 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1372 assert!(Image::from_raw(descriptor, vec![0; 4]).is_ok());
1373 assert_eq!(
1374 Image::from_raw(descriptor, vec![0; 3]).unwrap_err().code(),
1375 ErrorCode::InvalidArgument
1376 );
1377 assert_eq!(
1378 Image::from_raw(descriptor, vec![0; 5]).unwrap_err().code(),
1379 ErrorCode::InvalidArgument
1380 );
1381 }
1382
1383 #[test]
1384 fn a_chain_error_surfaces_at_the_terminal() {
1385 let result = ramp(4, 4)
1387 .crop(3, 3, 4, 4)
1388 .flip()
1389 .flop()
1390 .output(Format::Raw, EncodeOptions::default())
1391 .bytes();
1392 let err = result.unwrap_err();
1393 assert_eq!(err.code(), ErrorCode::InvalidArgument);
1394 }
1395
1396 #[test]
1397 fn an_error_short_circuits_later_ops() {
1398 let broken = ramp(4, 4).crop(0, 0, 0, 0);
1401 assert!(broken.metadata().is_err());
1402 assert!(broken.clone().flip().descriptor().is_err());
1403 }
1404
1405 #[test]
1406 fn metadata_is_free_and_reports_the_pipeline_shape() {
1407 let meta = ramp(8, 6).metadata().unwrap();
1408 assert_eq!((meta.width, meta.height), (8, 6));
1409 assert_eq!(meta.format, Format::Raw);
1410 assert_eq!(meta.pixel, PixelFormat::Gray8);
1411 let cropped = ramp(8, 6).crop(1, 1, 3, 2).metadata().unwrap();
1413 assert_eq!((cropped.width, cropped.height), (3, 2));
1414 }
1415
1416 #[cfg(not(feature = "avif"))]
1420 #[test]
1421 fn a_format_built_out_is_a_catchable_error() {
1422 let format = Format::Avif;
1423 let err = ramp(2, 2)
1424 .output(format, EncodeOptions::default())
1425 .bytes()
1426 .unwrap_err();
1427 assert_eq!(err.code(), ErrorCode::Unsupported, "{format}");
1428 assert!(err.to_string().contains(format.as_str()), "{err}");
1429 }
1430
1431 #[cfg(feature = "tiff")]
1436 #[test]
1437 fn tiff_round_trips_through_the_facade() {
1438 let bytes = ramp(12, 9)
1439 .output(Format::Tiff, EncodeOptions::default())
1440 .bytes()
1441 .unwrap();
1442 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1443 let metadata = image.metadata().unwrap();
1444 assert_eq!(metadata.format, Format::Tiff);
1445 assert_eq!((metadata.width, metadata.height), (12, 9));
1446
1447 let decoded = image
1449 .output(Format::Raw, EncodeOptions::default())
1450 .bytes()
1451 .unwrap();
1452 let expected: Vec<u8> = (0..12_u32 * 9).map(|i| i as u8).collect();
1453 assert_eq!(decoded, expected);
1454 }
1455
1456 #[cfg(feature = "jpeg")]
1459 #[test]
1460 fn jpeg_round_trips_through_the_facade() {
1461 let (width, height) = (32_u32, 24_u32);
1462 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1463 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1464 .map(|i| {
1465 let pixel = i / 3;
1468 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1469 match i % 3 {
1470 0 => (x * 255 / width) as u8,
1471 1 => (y * 255 / height) as u8,
1472 _ => 128,
1473 }
1474 })
1475 .collect();
1476
1477 let bytes = Image::from_raw(descriptor, pixels.clone())
1478 .unwrap()
1479 .output(Format::Jpeg, EncodeOptions::with_quality(95).unwrap())
1480 .bytes()
1481 .unwrap();
1482
1483 let image = Image::from_stream(std::io::Cursor::new(bytes.clone())).unwrap();
1484 let metadata = image.metadata().unwrap();
1485 assert_eq!(metadata.format, Format::Jpeg, "sniffing missed the JPEG");
1486 assert_eq!((metadata.width, metadata.height), (width, height));
1487 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1488
1489 let decoded = image
1490 .output(Format::Raw, EncodeOptions::default())
1491 .bytes()
1492 .unwrap();
1493 assert_eq!(decoded.len(), pixels.len());
1494 let worst = decoded
1495 .iter()
1496 .zip(&pixels)
1497 .map(|(&a, &b)| a.abs_diff(b))
1498 .max()
1499 .unwrap_or(0);
1500 assert!(worst <= 12, "worst sample differs by {worst}");
1501 }
1502
1503 #[cfg(feature = "jpeg")]
1514 #[test]
1515 fn a_streaming_resize_matches_the_reference_evaluator() {
1516 for &(width, height, target) in &[
1517 (512_u32, 512_u32, 300_u32),
1518 (512, 512, 256),
1519 (256, 256, 200),
1520 (320, 240, 129),
1521 (64, 64, 16),
1522 ] {
1523 let source = jpeg_source(width, height);
1524 let scheduled = Image::from_stream(std::io::Cursor::new(source.clone()))
1525 .unwrap()
1526 .resize(target, target)
1527 .output(Format::Raw, EncodeOptions::default())
1528 .bytes()
1529 .unwrap_or_else(|e| panic!("{width}x{height} -> {target}: {e}"));
1530 let reference = Image::from_stream(std::io::Cursor::new(source))
1531 .unwrap()
1532 .resize(target, target)
1533 .output(Format::Raw, EncodeOptions::default())
1534 .bytes_via_reference()
1535 .unwrap();
1536
1537 assert_eq!(
1538 scheduled.len(),
1539 (target * target * 3) as usize,
1540 "{width}x{height} -> {target}: size"
1541 );
1542 assert_eq!(
1543 scheduled, reference,
1544 "{width}x{height} -> {target}: the scheduler and the oracle disagree"
1545 );
1546 }
1547 }
1548
1549 #[cfg(feature = "webp")]
1555 #[test]
1556 fn webp_round_trips_through_the_facade() {
1557 let bytes = ramp(20, 12)
1558 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1559 .bytes()
1560 .unwrap();
1561 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1562 let metadata = image.metadata().unwrap();
1563 assert_eq!(metadata.format, Format::WebP, "sniffing missed the WebP");
1564 assert_eq!((metadata.width, metadata.height), (20, 12));
1565 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1567
1568 let decoded = image
1569 .output(Format::Raw, EncodeOptions::default())
1570 .bytes()
1571 .unwrap();
1572 let expected: Vec<u8> = (0..20_u32 * 12)
1573 .flat_map(|i| {
1574 let value = i as u8;
1575 [value, value, value]
1576 })
1577 .collect();
1578 assert_eq!(decoded, expected, "a lossless round trip lost pixels");
1579 }
1580
1581 #[cfg(feature = "webp")]
1584 #[test]
1585 fn webp_keeps_alpha_through_the_facade() {
1586 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::Rgba8).unwrap();
1587 let pixels: Vec<u8> = (0..(9 * 7))
1588 .flat_map(|i| [(i * 3) as u8, 40, 200, (i * 7) as u8])
1589 .collect();
1590 let bytes = Image::from_raw(descriptor, pixels.clone())
1591 .unwrap()
1592 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1593 .bytes()
1594 .unwrap();
1595
1596 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1597 assert_eq!(image.metadata().unwrap().pixel, PixelFormat::Rgba8);
1598 let decoded = image
1599 .output(Format::Raw, EncodeOptions::default())
1600 .bytes()
1601 .unwrap();
1602 assert_eq!(decoded, pixels);
1603 }
1604
1605 #[cfg(feature = "webp")]
1609 #[test]
1610 fn webp_is_lossy_by_default_and_keeps_alpha_exact() {
1611 let (w, h) = (48_u32, 32_u32);
1612 let descriptor = ImageDescriptor::new(w, h, PixelFormat::Rgba8).unwrap();
1613 let pixels: Vec<u8> = (0..w * h)
1614 .flat_map(|i| {
1615 let (x, y) = (i % w, i / w);
1616 [(x * 5) as u8, (y * 7) as u8, 120, (x * 3 + y) as u8]
1617 })
1618 .collect();
1619 let encode = |options: EncodeOptions| {
1620 Image::from_raw(descriptor, pixels.clone())
1621 .unwrap()
1622 .output(Format::WebP, options)
1623 .bytes()
1624 .unwrap()
1625 };
1626 let good = encode(EncodeOptions::default());
1627 let rough = encode(EncodeOptions::with_quality(10).unwrap());
1628 assert!(
1629 rough.len() < good.len(),
1630 "{} vs {}",
1631 rough.len(),
1632 good.len()
1633 );
1634
1635 let decoded = Image::from_stream(std::io::Cursor::new(good))
1636 .unwrap()
1637 .output(Format::Raw, EncodeOptions::default())
1638 .bytes()
1639 .unwrap();
1640 for (ours, source) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
1641 assert_eq!(ours[3], source[3], "alpha changed");
1642 for c in 0..3 {
1643 assert!(ours[c].abs_diff(source[c]) <= 12, "{ours:?} vs {source:?}");
1644 }
1645 }
1646 }
1647
1648 #[cfg(feature = "jpeg")]
1651 #[test]
1652 fn a_thumbnail_pipeline_shrinks_the_jpeg_on_load() {
1653 let source = jpeg_source(512, 512);
1655
1656 let image = Image::from_stream(std::io::Cursor::new(source)).unwrap();
1657 assert_eq!(
1658 image.metadata().unwrap().width,
1659 512,
1660 "metadata is full size"
1661 );
1662
1663 let mut bytes = Vec::new();
1666 let stats = image
1667 .resize(32, 32)
1668 .output(Format::Raw, EncodeOptions::default())
1669 .write_with_stats(&mut bytes)
1670 .unwrap();
1671
1672 let reduction = stats
1673 .reduction
1674 .expect("a 512 to 32 resize should shrink the source on load");
1675 assert_eq!(reduction.from, (512, 512));
1676 assert_eq!(
1677 reduction.to,
1678 (64, 64),
1679 "1/8 is the coarsest scale that still covers 32"
1680 );
1681 assert!((reduction.factor() - 64.0).abs() < 0.01);
1682 assert_eq!(bytes.len(), 32 * 32 * 3);
1684 }
1685
1686 #[cfg(feature = "jpeg")]
1689 #[test]
1690 fn a_crop_blocks_shrink_on_load() {
1691 let source = jpeg_source(512, 512);
1692 let stats = Image::from_stream(std::io::Cursor::new(source))
1693 .unwrap()
1694 .crop(256, 256, 128, 128)
1695 .resize(32, 32)
1696 .output(Format::Raw, EncodeOptions::default())
1697 .write_with_stats(&mut Vec::new())
1698 .unwrap();
1699 assert!(
1700 stats.reduction.is_none(),
1701 "a crop must block shrink-on-load: {:?}",
1702 stats.reduction
1703 );
1704 }
1705
1706 #[cfg(feature = "jpeg")]
1709 #[test]
1710 fn a_crop_after_the_resize_still_allows_shrinking() {
1711 let source = jpeg_source(512, 512);
1712 let stats = Image::from_stream(std::io::Cursor::new(source))
1713 .unwrap()
1714 .resize(64, 64)
1715 .crop(0, 0, 32, 32)
1716 .output(Format::Raw, EncodeOptions::default())
1717 .write_with_stats(&mut Vec::new())
1718 .unwrap();
1719 assert!(stats.reduction.is_none());
1723 }
1724
1725 #[cfg(feature = "jpeg")]
1728 #[test]
1729 fn a_mild_resize_shrinks_by_less_or_not_at_all() {
1730 let source = jpeg_source(512, 512);
1731 let stats = Image::from_stream(std::io::Cursor::new(source.clone()))
1733 .unwrap()
1734 .resize(300, 300)
1735 .output(Format::Raw, EncodeOptions::default())
1736 .write_with_stats(&mut Vec::new())
1737 .unwrap();
1738 assert!(stats.reduction.is_none(), "{:?}", stats.reduction);
1739
1740 let stats = Image::from_stream(std::io::Cursor::new(source))
1742 .unwrap()
1743 .resize(200, 200)
1744 .output(Format::Raw, EncodeOptions::default())
1745 .write_with_stats(&mut Vec::new())
1746 .unwrap();
1747 assert_eq!(stats.reduction.map(|r| r.to), Some((256, 256)));
1748 }
1749
1750 #[cfg(feature = "jpeg")]
1752 fn jpeg_source(width: u32, height: u32) -> Vec<u8> {
1753 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1754 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1755 .map(|i| {
1756 let pixel = i / 3;
1757 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1758 match i % 3 {
1759 0 => (x * 255 / width) as u8,
1760 1 => (y * 255 / height) as u8,
1761 _ => 96,
1762 }
1763 })
1764 .collect();
1765 Image::from_raw(descriptor, pixels)
1766 .unwrap()
1767 .output(Format::Jpeg, EncodeOptions::with_quality(85).unwrap())
1768 .bytes()
1769 .unwrap()
1770 }
1771
1772 #[cfg(feature = "jpeg")]
1774 #[test]
1775 fn a_jpeg_pipeline_resizes_and_re_encodes() {
1776 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
1777 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1778 .map(|i| ((i / 3) % 251) as u8)
1779 .collect();
1780 let source = Image::from_raw(descriptor, pixels)
1781 .unwrap()
1782 .output(Format::Jpeg, EncodeOptions::default())
1783 .bytes()
1784 .unwrap();
1785
1786 let thumbnail = Image::from_stream(std::io::Cursor::new(source.clone()))
1787 .unwrap()
1788 .resize(16, 16)
1789 .output(Format::Jpeg, EncodeOptions::with_quality(70).unwrap())
1790 .bytes()
1791 .unwrap();
1792
1793 let metadata = Image::from_stream(std::io::Cursor::new(thumbnail.clone()))
1794 .unwrap()
1795 .metadata()
1796 .unwrap();
1797 assert_eq!((metadata.width, metadata.height), (16, 16));
1798 assert!(
1799 thumbnail.len() < source.len(),
1800 "a 16x16 thumbnail is {} bytes against a 64x64 source's {}",
1801 thumbnail.len(),
1802 source.len()
1803 );
1804 }
1805
1806 #[cfg(feature = "jpeg")]
1809 #[test]
1810 fn grayscale_jpeg_keeps_one_channel_through_the_facade() {
1811 let bytes = ramp(24, 16)
1812 .output(Format::Jpeg, EncodeOptions::with_quality(90).unwrap())
1813 .bytes()
1814 .unwrap();
1815 let metadata = Image::from_stream(std::io::Cursor::new(bytes))
1816 .unwrap()
1817 .metadata()
1818 .unwrap();
1819 assert_eq!(metadata.pixel, PixelFormat::Gray8);
1820 assert_eq!((metadata.width, metadata.height), (24, 16));
1821 }
1822
1823 #[test]
1824 fn write_streams_into_any_sink() {
1825 let mut sink = Vec::new();
1826 ramp(2, 2)
1827 .output(Format::Raw, EncodeOptions::default())
1828 .write(&mut sink)
1829 .unwrap();
1830 assert_eq!(sink, [0, 1, 2, 3]);
1831 }
1832
1833 #[test]
1834 fn output_reports_its_settings() {
1835 let options = EncodeOptions::with_quality(55).unwrap();
1836 let output = ramp(2, 2).output(Format::Raw, options);
1837 assert_eq!(output.format(), Format::Raw);
1838 assert_eq!(output.options().quality, 55);
1839 }
1840
1841 #[cfg(feature = "raw")]
1842 #[test]
1843 fn from_raw_stream_defers_decoding_to_the_terminal() {
1844 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1845 let layout = RawFormat::packed(descriptor);
1846 let cursor = std::io::Cursor::new(vec![1_u8, 2, 3, 4]);
1847 let image = Image::from_raw_stream(layout, cursor).unwrap();
1848 assert_eq!(image.metadata().unwrap().width, 2);
1850 let bytes = image
1851 .output(Format::Raw, EncodeOptions::default())
1852 .bytes()
1853 .unwrap();
1854 assert_eq!(bytes, [1, 2, 3, 4]);
1855 }
1856
1857 #[cfg(feature = "raw")]
1858 #[test]
1859 fn a_truncated_stream_fails_the_terminal_without_panicking() {
1860 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
1861 let layout = RawFormat::packed(descriptor);
1862 let cursor = std::io::Cursor::new(vec![1_u8; 5]);
1863 let image = Image::from_raw_stream(layout, cursor).unwrap();
1864 let err = image
1865 .output(Format::Raw, EncodeOptions::default())
1866 .bytes()
1867 .unwrap_err();
1868 assert_eq!(err.code(), ErrorCode::Malformed);
1869 }
1870
1871 #[test]
1872 fn images_are_send_sync_and_cheap_to_clone() {
1873 const fn assert_send_sync<T: Send + Sync>() {}
1874 assert_send_sync::<Image>();
1875 assert_send_sync::<Output>();
1876 let image = ramp(4, 4);
1877 let clone = image.clone();
1878 assert_eq!(
1879 image.metadata().unwrap().width,
1880 clone.metadata().unwrap().width
1881 );
1882 }
1883
1884 #[cfg(feature = "png")]
1885 #[test]
1886 fn a_png_round_trips_through_the_facade() {
1887 let png = ramp(37, 21)
1888 .output(Format::Png, EncodeOptions::default())
1889 .bytes()
1890 .unwrap();
1891 let image = Image::from_stream(std::io::Cursor::new(png)).unwrap();
1892 assert_eq!(image.descriptor().unwrap().width, 37);
1893 assert_eq!(image.metadata().unwrap().format, Format::Png);
1894
1895 let back = image
1896 .output(Format::Raw, EncodeOptions::default())
1897 .bytes()
1898 .unwrap();
1899 let expected = ramp(37, 21)
1900 .output(Format::Raw, EncodeOptions::default())
1901 .bytes()
1902 .unwrap();
1903 assert_eq!(back, expected, "pixels changed across a PNG round trip");
1904 }
1905
1906 #[cfg(feature = "png")]
1907 #[test]
1908 fn a_pipeline_survives_a_png_round_trip() {
1909 let png = ramp(16, 16)
1912 .output(Format::Png, EncodeOptions::default())
1913 .bytes()
1914 .unwrap();
1915 let cropped = Image::from_stream(std::io::Cursor::new(png))
1916 .unwrap()
1917 .crop(2, 3, 8, 5)
1918 .flip()
1919 .output(Format::Raw, EncodeOptions::default())
1920 .bytes()
1921 .unwrap();
1922 let direct = ramp(16, 16)
1923 .crop(2, 3, 8, 5)
1924 .flip()
1925 .output(Format::Raw, EncodeOptions::default())
1926 .bytes()
1927 .unwrap();
1928 assert_eq!(cropped, direct);
1929 }
1930
1931 #[cfg(feature = "png")]
1932 #[test]
1933 fn open_ignores_the_extension_and_reads_the_bytes() {
1934 let dir = std::env::temp_dir().join(format!("otf-pixels-open-{}", std::process::id()));
1935 std::fs::create_dir_all(&dir).unwrap();
1936 let path = dir.join("actually-a-png.jpg");
1937 let png = ramp(8, 8)
1938 .output(Format::Png, EncodeOptions::default())
1939 .bytes()
1940 .unwrap();
1941 std::fs::write(&path, &png).unwrap();
1942
1943 let image = Image::open(&path).unwrap();
1944 assert_eq!(
1945 image.metadata().unwrap().format,
1946 Format::Png,
1947 "extension won over content"
1948 );
1949 std::fs::remove_dir_all(&dir).ok();
1950 }
1951
1952 #[test]
1953 fn open_names_the_file_it_could_not_read() {
1954 let error = Image::open("/nonexistent/otf-pixels/missing.png").unwrap_err();
1955 assert_eq!(error.code(), ErrorCode::Io);
1956 assert!(error.to_string().contains("missing.png"), "{error}");
1957 }
1958
1959 #[test]
1960 fn an_unrecognised_stream_is_unsupported_not_a_guess() {
1961 for bytes in [&b""[..], &b"not an image"[..], &[0_u8; 64][..]] {
1962 let error = Image::from_stream(std::io::Cursor::new(bytes.to_vec())).unwrap_err();
1963 assert_eq!(error.code(), ErrorCode::Unsupported, "{bytes:02x?}");
1964 }
1965 }
1966
1967 #[cfg(feature = "png")]
1968 #[test]
1969 fn a_truncated_png_is_malformed_not_a_panic() {
1970 let png = ramp(8, 8)
1971 .output(Format::Png, EncodeOptions::default())
1972 .bytes()
1973 .unwrap();
1974 for cut in [9, 16, 24, 32, png.len() - 1] {
1977 let truncated = png[..cut].to_vec();
1978 let result = Image::from_stream(std::io::Cursor::new(truncated))
1979 .and_then(|i| i.output(Format::Raw, EncodeOptions::default()).bytes());
1980 assert!(result.is_err(), "truncating to {cut} bytes should fail");
1981 }
1982 }
1983
1984 #[cfg(feature = "png")]
1985 #[test]
1986 fn sniffing_reads_only_the_magic_bytes_before_deciding() {
1987 let error = Image::from_stream(std::io::Cursor::new(
1991 otf_pixels_codec_png::SIGNATURE.to_vec(),
1992 ))
1993 .unwrap_err();
1994 assert_eq!(error.code(), ErrorCode::Malformed, "{error}");
1995 }
1996}
1997
1998const _: () = {
2001 const fn shareable<T: Send + Sync>() {}
2002 shareable::<Image>();
2003 shareable::<Output>();
2004 shareable::<OpenOptions>();
2005 shareable::<PixelsError>();
2006 shareable::<Scheduler>();
2007};