1#![cfg_attr(feature = "raw", doc = "```")]
13#![cfg_attr(not(feature = "raw"), doc = "```ignore")]
14#![cfg_attr(all(feature = "png", feature = "webp"), doc = "```")]
61#![cfg_attr(not(all(feature = "png", feature = "webp")), doc = "```ignore")]
62use otf_pixels_core::{BufferSource, Op, Prefixed, Producer, TileBuf};
115use std::sync::Arc;
116
117pub use otf_pixels_core::{
118 AccessPattern, Animation, ChannelLayout, Codec, ColorModel, Decoder, EncodeOptions, Encoder,
119 ErrorCode, Format, ImageDescriptor, Limit, Limits, Metadata, Orientation, PixelFormat,
120 PixelsError, PlanOptions, Region, Result, RunStats, SampleKind, Scheduler, SchedulerOptions,
121 Sink, Source, TileShape, evaluate as evaluate_reference,
122};
123pub use otf_pixels_ops::{
124 Blend, Composite, Conversion, ConvertFormat, Convolve, Crop, ExtractChannel, Filter, Fit,
125 Flatten, Flip, Flop, Kernel, Modulate, Quarter, Resize, ResizeOptions, Rotate, ToSrgb,
126 Unconvertible,
127};
128
129#[cfg(feature = "raw")]
130pub use otf_pixels_codec_raw::{RawCodec, RawDecoder, RawEncoder, RawFormat};
131
132#[cfg(feature = "png")]
133pub use otf_pixels_codec_png::{PngCodec, PngDecoder, PngEncoder};
134
135#[cfg(feature = "gif")]
136pub use otf_pixels_codec_gif::{GifCodec, GifDecoder, GifEncoder};
137
138#[cfg(feature = "jpeg")]
139pub use otf_pixels_codec_jpeg::{JpegCodec, JpegDecoder, JpegEncoder, Scale, Subsampling};
140
141#[cfg(feature = "tiff")]
142pub use otf_pixels_codec_tiff::{TiffCodec, TiffDecoder, TiffEncoder, TiffLayout};
143
144#[cfg(feature = "webp")]
145pub use otf_pixels_codec_webp::{WebPCodec, WebPDecoder, WebPEncoder};
146
147#[cfg(feature = "avif")]
148pub use otf_pixels_codec_avif::{AvifCodec, AvifDecoder, AvifEncoder};
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct OpenOptions {
154 pub auto_orient: bool,
162 pub to_srgb: bool,
171 pub animated: bool,
180 pub limits: Limits,
184}
185
186impl OpenOptions {
187 #[must_use]
192 pub const fn with_auto_orient(mut self, auto_orient: bool) -> Self {
193 self.auto_orient = auto_orient;
194 self
195 }
196
197 #[must_use]
199 pub const fn with_animated(mut self, animated: bool) -> Self {
200 self.animated = animated;
201 self
202 }
203
204 #[must_use]
206 pub const fn with_limits(mut self, limits: Limits) -> Self {
207 self.limits = limits;
208 self
209 }
210
211 #[must_use]
213 pub const fn with_to_srgb(mut self, to_srgb: bool) -> Self {
214 self.to_srgb = to_srgb;
215 self
216 }
217}
218
219impl Default for OpenOptions {
220 fn default() -> Self {
221 Self {
222 auto_orient: true,
223 to_srgb: true,
224 animated: false,
225 limits: Limits::default(),
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
235pub struct Image {
236 inner: std::result::Result<otf_pixels_core::Image, Arc<PixelsError>>,
238 icc: Option<Arc<[u8]>>,
241 animation: Option<Arc<Animation>>,
243}
244
245impl Image {
246 pub fn from_raw(descriptor: ImageDescriptor, bytes: Vec<u8>) -> Result<Self> {
255 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes)?;
256 let source = BufferSource::new(descriptor, Arc::new(buffer))?;
257 Ok(Self::from_producer(Arc::new(source), Format::Raw))
258 }
259
260 #[cfg(feature = "raw")]
270 pub fn from_raw_stream(
271 layout: RawFormat,
272 source: impl Source + std::fmt::Debug + 'static,
273 ) -> Result<Self> {
274 let decoder = RawDecoder::new(layout, source)?;
275 Ok(Self::from_decoder(Box::new(decoder), Format::Raw))
276 }
277
278 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
292 Self::open_with(path, OpenOptions::default())
293 }
294
295 pub fn open_with(path: impl AsRef<std::path::Path>, options: OpenOptions) -> Result<Self> {
301 let path = path.as_ref();
302 let file = std::fs::File::open(path).map_err(|e| {
303 let kind = e.kind();
307 let detail = std::io::Error::new(kind, format!("{}: {e}", path.display()));
308 PixelsError::io("opening image file", detail)
309 })?;
310 Self::from_stream_with(std::io::BufReader::new(file), options)
311 }
312
313 pub fn from_stream(source: impl Source + std::fmt::Debug + 'static) -> Result<Self> {
329 Self::from_stream_with(source, OpenOptions::default())
330 }
331
332 pub fn from_stream_with(
338 mut source: impl Source + std::fmt::Debug + 'static,
339 options: OpenOptions,
340 ) -> Result<Self> {
341 let codecs = sniffing_codecs();
342 let longest = codecs.iter().map(|c| c.magic_len()).max().unwrap_or(0);
343
344 let mut prefix = Vec::with_capacity(longest);
348 let mut buffer = vec![0_u8; longest];
349 while prefix.len() < longest {
350 let Some(rest) = buffer.get_mut(prefix.len()..) else {
351 break;
352 };
353 match source.read(rest)? {
354 0 => break,
355 n => {
356 let Some(read) = rest.get(..n) else { break };
357 prefix.extend_from_slice(read);
358 }
359 }
360 }
361
362 let Some(codec) = codecs.iter().find(|codec| codec.probe(&prefix)) else {
363 return Err(PixelsError::unsupported(format!(
364 "no codec recognises this stream; its first {} bytes are {:02x?}",
365 prefix.len().min(8),
366 prefix.get(..prefix.len().min(8)).unwrap_or(&[])
367 )));
368 };
369 let stream = Prefixed::new(prefix, source);
370 let _ = (&stream, options);
373
374 match codec.format() {
375 #[cfg(feature = "png")]
376 Format::Png => {
377 let decoder = PngDecoder::new(stream, options.limits)?;
378 Self::decoded(Box::new(decoder), Format::Png, options)
379 }
380 #[cfg(feature = "gif")]
381 Format::Gif => {
382 let decoder = GifDecoder::new(stream, options.limits)?;
383 Self::decoded(Box::new(decoder), Format::Gif, options)
384 }
385 #[cfg(feature = "jpeg")]
386 Format::Jpeg => {
387 let decoder = JpegDecoder::new(stream, options.limits)?;
388 Self::decoded(Box::new(decoder), Format::Jpeg, options)
389 }
390 #[cfg(feature = "tiff")]
391 Format::Tiff => {
392 let decoder = TiffDecoder::new(stream, options.limits)?;
393 Self::decoded(Box::new(decoder), Format::Tiff, options)
394 }
395 #[cfg(feature = "webp")]
396 Format::WebP => {
397 let decoder = WebPDecoder::new(stream, options.limits)?;
398 Self::decoded(Box::new(decoder), Format::WebP, options)
399 }
400 #[cfg(feature = "avif")]
401 Format::Avif => {
402 let decoder = AvifDecoder::new(stream, options.limits)?;
403 Self::decoded(Box::new(decoder), Format::Avif, options)
404 }
405 other => Err(PixelsError::unsupported(format!(
406 "{other} was detected but no decoder for it is compiled in"
407 ))),
408 }
409 }
410
411 #[cfg(any(
416 feature = "png",
417 feature = "gif",
418 feature = "jpeg",
419 feature = "tiff",
420 feature = "webp",
421 feature = "avif"
422 ))]
423 fn decoded(decoder: Box<dyn Decoder>, format: Format, options: OpenOptions) -> Result<Self> {
424 let orientation = decoder.orientation();
425 let icc = decoder.icc_profile().map(Vec::from);
426 let animation = decoder.animation().map(Arc::new);
427 if options.animated && animation.is_some() {
428 return Err(PixelsError::unsupported(format!(
429 "{format}: multi-frame (animated) pipelines are not implemented yet; \
430 open with `animated` off to process the first frame"
431 )));
432 }
433 let mut image = Self::from_decoder(decoder, format).with_icc_profile(icc);
434 image.animation = animation;
435 if options.to_srgb {
436 image = image.to_srgb();
437 }
438 Ok(if options.auto_orient {
439 image.orient(orientation)
440 } else {
441 image
442 })
443 }
444
445 #[must_use]
451 pub fn from_decoder(decoder: Box<dyn Decoder>, format: Format) -> Self {
452 let source = otf_pixels_core::DecodedSource::new(decoder);
453 Self::from_producer(Arc::new(source), format)
454 }
455
456 #[must_use]
458 pub fn from_producer(producer: Arc<dyn Producer>, format: Format) -> Self {
459 Self {
460 icc: None,
461 animation: None,
462 inner: Ok(otf_pixels_core::Image::from_producer(producer, format)),
463 }
464 }
465
466 pub fn metadata(&self) -> Result<Metadata> {
475 self.graph()?.metadata()
476 }
477
478 pub fn descriptor(&self) -> Result<ImageDescriptor> {
484 Ok(self.graph()?.descriptor())
485 }
486
487 #[must_use]
491 pub fn crop(self, x: u32, y: u32, width: u32, height: u32) -> Self {
492 match Crop::at(x, y, width, height) {
493 Ok(op) => self.apply(Arc::new(op)),
494 Err(error) => Self::failed(error),
495 }
496 }
497
498 #[must_use]
500 pub fn flip(self) -> Self {
501 self.apply(Arc::new(Flip))
502 }
503
504 #[must_use]
506 pub fn flop(self) -> Self {
507 self.apply(Arc::new(Flop))
508 }
509
510 #[must_use]
512 pub fn resize(self, width: u32, height: u32) -> Self {
513 self.resize_with(width, height, ResizeOptions::default())
514 }
515
516 #[must_use]
518 pub fn resize_with(self, width: u32, height: u32, options: ResizeOptions) -> Self {
519 match Resize::new(width, height, options) {
520 Ok(op) => self.apply(Arc::new(op)),
521 Err(error) => Self::failed(error),
522 }
523 }
524
525 #[must_use]
527 pub fn thumbnail(self, width: u32, height: u32) -> Self {
528 let options = ResizeOptions::default()
529 .with_fit(Fit::Inside)
530 .without_enlargement(true);
531 self.resize_with(width, height, options)
532 }
533
534 #[must_use]
542 pub fn orient(self, orientation: Orientation) -> Self {
543 let turns = orientation.clockwise_turns();
544 let image = if turns == 0 {
545 self
546 } else {
547 self.rotate(90 * i32::from(turns))
548 };
549 if orientation.mirrored() {
550 image.flop()
551 } else {
552 image
553 }
554 }
555
556 #[must_use]
558 pub fn rotate(self, degrees: i32) -> Self {
559 match Rotate::degrees(degrees) {
560 Ok(op) => self.apply(Arc::new(op)),
561 Err(error) => Self::failed(error),
562 }
563 }
564
565 #[must_use]
567 pub fn modulate(self, options: Modulate) -> Self {
568 self.apply(Arc::new(options))
569 }
570
571 #[must_use]
573 pub fn convolve(self, kernel: Kernel) -> Self {
574 self.apply(Arc::new(Convolve::new(kernel)))
575 }
576
577 #[must_use]
579 pub fn blur(self, sigma: f32) -> Self {
580 match Kernel::gaussian(sigma) {
581 Ok(kernel) => self.convolve(kernel),
582 Err(error) => Self::failed(error),
583 }
584 }
585
586 #[must_use]
588 pub fn sharpen(self, amount: f32) -> Self {
589 match Kernel::sharpen(amount) {
590 Ok(kernel) => self.convolve(kernel),
591 Err(error) => Self::failed(error),
592 }
593 }
594
595 #[must_use]
597 pub fn extract_channel(self, index: usize) -> Self {
598 self.apply(Arc::new(ExtractChannel::new(index)))
599 }
600
601 #[must_use]
603 pub fn flatten(self, red: u8, green: u8, blue: u8) -> Self {
604 self.apply(Arc::new(Flatten::onto(red, green, blue)))
605 }
606
607 #[must_use]
612 pub fn composite(self, overlay: Self, x: i64, y: i64) -> Self {
613 self.composite_with(overlay, x, y, Blend::Over)
614 }
615
616 #[must_use]
618 pub fn composite_with(self, overlay: Self, x: i64, y: i64, blend: Blend) -> Self {
619 let (icc, animation) = (self.icc, self.animation);
622 let (base, over) = match (self.inner, overlay.inner) {
623 (Ok(base), Ok(over)) => (base, over),
624 (Err(error), _) | (Ok(_), Err(error)) => return Self::failed_shared(error),
626 };
627 let op: Arc<dyn Op> = Arc::new(Composite::at(x, y, blend));
628 Self {
629 inner: otf_pixels_core::Image::combine(&[base, over], op).map_err(Arc::new),
630 icc,
631 animation,
632 }
633 }
634
635 fn failed(error: PixelsError) -> Self {
637 Self::failed_shared(Arc::new(error))
638 }
639
640 const fn failed_shared(error: Arc<PixelsError>) -> Self {
642 Self {
643 inner: Err(error),
644 icc: None,
645 animation: None,
646 }
647 }
648
649 #[must_use]
656 pub fn animation(&self) -> Option<&Animation> {
657 self.animation.as_deref()
658 }
659
660 #[must_use]
666 pub fn icc_profile(&self) -> Option<&[u8]> {
667 self.icc.as_deref()
668 }
669
670 #[must_use]
680 pub fn to_srgb(self) -> Self {
681 let Some(profile) = self.icc.clone() else {
682 return self;
683 };
684 let Ok(descriptor) = self.descriptor() else {
685 return self;
686 };
687 match ToSrgb::from_profile(&profile) {
688 Conversion::Convert(op) if op.applies_to(descriptor.pixel) => {
689 self.apply(Arc::new(op)).with_icc_profile(None)
690 }
691 Conversion::AlreadySrgb => self.with_icc_profile(None),
692 _ => self,
693 }
694 }
695
696 #[must_use]
704 pub fn to_pixel_format(self, pixel: PixelFormat) -> Self {
705 match self.descriptor() {
706 Ok(descriptor) if descriptor.pixel == pixel => self,
707 _ => self.apply(Arc::new(ConvertFormat::to(pixel))),
708 }
709 }
710
711 fn encodable_as(self, format: Format) -> Self {
715 let Ok(descriptor) = self.descriptor() else {
716 return self;
717 };
718 let pixel = descriptor.pixel;
719 let kind = match (format, pixel.sample_kind()) {
720 (
721 Format::Jpeg | Format::WebP | Format::Avif | Format::Gif,
722 SampleKind::U16 | SampleKind::F32,
723 ) => SampleKind::U8,
724 (Format::Png | Format::Tiff, SampleKind::F32) => SampleKind::U16,
725 _ => return self,
726 };
727 match PixelFormat::from_parts(pixel.layout(), kind) {
728 Some(target) => self.to_pixel_format(target),
729 None => self,
730 }
731 }
732
733 #[must_use]
736 pub fn with_icc_profile(mut self, profile: Option<Vec<u8>>) -> Self {
737 self.icc = profile.map(Arc::from);
738 self
739 }
740
741 #[must_use]
746 pub fn apply(self, op: Arc<dyn Op>) -> Self {
747 Self {
748 icc: self.icc,
749 animation: self.animation,
750 inner: match self.inner {
751 Ok(image) => image.apply(op).map_err(Arc::new),
752 Err(error) => Err(error),
754 },
755 }
756 }
757
758 #[must_use]
764 pub fn output(self, format: Format, options: EncodeOptions) -> Output {
765 Output {
766 image: self,
767 format,
768 options,
769 scheduler: SchedulerOptions::default(),
770 shared: None,
771 }
772 }
773
774 fn graph(&self) -> Result<&otf_pixels_core::Image> {
776 match &self.inner {
777 Ok(image) => Ok(image),
778 Err(error) => Err(rebuild(error)),
780 }
781 }
782}
783
784fn rebuild(error: &Arc<PixelsError>) -> PixelsError {
790 let detail = error.to_string();
791 match error.code() {
792 ErrorCode::Io => PixelsError::io("running the pipeline", std::io::Error::other(detail)),
793 ErrorCode::Malformed => PixelsError::malformed("pipeline", detail),
794 ErrorCode::Unsupported => PixelsError::unsupported(detail),
795 ErrorCode::InvalidArgument => PixelsError::invalid_argument("pipeline", detail),
796 ErrorCode::Graph => PixelsError::graph(detail),
797 ErrorCode::LimitExceeded => match **error {
798 PixelsError::LimitExceeded {
799 limit,
800 requested,
801 allowed,
802 } => PixelsError::limit_exceeded(limit, requested, allowed),
803 _ => PixelsError::graph(detail),
804 },
805 _ => PixelsError::graph(detail),
807 }
808}
809
810#[derive(Debug, Clone)]
815pub struct Output {
816 image: Image,
817 format: Format,
818 options: EncodeOptions,
819 scheduler: SchedulerOptions,
820 shared: Option<Arc<Scheduler>>,
822}
823
824impl Output {
825 #[must_use]
827 pub const fn format(&self) -> Format {
828 self.format
829 }
830
831 #[must_use]
833 pub const fn options(&self) -> EncodeOptions {
834 self.options
835 }
836
837 #[must_use]
843 pub const fn threads(mut self, threads: usize) -> Self {
844 self.scheduler.threads = threads;
845 self
846 }
847
848 #[must_use]
859 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
860 self.shared = Some(scheduler);
861 self
862 }
863
864 #[must_use]
866 pub const fn scheduler_options(mut self, options: SchedulerOptions) -> Self {
867 self.scheduler = options;
868 self
869 }
870
871 pub fn write(self, sink: impl Sink) -> Result<()> {
884 self.write_with_stats(sink).map(|_| ())
885 }
886
887 pub fn write_with_stats(self, mut sink: impl Sink) -> Result<RunStats> {
899 let encodable = self.image.clone().encodable_as(self.format);
902 let (image, reduction) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
903 let descriptor = image.descriptor();
904 let mut encoder = encoder_for(self.format, self.options)?;
905 encoder.set_icc_profile(self.image.icc.as_deref())?;
906 encoder.write_header(&descriptor, &mut sink)?;
907
908 let owned;
912 let scheduler = match &self.shared {
913 Some(shared) => shared.as_ref(),
914 None => {
915 owned = Scheduler::new(self.scheduler)?;
916 &owned
917 }
918 };
919 let mut rows = RowAssembler::new(descriptor);
920 let mut stats = scheduler.run(&image, |region, tile| {
921 rows.accept(region, tile, &mut |row| encoder.write_row(row, &mut sink))
922 })?;
923 rows.finish(&mut |row| encoder.write_row(row, &mut sink))?;
924 encoder.finish(&mut sink)?;
925 stats.reduction = reduction;
926 Ok(stats)
927 }
928
929 pub fn bytes_via_reference(self) -> Result<Vec<u8>> {
946 let encodable = self.image.clone().encodable_as(self.format);
950 let (image, _) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
951 let descriptor = image.descriptor();
952 let mut encoder = encoder_for(self.format, self.options)?;
953 encoder.set_icc_profile(self.image.icc.as_deref())?;
954 let mut sink = Vec::with_capacity(descriptor.byte_len().unwrap_or_default());
955 encoder.write_header(&descriptor, &mut sink)?;
956 otf_pixels_core::evaluate_rows(&image, |_, row| encoder.write_row(row, &mut sink))?;
957 encoder.finish(&mut sink)?;
958 Ok(sink)
959 }
960
961 pub fn bytes(self) -> Result<Vec<u8>> {
967 let hint = self
970 .image
971 .descriptor()
972 .ok()
973 .and_then(|d| d.byte_len())
974 .unwrap_or_default();
975 let mut buffer = Vec::with_capacity(hint);
976 self.write(&mut buffer)?;
977 Ok(buffer)
978 }
979}
980
981#[derive(Debug)]
989struct RowAssembler {
990 descriptor: ImageDescriptor,
991 band: Option<otf_pixels_core::TileBuf>,
993 next_row: u32,
995}
996
997impl RowAssembler {
998 const fn new(descriptor: ImageDescriptor) -> Self {
999 Self {
1000 descriptor,
1001 band: None,
1002 next_row: 0,
1003 }
1004 }
1005
1006 fn accept(
1008 &mut self,
1009 region: Region,
1010 tile: &otf_pixels_core::Tile<'_>,
1011 emit: &mut impl FnMut(&[u8]) -> Result<()>,
1012 ) -> Result<()> {
1013 if region.width == self.descriptor.width && self.band.is_none() {
1015 for y in region.y..region.y.saturating_add(region.height) {
1016 let row = tile
1017 .row(y)
1018 .ok_or_else(|| PixelsError::graph(format!("output tile is missing row {y}")))?;
1019 emit(row)?;
1020 self.next_row = y.saturating_add(1);
1021 }
1022 return Ok(());
1023 }
1024
1025 let band_region = Region::new(0, region.y, self.descriptor.width, region.height);
1028 let starts_new_band = self
1029 .band
1030 .as_ref()
1031 .is_none_or(|band| band.region().y != region.y);
1032 if starts_new_band {
1033 self.flush(emit)?;
1034 self.band = Some(otf_pixels_core::TileBuf::zeroed(
1035 band_region,
1036 self.descriptor.pixel,
1037 )?);
1038 }
1039 let Some(band) = self.band.as_mut() else {
1040 return Err(PixelsError::graph("row band vanished"));
1041 };
1042 otf_pixels_core::copy_region(tile, &mut band.as_tile_mut()?, region)?;
1043
1044 if region.right() >= u64::from(self.descriptor.width) {
1046 self.flush(emit)?;
1047 }
1048 Ok(())
1049 }
1050
1051 fn flush(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1053 let Some(band) = self.band.take() else {
1054 return Ok(());
1055 };
1056 let region = band.region();
1057 let view = band.as_tile()?;
1058 for y in region.y..region.y.saturating_add(region.height) {
1059 let row = view
1060 .row(y)
1061 .ok_or_else(|| PixelsError::graph(format!("row band is missing row {y}")))?;
1062 emit(row)?;
1063 self.next_row = y.saturating_add(1);
1064 }
1065 Ok(())
1066 }
1067
1068 fn finish(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1070 self.flush(emit)
1071 }
1072}
1073
1074fn sniffing_codecs() -> Vec<Box<dyn Codec>> {
1079 let codecs: Vec<Box<dyn Codec>> = vec![
1080 #[cfg(feature = "png")]
1081 Box::new(PngCodec),
1082 #[cfg(feature = "gif")]
1083 Box::new(GifCodec),
1084 #[cfg(feature = "jpeg")]
1085 Box::new(JpegCodec),
1086 #[cfg(feature = "tiff")]
1087 Box::new(TiffCodec),
1088 #[cfg(feature = "webp")]
1089 Box::new(WebPCodec),
1090 #[cfg(feature = "avif")]
1091 Box::new(AvifCodec),
1092 ];
1093 codecs
1094}
1095
1096fn encoder_for(format: Format, options: EncodeOptions) -> Result<Box<dyn Encoder>> {
1098 let _ = &options;
1101 match format {
1102 #[cfg(feature = "raw")]
1103 Format::Raw => Ok(Box::new(RawEncoder::new())),
1104 #[cfg(feature = "png")]
1105 Format::Png => Ok(Box::new(PngEncoder::from_options(&options))),
1106 #[cfg(feature = "gif")]
1107 Format::Gif => Ok(Box::new(GifEncoder::from_options(&options))),
1108 #[cfg(feature = "jpeg")]
1109 Format::Jpeg => Ok(Box::new(JpegEncoder::from_options(&options))),
1110 #[cfg(feature = "tiff")]
1111 Format::Tiff => Ok(Box::new(TiffEncoder::from_options(&options))),
1112 #[cfg(feature = "webp")]
1113 Format::WebP => Ok(Box::new(WebPEncoder::from_options(&options))),
1114 #[cfg(feature = "avif")]
1115 Format::Avif => Ok(Box::new(AvifEncoder::from_options(&options))),
1116 #[cfg(not(feature = "raw"))]
1117 Format::Raw => Err(PixelsError::unsupported(
1118 "raw encoding requires the `raw` feature of otf-pixels",
1119 )),
1120 other => Err(PixelsError::unsupported(format!(
1121 "encoding {other} is not implemented yet; \
1122 see the ROADMAP for which milestone lands it"
1123 ))),
1124 }
1125}
1126
1127#[cfg(all(test, feature = "raw"))]
1132#[allow(
1133 clippy::unwrap_used,
1134 clippy::expect_used,
1135 clippy::indexing_slicing,
1136 clippy::panic,
1137 reason = "tests operate on known-good values and assert shapes directly"
1138)]
1139mod tests {
1140 use super::*;
1141
1142 fn ramp(width: u32, height: u32) -> Image {
1143 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
1144 let len = descriptor.byte_len().unwrap();
1145 Image::from_raw(descriptor, (0..len).map(|i| i as u8).collect()).unwrap()
1146 }
1147
1148 #[test]
1149 fn from_raw_requires_an_exactly_sized_buffer() {
1150 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1151 assert!(Image::from_raw(descriptor, vec![0; 4]).is_ok());
1152 assert_eq!(
1153 Image::from_raw(descriptor, vec![0; 3]).unwrap_err().code(),
1154 ErrorCode::InvalidArgument
1155 );
1156 assert_eq!(
1157 Image::from_raw(descriptor, vec![0; 5]).unwrap_err().code(),
1158 ErrorCode::InvalidArgument
1159 );
1160 }
1161
1162 #[test]
1163 fn a_chain_error_surfaces_at_the_terminal() {
1164 let result = ramp(4, 4)
1166 .crop(3, 3, 4, 4)
1167 .flip()
1168 .flop()
1169 .output(Format::Raw, EncodeOptions::default())
1170 .bytes();
1171 let err = result.unwrap_err();
1172 assert_eq!(err.code(), ErrorCode::InvalidArgument);
1173 }
1174
1175 #[test]
1176 fn an_error_short_circuits_later_ops() {
1177 let broken = ramp(4, 4).crop(0, 0, 0, 0);
1180 assert!(broken.metadata().is_err());
1181 assert!(broken.clone().flip().descriptor().is_err());
1182 }
1183
1184 #[test]
1185 fn metadata_is_free_and_reports_the_pipeline_shape() {
1186 let meta = ramp(8, 6).metadata().unwrap();
1187 assert_eq!((meta.width, meta.height), (8, 6));
1188 assert_eq!(meta.format, Format::Raw);
1189 assert_eq!(meta.pixel, PixelFormat::Gray8);
1190 let cropped = ramp(8, 6).crop(1, 1, 3, 2).metadata().unwrap();
1192 assert_eq!((cropped.width, cropped.height), (3, 2));
1193 }
1194
1195 #[cfg(not(feature = "avif"))]
1199 #[test]
1200 fn a_format_built_out_is_a_catchable_error() {
1201 let format = Format::Avif;
1202 let err = ramp(2, 2)
1203 .output(format, EncodeOptions::default())
1204 .bytes()
1205 .unwrap_err();
1206 assert_eq!(err.code(), ErrorCode::Unsupported, "{format}");
1207 assert!(err.to_string().contains(format.as_str()), "{err}");
1208 }
1209
1210 #[cfg(feature = "tiff")]
1215 #[test]
1216 fn tiff_round_trips_through_the_facade() {
1217 let bytes = ramp(12, 9)
1218 .output(Format::Tiff, EncodeOptions::default())
1219 .bytes()
1220 .unwrap();
1221 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1222 let metadata = image.metadata().unwrap();
1223 assert_eq!(metadata.format, Format::Tiff);
1224 assert_eq!((metadata.width, metadata.height), (12, 9));
1225
1226 let decoded = image
1228 .output(Format::Raw, EncodeOptions::default())
1229 .bytes()
1230 .unwrap();
1231 let expected: Vec<u8> = (0..12_u32 * 9).map(|i| i as u8).collect();
1232 assert_eq!(decoded, expected);
1233 }
1234
1235 #[cfg(feature = "jpeg")]
1238 #[test]
1239 fn jpeg_round_trips_through_the_facade() {
1240 let (width, height) = (32_u32, 24_u32);
1241 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1242 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1243 .map(|i| {
1244 let pixel = i / 3;
1247 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1248 match i % 3 {
1249 0 => (x * 255 / width) as u8,
1250 1 => (y * 255 / height) as u8,
1251 _ => 128,
1252 }
1253 })
1254 .collect();
1255
1256 let bytes = Image::from_raw(descriptor, pixels.clone())
1257 .unwrap()
1258 .output(Format::Jpeg, EncodeOptions::with_quality(95).unwrap())
1259 .bytes()
1260 .unwrap();
1261
1262 let image = Image::from_stream(std::io::Cursor::new(bytes.clone())).unwrap();
1263 let metadata = image.metadata().unwrap();
1264 assert_eq!(metadata.format, Format::Jpeg, "sniffing missed the JPEG");
1265 assert_eq!((metadata.width, metadata.height), (width, height));
1266 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1267
1268 let decoded = image
1269 .output(Format::Raw, EncodeOptions::default())
1270 .bytes()
1271 .unwrap();
1272 assert_eq!(decoded.len(), pixels.len());
1273 let worst = decoded
1274 .iter()
1275 .zip(&pixels)
1276 .map(|(&a, &b)| a.abs_diff(b))
1277 .max()
1278 .unwrap_or(0);
1279 assert!(worst <= 12, "worst sample differs by {worst}");
1280 }
1281
1282 #[cfg(feature = "jpeg")]
1293 #[test]
1294 fn a_streaming_resize_matches_the_reference_evaluator() {
1295 for &(width, height, target) in &[
1296 (512_u32, 512_u32, 300_u32),
1297 (512, 512, 256),
1298 (256, 256, 200),
1299 (320, 240, 129),
1300 (64, 64, 16),
1301 ] {
1302 let source = jpeg_source(width, height);
1303 let scheduled = Image::from_stream(std::io::Cursor::new(source.clone()))
1304 .unwrap()
1305 .resize(target, target)
1306 .output(Format::Raw, EncodeOptions::default())
1307 .bytes()
1308 .unwrap_or_else(|e| panic!("{width}x{height} -> {target}: {e}"));
1309 let reference = Image::from_stream(std::io::Cursor::new(source))
1310 .unwrap()
1311 .resize(target, target)
1312 .output(Format::Raw, EncodeOptions::default())
1313 .bytes_via_reference()
1314 .unwrap();
1315
1316 assert_eq!(
1317 scheduled.len(),
1318 (target * target * 3) as usize,
1319 "{width}x{height} -> {target}: size"
1320 );
1321 assert_eq!(
1322 scheduled, reference,
1323 "{width}x{height} -> {target}: the scheduler and the oracle disagree"
1324 );
1325 }
1326 }
1327
1328 #[cfg(feature = "webp")]
1334 #[test]
1335 fn webp_round_trips_through_the_facade() {
1336 let bytes = ramp(20, 12)
1337 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1338 .bytes()
1339 .unwrap();
1340 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1341 let metadata = image.metadata().unwrap();
1342 assert_eq!(metadata.format, Format::WebP, "sniffing missed the WebP");
1343 assert_eq!((metadata.width, metadata.height), (20, 12));
1344 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1346
1347 let decoded = image
1348 .output(Format::Raw, EncodeOptions::default())
1349 .bytes()
1350 .unwrap();
1351 let expected: Vec<u8> = (0..20_u32 * 12)
1352 .flat_map(|i| {
1353 let value = i as u8;
1354 [value, value, value]
1355 })
1356 .collect();
1357 assert_eq!(decoded, expected, "a lossless round trip lost pixels");
1358 }
1359
1360 #[cfg(feature = "webp")]
1363 #[test]
1364 fn webp_keeps_alpha_through_the_facade() {
1365 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::Rgba8).unwrap();
1366 let pixels: Vec<u8> = (0..(9 * 7))
1367 .flat_map(|i| [(i * 3) as u8, 40, 200, (i * 7) as u8])
1368 .collect();
1369 let bytes = Image::from_raw(descriptor, pixels.clone())
1370 .unwrap()
1371 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1372 .bytes()
1373 .unwrap();
1374
1375 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1376 assert_eq!(image.metadata().unwrap().pixel, PixelFormat::Rgba8);
1377 let decoded = image
1378 .output(Format::Raw, EncodeOptions::default())
1379 .bytes()
1380 .unwrap();
1381 assert_eq!(decoded, pixels);
1382 }
1383
1384 #[cfg(feature = "webp")]
1388 #[test]
1389 fn webp_is_lossy_by_default_and_keeps_alpha_exact() {
1390 let (w, h) = (48_u32, 32_u32);
1391 let descriptor = ImageDescriptor::new(w, h, PixelFormat::Rgba8).unwrap();
1392 let pixels: Vec<u8> = (0..w * h)
1393 .flat_map(|i| {
1394 let (x, y) = (i % w, i / w);
1395 [(x * 5) as u8, (y * 7) as u8, 120, (x * 3 + y) as u8]
1396 })
1397 .collect();
1398 let encode = |options: EncodeOptions| {
1399 Image::from_raw(descriptor, pixels.clone())
1400 .unwrap()
1401 .output(Format::WebP, options)
1402 .bytes()
1403 .unwrap()
1404 };
1405 let good = encode(EncodeOptions::default());
1406 let rough = encode(EncodeOptions::with_quality(10).unwrap());
1407 assert!(
1408 rough.len() < good.len(),
1409 "{} vs {}",
1410 rough.len(),
1411 good.len()
1412 );
1413
1414 let decoded = Image::from_stream(std::io::Cursor::new(good))
1415 .unwrap()
1416 .output(Format::Raw, EncodeOptions::default())
1417 .bytes()
1418 .unwrap();
1419 for (ours, source) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
1420 assert_eq!(ours[3], source[3], "alpha changed");
1421 for c in 0..3 {
1422 assert!(ours[c].abs_diff(source[c]) <= 12, "{ours:?} vs {source:?}");
1423 }
1424 }
1425 }
1426
1427 #[cfg(feature = "jpeg")]
1430 #[test]
1431 fn a_thumbnail_pipeline_shrinks_the_jpeg_on_load() {
1432 let source = jpeg_source(512, 512);
1434
1435 let image = Image::from_stream(std::io::Cursor::new(source)).unwrap();
1436 assert_eq!(
1437 image.metadata().unwrap().width,
1438 512,
1439 "metadata is full size"
1440 );
1441
1442 let mut bytes = Vec::new();
1445 let stats = image
1446 .resize(32, 32)
1447 .output(Format::Raw, EncodeOptions::default())
1448 .write_with_stats(&mut bytes)
1449 .unwrap();
1450
1451 let reduction = stats
1452 .reduction
1453 .expect("a 512 to 32 resize should shrink the source on load");
1454 assert_eq!(reduction.from, (512, 512));
1455 assert_eq!(
1456 reduction.to,
1457 (64, 64),
1458 "1/8 is the coarsest scale that still covers 32"
1459 );
1460 assert!((reduction.factor() - 64.0).abs() < 0.01);
1461 assert_eq!(bytes.len(), 32 * 32 * 3);
1463 }
1464
1465 #[cfg(feature = "jpeg")]
1468 #[test]
1469 fn a_crop_blocks_shrink_on_load() {
1470 let source = jpeg_source(512, 512);
1471 let stats = Image::from_stream(std::io::Cursor::new(source))
1472 .unwrap()
1473 .crop(256, 256, 128, 128)
1474 .resize(32, 32)
1475 .output(Format::Raw, EncodeOptions::default())
1476 .write_with_stats(&mut Vec::new())
1477 .unwrap();
1478 assert!(
1479 stats.reduction.is_none(),
1480 "a crop must block shrink-on-load: {:?}",
1481 stats.reduction
1482 );
1483 }
1484
1485 #[cfg(feature = "jpeg")]
1488 #[test]
1489 fn a_crop_after_the_resize_still_allows_shrinking() {
1490 let source = jpeg_source(512, 512);
1491 let stats = Image::from_stream(std::io::Cursor::new(source))
1492 .unwrap()
1493 .resize(64, 64)
1494 .crop(0, 0, 32, 32)
1495 .output(Format::Raw, EncodeOptions::default())
1496 .write_with_stats(&mut Vec::new())
1497 .unwrap();
1498 assert!(stats.reduction.is_none());
1502 }
1503
1504 #[cfg(feature = "jpeg")]
1507 #[test]
1508 fn a_mild_resize_shrinks_by_less_or_not_at_all() {
1509 let source = jpeg_source(512, 512);
1510 let stats = Image::from_stream(std::io::Cursor::new(source.clone()))
1512 .unwrap()
1513 .resize(300, 300)
1514 .output(Format::Raw, EncodeOptions::default())
1515 .write_with_stats(&mut Vec::new())
1516 .unwrap();
1517 assert!(stats.reduction.is_none(), "{:?}", stats.reduction);
1518
1519 let stats = Image::from_stream(std::io::Cursor::new(source))
1521 .unwrap()
1522 .resize(200, 200)
1523 .output(Format::Raw, EncodeOptions::default())
1524 .write_with_stats(&mut Vec::new())
1525 .unwrap();
1526 assert_eq!(stats.reduction.map(|r| r.to), Some((256, 256)));
1527 }
1528
1529 #[cfg(feature = "jpeg")]
1531 fn jpeg_source(width: u32, height: u32) -> Vec<u8> {
1532 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1533 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1534 .map(|i| {
1535 let pixel = i / 3;
1536 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1537 match i % 3 {
1538 0 => (x * 255 / width) as u8,
1539 1 => (y * 255 / height) as u8,
1540 _ => 96,
1541 }
1542 })
1543 .collect();
1544 Image::from_raw(descriptor, pixels)
1545 .unwrap()
1546 .output(Format::Jpeg, EncodeOptions::with_quality(85).unwrap())
1547 .bytes()
1548 .unwrap()
1549 }
1550
1551 #[cfg(feature = "jpeg")]
1553 #[test]
1554 fn a_jpeg_pipeline_resizes_and_re_encodes() {
1555 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
1556 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1557 .map(|i| ((i / 3) % 251) as u8)
1558 .collect();
1559 let source = Image::from_raw(descriptor, pixels)
1560 .unwrap()
1561 .output(Format::Jpeg, EncodeOptions::default())
1562 .bytes()
1563 .unwrap();
1564
1565 let thumbnail = Image::from_stream(std::io::Cursor::new(source.clone()))
1566 .unwrap()
1567 .resize(16, 16)
1568 .output(Format::Jpeg, EncodeOptions::with_quality(70).unwrap())
1569 .bytes()
1570 .unwrap();
1571
1572 let metadata = Image::from_stream(std::io::Cursor::new(thumbnail.clone()))
1573 .unwrap()
1574 .metadata()
1575 .unwrap();
1576 assert_eq!((metadata.width, metadata.height), (16, 16));
1577 assert!(
1578 thumbnail.len() < source.len(),
1579 "a 16x16 thumbnail is {} bytes against a 64x64 source's {}",
1580 thumbnail.len(),
1581 source.len()
1582 );
1583 }
1584
1585 #[cfg(feature = "jpeg")]
1588 #[test]
1589 fn grayscale_jpeg_keeps_one_channel_through_the_facade() {
1590 let bytes = ramp(24, 16)
1591 .output(Format::Jpeg, EncodeOptions::with_quality(90).unwrap())
1592 .bytes()
1593 .unwrap();
1594 let metadata = Image::from_stream(std::io::Cursor::new(bytes))
1595 .unwrap()
1596 .metadata()
1597 .unwrap();
1598 assert_eq!(metadata.pixel, PixelFormat::Gray8);
1599 assert_eq!((metadata.width, metadata.height), (24, 16));
1600 }
1601
1602 #[test]
1603 fn write_streams_into_any_sink() {
1604 let mut sink = Vec::new();
1605 ramp(2, 2)
1606 .output(Format::Raw, EncodeOptions::default())
1607 .write(&mut sink)
1608 .unwrap();
1609 assert_eq!(sink, [0, 1, 2, 3]);
1610 }
1611
1612 #[test]
1613 fn output_reports_its_settings() {
1614 let options = EncodeOptions::with_quality(55).unwrap();
1615 let output = ramp(2, 2).output(Format::Raw, options);
1616 assert_eq!(output.format(), Format::Raw);
1617 assert_eq!(output.options().quality, 55);
1618 }
1619
1620 #[cfg(feature = "raw")]
1621 #[test]
1622 fn from_raw_stream_defers_decoding_to_the_terminal() {
1623 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1624 let layout = RawFormat::packed(descriptor);
1625 let cursor = std::io::Cursor::new(vec![1_u8, 2, 3, 4]);
1626 let image = Image::from_raw_stream(layout, cursor).unwrap();
1627 assert_eq!(image.metadata().unwrap().width, 2);
1629 let bytes = image
1630 .output(Format::Raw, EncodeOptions::default())
1631 .bytes()
1632 .unwrap();
1633 assert_eq!(bytes, [1, 2, 3, 4]);
1634 }
1635
1636 #[cfg(feature = "raw")]
1637 #[test]
1638 fn a_truncated_stream_fails_the_terminal_without_panicking() {
1639 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
1640 let layout = RawFormat::packed(descriptor);
1641 let cursor = std::io::Cursor::new(vec![1_u8; 5]);
1642 let image = Image::from_raw_stream(layout, cursor).unwrap();
1643 let err = image
1644 .output(Format::Raw, EncodeOptions::default())
1645 .bytes()
1646 .unwrap_err();
1647 assert_eq!(err.code(), ErrorCode::Malformed);
1648 }
1649
1650 #[test]
1651 fn images_are_send_sync_and_cheap_to_clone() {
1652 const fn assert_send_sync<T: Send + Sync>() {}
1653 assert_send_sync::<Image>();
1654 assert_send_sync::<Output>();
1655 let image = ramp(4, 4);
1656 let clone = image.clone();
1657 assert_eq!(
1658 image.metadata().unwrap().width,
1659 clone.metadata().unwrap().width
1660 );
1661 }
1662
1663 #[cfg(feature = "png")]
1664 #[test]
1665 fn a_png_round_trips_through_the_facade() {
1666 let png = ramp(37, 21)
1667 .output(Format::Png, EncodeOptions::default())
1668 .bytes()
1669 .unwrap();
1670 let image = Image::from_stream(std::io::Cursor::new(png)).unwrap();
1671 assert_eq!(image.descriptor().unwrap().width, 37);
1672 assert_eq!(image.metadata().unwrap().format, Format::Png);
1673
1674 let back = image
1675 .output(Format::Raw, EncodeOptions::default())
1676 .bytes()
1677 .unwrap();
1678 let expected = ramp(37, 21)
1679 .output(Format::Raw, EncodeOptions::default())
1680 .bytes()
1681 .unwrap();
1682 assert_eq!(back, expected, "pixels changed across a PNG round trip");
1683 }
1684
1685 #[cfg(feature = "png")]
1686 #[test]
1687 fn a_pipeline_survives_a_png_round_trip() {
1688 let png = ramp(16, 16)
1691 .output(Format::Png, EncodeOptions::default())
1692 .bytes()
1693 .unwrap();
1694 let cropped = Image::from_stream(std::io::Cursor::new(png))
1695 .unwrap()
1696 .crop(2, 3, 8, 5)
1697 .flip()
1698 .output(Format::Raw, EncodeOptions::default())
1699 .bytes()
1700 .unwrap();
1701 let direct = ramp(16, 16)
1702 .crop(2, 3, 8, 5)
1703 .flip()
1704 .output(Format::Raw, EncodeOptions::default())
1705 .bytes()
1706 .unwrap();
1707 assert_eq!(cropped, direct);
1708 }
1709
1710 #[cfg(feature = "png")]
1711 #[test]
1712 fn open_ignores_the_extension_and_reads_the_bytes() {
1713 let dir = std::env::temp_dir().join(format!("otf-pixels-open-{}", std::process::id()));
1714 std::fs::create_dir_all(&dir).unwrap();
1715 let path = dir.join("actually-a-png.jpg");
1716 let png = ramp(8, 8)
1717 .output(Format::Png, EncodeOptions::default())
1718 .bytes()
1719 .unwrap();
1720 std::fs::write(&path, &png).unwrap();
1721
1722 let image = Image::open(&path).unwrap();
1723 assert_eq!(
1724 image.metadata().unwrap().format,
1725 Format::Png,
1726 "extension won over content"
1727 );
1728 std::fs::remove_dir_all(&dir).ok();
1729 }
1730
1731 #[test]
1732 fn open_names_the_file_it_could_not_read() {
1733 let error = Image::open("/nonexistent/otf-pixels/missing.png").unwrap_err();
1734 assert_eq!(error.code(), ErrorCode::Io);
1735 assert!(error.to_string().contains("missing.png"), "{error}");
1736 }
1737
1738 #[test]
1739 fn an_unrecognised_stream_is_unsupported_not_a_guess() {
1740 for bytes in [&b""[..], &b"not an image"[..], &[0_u8; 64][..]] {
1741 let error = Image::from_stream(std::io::Cursor::new(bytes.to_vec())).unwrap_err();
1742 assert_eq!(error.code(), ErrorCode::Unsupported, "{bytes:02x?}");
1743 }
1744 }
1745
1746 #[cfg(feature = "png")]
1747 #[test]
1748 fn a_truncated_png_is_malformed_not_a_panic() {
1749 let png = ramp(8, 8)
1750 .output(Format::Png, EncodeOptions::default())
1751 .bytes()
1752 .unwrap();
1753 for cut in [9, 16, 24, 32, png.len() - 1] {
1756 let truncated = png[..cut].to_vec();
1757 let result = Image::from_stream(std::io::Cursor::new(truncated))
1758 .and_then(|i| i.output(Format::Raw, EncodeOptions::default()).bytes());
1759 assert!(result.is_err(), "truncating to {cut} bytes should fail");
1760 }
1761 }
1762
1763 #[cfg(feature = "png")]
1764 #[test]
1765 fn sniffing_reads_only_the_magic_bytes_before_deciding() {
1766 let error = Image::from_stream(std::io::Cursor::new(
1770 otf_pixels_codec_png::SIGNATURE.to_vec(),
1771 ))
1772 .unwrap_err();
1773 assert_eq!(error.code(), ErrorCode::Malformed, "{error}");
1774 }
1775}
1776
1777const _: () = {
1780 const fn shareable<T: Send + Sync>() {}
1781 shareable::<Image>();
1782 shareable::<Output>();
1783 shareable::<OpenOptions>();
1784 shareable::<PixelsError>();
1785 shareable::<Scheduler>();
1786};