1pub use crate::entropy_coding::Lz77Method;
23pub use enough::{Stop, Unstoppable};
24pub use whereat::{At, ResultAtExt, at};
25
26#[derive(Debug)]
30#[non_exhaustive]
31pub enum EncodeError {
32 InvalidInput { message: String },
34 InvalidConfig { message: String },
36 UnsupportedPixelLayout(PixelLayout),
38 LimitExceeded { message: String },
40 Cancelled,
42 Oom(std::collections::TryReserveError),
44 #[cfg(feature = "std")]
46 Io(std::io::Error),
47 Internal { message: String },
49}
50
51impl core::fmt::Display for EncodeError {
52 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53 match self {
54 Self::InvalidInput { message } => write!(f, "invalid input: {message}"),
55 Self::InvalidConfig { message } => write!(f, "invalid config: {message}"),
56 Self::UnsupportedPixelLayout(layout) => {
57 write!(f, "unsupported pixel layout: {layout:?}")
58 }
59 Self::LimitExceeded { message } => write!(f, "limit exceeded: {message}"),
60 Self::Cancelled => write!(f, "encoding cancelled"),
61 Self::Oom(e) => write!(f, "out of memory: {e}"),
62 #[cfg(feature = "std")]
63 Self::Io(e) => write!(f, "I/O error: {e}"),
64 Self::Internal { message } => write!(f, "internal error: {message}"),
65 }
66 }
67}
68
69impl core::error::Error for EncodeError {
70 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
71 match self {
72 Self::Oom(e) => Some(e),
73 #[cfg(feature = "std")]
74 Self::Io(e) => Some(e),
75 _ => None,
76 }
77 }
78}
79
80impl From<crate::error::Error> for EncodeError {
81 fn from(e: crate::error::Error) -> Self {
82 match e {
83 crate::error::Error::InvalidImageDimensions(w, h) => Self::InvalidInput {
84 message: format!("invalid dimensions: {w}x{h}"),
85 },
86 crate::error::Error::ImageTooLarge(w, h, mw, mh) => Self::LimitExceeded {
87 message: format!("image {w}x{h} exceeds max {mw}x{mh}"),
88 },
89 crate::error::Error::InvalidInput(msg) => Self::InvalidInput { message: msg },
90 crate::error::Error::OutOfMemory(e) => Self::Oom(e),
91 #[cfg(feature = "std")]
92 crate::error::Error::IoError(e) => Self::Io(e),
93 crate::error::Error::Cancelled => Self::Cancelled,
94 other => Self::Internal {
95 message: format!("{other}"),
96 },
97 }
98 }
99}
100
101#[cfg(feature = "std")]
102impl From<std::io::Error> for EncodeError {
103 fn from(e: std::io::Error) -> Self {
104 Self::Io(e)
105 }
106}
107
108impl From<enough::StopReason> for EncodeError {
109 fn from(_: enough::StopReason) -> Self {
110 Self::Cancelled
111 }
112}
113
114pub type Result<T> = core::result::Result<T, At<EncodeError>>;
119
120#[derive(Clone, Debug)]
128pub struct EncodeResult {
129 data: Option<Vec<u8>>,
130 stats: EncodeStats,
131}
132
133impl EncodeResult {
134 pub fn data(&self) -> Option<&[u8]> {
136 self.data.as_deref()
137 }
138
139 pub fn take_data(&mut self) -> Option<Vec<u8>> {
141 self.data.take()
142 }
143
144 pub fn stats(&self) -> &EncodeStats {
146 &self.stats
147 }
148}
149
150#[derive(Clone, Debug, Default)]
152#[non_exhaustive]
153pub struct EncodeStats {
154 codestream_size: usize,
155 output_size: usize,
156 mode: EncodeMode,
157 strategy_counts: [u32; 19],
159 gaborish: bool,
160 ans: bool,
161 butteraugli_iters: u32,
162 pixel_domain_loss: bool,
163}
164
165impl EncodeStats {
166 pub fn codestream_size(&self) -> usize {
168 self.codestream_size
169 }
170
171 pub fn output_size(&self) -> usize {
173 self.output_size
174 }
175
176 pub fn mode(&self) -> EncodeMode {
178 self.mode
179 }
180
181 pub fn strategy_counts(&self) -> &[u32; 19] {
183 &self.strategy_counts
184 }
185
186 pub fn gaborish(&self) -> bool {
188 self.gaborish
189 }
190
191 pub fn ans(&self) -> bool {
193 self.ans
194 }
195
196 pub fn butteraugli_iters(&self) -> u32 {
198 self.butteraugli_iters
199 }
200
201 pub fn pixel_domain_loss(&self) -> bool {
203 self.pixel_domain_loss
204 }
205}
206
207#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209pub enum EncodeMode {
210 #[default]
212 Lossy,
213 Lossless,
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221#[non_exhaustive]
222pub enum PixelLayout {
223 Rgb8,
225 Rgba8,
227 Bgr8,
229 Bgra8,
231 Gray8,
233 GrayAlpha8,
235 Rgb16,
237 Rgba16,
239 Gray16,
241 RgbLinearF32,
243}
244
245impl PixelLayout {
246 pub const fn bytes_per_pixel(self) -> usize {
248 match self {
249 Self::Rgb8 | Self::Bgr8 => 3,
250 Self::Rgba8 | Self::Bgra8 => 4,
251 Self::Gray8 => 1,
252 Self::GrayAlpha8 => 2,
253 Self::Rgb16 => 6,
254 Self::Rgba16 => 8,
255 Self::Gray16 => 2,
256 Self::RgbLinearF32 => 12,
257 }
258 }
259
260 pub const fn is_linear(self) -> bool {
262 matches!(self, Self::RgbLinearF32)
263 }
264
265 pub const fn is_16bit(self) -> bool {
267 matches!(self, Self::Rgb16 | Self::Rgba16 | Self::Gray16)
268 }
269
270 pub const fn has_alpha(self) -> bool {
272 matches!(
273 self,
274 Self::Rgba8 | Self::Bgra8 | Self::GrayAlpha8 | Self::Rgba16
275 )
276 }
277
278 pub const fn is_grayscale(self) -> bool {
280 matches!(self, Self::Gray8 | Self::GrayAlpha8 | Self::Gray16)
281 }
282}
283
284#[derive(Clone, Copy, Debug)]
288#[non_exhaustive]
289pub enum Quality {
290 Distance(f32),
292 Percent(u32),
294}
295
296impl Quality {
297 fn to_distance(self) -> core::result::Result<f32, EncodeError> {
299 match self {
300 Self::Distance(d) => {
301 if d <= 0.0 {
302 return Err(EncodeError::InvalidConfig {
303 message: format!("lossy distance must be > 0.0, got {d}"),
304 });
305 }
306 Ok(d)
307 }
308 Self::Percent(q) => {
309 if q >= 100 {
310 return Err(EncodeError::InvalidConfig {
311 message: "quality 100 is lossless; use LosslessConfig instead".into(),
312 });
313 }
314 Ok(percent_to_distance(q))
315 }
316 }
317 }
318}
319
320fn percent_to_distance(quality: u32) -> f32 {
321 if quality >= 100 {
322 0.0
323 } else if quality >= 90 {
324 (100 - quality) as f32 / 10.0
325 } else if quality >= 70 {
326 1.0 + (90 - quality) as f32 / 20.0
327 } else {
328 2.0 + (70 - quality) as f32 / 10.0
329 }
330}
331
332#[derive(Clone, Debug, Default)]
336pub struct ImageMetadata<'a> {
337 icc_profile: Option<&'a [u8]>,
338 exif: Option<&'a [u8]>,
339 xmp: Option<&'a [u8]>,
340}
341
342impl<'a> ImageMetadata<'a> {
343 pub fn new() -> Self {
345 Self::default()
346 }
347
348 pub fn with_icc_profile(mut self, data: &'a [u8]) -> Self {
350 self.icc_profile = Some(data);
351 self
352 }
353
354 pub fn with_exif(mut self, data: &'a [u8]) -> Self {
356 self.exif = Some(data);
357 self
358 }
359
360 pub fn with_xmp(mut self, data: &'a [u8]) -> Self {
362 self.xmp = Some(data);
363 self
364 }
365
366 pub fn icc_profile(&self) -> Option<&[u8]> {
368 self.icc_profile
369 }
370
371 pub fn exif(&self) -> Option<&[u8]> {
373 self.exif
374 }
375
376 pub fn xmp(&self) -> Option<&[u8]> {
378 self.xmp
379 }
380}
381
382#[derive(Clone, Debug, Default)]
384pub struct Limits {
385 max_width: Option<u64>,
386 max_height: Option<u64>,
387 max_pixels: Option<u64>,
388 max_memory_bytes: Option<u64>,
389}
390
391impl Limits {
392 pub fn new() -> Self {
394 Self::default()
395 }
396
397 pub fn with_max_width(mut self, w: u64) -> Self {
399 self.max_width = Some(w);
400 self
401 }
402
403 pub fn with_max_height(mut self, h: u64) -> Self {
405 self.max_height = Some(h);
406 self
407 }
408
409 pub fn with_max_pixels(mut self, p: u64) -> Self {
411 self.max_pixels = Some(p);
412 self
413 }
414
415 pub fn with_max_memory_bytes(mut self, bytes: u64) -> Self {
417 self.max_memory_bytes = Some(bytes);
418 self
419 }
420
421 pub fn max_width(&self) -> Option<u64> {
423 self.max_width
424 }
425
426 pub fn max_height(&self) -> Option<u64> {
428 self.max_height
429 }
430
431 pub fn max_pixels(&self) -> Option<u64> {
433 self.max_pixels
434 }
435
436 pub fn max_memory_bytes(&self) -> Option<u64> {
438 self.max_memory_bytes
439 }
440}
441
442#[derive(Clone, Debug)]
446pub struct AnimationParams {
447 pub tps_numerator: u32,
449 pub tps_denominator: u32,
451 pub num_loops: u32,
453}
454
455impl Default for AnimationParams {
456 fn default() -> Self {
457 Self {
458 tps_numerator: 100,
459 tps_denominator: 1,
460 num_loops: 0,
461 }
462 }
463}
464
465pub struct AnimationFrame<'a> {
467 pub pixels: &'a [u8],
469 pub duration: u32,
471}
472
473#[derive(Clone, Debug)]
479pub struct LosslessConfig {
480 effort: u8,
481 use_ans: bool,
482 squeeze: bool,
483 tree_learning: bool,
484 lz77: bool,
485 lz77_method: Lz77Method,
486}
487
488impl Default for LosslessConfig {
489 fn default() -> Self {
490 Self {
491 effort: 7,
492 use_ans: true,
493 squeeze: false,
494 tree_learning: false,
495 lz77: false,
496 lz77_method: Lz77Method::Greedy,
497 }
498 }
499}
500
501impl LosslessConfig {
502 pub fn new() -> Self {
504 Self::default()
505 }
506
507 pub fn with_effort(mut self, effort: u8) -> Self {
509 self.effort = effort;
510 self
511 }
512
513 pub fn with_ans(mut self, enable: bool) -> Self {
515 self.use_ans = enable;
516 self
517 }
518
519 pub fn with_squeeze(mut self, enable: bool) -> Self {
521 self.squeeze = enable;
522 self
523 }
524
525 pub fn with_tree_learning(mut self, enable: bool) -> Self {
527 self.tree_learning = enable;
528 self
529 }
530
531 pub fn with_lz77(mut self, enable: bool) -> Self {
533 self.lz77 = enable;
534 self
535 }
536
537 pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
539 self.lz77_method = method;
540 self
541 }
542
543 pub fn effort(&self) -> u8 {
547 self.effort
548 }
549
550 pub fn ans(&self) -> bool {
552 self.use_ans
553 }
554
555 pub fn squeeze(&self) -> bool {
557 self.squeeze
558 }
559
560 pub fn tree_learning(&self) -> bool {
562 self.tree_learning
563 }
564
565 pub fn lz77(&self) -> bool {
567 self.lz77
568 }
569
570 pub fn lz77_method(&self) -> Lz77Method {
572 self.lz77_method
573 }
574
575 pub fn encode_request(
581 &self,
582 width: u32,
583 height: u32,
584 layout: PixelLayout,
585 ) -> EncodeRequest<'_> {
586 EncodeRequest {
587 config: ConfigRef::Lossless(self),
588 width,
589 height,
590 layout,
591 metadata: None,
592 limits: None,
593 stop: None,
594 }
595 }
596
597 #[track_caller]
606 pub fn encode(
607 &self,
608 pixels: &[u8],
609 width: u32,
610 height: u32,
611 layout: PixelLayout,
612 ) -> Result<Vec<u8>> {
613 self.encode_request(width, height, layout).encode(pixels)
614 }
615
616 #[track_caller]
618 pub fn encode_into(
619 &self,
620 pixels: &[u8],
621 width: u32,
622 height: u32,
623 layout: PixelLayout,
624 out: &mut Vec<u8>,
625 ) -> Result<()> {
626 self.encode_request(width, height, layout)
627 .encode_into(pixels, out)
628 .map(|_| ())
629 }
630
631 #[track_caller]
636 pub fn encode_animation(
637 &self,
638 width: u32,
639 height: u32,
640 layout: PixelLayout,
641 animation: &AnimationParams,
642 frames: &[AnimationFrame<'_>],
643 ) -> Result<Vec<u8>> {
644 encode_animation_lossless(self, width, height, layout, animation, frames).map_err(at)
645 }
646}
647
648#[cfg(feature = "butteraugli-loop")]
651fn butteraugli_iters_for_effort(effort: u8) -> u32 {
652 match effort {
653 0..=7 => 0,
654 8 => 2,
655 _ => 4,
656 }
657}
658
659#[derive(Clone, Debug)]
663pub struct LossyConfig {
664 distance: f32,
665 effort: u8,
666 use_ans: bool,
667 gaborish: bool,
668 noise: bool,
669 denoise: bool,
670 error_diffusion: bool,
671 pixel_domain_loss: bool,
672 lz77: bool,
673 lz77_method: Lz77Method,
674 force_strategy: Option<u8>,
675 #[cfg(feature = "butteraugli-loop")]
676 butteraugli_iters: u32,
677 #[cfg(feature = "butteraugli-loop")]
678 butteraugli_iters_explicit: bool,
679}
680
681impl LossyConfig {
682 pub fn new(distance: f32) -> Self {
684 let effort = 7;
685 Self {
686 distance,
687 effort,
688 use_ans: true,
689 gaborish: true,
690 noise: false,
691 denoise: false,
692 error_diffusion: true,
693 pixel_domain_loss: true,
694 lz77: false,
695 lz77_method: Lz77Method::Greedy,
696 force_strategy: None,
697 #[cfg(feature = "butteraugli-loop")]
698 butteraugli_iters: butteraugli_iters_for_effort(effort),
699 #[cfg(feature = "butteraugli-loop")]
700 butteraugli_iters_explicit: false,
701 }
702 }
703
704 pub fn from_quality(quality: Quality) -> core::result::Result<Self, EncodeError> {
706 let distance = quality.to_distance()?;
707 Ok(Self::new(distance))
708 }
709
710 pub fn with_effort(mut self, effort: u8) -> Self {
715 self.effort = effort;
716 #[cfg(feature = "butteraugli-loop")]
717 if !self.butteraugli_iters_explicit {
718 self.butteraugli_iters = butteraugli_iters_for_effort(effort);
719 }
720 self
721 }
722
723 pub fn with_ans(mut self, enable: bool) -> Self {
725 self.use_ans = enable;
726 self
727 }
728
729 pub fn with_gaborish(mut self, enable: bool) -> Self {
731 self.gaborish = enable;
732 self
733 }
734
735 pub fn with_noise(mut self, enable: bool) -> Self {
737 self.noise = enable;
738 self
739 }
740
741 pub fn with_denoise(mut self, enable: bool) -> Self {
743 self.denoise = enable;
744 if enable {
745 self.noise = true;
746 }
747 self
748 }
749
750 pub fn with_error_diffusion(mut self, enable: bool) -> Self {
752 self.error_diffusion = enable;
753 self
754 }
755
756 pub fn with_pixel_domain_loss(mut self, enable: bool) -> Self {
758 self.pixel_domain_loss = enable;
759 self
760 }
761
762 pub fn with_lz77(mut self, enable: bool) -> Self {
764 self.lz77 = enable;
765 self
766 }
767
768 pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
770 self.lz77_method = method;
771 self
772 }
773
774 pub fn with_force_strategy(mut self, strategy: Option<u8>) -> Self {
776 self.force_strategy = strategy;
777 self
778 }
779
780 #[cfg(feature = "butteraugli-loop")]
785 pub fn with_butteraugli_iters(mut self, n: u32) -> Self {
786 self.butteraugli_iters = n;
787 self.butteraugli_iters_explicit = true;
788 self
789 }
790
791 pub fn distance(&self) -> f32 {
795 self.distance
796 }
797
798 pub fn effort(&self) -> u8 {
800 self.effort
801 }
802
803 pub fn ans(&self) -> bool {
805 self.use_ans
806 }
807
808 pub fn gaborish(&self) -> bool {
810 self.gaborish
811 }
812
813 pub fn noise(&self) -> bool {
815 self.noise
816 }
817
818 pub fn denoise(&self) -> bool {
820 self.denoise
821 }
822
823 pub fn error_diffusion(&self) -> bool {
825 self.error_diffusion
826 }
827
828 pub fn pixel_domain_loss(&self) -> bool {
830 self.pixel_domain_loss
831 }
832
833 pub fn lz77(&self) -> bool {
835 self.lz77
836 }
837
838 pub fn lz77_method(&self) -> Lz77Method {
840 self.lz77_method
841 }
842
843 pub fn force_strategy(&self) -> Option<u8> {
845 self.force_strategy
846 }
847
848 #[cfg(feature = "butteraugli-loop")]
850 pub fn butteraugli_iters(&self) -> u32 {
851 self.butteraugli_iters
852 }
853
854 pub fn encode_request(
860 &self,
861 width: u32,
862 height: u32,
863 layout: PixelLayout,
864 ) -> EncodeRequest<'_> {
865 EncodeRequest {
866 config: ConfigRef::Lossy(self),
867 width,
868 height,
869 layout,
870 metadata: None,
871 limits: None,
872 stop: None,
873 }
874 }
875
876 #[track_caller]
885 pub fn encode(
886 &self,
887 pixels: &[u8],
888 width: u32,
889 height: u32,
890 layout: PixelLayout,
891 ) -> Result<Vec<u8>> {
892 self.encode_request(width, height, layout).encode(pixels)
893 }
894
895 #[track_caller]
897 pub fn encode_into(
898 &self,
899 pixels: &[u8],
900 width: u32,
901 height: u32,
902 layout: PixelLayout,
903 out: &mut Vec<u8>,
904 ) -> Result<()> {
905 self.encode_request(width, height, layout)
906 .encode_into(pixels, out)
907 .map(|_| ())
908 }
909
910 #[track_caller]
915 pub fn encode_animation(
916 &self,
917 width: u32,
918 height: u32,
919 layout: PixelLayout,
920 animation: &AnimationParams,
921 frames: &[AnimationFrame<'_>],
922 ) -> Result<Vec<u8>> {
923 encode_animation_lossy(self, width, height, layout, animation, frames).map_err(at)
924 }
925}
926
927#[derive(Clone, Copy, Debug)]
931enum ConfigRef<'a> {
932 Lossless(&'a LosslessConfig),
933 Lossy(&'a LossyConfig),
934}
935
936pub struct EncodeRequest<'a> {
940 config: ConfigRef<'a>,
941 width: u32,
942 height: u32,
943 layout: PixelLayout,
944 metadata: Option<&'a ImageMetadata<'a>>,
945 limits: Option<&'a Limits>,
946 stop: Option<&'a dyn Stop>,
947}
948
949impl<'a> EncodeRequest<'a> {
950 pub fn with_metadata(mut self, meta: &'a ImageMetadata<'a>) -> Self {
952 self.metadata = Some(meta);
953 self
954 }
955
956 pub fn with_limits(mut self, limits: &'a Limits) -> Self {
958 self.limits = Some(limits);
959 self
960 }
961
962 pub fn with_stop(mut self, stop: &'a dyn Stop) -> Self {
967 self.stop = Some(stop);
968 self
969 }
970
971 #[track_caller]
973 pub fn encode(self, pixels: &[u8]) -> Result<Vec<u8>> {
974 self.encode_inner(pixels)
975 .map(|mut r| r.take_data().unwrap())
976 .map_err(at)
977 }
978
979 #[track_caller]
981 pub fn encode_with_stats(self, pixels: &[u8]) -> Result<EncodeResult> {
982 self.encode_inner(pixels).map_err(at)
983 }
984
985 #[track_caller]
987 pub fn encode_into(self, pixels: &[u8], out: &mut Vec<u8>) -> Result<EncodeResult> {
988 let mut result = self.encode_inner(pixels).map_err(at)?;
989 if let Some(data) = result.data.take() {
990 out.extend_from_slice(&data);
991 }
992 Ok(result)
993 }
994
995 #[cfg(feature = "std")]
997 #[track_caller]
998 pub fn encode_to(self, pixels: &[u8], mut dest: impl std::io::Write) -> Result<EncodeResult> {
999 let mut result = self.encode_inner(pixels).map_err(at)?;
1000 if let Some(data) = result.data.take() {
1001 dest.write_all(&data)
1002 .map_err(|e| at(EncodeError::from(e)))?;
1003 }
1004 Ok(result)
1005 }
1006
1007 fn encode_inner(&self, pixels: &[u8]) -> core::result::Result<EncodeResult, EncodeError> {
1008 self.validate_pixels(pixels)?;
1009 self.check_limits()?;
1010
1011 let (codestream, mut stats) = match self.config {
1012 ConfigRef::Lossless(cfg) => self.encode_lossless(cfg, pixels),
1013 ConfigRef::Lossy(cfg) => self.encode_lossy(cfg, pixels),
1014 }?;
1015
1016 stats.codestream_size = codestream.len();
1017
1018 let output = if let Some(meta) = self.metadata
1020 && (meta.exif.is_some() || meta.xmp.is_some())
1021 {
1022 crate::container::wrap_in_container(&codestream, meta.exif, meta.xmp)
1023 } else {
1024 codestream
1025 };
1026
1027 stats.output_size = output.len();
1028
1029 Ok(EncodeResult {
1030 data: Some(output),
1031 stats,
1032 })
1033 }
1034
1035 fn validate_pixels(&self, pixels: &[u8]) -> core::result::Result<(), EncodeError> {
1036 let w = self.width as usize;
1037 let h = self.height as usize;
1038 if w == 0 || h == 0 {
1039 return Err(EncodeError::InvalidInput {
1040 message: format!("zero dimensions: {w}x{h}"),
1041 });
1042 }
1043 let expected = w
1044 .checked_mul(h)
1045 .and_then(|n| n.checked_mul(self.layout.bytes_per_pixel()));
1046 match expected {
1047 Some(expected) if pixels.len() == expected => Ok(()),
1048 Some(expected) => Err(EncodeError::InvalidInput {
1049 message: format!(
1050 "pixel buffer size mismatch: expected {expected} bytes for {w}x{h} {:?}, got {}",
1051 self.layout,
1052 pixels.len()
1053 ),
1054 }),
1055 None => Err(EncodeError::InvalidInput {
1056 message: "image dimensions overflow".into(),
1057 }),
1058 }
1059 }
1060
1061 fn check_limits(&self) -> core::result::Result<(), EncodeError> {
1062 let Some(limits) = self.limits else {
1063 return Ok(());
1064 };
1065 let w = self.width as u64;
1066 let h = self.height as u64;
1067 if let Some(max_w) = limits.max_width
1068 && w > max_w
1069 {
1070 return Err(EncodeError::LimitExceeded {
1071 message: format!("width {w} > max {max_w}"),
1072 });
1073 }
1074 if let Some(max_h) = limits.max_height
1075 && h > max_h
1076 {
1077 return Err(EncodeError::LimitExceeded {
1078 message: format!("height {h} > max {max_h}"),
1079 });
1080 }
1081 if let Some(max_px) = limits.max_pixels
1082 && w * h > max_px
1083 {
1084 return Err(EncodeError::LimitExceeded {
1085 message: format!("pixels {}x{} = {} > max {max_px}", w, h, w * h),
1086 });
1087 }
1088 Ok(())
1089 }
1090
1091 fn encode_lossless(
1094 &self,
1095 cfg: &LosslessConfig,
1096 pixels: &[u8],
1097 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1098 use crate::bit_writer::BitWriter;
1099 use crate::headers::{ColorEncoding, FileHeader};
1100 use crate::modular::channel::ModularImage;
1101 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
1102
1103 let w = self.width as usize;
1104 let h = self.height as usize;
1105
1106 let image = match self.layout {
1108 PixelLayout::Rgb8 => ModularImage::from_rgb8(pixels, w, h),
1109 PixelLayout::Rgba8 => ModularImage::from_rgba8(pixels, w, h),
1110 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(pixels, 3), w, h),
1111 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(pixels, 4), w, h),
1112 PixelLayout::Gray8 => ModularImage::from_gray8(pixels, w, h),
1113 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(pixels, w, h),
1114 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(pixels, w, h),
1115 PixelLayout::Gray16 => ModularImage::from_gray16_native(pixels, w, h),
1116 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1117 }
1118 .map_err(EncodeError::from)?;
1119
1120 let mut file_header = if image.is_grayscale {
1122 FileHeader::new_gray(self.width, self.height)
1123 } else if image.has_alpha {
1124 FileHeader::new_rgba(self.width, self.height)
1125 } else {
1126 FileHeader::new_rgb(self.width, self.height)
1127 };
1128 if image.bit_depth == 16 {
1129 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
1130 for ec in &mut file_header.metadata.extra_channels {
1131 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
1132 }
1133 }
1134 if let Some(meta) = self.metadata
1135 && meta.icc_profile.is_some()
1136 {
1137 file_header.metadata.color_encoding.want_icc = true;
1138 }
1139
1140 let mut writer = BitWriter::new();
1142 file_header.write(&mut writer).map_err(EncodeError::from)?;
1143 if let Some(meta) = self.metadata
1144 && let Some(icc) = meta.icc_profile
1145 {
1146 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
1147 }
1148 writer.zero_pad_to_byte();
1149
1150 let frame_encoder = FrameEncoder::new(
1152 w,
1153 h,
1154 FrameEncoderOptions {
1155 use_modular: true,
1156 effort: cfg.effort,
1157 use_ans: cfg.use_ans,
1158 use_tree_learning: cfg.tree_learning,
1159 use_squeeze: cfg.squeeze,
1160 have_animation: false,
1161 duration: 0,
1162 is_last: true,
1163 crop: None,
1164 },
1165 );
1166 let color_encoding = ColorEncoding::srgb();
1167 frame_encoder
1168 .encode_modular(&image, &color_encoding, &mut writer)
1169 .map_err(EncodeError::from)?;
1170
1171 let stats = EncodeStats {
1172 mode: EncodeMode::Lossless,
1173 ans: cfg.use_ans,
1174 ..Default::default()
1175 };
1176 Ok((writer.finish_with_padding(), stats))
1177 }
1178
1179 fn encode_lossy(
1182 &self,
1183 cfg: &LossyConfig,
1184 pixels: &[u8],
1185 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1186 let w = self.width as usize;
1187 let h = self.height as usize;
1188
1189 let (linear_rgb, alpha, bit_depth_16) = match self.layout {
1191 PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(pixels, 3), None, false),
1192 PixelLayout::Bgr8 => (
1193 srgb_u8_to_linear_f32(&bgr_to_rgb(pixels, 3), 3),
1194 None,
1195 false,
1196 ),
1197 PixelLayout::Rgba8 => {
1198 let rgb = srgb_u8_to_linear_f32(pixels, 4);
1199 let alpha = extract_alpha(pixels, 4, 3);
1200 (rgb, Some(alpha), false)
1201 }
1202 PixelLayout::Bgra8 => {
1203 let swapped = bgr_to_rgb(pixels, 4);
1204 let rgb = srgb_u8_to_linear_f32(&swapped, 4);
1205 let alpha = extract_alpha(pixels, 4, 3);
1206 (rgb, Some(alpha), false)
1207 }
1208 PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(pixels, 3), None, true),
1209 PixelLayout::Rgba16 => {
1210 let rgb = srgb_u16_to_linear_f32(pixels, 4);
1211 let alpha = extract_alpha_u16(pixels, 4, 3);
1212 (rgb, Some(alpha), true)
1213 }
1214 PixelLayout::RgbLinearF32 => {
1215 let floats: &[f32] = bytemuck::cast_slice(pixels);
1216 (floats.to_vec(), None, false)
1217 }
1218 PixelLayout::Gray8 | PixelLayout::GrayAlpha8 | PixelLayout::Gray16 => {
1219 return Err(EncodeError::UnsupportedPixelLayout(self.layout));
1220 }
1221 };
1222
1223 let mut tiny = crate::vardct::VarDctEncoder::new(cfg.distance);
1224 tiny.use_ans = cfg.use_ans;
1225 tiny.optimize_codes = true;
1226 tiny.custom_orders = true;
1227 tiny.enable_noise = cfg.noise;
1228 tiny.enable_denoise = cfg.denoise;
1229 tiny.enable_gaborish = cfg.gaborish;
1230 tiny.error_diffusion = cfg.error_diffusion;
1231 tiny.pixel_domain_loss = cfg.pixel_domain_loss;
1232 tiny.enable_lz77 = cfg.lz77;
1233 tiny.lz77_method = cfg.lz77_method;
1234 tiny.force_strategy = cfg.force_strategy;
1235 #[cfg(feature = "butteraugli-loop")]
1236 {
1237 tiny.butteraugli_iters = cfg.butteraugli_iters;
1238 }
1239
1240 tiny.bit_depth_16 = bit_depth_16;
1241
1242 if let Some(meta) = self.metadata
1244 && let Some(icc) = meta.icc_profile
1245 {
1246 tiny.icc_profile = Some(icc.to_vec());
1247 }
1248
1249 let output = tiny
1250 .encode(w, h, &linear_rgb, alpha.as_deref())
1251 .map_err(EncodeError::from)?;
1252
1253 #[cfg(feature = "butteraugli-loop")]
1254 let butteraugli_iters_actual = cfg.butteraugli_iters;
1255 #[cfg(not(feature = "butteraugli-loop"))]
1256 let butteraugli_iters_actual = 0u32;
1257
1258 let stats = EncodeStats {
1259 mode: EncodeMode::Lossy,
1260 strategy_counts: output.strategy_counts,
1261 gaborish: cfg.gaborish,
1262 ans: cfg.use_ans,
1263 butteraugli_iters: butteraugli_iters_actual,
1264 pixel_domain_loss: cfg.pixel_domain_loss,
1265 ..Default::default()
1266 };
1267 Ok((output.data, stats))
1268 }
1269}
1270
1271fn validate_animation_input(
1274 width: u32,
1275 height: u32,
1276 layout: PixelLayout,
1277 frames: &[AnimationFrame<'_>],
1278) -> core::result::Result<(), EncodeError> {
1279 if width == 0 || height == 0 {
1280 return Err(EncodeError::InvalidInput {
1281 message: format!("zero dimensions: {width}x{height}"),
1282 });
1283 }
1284 if frames.is_empty() {
1285 return Err(EncodeError::InvalidInput {
1286 message: "animation requires at least one frame".into(),
1287 });
1288 }
1289 let expected_size = (width as usize)
1290 .checked_mul(height as usize)
1291 .and_then(|n| n.checked_mul(layout.bytes_per_pixel()))
1292 .ok_or_else(|| EncodeError::InvalidInput {
1293 message: "image dimensions overflow".into(),
1294 })?;
1295 for (i, frame) in frames.iter().enumerate() {
1296 if frame.pixels.len() != expected_size {
1297 return Err(EncodeError::InvalidInput {
1298 message: format!(
1299 "frame {} pixel buffer size mismatch: expected {expected_size}, got {}",
1300 i,
1301 frame.pixels.len()
1302 ),
1303 });
1304 }
1305 }
1306 Ok(())
1307}
1308
1309fn encode_animation_lossless(
1310 cfg: &LosslessConfig,
1311 width: u32,
1312 height: u32,
1313 layout: PixelLayout,
1314 animation: &AnimationParams,
1315 frames: &[AnimationFrame<'_>],
1316) -> core::result::Result<Vec<u8>, EncodeError> {
1317 use crate::bit_writer::BitWriter;
1318 use crate::headers::file_header::AnimationHeader;
1319 use crate::headers::{ColorEncoding, FileHeader};
1320 use crate::modular::channel::ModularImage;
1321 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
1322
1323 validate_animation_input(width, height, layout, frames)?;
1324
1325 let w = width as usize;
1326 let h = height as usize;
1327 let num_frames = frames.len();
1328
1329 let sample_image = match layout {
1331 PixelLayout::Rgb8 => ModularImage::from_rgb8(frames[0].pixels, w, h),
1332 PixelLayout::Rgba8 => ModularImage::from_rgba8(frames[0].pixels, w, h),
1333 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(frames[0].pixels, 3), w, h),
1334 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(frames[0].pixels, 4), w, h),
1335 PixelLayout::Gray8 => ModularImage::from_gray8(frames[0].pixels, w, h),
1336 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frames[0].pixels, w, h),
1337 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frames[0].pixels, w, h),
1338 PixelLayout::Gray16 => ModularImage::from_gray16_native(frames[0].pixels, w, h),
1339 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1340 }
1341 .map_err(EncodeError::from)?;
1342
1343 let mut file_header = if sample_image.is_grayscale {
1344 FileHeader::new_gray(width, height)
1345 } else if sample_image.has_alpha {
1346 FileHeader::new_rgba(width, height)
1347 } else {
1348 FileHeader::new_rgb(width, height)
1349 };
1350 if sample_image.bit_depth == 16 {
1351 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
1352 for ec in &mut file_header.metadata.extra_channels {
1353 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
1354 }
1355 }
1356 file_header.metadata.animation = Some(AnimationHeader {
1357 tps_numerator: animation.tps_numerator,
1358 tps_denominator: animation.tps_denominator,
1359 num_loops: animation.num_loops,
1360 have_timecodes: false,
1361 });
1362
1363 let mut writer = BitWriter::new();
1365 file_header.write(&mut writer).map_err(EncodeError::from)?;
1366 writer.zero_pad_to_byte();
1367
1368 let color_encoding = ColorEncoding::srgb();
1370 let bpp = layout.bytes_per_pixel();
1371 let mut prev_pixels: Option<&[u8]> = None;
1372
1373 for (i, frame) in frames.iter().enumerate() {
1374 let crop = if let Some(prev) = prev_pixels {
1377 match detect_frame_crop(prev, frame.pixels, w, h, bpp, false) {
1378 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
1379 Some(_) => None, None => {
1381 Some(FrameCrop {
1383 x0: 0,
1384 y0: 0,
1385 width: 1,
1386 height: 1,
1387 })
1388 }
1389 }
1390 } else {
1391 None };
1393
1394 let (frame_w, frame_h, frame_pixels_owned);
1396 let frame_pixels: &[u8] = if let Some(ref crop) = crop {
1397 frame_w = crop.width as usize;
1398 frame_h = crop.height as usize;
1399 frame_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
1400 &frame_pixels_owned
1401 } else {
1402 frame_w = w;
1403 frame_h = h;
1404 frame_pixels_owned = Vec::new();
1405 let _ = &frame_pixels_owned; frame.pixels
1407 };
1408
1409 let image = match layout {
1410 PixelLayout::Rgb8 => ModularImage::from_rgb8(frame_pixels, frame_w, frame_h),
1411 PixelLayout::Rgba8 => ModularImage::from_rgba8(frame_pixels, frame_w, frame_h),
1412 PixelLayout::Bgr8 => {
1413 ModularImage::from_rgb8(&bgr_to_rgb(frame_pixels, 3), frame_w, frame_h)
1414 }
1415 PixelLayout::Bgra8 => {
1416 ModularImage::from_rgba8(&bgr_to_rgb(frame_pixels, 4), frame_w, frame_h)
1417 }
1418 PixelLayout::Gray8 => ModularImage::from_gray8(frame_pixels, frame_w, frame_h),
1419 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frame_pixels, frame_w, frame_h),
1420 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frame_pixels, frame_w, frame_h),
1421 PixelLayout::Gray16 => ModularImage::from_gray16_native(frame_pixels, frame_w, frame_h),
1422 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1423 }
1424 .map_err(EncodeError::from)?;
1425
1426 let frame_encoder = FrameEncoder::new(
1427 frame_w,
1428 frame_h,
1429 FrameEncoderOptions {
1430 use_modular: true,
1431 effort: cfg.effort,
1432 use_ans: cfg.use_ans,
1433 use_tree_learning: cfg.tree_learning,
1434 use_squeeze: cfg.squeeze,
1435 have_animation: true,
1436 duration: frame.duration,
1437 is_last: i == num_frames - 1,
1438 crop,
1439 },
1440 );
1441 frame_encoder
1442 .encode_modular(&image, &color_encoding, &mut writer)
1443 .map_err(EncodeError::from)?;
1444
1445 prev_pixels = Some(frame.pixels);
1446 }
1447
1448 Ok(writer.finish_with_padding())
1449}
1450
1451fn encode_animation_lossy(
1452 cfg: &LossyConfig,
1453 width: u32,
1454 height: u32,
1455 layout: PixelLayout,
1456 animation: &AnimationParams,
1457 frames: &[AnimationFrame<'_>],
1458) -> core::result::Result<Vec<u8>, EncodeError> {
1459 use crate::bit_writer::BitWriter;
1460 use crate::headers::file_header::AnimationHeader;
1461 use crate::headers::frame_header::FrameOptions;
1462
1463 validate_animation_input(width, height, layout, frames)?;
1464
1465 let w = width as usize;
1466 let h = height as usize;
1467 let num_frames = frames.len();
1468
1469 let mut tiny = crate::vardct::VarDctEncoder::new(cfg.distance);
1471 tiny.use_ans = cfg.use_ans;
1472 tiny.optimize_codes = true;
1473 tiny.custom_orders = true;
1474 tiny.enable_noise = cfg.noise;
1475 tiny.enable_denoise = cfg.denoise;
1476 tiny.enable_gaborish = cfg.gaborish;
1477 tiny.error_diffusion = cfg.error_diffusion;
1478 tiny.pixel_domain_loss = cfg.pixel_domain_loss;
1479 tiny.enable_lz77 = cfg.lz77;
1480 tiny.lz77_method = cfg.lz77_method;
1481 tiny.force_strategy = cfg.force_strategy;
1482 #[cfg(feature = "butteraugli-loop")]
1483 {
1484 tiny.butteraugli_iters = cfg.butteraugli_iters;
1485 }
1486
1487 let has_alpha = layout.has_alpha();
1489 let bit_depth_16 = matches!(layout, PixelLayout::Rgb16 | PixelLayout::Rgba16);
1490 tiny.bit_depth_16 = bit_depth_16;
1491
1492 let mut file_header = tiny.build_file_header(w, h, has_alpha);
1495 file_header.metadata.animation = Some(AnimationHeader {
1496 tps_numerator: animation.tps_numerator,
1497 tps_denominator: animation.tps_denominator,
1498 num_loops: animation.num_loops,
1499 have_timecodes: false,
1500 });
1501
1502 let mut writer = BitWriter::with_capacity(w * h * 4);
1503 file_header.write(&mut writer).map_err(EncodeError::from)?;
1504 if let Some(ref icc) = tiny.icc_profile {
1505 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
1506 }
1507 writer.zero_pad_to_byte();
1508
1509 let bpp = layout.bytes_per_pixel();
1511 let mut prev_pixels: Option<&[u8]> = None;
1512
1513 for (i, frame) in frames.iter().enumerate() {
1514 let crop = if let Some(prev) = prev_pixels {
1517 match detect_frame_crop(prev, frame.pixels, w, h, bpp, true) {
1518 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
1519 Some(_) => None, None => {
1521 Some(FrameCrop {
1523 x0: 0,
1524 y0: 0,
1525 width: 8.min(width),
1526 height: 8.min(height),
1527 })
1528 }
1529 }
1530 } else {
1531 None };
1533
1534 let (frame_w, frame_h) = if let Some(ref crop) = crop {
1536 (crop.width as usize, crop.height as usize)
1537 } else {
1538 (w, h)
1539 };
1540
1541 let crop_pixels_owned;
1542 let src_pixels: &[u8] = if let Some(ref crop) = crop {
1543 crop_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
1544 &crop_pixels_owned
1545 } else {
1546 crop_pixels_owned = Vec::new();
1547 let _ = &crop_pixels_owned;
1548 frame.pixels
1549 };
1550
1551 let (linear_rgb, alpha) = match layout {
1552 PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(src_pixels, 3), None),
1553 PixelLayout::Bgr8 => (srgb_u8_to_linear_f32(&bgr_to_rgb(src_pixels, 3), 3), None),
1554 PixelLayout::Rgba8 => {
1555 let rgb = srgb_u8_to_linear_f32(src_pixels, 4);
1556 let alpha = extract_alpha(src_pixels, 4, 3);
1557 (rgb, Some(alpha))
1558 }
1559 PixelLayout::Bgra8 => {
1560 let swapped = bgr_to_rgb(src_pixels, 4);
1561 let rgb = srgb_u8_to_linear_f32(&swapped, 4);
1562 let alpha = extract_alpha(src_pixels, 4, 3);
1563 (rgb, Some(alpha))
1564 }
1565 PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(src_pixels, 3), None),
1566 PixelLayout::Rgba16 => {
1567 let rgb = srgb_u16_to_linear_f32(src_pixels, 4);
1568 let alpha = extract_alpha_u16(src_pixels, 4, 3);
1569 (rgb, Some(alpha))
1570 }
1571 PixelLayout::RgbLinearF32 => {
1572 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
1573 (floats.to_vec(), None)
1574 }
1575 PixelLayout::Gray8 | PixelLayout::GrayAlpha8 | PixelLayout::Gray16 => {
1576 return Err(EncodeError::UnsupportedPixelLayout(layout));
1577 }
1578 };
1579
1580 let frame_options = FrameOptions {
1581 have_animation: true,
1582 have_timecodes: false,
1583 duration: frame.duration,
1584 is_last: i == num_frames - 1,
1585 crop,
1586 };
1587
1588 tiny.encode_frame_to_writer(
1589 frame_w,
1590 frame_h,
1591 &linear_rgb,
1592 alpha.as_deref(),
1593 &frame_options,
1594 &mut writer,
1595 )
1596 .map_err(EncodeError::from)?;
1597
1598 prev_pixels = Some(frame.pixels);
1599 }
1600
1601 Ok(writer.finish_with_padding())
1602}
1603
1604use crate::headers::frame_header::FrameCrop;
1607
1608fn detect_frame_crop(
1616 prev: &[u8],
1617 curr: &[u8],
1618 width: usize,
1619 height: usize,
1620 bytes_per_pixel: usize,
1621 align_to_8x8: bool,
1622) -> Option<FrameCrop> {
1623 let stride = width * bytes_per_pixel;
1624 debug_assert_eq!(prev.len(), height * stride);
1625 debug_assert_eq!(curr.len(), height * stride);
1626
1627 let mut top = height;
1629 let mut bottom = 0;
1630 let mut left = width;
1631 let mut right = 0;
1632
1633 for y in 0..height {
1634 let row_start = y * stride;
1635 let prev_row = &prev[row_start..row_start + stride];
1636 let curr_row = &curr[row_start..row_start + stride];
1637
1638 let (prev_prefix, prev_u64, prev_suffix) = bytemuck::pod_align_to::<u8, u64>(prev_row);
1640 let (curr_prefix, curr_u64, curr_suffix) = bytemuck::pod_align_to::<u8, u64>(curr_row);
1641 if prev_prefix == curr_prefix && prev_u64 == curr_u64 && prev_suffix == curr_suffix {
1642 continue;
1643 }
1644
1645 if top == height {
1647 top = y;
1648 }
1649 bottom = y;
1650
1651 for x in 0..width {
1653 let px_start = x * bytes_per_pixel;
1654 if prev_row[px_start..px_start + bytes_per_pixel]
1655 != curr_row[px_start..px_start + bytes_per_pixel]
1656 {
1657 left = left.min(x);
1658 break;
1659 }
1660 }
1661 for x in (0..width).rev() {
1663 let px_start = x * bytes_per_pixel;
1664 if prev_row[px_start..px_start + bytes_per_pixel]
1665 != curr_row[px_start..px_start + bytes_per_pixel]
1666 {
1667 right = right.max(x);
1668 break;
1669 }
1670 }
1671 }
1672
1673 if top == height {
1674 return None;
1676 }
1677
1678 let mut crop_x = left as i32;
1680 let mut crop_y = top as i32;
1681 let mut crop_w = (right - left + 1) as u32;
1682 let mut crop_h = (bottom - top + 1) as u32;
1683
1684 if align_to_8x8 {
1685 let aligned_x = (crop_x / 8) * 8;
1687 let aligned_y = (crop_y / 8) * 8;
1688 let end_x = (crop_x as u32 + crop_w).div_ceil(8) * 8;
1689 let end_y = (crop_y as u32 + crop_h).div_ceil(8) * 8;
1690 crop_x = aligned_x;
1691 crop_y = aligned_y;
1692 crop_w = end_x.min(width as u32) - aligned_x as u32;
1693 crop_h = end_y.min(height as u32) - aligned_y as u32;
1694 }
1695
1696 Some(FrameCrop {
1697 x0: crop_x,
1698 y0: crop_y,
1699 width: crop_w,
1700 height: crop_h,
1701 })
1702}
1703
1704fn extract_pixel_crop(
1708 pixels: &[u8],
1709 full_width: usize,
1710 crop: &FrameCrop,
1711 bytes_per_pixel: usize,
1712) -> Vec<u8> {
1713 let cx = crop.x0 as usize;
1714 let cy = crop.y0 as usize;
1715 let cw = crop.width as usize;
1716 let ch = crop.height as usize;
1717 let stride = full_width * bytes_per_pixel;
1718
1719 let mut out = Vec::with_capacity(cw * ch * bytes_per_pixel);
1720 for y in cy..cy + ch {
1721 let row_start = y * stride + cx * bytes_per_pixel;
1722 out.extend_from_slice(&pixels[row_start..row_start + cw * bytes_per_pixel]);
1723 }
1724 out
1725}
1726
1727#[inline]
1731fn srgb_to_linear(c: u8) -> f32 {
1732 srgb_to_linear_f(c as f32 / 255.0)
1733}
1734
1735fn srgb_u8_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
1736 data.chunks(channels)
1737 .flat_map(|px| {
1738 [
1739 srgb_to_linear(px[0]),
1740 srgb_to_linear(px[1]),
1741 srgb_to_linear(px[2]),
1742 ]
1743 })
1744 .collect()
1745}
1746
1747fn srgb_u16_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
1749 let pixels: &[u16] = bytemuck::cast_slice(data);
1750 pixels
1751 .chunks(channels)
1752 .flat_map(|px| {
1753 [
1754 srgb_to_linear_f(px[0] as f32 / 65535.0),
1755 srgb_to_linear_f(px[1] as f32 / 65535.0),
1756 srgb_to_linear_f(px[2] as f32 / 65535.0),
1757 ]
1758 })
1759 .collect()
1760}
1761
1762#[inline]
1764fn srgb_to_linear_f(c: f32) -> f32 {
1765 if c <= 0.04045 {
1766 c / 12.92
1767 } else {
1768 ((c + 0.055) / 1.055).powf(2.4)
1769 }
1770}
1771
1772fn extract_alpha_u16(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
1774 let pixels: &[u16] = bytemuck::cast_slice(data);
1775 pixels
1776 .chunks(stride)
1777 .map(|px| (px[alpha_offset] >> 8) as u8)
1778 .collect()
1779}
1780
1781fn bgr_to_rgb(data: &[u8], stride: usize) -> Vec<u8> {
1783 let mut out = data.to_vec();
1784 for chunk in out.chunks_mut(stride) {
1785 chunk.swap(0, 2);
1786 }
1787 out
1788}
1789
1790fn extract_alpha(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
1792 data.chunks(stride).map(|px| px[alpha_offset]).collect()
1793}
1794
1795#[cfg(test)]
1798mod tests {
1799 use super::*;
1800
1801 #[test]
1802 fn test_lossless_config_builder_and_getters() {
1803 let cfg = LosslessConfig::new()
1804 .with_effort(5)
1805 .with_ans(false)
1806 .with_squeeze(true)
1807 .with_tree_learning(true);
1808 assert_eq!(cfg.effort(), 5);
1809 assert!(!cfg.ans());
1810 assert!(cfg.squeeze());
1811 assert!(cfg.tree_learning());
1812 }
1813
1814 #[test]
1815 fn test_lossy_config_builder_and_getters() {
1816 let cfg = LossyConfig::new(2.0)
1817 .with_effort(3)
1818 .with_gaborish(false)
1819 .with_noise(true);
1820 assert_eq!(cfg.distance(), 2.0);
1821 assert_eq!(cfg.effort(), 3);
1822 assert!(!cfg.gaborish());
1823 assert!(cfg.noise());
1824 }
1825
1826 #[test]
1827 fn test_pixel_layout_helpers() {
1828 assert_eq!(PixelLayout::Rgb8.bytes_per_pixel(), 3);
1829 assert_eq!(PixelLayout::Rgba8.bytes_per_pixel(), 4);
1830 assert_eq!(PixelLayout::Bgr8.bytes_per_pixel(), 3);
1831 assert_eq!(PixelLayout::Bgra8.bytes_per_pixel(), 4);
1832 assert_eq!(PixelLayout::Gray8.bytes_per_pixel(), 1);
1833 assert_eq!(PixelLayout::Rgb16.bytes_per_pixel(), 6);
1834 assert_eq!(PixelLayout::Rgba16.bytes_per_pixel(), 8);
1835 assert_eq!(PixelLayout::Gray16.bytes_per_pixel(), 2);
1836 assert!(!PixelLayout::Rgb8.is_linear());
1837 assert!(PixelLayout::RgbLinearF32.is_linear());
1838 assert!(!PixelLayout::Rgb16.is_linear());
1839 assert!(!PixelLayout::Rgb8.has_alpha());
1840 assert!(PixelLayout::Rgba8.has_alpha());
1841 assert!(PixelLayout::Bgra8.has_alpha());
1842 assert!(PixelLayout::GrayAlpha8.has_alpha());
1843 assert!(PixelLayout::Rgba16.has_alpha());
1844 assert!(!PixelLayout::Rgb16.has_alpha());
1845 assert!(PixelLayout::Rgb16.is_16bit());
1846 assert!(PixelLayout::Rgba16.is_16bit());
1847 assert!(PixelLayout::Gray16.is_16bit());
1848 assert!(!PixelLayout::Rgb8.is_16bit());
1849 assert!(PixelLayout::Gray8.is_grayscale());
1850 assert!(PixelLayout::Gray16.is_grayscale());
1851 assert!(!PixelLayout::Rgb16.is_grayscale());
1852 }
1853
1854 #[test]
1855 fn test_quality_to_distance() {
1856 assert!(Quality::Distance(1.0).to_distance().unwrap() == 1.0);
1857 assert!(Quality::Distance(-1.0).to_distance().is_err());
1858 assert!(Quality::Percent(100).to_distance().is_err()); assert!(Quality::Percent(90).to_distance().unwrap() == 1.0);
1860 }
1861
1862 #[test]
1863 fn test_pixel_validation() {
1864 let cfg = LosslessConfig::new();
1865 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
1866 assert!(req.validate_pixels(&[0u8; 12]).is_ok());
1867 }
1868
1869 #[test]
1870 fn test_pixel_validation_wrong_size() {
1871 let cfg = LosslessConfig::new();
1872 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
1873 assert!(req.validate_pixels(&[0u8; 11]).is_err());
1874 }
1875
1876 #[test]
1877 fn test_limits_check() {
1878 let limits = Limits::new().with_max_width(100);
1879 let cfg = LosslessConfig::new();
1880 let req = cfg
1881 .encode_request(200, 100, PixelLayout::Rgb8)
1882 .with_limits(&limits);
1883 assert!(req.check_limits().is_err());
1884 }
1885
1886 #[test]
1887 fn test_lossless_encode_rgb8_small() {
1888 let pixels = [255u8, 0, 0].repeat(16);
1890 let result = LosslessConfig::new()
1891 .encode_request(4, 4, PixelLayout::Rgb8)
1892 .encode(&pixels);
1893 assert!(result.is_ok());
1894 let jxl = result.unwrap();
1895 assert_eq!(&jxl[..2], &[0xFF, 0x0A]); }
1897
1898 #[test]
1899 fn test_lossy_encode_rgb8_small() {
1900 let mut pixels = Vec::with_capacity(8 * 8 * 3);
1902 for y in 0..8u8 {
1903 for x in 0..8u8 {
1904 pixels.push(x * 32);
1905 pixels.push(y * 32);
1906 pixels.push(128);
1907 }
1908 }
1909 let result = LossyConfig::new(2.0)
1910 .with_gaborish(false)
1911 .encode_request(8, 8, PixelLayout::Rgb8)
1912 .encode(&pixels);
1913 assert!(result.is_ok());
1914 let jxl = result.unwrap();
1915 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
1916 }
1917
1918 #[test]
1919 fn test_fluent_lossless() {
1920 let pixels = vec![128u8; 4 * 4 * 3];
1921 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Rgb8);
1922 assert!(result.is_ok());
1923 }
1924
1925 #[test]
1926 fn test_lossy_unsupported_gray() {
1927 let pixels = vec![128u8; 8 * 8];
1928 let result = LossyConfig::new(1.0)
1929 .encode_request(8, 8, PixelLayout::Gray8)
1930 .encode(&pixels);
1931 assert!(matches!(
1932 result.as_ref().map_err(|e| e.error()),
1933 Err(EncodeError::UnsupportedPixelLayout(_))
1934 ));
1935 }
1936
1937 #[test]
1938 fn test_bgra_lossless() {
1939 let pixels = [0u8, 0, 255, 255].repeat(16);
1941 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Bgra8);
1942 assert!(result.is_ok());
1943 let jxl = result.unwrap();
1944 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
1945 }
1946
1947 #[test]
1948 fn test_lossy_alpha_encodes() {
1949 let pixels = [255u8, 0, 0, 255].repeat(64);
1951 let result =
1952 LossyConfig::new(2.0)
1953 .with_gaborish(false)
1954 .encode(&pixels, 8, 8, PixelLayout::Bgra8);
1955 assert!(
1956 result.is_ok(),
1957 "BGRA lossy encode failed: {:?}",
1958 result.err()
1959 );
1960
1961 let result2 = LossyConfig::new(2.0).encode(&pixels, 8, 8, PixelLayout::Rgba8);
1962 assert!(
1963 result2.is_ok(),
1964 "RGBA lossy encode failed: {:?}",
1965 result2.err()
1966 );
1967 }
1968
1969 #[test]
1970 fn test_stop_cancellation() {
1971 use enough::Unstoppable;
1972 let pixels = vec![128u8; 4 * 4 * 3];
1974 let cfg = LosslessConfig::new();
1975 let result = cfg
1976 .encode_request(4, 4, PixelLayout::Rgb8)
1977 .with_stop(&Unstoppable)
1978 .encode(&pixels);
1979 assert!(result.is_ok());
1980 }
1981}