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, ExtractChannel, Filter, Fit,
136 Flatten, Flip, Flop, Kernel, Modulate, Quarter, Resize, ResizeOptions, Rotate, ToSrgb,
137 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 limits: Limits,
195}
196
197impl OpenOptions {
198 #[must_use]
203 pub const fn with_auto_orient(mut self, auto_orient: bool) -> Self {
204 self.auto_orient = auto_orient;
205 self
206 }
207
208 #[must_use]
210 pub const fn with_animated(mut self, animated: bool) -> Self {
211 self.animated = animated;
212 self
213 }
214
215 #[must_use]
217 pub const fn with_limits(mut self, limits: Limits) -> Self {
218 self.limits = limits;
219 self
220 }
221
222 #[must_use]
224 pub const fn with_to_srgb(mut self, to_srgb: bool) -> Self {
225 self.to_srgb = to_srgb;
226 self
227 }
228}
229
230impl Default for OpenOptions {
231 fn default() -> Self {
232 Self {
233 auto_orient: true,
234 to_srgb: true,
235 animated: false,
236 limits: Limits::default(),
237 }
238 }
239}
240
241#[derive(Debug, Clone)]
246pub struct Image {
247 inner: std::result::Result<otf_pixels_core::Image, Arc<PixelsError>>,
249 icc: Option<Arc<[u8]>>,
252 animation: Option<Arc<Animation>>,
254}
255
256impl Image {
257 pub fn from_raw(descriptor: ImageDescriptor, bytes: Vec<u8>) -> Result<Self> {
266 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes)?;
267 let source = BufferSource::new(descriptor, Arc::new(buffer))?;
268 Ok(Self::from_producer(Arc::new(source), Format::Raw))
269 }
270
271 #[cfg(feature = "raw")]
281 pub fn from_raw_stream(
282 layout: RawFormat,
283 source: impl Source + std::fmt::Debug + 'static,
284 ) -> Result<Self> {
285 let decoder = RawDecoder::new(layout, source)?;
286 Ok(Self::from_decoder(Box::new(decoder), Format::Raw))
287 }
288
289 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
303 Self::open_with(path, OpenOptions::default())
304 }
305
306 pub fn open_with(path: impl AsRef<std::path::Path>, options: OpenOptions) -> Result<Self> {
312 let path = path.as_ref();
313 let file = std::fs::File::open(path).map_err(|e| {
314 let kind = e.kind();
318 let detail = std::io::Error::new(kind, format!("{}: {e}", path.display()));
319 PixelsError::io("opening image file", detail)
320 })?;
321 Self::from_stream_with(std::io::BufReader::new(file), options)
322 }
323
324 pub fn from_stream(source: impl Source + std::fmt::Debug + 'static) -> Result<Self> {
340 Self::from_stream_with(source, OpenOptions::default())
341 }
342
343 pub fn from_stream_with(
349 mut source: impl Source + std::fmt::Debug + 'static,
350 options: OpenOptions,
351 ) -> Result<Self> {
352 let codecs = sniffing_codecs();
353 let longest = codecs.iter().map(|c| c.magic_len()).max().unwrap_or(0);
354
355 let mut prefix = Vec::with_capacity(longest);
359 let mut buffer = vec![0_u8; longest];
360 while prefix.len() < longest {
361 let Some(rest) = buffer.get_mut(prefix.len()..) else {
362 break;
363 };
364 match source.read(rest)? {
365 0 => break,
366 n => {
367 let Some(read) = rest.get(..n) else { break };
368 prefix.extend_from_slice(read);
369 }
370 }
371 }
372
373 let Some(codec) = codecs.iter().find(|codec| codec.probe(&prefix)) else {
374 return Err(PixelsError::unsupported(format!(
375 "no codec recognises this stream; its first {} bytes are {:02x?}",
376 prefix.len().min(8),
377 prefix.get(..prefix.len().min(8)).unwrap_or(&[])
378 )));
379 };
380 let stream = Prefixed::new(prefix, source);
381 let _ = (&stream, options);
384
385 match codec.format() {
386 #[cfg(feature = "png")]
387 Format::Png => {
388 let decoder = PngDecoder::new(stream, options.limits)?;
389 Self::decoded(Box::new(decoder), Format::Png, options)
390 }
391 #[cfg(feature = "gif")]
392 Format::Gif => {
393 let decoder = GifDecoder::new(stream, options.limits)?;
394 Self::decoded(Box::new(decoder), Format::Gif, options)
395 }
396 #[cfg(feature = "jpeg")]
397 Format::Jpeg => {
398 let decoder = JpegDecoder::new(stream, options.limits)?;
399 Self::decoded(Box::new(decoder), Format::Jpeg, options)
400 }
401 #[cfg(feature = "tiff")]
402 Format::Tiff => {
403 let decoder = TiffDecoder::new(stream, options.limits)?;
404 Self::decoded(Box::new(decoder), Format::Tiff, options)
405 }
406 #[cfg(feature = "webp")]
407 Format::WebP => {
408 let decoder = WebPDecoder::new(stream, options.limits)?;
409 Self::decoded(Box::new(decoder), Format::WebP, options)
410 }
411 #[cfg(feature = "avif")]
412 Format::Avif => {
413 let decoder = AvifDecoder::new(stream, options.limits)?;
414 Self::decoded(Box::new(decoder), Format::Avif, options)
415 }
416 other => Err(PixelsError::unsupported(format!(
417 "{other} was detected but no decoder for it is compiled in"
418 ))),
419 }
420 }
421
422 #[cfg(any(
427 feature = "png",
428 feature = "gif",
429 feature = "jpeg",
430 feature = "tiff",
431 feature = "webp",
432 feature = "avif"
433 ))]
434 fn decoded(decoder: Box<dyn Decoder>, format: Format, options: OpenOptions) -> Result<Self> {
435 let orientation = decoder.orientation();
436 let icc = decoder.icc_profile().map(Vec::from);
437 let animation = decoder.animation().map(Arc::new);
438 if options.animated && animation.is_some() {
439 return Err(PixelsError::unsupported(format!(
440 "{format}: multi-frame (animated) pipelines are not implemented yet; \
441 open with `animated` off to process the first frame"
442 )));
443 }
444 let mut image = Self::from_decoder(decoder, format).with_icc_profile(icc);
445 image.animation = animation;
446 if options.to_srgb {
447 image = image.to_srgb();
448 }
449 Ok(if options.auto_orient {
450 image.orient(orientation)
451 } else {
452 image
453 })
454 }
455
456 #[must_use]
462 pub fn from_decoder(decoder: Box<dyn Decoder>, format: Format) -> Self {
463 let source = otf_pixels_core::DecodedSource::new(decoder);
464 Self::from_producer(Arc::new(source), format)
465 }
466
467 #[must_use]
469 pub fn from_producer(producer: Arc<dyn Producer>, format: Format) -> Self {
470 Self {
471 icc: None,
472 animation: None,
473 inner: Ok(otf_pixels_core::Image::from_producer(producer, format)),
474 }
475 }
476
477 pub fn metadata(&self) -> Result<Metadata> {
486 self.graph()?.metadata()
487 }
488
489 pub fn descriptor(&self) -> Result<ImageDescriptor> {
495 Ok(self.graph()?.descriptor())
496 }
497
498 #[must_use]
502 pub fn crop(self, x: u32, y: u32, width: u32, height: u32) -> Self {
503 match Crop::at(x, y, width, height) {
504 Ok(op) => self.apply(Arc::new(op)),
505 Err(error) => Self::failed(error),
506 }
507 }
508
509 #[must_use]
511 pub fn flip(self) -> Self {
512 self.apply(Arc::new(Flip))
513 }
514
515 #[must_use]
517 pub fn flop(self) -> Self {
518 self.apply(Arc::new(Flop))
519 }
520
521 #[must_use]
523 pub fn resize(self, width: u32, height: u32) -> Self {
524 self.resize_with(width, height, ResizeOptions::default())
525 }
526
527 #[must_use]
529 pub fn resize_with(self, width: u32, height: u32, options: ResizeOptions) -> Self {
530 match Resize::new(width, height, options) {
531 Ok(op) => self.apply(Arc::new(op)),
532 Err(error) => Self::failed(error),
533 }
534 }
535
536 #[must_use]
538 pub fn thumbnail(self, width: u32, height: u32) -> Self {
539 let options = ResizeOptions::default()
540 .with_fit(Fit::Inside)
541 .without_enlargement(true);
542 self.resize_with(width, height, options)
543 }
544
545 #[must_use]
553 pub fn orient(self, orientation: Orientation) -> Self {
554 let turns = orientation.clockwise_turns();
555 let image = if turns == 0 {
556 self
557 } else {
558 self.rotate(90 * i32::from(turns))
559 };
560 if orientation.mirrored() {
561 image.flop()
562 } else {
563 image
564 }
565 }
566
567 #[must_use]
569 pub fn rotate(self, degrees: i32) -> Self {
570 match Rotate::degrees(degrees) {
571 Ok(op) => self.apply(Arc::new(op)),
572 Err(error) => Self::failed(error),
573 }
574 }
575
576 #[must_use]
578 pub fn modulate(self, options: Modulate) -> Self {
579 self.apply(Arc::new(options))
580 }
581
582 #[must_use]
584 pub fn convolve(self, kernel: Kernel) -> Self {
585 self.apply(Arc::new(Convolve::new(kernel)))
586 }
587
588 #[must_use]
590 pub fn blur(self, sigma: f32) -> Self {
591 match Kernel::gaussian(sigma) {
592 Ok(kernel) => self.convolve(kernel),
593 Err(error) => Self::failed(error),
594 }
595 }
596
597 #[must_use]
599 pub fn sharpen(self, amount: f32) -> Self {
600 match Kernel::sharpen(amount) {
601 Ok(kernel) => self.convolve(kernel),
602 Err(error) => Self::failed(error),
603 }
604 }
605
606 #[must_use]
608 pub fn extract_channel(self, index: usize) -> Self {
609 self.apply(Arc::new(ExtractChannel::new(index)))
610 }
611
612 #[must_use]
614 pub fn flatten(self, red: u8, green: u8, blue: u8) -> Self {
615 self.apply(Arc::new(Flatten::onto(red, green, blue)))
616 }
617
618 #[must_use]
623 pub fn composite(self, overlay: Self, x: i64, y: i64) -> Self {
624 self.composite_with(overlay, x, y, Blend::Over)
625 }
626
627 #[must_use]
629 pub fn composite_with(self, overlay: Self, x: i64, y: i64, blend: Blend) -> Self {
630 let (icc, animation) = (self.icc, self.animation);
633 let (base, over) = match (self.inner, overlay.inner) {
634 (Ok(base), Ok(over)) => (base, over),
635 (Err(error), _) | (Ok(_), Err(error)) => return Self::failed_shared(error),
637 };
638 let op: Arc<dyn Op> = Arc::new(Composite::at(x, y, blend));
639 Self {
640 inner: otf_pixels_core::Image::combine(&[base, over], op).map_err(Arc::new),
641 icc,
642 animation,
643 }
644 }
645
646 fn failed(error: PixelsError) -> Self {
648 Self::failed_shared(Arc::new(error))
649 }
650
651 const fn failed_shared(error: Arc<PixelsError>) -> Self {
653 Self {
654 inner: Err(error),
655 icc: None,
656 animation: None,
657 }
658 }
659
660 #[must_use]
667 pub fn animation(&self) -> Option<&Animation> {
668 self.animation.as_deref()
669 }
670
671 #[must_use]
677 pub fn icc_profile(&self) -> Option<&[u8]> {
678 self.icc.as_deref()
679 }
680
681 #[must_use]
691 pub fn to_srgb(self) -> Self {
692 let Some(profile) = self.icc.clone() else {
693 return self;
694 };
695 let Ok(descriptor) = self.descriptor() else {
696 return self;
697 };
698 match ToSrgb::from_profile(&profile) {
699 Conversion::Convert(op) if op.applies_to(descriptor.pixel) => {
700 self.apply(Arc::new(op)).with_icc_profile(None)
701 }
702 Conversion::AlreadySrgb => self.with_icc_profile(None),
703 _ => self,
704 }
705 }
706
707 #[must_use]
715 pub fn to_pixel_format(self, pixel: PixelFormat) -> Self {
716 match self.descriptor() {
717 Ok(descriptor) if descriptor.pixel == pixel => self,
718 _ => self.apply(Arc::new(ConvertFormat::to(pixel))),
719 }
720 }
721
722 fn encodable_as(self, format: Format) -> Self {
726 let Ok(descriptor) = self.descriptor() else {
727 return self;
728 };
729 let pixel = descriptor.pixel;
730 let kind = match (format, pixel.sample_kind()) {
731 (
732 Format::Jpeg | Format::WebP | Format::Avif | Format::Gif,
733 SampleKind::U16 | SampleKind::F32,
734 ) => SampleKind::U8,
735 (Format::Png | Format::Tiff, SampleKind::F32) => SampleKind::U16,
736 _ => return self,
737 };
738 match PixelFormat::from_parts(pixel.layout(), kind) {
739 Some(target) => self.to_pixel_format(target),
740 None => self,
741 }
742 }
743
744 #[must_use]
747 pub fn with_icc_profile(mut self, profile: Option<Vec<u8>>) -> Self {
748 self.icc = profile.map(Arc::from);
749 self
750 }
751
752 #[must_use]
757 pub fn apply(self, op: Arc<dyn Op>) -> Self {
758 Self {
759 icc: self.icc,
760 animation: self.animation,
761 inner: match self.inner {
762 Ok(image) => image.apply(op).map_err(Arc::new),
763 Err(error) => Err(error),
765 },
766 }
767 }
768
769 #[must_use]
775 pub fn output(self, format: Format, options: EncodeOptions) -> Output {
776 Output {
777 image: self,
778 format,
779 options,
780 scheduler: None,
781 shared: None,
782 }
783 }
784
785 fn graph(&self) -> Result<&otf_pixels_core::Image> {
787 match &self.inner {
788 Ok(image) => Ok(image),
789 Err(error) => Err(rebuild(error)),
791 }
792 }
793}
794
795fn rebuild(error: &Arc<PixelsError>) -> PixelsError {
801 let detail = error.to_string();
802 match error.code() {
803 ErrorCode::Io => PixelsError::io("running the pipeline", std::io::Error::other(detail)),
804 ErrorCode::Malformed => PixelsError::malformed("pipeline", detail),
805 ErrorCode::Unsupported => PixelsError::unsupported(detail),
806 ErrorCode::InvalidArgument => PixelsError::invalid_argument("pipeline", detail),
807 ErrorCode::Graph => PixelsError::graph(detail),
808 ErrorCode::LimitExceeded => match **error {
809 PixelsError::LimitExceeded {
810 limit,
811 requested,
812 allowed,
813 } => PixelsError::limit_exceeded(limit, requested, allowed),
814 _ => PixelsError::graph(detail),
815 },
816 _ => PixelsError::graph(detail),
818 }
819}
820
821#[derive(Debug, Clone)]
826pub struct Output {
827 image: Image,
828 format: Format,
829 options: EncodeOptions,
830 scheduler: Option<SchedulerOptions>,
832 shared: Option<Arc<Scheduler>>,
834}
835
836impl Output {
837 #[must_use]
839 pub const fn format(&self) -> Format {
840 self.format
841 }
842
843 #[must_use]
845 pub const fn options(&self) -> EncodeOptions {
846 self.options
847 }
848
849 #[must_use]
861 pub fn threads(mut self, threads: usize) -> Self {
862 self.scheduler = Some(self.scheduler.unwrap_or_default().with_threads(threads));
863 self
864 }
865
866 #[must_use]
876 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
877 self.shared = Some(scheduler);
878 self
879 }
880
881 #[must_use]
887 pub const fn scheduler_options(mut self, options: SchedulerOptions) -> Self {
888 self.scheduler = Some(options);
889 self
890 }
891
892 pub fn write(self, sink: impl Sink) -> Result<()> {
905 self.write_with_stats(sink).map(|_| ())
906 }
907
908 pub fn write_with_stats(self, mut sink: impl Sink) -> Result<RunStats> {
920 let encodable = self.image.clone().encodable_as(self.format);
923 let (image, reduction) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
924 let descriptor = image.descriptor();
925 let mut encoder = encoder_for(self.format, self.options)?;
926 encoder.set_icc_profile(self.image.icc.as_deref())?;
927 encoder.write_header(&descriptor, &mut sink)?;
928
929 let scheduler = match (&self.shared, self.scheduler) {
935 (Some(shared), _) => Arc::clone(shared),
936 (None, Some(options)) => Arc::new(Scheduler::new(options)?),
937 (None, None) if otf_pixels_core::ThreadPool::on_worker_thread() => {
938 Arc::new(Scheduler::with_defaults()?)
939 }
940 (None, None) => Scheduler::global()?,
941 };
942 let mut rows = RowAssembler::new(descriptor);
943 let mut stats = scheduler.run(&image, |region, tile| {
944 rows.accept(region, tile, &mut |row| encoder.write_row(row, &mut sink))
945 })?;
946 rows.finish(&mut |row| encoder.write_row(row, &mut sink))?;
947 encoder.finish(&mut sink)?;
948 stats.reduction = reduction;
949 Ok(stats)
950 }
951
952 pub fn bytes_via_reference(self) -> Result<Vec<u8>> {
969 let encodable = self.image.clone().encodable_as(self.format);
973 let (image, _) = otf_pixels_core::shrink_on_load(encodable.graph()?)?;
974 let descriptor = image.descriptor();
975 let mut encoder = encoder_for(self.format, self.options)?;
976 encoder.set_icc_profile(self.image.icc.as_deref())?;
977 let mut sink = Vec::with_capacity(descriptor.byte_len().unwrap_or_default());
978 encoder.write_header(&descriptor, &mut sink)?;
979 otf_pixels_core::evaluate_rows(&image, |_, row| encoder.write_row(row, &mut sink))?;
980 encoder.finish(&mut sink)?;
981 Ok(sink)
982 }
983
984 pub fn bytes(self) -> Result<Vec<u8>> {
990 let hint = self
993 .image
994 .descriptor()
995 .ok()
996 .and_then(|d| d.byte_len())
997 .unwrap_or_default();
998 let mut buffer = Vec::with_capacity(hint);
999 self.write(&mut buffer)?;
1000 Ok(buffer)
1001 }
1002}
1003
1004#[derive(Debug)]
1012struct RowAssembler {
1013 descriptor: ImageDescriptor,
1014 band: Option<otf_pixels_core::TileBuf>,
1016 next_row: u32,
1018}
1019
1020impl RowAssembler {
1021 const fn new(descriptor: ImageDescriptor) -> Self {
1022 Self {
1023 descriptor,
1024 band: None,
1025 next_row: 0,
1026 }
1027 }
1028
1029 fn accept(
1031 &mut self,
1032 region: Region,
1033 tile: &otf_pixels_core::Tile<'_>,
1034 emit: &mut impl FnMut(&[u8]) -> Result<()>,
1035 ) -> Result<()> {
1036 if region.width == self.descriptor.width && self.band.is_none() {
1038 for y in region.y..region.y.saturating_add(region.height) {
1039 let row = tile
1040 .row(y)
1041 .ok_or_else(|| PixelsError::graph(format!("output tile is missing row {y}")))?;
1042 emit(row)?;
1043 self.next_row = y.saturating_add(1);
1044 }
1045 return Ok(());
1046 }
1047
1048 let band_region = Region::new(0, region.y, self.descriptor.width, region.height);
1051 let starts_new_band = self
1052 .band
1053 .as_ref()
1054 .is_none_or(|band| band.region().y != region.y);
1055 if starts_new_band {
1056 self.flush(emit)?;
1057 self.band = Some(otf_pixels_core::TileBuf::zeroed(
1058 band_region,
1059 self.descriptor.pixel,
1060 )?);
1061 }
1062 let Some(band) = self.band.as_mut() else {
1063 return Err(PixelsError::graph("row band vanished"));
1064 };
1065 otf_pixels_core::copy_region(tile, &mut band.as_tile_mut()?, region)?;
1066
1067 if region.right() >= u64::from(self.descriptor.width) {
1069 self.flush(emit)?;
1070 }
1071 Ok(())
1072 }
1073
1074 fn flush(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1076 let Some(band) = self.band.take() else {
1077 return Ok(());
1078 };
1079 let region = band.region();
1080 let view = band.as_tile()?;
1081 for y in region.y..region.y.saturating_add(region.height) {
1082 let row = view
1083 .row(y)
1084 .ok_or_else(|| PixelsError::graph(format!("row band is missing row {y}")))?;
1085 emit(row)?;
1086 self.next_row = y.saturating_add(1);
1087 }
1088 Ok(())
1089 }
1090
1091 fn finish(&mut self, emit: &mut impl FnMut(&[u8]) -> Result<()>) -> Result<()> {
1093 self.flush(emit)
1094 }
1095}
1096
1097fn sniffing_codecs() -> Vec<Box<dyn Codec>> {
1102 let codecs: Vec<Box<dyn Codec>> = vec![
1103 #[cfg(feature = "png")]
1104 Box::new(PngCodec),
1105 #[cfg(feature = "gif")]
1106 Box::new(GifCodec),
1107 #[cfg(feature = "jpeg")]
1108 Box::new(JpegCodec),
1109 #[cfg(feature = "tiff")]
1110 Box::new(TiffCodec),
1111 #[cfg(feature = "webp")]
1112 Box::new(WebPCodec),
1113 #[cfg(feature = "avif")]
1114 Box::new(AvifCodec),
1115 ];
1116 codecs
1117}
1118
1119fn encoder_for(format: Format, options: EncodeOptions) -> Result<Box<dyn Encoder>> {
1121 let _ = &options;
1124 match format {
1125 #[cfg(feature = "raw")]
1126 Format::Raw => Ok(Box::new(RawEncoder::new())),
1127 #[cfg(feature = "png")]
1128 Format::Png => Ok(Box::new(PngEncoder::from_options(&options))),
1129 #[cfg(feature = "gif")]
1130 Format::Gif => Ok(Box::new(GifEncoder::from_options(&options))),
1131 #[cfg(feature = "jpeg")]
1132 Format::Jpeg => Ok(Box::new(JpegEncoder::from_options(&options))),
1133 #[cfg(feature = "tiff")]
1134 Format::Tiff => Ok(Box::new(TiffEncoder::from_options(&options))),
1135 #[cfg(feature = "webp")]
1136 Format::WebP => Ok(Box::new(WebPEncoder::from_options(&options))),
1137 #[cfg(feature = "avif")]
1138 Format::Avif => Ok(Box::new(AvifEncoder::from_options(&options))),
1139 #[cfg(not(feature = "raw"))]
1140 Format::Raw => Err(PixelsError::unsupported(
1141 "raw encoding requires the `raw` feature of otf-pixels",
1142 )),
1143 other => Err(PixelsError::unsupported(format!(
1144 "encoding {other} is not implemented yet; \
1145 see the ROADMAP for which milestone lands it"
1146 ))),
1147 }
1148}
1149
1150#[cfg(all(test, feature = "raw"))]
1155#[allow(
1156 clippy::unwrap_used,
1157 clippy::expect_used,
1158 clippy::indexing_slicing,
1159 clippy::panic,
1160 reason = "tests operate on known-good values and assert shapes directly"
1161)]
1162mod tests {
1163 use super::*;
1164
1165 fn ramp(width: u32, height: u32) -> Image {
1166 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
1167 let len = descriptor.byte_len().unwrap();
1168 Image::from_raw(descriptor, (0..len).map(|i| i as u8).collect()).unwrap()
1169 }
1170
1171 #[test]
1172 fn from_raw_requires_an_exactly_sized_buffer() {
1173 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1174 assert!(Image::from_raw(descriptor, vec![0; 4]).is_ok());
1175 assert_eq!(
1176 Image::from_raw(descriptor, vec![0; 3]).unwrap_err().code(),
1177 ErrorCode::InvalidArgument
1178 );
1179 assert_eq!(
1180 Image::from_raw(descriptor, vec![0; 5]).unwrap_err().code(),
1181 ErrorCode::InvalidArgument
1182 );
1183 }
1184
1185 #[test]
1186 fn a_chain_error_surfaces_at_the_terminal() {
1187 let result = ramp(4, 4)
1189 .crop(3, 3, 4, 4)
1190 .flip()
1191 .flop()
1192 .output(Format::Raw, EncodeOptions::default())
1193 .bytes();
1194 let err = result.unwrap_err();
1195 assert_eq!(err.code(), ErrorCode::InvalidArgument);
1196 }
1197
1198 #[test]
1199 fn an_error_short_circuits_later_ops() {
1200 let broken = ramp(4, 4).crop(0, 0, 0, 0);
1203 assert!(broken.metadata().is_err());
1204 assert!(broken.clone().flip().descriptor().is_err());
1205 }
1206
1207 #[test]
1208 fn metadata_is_free_and_reports_the_pipeline_shape() {
1209 let meta = ramp(8, 6).metadata().unwrap();
1210 assert_eq!((meta.width, meta.height), (8, 6));
1211 assert_eq!(meta.format, Format::Raw);
1212 assert_eq!(meta.pixel, PixelFormat::Gray8);
1213 let cropped = ramp(8, 6).crop(1, 1, 3, 2).metadata().unwrap();
1215 assert_eq!((cropped.width, cropped.height), (3, 2));
1216 }
1217
1218 #[cfg(not(feature = "avif"))]
1222 #[test]
1223 fn a_format_built_out_is_a_catchable_error() {
1224 let format = Format::Avif;
1225 let err = ramp(2, 2)
1226 .output(format, EncodeOptions::default())
1227 .bytes()
1228 .unwrap_err();
1229 assert_eq!(err.code(), ErrorCode::Unsupported, "{format}");
1230 assert!(err.to_string().contains(format.as_str()), "{err}");
1231 }
1232
1233 #[cfg(feature = "tiff")]
1238 #[test]
1239 fn tiff_round_trips_through_the_facade() {
1240 let bytes = ramp(12, 9)
1241 .output(Format::Tiff, EncodeOptions::default())
1242 .bytes()
1243 .unwrap();
1244 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1245 let metadata = image.metadata().unwrap();
1246 assert_eq!(metadata.format, Format::Tiff);
1247 assert_eq!((metadata.width, metadata.height), (12, 9));
1248
1249 let decoded = image
1251 .output(Format::Raw, EncodeOptions::default())
1252 .bytes()
1253 .unwrap();
1254 let expected: Vec<u8> = (0..12_u32 * 9).map(|i| i as u8).collect();
1255 assert_eq!(decoded, expected);
1256 }
1257
1258 #[cfg(feature = "jpeg")]
1261 #[test]
1262 fn jpeg_round_trips_through_the_facade() {
1263 let (width, height) = (32_u32, 24_u32);
1264 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1265 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1266 .map(|i| {
1267 let pixel = i / 3;
1270 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1271 match i % 3 {
1272 0 => (x * 255 / width) as u8,
1273 1 => (y * 255 / height) as u8,
1274 _ => 128,
1275 }
1276 })
1277 .collect();
1278
1279 let bytes = Image::from_raw(descriptor, pixels.clone())
1280 .unwrap()
1281 .output(Format::Jpeg, EncodeOptions::with_quality(95).unwrap())
1282 .bytes()
1283 .unwrap();
1284
1285 let image = Image::from_stream(std::io::Cursor::new(bytes.clone())).unwrap();
1286 let metadata = image.metadata().unwrap();
1287 assert_eq!(metadata.format, Format::Jpeg, "sniffing missed the JPEG");
1288 assert_eq!((metadata.width, metadata.height), (width, height));
1289 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1290
1291 let decoded = image
1292 .output(Format::Raw, EncodeOptions::default())
1293 .bytes()
1294 .unwrap();
1295 assert_eq!(decoded.len(), pixels.len());
1296 let worst = decoded
1297 .iter()
1298 .zip(&pixels)
1299 .map(|(&a, &b)| a.abs_diff(b))
1300 .max()
1301 .unwrap_or(0);
1302 assert!(worst <= 12, "worst sample differs by {worst}");
1303 }
1304
1305 #[cfg(feature = "jpeg")]
1316 #[test]
1317 fn a_streaming_resize_matches_the_reference_evaluator() {
1318 for &(width, height, target) in &[
1319 (512_u32, 512_u32, 300_u32),
1320 (512, 512, 256),
1321 (256, 256, 200),
1322 (320, 240, 129),
1323 (64, 64, 16),
1324 ] {
1325 let source = jpeg_source(width, height);
1326 let scheduled = Image::from_stream(std::io::Cursor::new(source.clone()))
1327 .unwrap()
1328 .resize(target, target)
1329 .output(Format::Raw, EncodeOptions::default())
1330 .bytes()
1331 .unwrap_or_else(|e| panic!("{width}x{height} -> {target}: {e}"));
1332 let reference = Image::from_stream(std::io::Cursor::new(source))
1333 .unwrap()
1334 .resize(target, target)
1335 .output(Format::Raw, EncodeOptions::default())
1336 .bytes_via_reference()
1337 .unwrap();
1338
1339 assert_eq!(
1340 scheduled.len(),
1341 (target * target * 3) as usize,
1342 "{width}x{height} -> {target}: size"
1343 );
1344 assert_eq!(
1345 scheduled, reference,
1346 "{width}x{height} -> {target}: the scheduler and the oracle disagree"
1347 );
1348 }
1349 }
1350
1351 #[cfg(feature = "webp")]
1357 #[test]
1358 fn webp_round_trips_through_the_facade() {
1359 let bytes = ramp(20, 12)
1360 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1361 .bytes()
1362 .unwrap();
1363 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1364 let metadata = image.metadata().unwrap();
1365 assert_eq!(metadata.format, Format::WebP, "sniffing missed the WebP");
1366 assert_eq!((metadata.width, metadata.height), (20, 12));
1367 assert_eq!(metadata.pixel, PixelFormat::Rgb8);
1369
1370 let decoded = image
1371 .output(Format::Raw, EncodeOptions::default())
1372 .bytes()
1373 .unwrap();
1374 let expected: Vec<u8> = (0..20_u32 * 12)
1375 .flat_map(|i| {
1376 let value = i as u8;
1377 [value, value, value]
1378 })
1379 .collect();
1380 assert_eq!(decoded, expected, "a lossless round trip lost pixels");
1381 }
1382
1383 #[cfg(feature = "webp")]
1386 #[test]
1387 fn webp_keeps_alpha_through_the_facade() {
1388 let descriptor = ImageDescriptor::new(9, 7, PixelFormat::Rgba8).unwrap();
1389 let pixels: Vec<u8> = (0..(9 * 7))
1390 .flat_map(|i| [(i * 3) as u8, 40, 200, (i * 7) as u8])
1391 .collect();
1392 let bytes = Image::from_raw(descriptor, pixels.clone())
1393 .unwrap()
1394 .output(Format::WebP, EncodeOptions::default().with_lossless(true))
1395 .bytes()
1396 .unwrap();
1397
1398 let image = Image::from_stream(std::io::Cursor::new(bytes)).unwrap();
1399 assert_eq!(image.metadata().unwrap().pixel, PixelFormat::Rgba8);
1400 let decoded = image
1401 .output(Format::Raw, EncodeOptions::default())
1402 .bytes()
1403 .unwrap();
1404 assert_eq!(decoded, pixels);
1405 }
1406
1407 #[cfg(feature = "webp")]
1411 #[test]
1412 fn webp_is_lossy_by_default_and_keeps_alpha_exact() {
1413 let (w, h) = (48_u32, 32_u32);
1414 let descriptor = ImageDescriptor::new(w, h, PixelFormat::Rgba8).unwrap();
1415 let pixels: Vec<u8> = (0..w * h)
1416 .flat_map(|i| {
1417 let (x, y) = (i % w, i / w);
1418 [(x * 5) as u8, (y * 7) as u8, 120, (x * 3 + y) as u8]
1419 })
1420 .collect();
1421 let encode = |options: EncodeOptions| {
1422 Image::from_raw(descriptor, pixels.clone())
1423 .unwrap()
1424 .output(Format::WebP, options)
1425 .bytes()
1426 .unwrap()
1427 };
1428 let good = encode(EncodeOptions::default());
1429 let rough = encode(EncodeOptions::with_quality(10).unwrap());
1430 assert!(
1431 rough.len() < good.len(),
1432 "{} vs {}",
1433 rough.len(),
1434 good.len()
1435 );
1436
1437 let decoded = Image::from_stream(std::io::Cursor::new(good))
1438 .unwrap()
1439 .output(Format::Raw, EncodeOptions::default())
1440 .bytes()
1441 .unwrap();
1442 for (ours, source) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
1443 assert_eq!(ours[3], source[3], "alpha changed");
1444 for c in 0..3 {
1445 assert!(ours[c].abs_diff(source[c]) <= 12, "{ours:?} vs {source:?}");
1446 }
1447 }
1448 }
1449
1450 #[cfg(feature = "jpeg")]
1453 #[test]
1454 fn a_thumbnail_pipeline_shrinks_the_jpeg_on_load() {
1455 let source = jpeg_source(512, 512);
1457
1458 let image = Image::from_stream(std::io::Cursor::new(source)).unwrap();
1459 assert_eq!(
1460 image.metadata().unwrap().width,
1461 512,
1462 "metadata is full size"
1463 );
1464
1465 let mut bytes = Vec::new();
1468 let stats = image
1469 .resize(32, 32)
1470 .output(Format::Raw, EncodeOptions::default())
1471 .write_with_stats(&mut bytes)
1472 .unwrap();
1473
1474 let reduction = stats
1475 .reduction
1476 .expect("a 512 to 32 resize should shrink the source on load");
1477 assert_eq!(reduction.from, (512, 512));
1478 assert_eq!(
1479 reduction.to,
1480 (64, 64),
1481 "1/8 is the coarsest scale that still covers 32"
1482 );
1483 assert!((reduction.factor() - 64.0).abs() < 0.01);
1484 assert_eq!(bytes.len(), 32 * 32 * 3);
1486 }
1487
1488 #[cfg(feature = "jpeg")]
1491 #[test]
1492 fn a_crop_blocks_shrink_on_load() {
1493 let source = jpeg_source(512, 512);
1494 let stats = Image::from_stream(std::io::Cursor::new(source))
1495 .unwrap()
1496 .crop(256, 256, 128, 128)
1497 .resize(32, 32)
1498 .output(Format::Raw, EncodeOptions::default())
1499 .write_with_stats(&mut Vec::new())
1500 .unwrap();
1501 assert!(
1502 stats.reduction.is_none(),
1503 "a crop must block shrink-on-load: {:?}",
1504 stats.reduction
1505 );
1506 }
1507
1508 #[cfg(feature = "jpeg")]
1511 #[test]
1512 fn a_crop_after_the_resize_still_allows_shrinking() {
1513 let source = jpeg_source(512, 512);
1514 let stats = Image::from_stream(std::io::Cursor::new(source))
1515 .unwrap()
1516 .resize(64, 64)
1517 .crop(0, 0, 32, 32)
1518 .output(Format::Raw, EncodeOptions::default())
1519 .write_with_stats(&mut Vec::new())
1520 .unwrap();
1521 assert!(stats.reduction.is_none());
1525 }
1526
1527 #[cfg(feature = "jpeg")]
1530 #[test]
1531 fn a_mild_resize_shrinks_by_less_or_not_at_all() {
1532 let source = jpeg_source(512, 512);
1533 let stats = Image::from_stream(std::io::Cursor::new(source.clone()))
1535 .unwrap()
1536 .resize(300, 300)
1537 .output(Format::Raw, EncodeOptions::default())
1538 .write_with_stats(&mut Vec::new())
1539 .unwrap();
1540 assert!(stats.reduction.is_none(), "{:?}", stats.reduction);
1541
1542 let stats = Image::from_stream(std::io::Cursor::new(source))
1544 .unwrap()
1545 .resize(200, 200)
1546 .output(Format::Raw, EncodeOptions::default())
1547 .write_with_stats(&mut Vec::new())
1548 .unwrap();
1549 assert_eq!(stats.reduction.map(|r| r.to), Some((256, 256)));
1550 }
1551
1552 #[cfg(feature = "jpeg")]
1554 fn jpeg_source(width: u32, height: u32) -> Vec<u8> {
1555 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgb8).unwrap();
1556 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1557 .map(|i| {
1558 let pixel = i / 3;
1559 let (x, y) = (pixel as u32 % width, pixel as u32 / width);
1560 match i % 3 {
1561 0 => (x * 255 / width) as u8,
1562 1 => (y * 255 / height) as u8,
1563 _ => 96,
1564 }
1565 })
1566 .collect();
1567 Image::from_raw(descriptor, pixels)
1568 .unwrap()
1569 .output(Format::Jpeg, EncodeOptions::with_quality(85).unwrap())
1570 .bytes()
1571 .unwrap()
1572 }
1573
1574 #[cfg(feature = "jpeg")]
1576 #[test]
1577 fn a_jpeg_pipeline_resizes_and_re_encodes() {
1578 let descriptor = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
1579 let pixels: Vec<u8> = (0..descriptor.byte_len().unwrap())
1580 .map(|i| ((i / 3) % 251) as u8)
1581 .collect();
1582 let source = Image::from_raw(descriptor, pixels)
1583 .unwrap()
1584 .output(Format::Jpeg, EncodeOptions::default())
1585 .bytes()
1586 .unwrap();
1587
1588 let thumbnail = Image::from_stream(std::io::Cursor::new(source.clone()))
1589 .unwrap()
1590 .resize(16, 16)
1591 .output(Format::Jpeg, EncodeOptions::with_quality(70).unwrap())
1592 .bytes()
1593 .unwrap();
1594
1595 let metadata = Image::from_stream(std::io::Cursor::new(thumbnail.clone()))
1596 .unwrap()
1597 .metadata()
1598 .unwrap();
1599 assert_eq!((metadata.width, metadata.height), (16, 16));
1600 assert!(
1601 thumbnail.len() < source.len(),
1602 "a 16x16 thumbnail is {} bytes against a 64x64 source's {}",
1603 thumbnail.len(),
1604 source.len()
1605 );
1606 }
1607
1608 #[cfg(feature = "jpeg")]
1611 #[test]
1612 fn grayscale_jpeg_keeps_one_channel_through_the_facade() {
1613 let bytes = ramp(24, 16)
1614 .output(Format::Jpeg, EncodeOptions::with_quality(90).unwrap())
1615 .bytes()
1616 .unwrap();
1617 let metadata = Image::from_stream(std::io::Cursor::new(bytes))
1618 .unwrap()
1619 .metadata()
1620 .unwrap();
1621 assert_eq!(metadata.pixel, PixelFormat::Gray8);
1622 assert_eq!((metadata.width, metadata.height), (24, 16));
1623 }
1624
1625 #[test]
1626 fn write_streams_into_any_sink() {
1627 let mut sink = Vec::new();
1628 ramp(2, 2)
1629 .output(Format::Raw, EncodeOptions::default())
1630 .write(&mut sink)
1631 .unwrap();
1632 assert_eq!(sink, [0, 1, 2, 3]);
1633 }
1634
1635 #[test]
1636 fn output_reports_its_settings() {
1637 let options = EncodeOptions::with_quality(55).unwrap();
1638 let output = ramp(2, 2).output(Format::Raw, options);
1639 assert_eq!(output.format(), Format::Raw);
1640 assert_eq!(output.options().quality, 55);
1641 }
1642
1643 #[cfg(feature = "raw")]
1644 #[test]
1645 fn from_raw_stream_defers_decoding_to_the_terminal() {
1646 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
1647 let layout = RawFormat::packed(descriptor);
1648 let cursor = std::io::Cursor::new(vec![1_u8, 2, 3, 4]);
1649 let image = Image::from_raw_stream(layout, cursor).unwrap();
1650 assert_eq!(image.metadata().unwrap().width, 2);
1652 let bytes = image
1653 .output(Format::Raw, EncodeOptions::default())
1654 .bytes()
1655 .unwrap();
1656 assert_eq!(bytes, [1, 2, 3, 4]);
1657 }
1658
1659 #[cfg(feature = "raw")]
1660 #[test]
1661 fn a_truncated_stream_fails_the_terminal_without_panicking() {
1662 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
1663 let layout = RawFormat::packed(descriptor);
1664 let cursor = std::io::Cursor::new(vec![1_u8; 5]);
1665 let image = Image::from_raw_stream(layout, cursor).unwrap();
1666 let err = image
1667 .output(Format::Raw, EncodeOptions::default())
1668 .bytes()
1669 .unwrap_err();
1670 assert_eq!(err.code(), ErrorCode::Malformed);
1671 }
1672
1673 #[test]
1674 fn images_are_send_sync_and_cheap_to_clone() {
1675 const fn assert_send_sync<T: Send + Sync>() {}
1676 assert_send_sync::<Image>();
1677 assert_send_sync::<Output>();
1678 let image = ramp(4, 4);
1679 let clone = image.clone();
1680 assert_eq!(
1681 image.metadata().unwrap().width,
1682 clone.metadata().unwrap().width
1683 );
1684 }
1685
1686 #[cfg(feature = "png")]
1687 #[test]
1688 fn a_png_round_trips_through_the_facade() {
1689 let png = ramp(37, 21)
1690 .output(Format::Png, EncodeOptions::default())
1691 .bytes()
1692 .unwrap();
1693 let image = Image::from_stream(std::io::Cursor::new(png)).unwrap();
1694 assert_eq!(image.descriptor().unwrap().width, 37);
1695 assert_eq!(image.metadata().unwrap().format, Format::Png);
1696
1697 let back = image
1698 .output(Format::Raw, EncodeOptions::default())
1699 .bytes()
1700 .unwrap();
1701 let expected = ramp(37, 21)
1702 .output(Format::Raw, EncodeOptions::default())
1703 .bytes()
1704 .unwrap();
1705 assert_eq!(back, expected, "pixels changed across a PNG round trip");
1706 }
1707
1708 #[cfg(feature = "png")]
1709 #[test]
1710 fn a_pipeline_survives_a_png_round_trip() {
1711 let png = ramp(16, 16)
1714 .output(Format::Png, EncodeOptions::default())
1715 .bytes()
1716 .unwrap();
1717 let cropped = Image::from_stream(std::io::Cursor::new(png))
1718 .unwrap()
1719 .crop(2, 3, 8, 5)
1720 .flip()
1721 .output(Format::Raw, EncodeOptions::default())
1722 .bytes()
1723 .unwrap();
1724 let direct = ramp(16, 16)
1725 .crop(2, 3, 8, 5)
1726 .flip()
1727 .output(Format::Raw, EncodeOptions::default())
1728 .bytes()
1729 .unwrap();
1730 assert_eq!(cropped, direct);
1731 }
1732
1733 #[cfg(feature = "png")]
1734 #[test]
1735 fn open_ignores_the_extension_and_reads_the_bytes() {
1736 let dir = std::env::temp_dir().join(format!("otf-pixels-open-{}", std::process::id()));
1737 std::fs::create_dir_all(&dir).unwrap();
1738 let path = dir.join("actually-a-png.jpg");
1739 let png = ramp(8, 8)
1740 .output(Format::Png, EncodeOptions::default())
1741 .bytes()
1742 .unwrap();
1743 std::fs::write(&path, &png).unwrap();
1744
1745 let image = Image::open(&path).unwrap();
1746 assert_eq!(
1747 image.metadata().unwrap().format,
1748 Format::Png,
1749 "extension won over content"
1750 );
1751 std::fs::remove_dir_all(&dir).ok();
1752 }
1753
1754 #[test]
1755 fn open_names_the_file_it_could_not_read() {
1756 let error = Image::open("/nonexistent/otf-pixels/missing.png").unwrap_err();
1757 assert_eq!(error.code(), ErrorCode::Io);
1758 assert!(error.to_string().contains("missing.png"), "{error}");
1759 }
1760
1761 #[test]
1762 fn an_unrecognised_stream_is_unsupported_not_a_guess() {
1763 for bytes in [&b""[..], &b"not an image"[..], &[0_u8; 64][..]] {
1764 let error = Image::from_stream(std::io::Cursor::new(bytes.to_vec())).unwrap_err();
1765 assert_eq!(error.code(), ErrorCode::Unsupported, "{bytes:02x?}");
1766 }
1767 }
1768
1769 #[cfg(feature = "png")]
1770 #[test]
1771 fn a_truncated_png_is_malformed_not_a_panic() {
1772 let png = ramp(8, 8)
1773 .output(Format::Png, EncodeOptions::default())
1774 .bytes()
1775 .unwrap();
1776 for cut in [9, 16, 24, 32, png.len() - 1] {
1779 let truncated = png[..cut].to_vec();
1780 let result = Image::from_stream(std::io::Cursor::new(truncated))
1781 .and_then(|i| i.output(Format::Raw, EncodeOptions::default()).bytes());
1782 assert!(result.is_err(), "truncating to {cut} bytes should fail");
1783 }
1784 }
1785
1786 #[cfg(feature = "png")]
1787 #[test]
1788 fn sniffing_reads_only_the_magic_bytes_before_deciding() {
1789 let error = Image::from_stream(std::io::Cursor::new(
1793 otf_pixels_codec_png::SIGNATURE.to_vec(),
1794 ))
1795 .unwrap_err();
1796 assert_eq!(error.code(), ErrorCode::Malformed, "{error}");
1797 }
1798}
1799
1800const _: () = {
1803 const fn shareable<T: Send + Sync>() {}
1804 shareable::<Image>();
1805 shareable::<Output>();
1806 shareable::<OpenOptions>();
1807 shareable::<PixelsError>();
1808 shareable::<Scheduler>();
1809};