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 {
655 0..=4 => 0,
656 5..=8 => 2,
657 _ => 4,
658 }
659}
660
661#[derive(Clone, Debug)]
665pub struct LossyConfig {
666 distance: f32,
667 effort: u8,
668 use_ans: bool,
669 gaborish: bool,
670 noise: bool,
671 denoise: bool,
672 error_diffusion: bool,
673 pixel_domain_loss: bool,
674 lz77: bool,
675 lz77_method: Lz77Method,
676 force_strategy: Option<u8>,
677 #[cfg(feature = "butteraugli-loop")]
678 butteraugli_iters: u32,
679 #[cfg(feature = "butteraugli-loop")]
680 butteraugli_iters_explicit: bool,
681}
682
683impl LossyConfig {
684 pub fn new(distance: f32) -> Self {
686 let effort = 7;
687 Self {
688 distance,
689 effort,
690 use_ans: true,
691 gaborish: true,
692 noise: false,
693 denoise: false,
694 error_diffusion: true,
695 pixel_domain_loss: true,
696 lz77: false,
697 lz77_method: Lz77Method::Greedy,
698 force_strategy: None,
699 #[cfg(feature = "butteraugli-loop")]
700 butteraugli_iters: butteraugli_iters_for_effort(effort),
701 #[cfg(feature = "butteraugli-loop")]
702 butteraugli_iters_explicit: false,
703 }
704 }
705
706 pub fn from_quality(quality: Quality) -> core::result::Result<Self, EncodeError> {
708 let distance = quality.to_distance()?;
709 Ok(Self::new(distance))
710 }
711
712 pub fn with_effort(mut self, effort: u8) -> Self {
717 self.effort = effort;
718 #[cfg(feature = "butteraugli-loop")]
719 if !self.butteraugli_iters_explicit {
720 self.butteraugli_iters = butteraugli_iters_for_effort(effort);
721 }
722 self
723 }
724
725 pub fn with_ans(mut self, enable: bool) -> Self {
727 self.use_ans = enable;
728 self
729 }
730
731 pub fn with_gaborish(mut self, enable: bool) -> Self {
733 self.gaborish = enable;
734 self
735 }
736
737 pub fn with_noise(mut self, enable: bool) -> Self {
739 self.noise = enable;
740 self
741 }
742
743 pub fn with_denoise(mut self, enable: bool) -> Self {
745 self.denoise = enable;
746 if enable {
747 self.noise = true;
748 }
749 self
750 }
751
752 pub fn with_error_diffusion(mut self, enable: bool) -> Self {
754 self.error_diffusion = enable;
755 self
756 }
757
758 pub fn with_pixel_domain_loss(mut self, enable: bool) -> Self {
760 self.pixel_domain_loss = enable;
761 self
762 }
763
764 pub fn with_lz77(mut self, enable: bool) -> Self {
766 self.lz77 = enable;
767 self
768 }
769
770 pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
772 self.lz77_method = method;
773 self
774 }
775
776 pub fn with_force_strategy(mut self, strategy: Option<u8>) -> Self {
778 self.force_strategy = strategy;
779 self
780 }
781
782 #[cfg(feature = "butteraugli-loop")]
787 pub fn with_butteraugli_iters(mut self, n: u32) -> Self {
788 self.butteraugli_iters = n;
789 self.butteraugli_iters_explicit = true;
790 self
791 }
792
793 pub fn distance(&self) -> f32 {
797 self.distance
798 }
799
800 pub fn effort(&self) -> u8 {
802 self.effort
803 }
804
805 pub fn ans(&self) -> bool {
807 self.use_ans
808 }
809
810 pub fn gaborish(&self) -> bool {
812 self.gaborish
813 }
814
815 pub fn noise(&self) -> bool {
817 self.noise
818 }
819
820 pub fn denoise(&self) -> bool {
822 self.denoise
823 }
824
825 pub fn error_diffusion(&self) -> bool {
827 self.error_diffusion
828 }
829
830 pub fn pixel_domain_loss(&self) -> bool {
832 self.pixel_domain_loss
833 }
834
835 pub fn lz77(&self) -> bool {
837 self.lz77
838 }
839
840 pub fn lz77_method(&self) -> Lz77Method {
842 self.lz77_method
843 }
844
845 pub fn force_strategy(&self) -> Option<u8> {
847 self.force_strategy
848 }
849
850 #[cfg(feature = "butteraugli-loop")]
852 pub fn butteraugli_iters(&self) -> u32 {
853 self.butteraugli_iters
854 }
855
856 pub fn encode_request(
862 &self,
863 width: u32,
864 height: u32,
865 layout: PixelLayout,
866 ) -> EncodeRequest<'_> {
867 EncodeRequest {
868 config: ConfigRef::Lossy(self),
869 width,
870 height,
871 layout,
872 metadata: None,
873 limits: None,
874 stop: None,
875 }
876 }
877
878 #[track_caller]
887 pub fn encode(
888 &self,
889 pixels: &[u8],
890 width: u32,
891 height: u32,
892 layout: PixelLayout,
893 ) -> Result<Vec<u8>> {
894 self.encode_request(width, height, layout).encode(pixels)
895 }
896
897 #[track_caller]
899 pub fn encode_into(
900 &self,
901 pixels: &[u8],
902 width: u32,
903 height: u32,
904 layout: PixelLayout,
905 out: &mut Vec<u8>,
906 ) -> Result<()> {
907 self.encode_request(width, height, layout)
908 .encode_into(pixels, out)
909 .map(|_| ())
910 }
911
912 #[track_caller]
917 pub fn encode_animation(
918 &self,
919 width: u32,
920 height: u32,
921 layout: PixelLayout,
922 animation: &AnimationParams,
923 frames: &[AnimationFrame<'_>],
924 ) -> Result<Vec<u8>> {
925 encode_animation_lossy(self, width, height, layout, animation, frames).map_err(at)
926 }
927}
928
929#[derive(Clone, Copy, Debug)]
933enum ConfigRef<'a> {
934 Lossless(&'a LosslessConfig),
935 Lossy(&'a LossyConfig),
936}
937
938pub struct EncodeRequest<'a> {
942 config: ConfigRef<'a>,
943 width: u32,
944 height: u32,
945 layout: PixelLayout,
946 metadata: Option<&'a ImageMetadata<'a>>,
947 limits: Option<&'a Limits>,
948 stop: Option<&'a dyn Stop>,
949}
950
951impl<'a> EncodeRequest<'a> {
952 pub fn with_metadata(mut self, meta: &'a ImageMetadata<'a>) -> Self {
954 self.metadata = Some(meta);
955 self
956 }
957
958 pub fn with_limits(mut self, limits: &'a Limits) -> Self {
960 self.limits = Some(limits);
961 self
962 }
963
964 pub fn with_stop(mut self, stop: &'a dyn Stop) -> Self {
969 self.stop = Some(stop);
970 self
971 }
972
973 #[track_caller]
975 pub fn encode(self, pixels: &[u8]) -> Result<Vec<u8>> {
976 self.encode_inner(pixels)
977 .map(|mut r| r.take_data().unwrap())
978 .map_err(at)
979 }
980
981 #[track_caller]
983 pub fn encode_with_stats(self, pixels: &[u8]) -> Result<EncodeResult> {
984 self.encode_inner(pixels).map_err(at)
985 }
986
987 #[track_caller]
989 pub fn encode_into(self, pixels: &[u8], out: &mut Vec<u8>) -> Result<EncodeResult> {
990 let mut result = self.encode_inner(pixels).map_err(at)?;
991 if let Some(data) = result.data.take() {
992 out.extend_from_slice(&data);
993 }
994 Ok(result)
995 }
996
997 #[cfg(feature = "std")]
999 #[track_caller]
1000 pub fn encode_to(self, pixels: &[u8], mut dest: impl std::io::Write) -> Result<EncodeResult> {
1001 let mut result = self.encode_inner(pixels).map_err(at)?;
1002 if let Some(data) = result.data.take() {
1003 dest.write_all(&data)
1004 .map_err(|e| at(EncodeError::from(e)))?;
1005 }
1006 Ok(result)
1007 }
1008
1009 fn encode_inner(&self, pixels: &[u8]) -> core::result::Result<EncodeResult, EncodeError> {
1010 self.validate_pixels(pixels)?;
1011 self.check_limits()?;
1012
1013 let (codestream, mut stats) = match self.config {
1014 ConfigRef::Lossless(cfg) => self.encode_lossless(cfg, pixels),
1015 ConfigRef::Lossy(cfg) => self.encode_lossy(cfg, pixels),
1016 }?;
1017
1018 stats.codestream_size = codestream.len();
1019
1020 let output = if let Some(meta) = self.metadata
1022 && (meta.exif.is_some() || meta.xmp.is_some())
1023 {
1024 crate::container::wrap_in_container(&codestream, meta.exif, meta.xmp)
1025 } else {
1026 codestream
1027 };
1028
1029 stats.output_size = output.len();
1030
1031 Ok(EncodeResult {
1032 data: Some(output),
1033 stats,
1034 })
1035 }
1036
1037 fn validate_pixels(&self, pixels: &[u8]) -> core::result::Result<(), EncodeError> {
1038 let w = self.width as usize;
1039 let h = self.height as usize;
1040 if w == 0 || h == 0 {
1041 return Err(EncodeError::InvalidInput {
1042 message: format!("zero dimensions: {w}x{h}"),
1043 });
1044 }
1045 let expected = w
1046 .checked_mul(h)
1047 .and_then(|n| n.checked_mul(self.layout.bytes_per_pixel()));
1048 match expected {
1049 Some(expected) if pixels.len() == expected => Ok(()),
1050 Some(expected) => Err(EncodeError::InvalidInput {
1051 message: format!(
1052 "pixel buffer size mismatch: expected {expected} bytes for {w}x{h} {:?}, got {}",
1053 self.layout,
1054 pixels.len()
1055 ),
1056 }),
1057 None => Err(EncodeError::InvalidInput {
1058 message: "image dimensions overflow".into(),
1059 }),
1060 }
1061 }
1062
1063 fn check_limits(&self) -> core::result::Result<(), EncodeError> {
1064 let Some(limits) = self.limits else {
1065 return Ok(());
1066 };
1067 let w = self.width as u64;
1068 let h = self.height as u64;
1069 if let Some(max_w) = limits.max_width
1070 && w > max_w
1071 {
1072 return Err(EncodeError::LimitExceeded {
1073 message: format!("width {w} > max {max_w}"),
1074 });
1075 }
1076 if let Some(max_h) = limits.max_height
1077 && h > max_h
1078 {
1079 return Err(EncodeError::LimitExceeded {
1080 message: format!("height {h} > max {max_h}"),
1081 });
1082 }
1083 if let Some(max_px) = limits.max_pixels
1084 && w * h > max_px
1085 {
1086 return Err(EncodeError::LimitExceeded {
1087 message: format!("pixels {}x{} = {} > max {max_px}", w, h, w * h),
1088 });
1089 }
1090 Ok(())
1091 }
1092
1093 fn encode_lossless(
1096 &self,
1097 cfg: &LosslessConfig,
1098 pixels: &[u8],
1099 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1100 use crate::bit_writer::BitWriter;
1101 use crate::headers::{ColorEncoding, FileHeader};
1102 use crate::modular::channel::ModularImage;
1103 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
1104
1105 let w = self.width as usize;
1106 let h = self.height as usize;
1107
1108 let image = match self.layout {
1110 PixelLayout::Rgb8 => ModularImage::from_rgb8(pixels, w, h),
1111 PixelLayout::Rgba8 => ModularImage::from_rgba8(pixels, w, h),
1112 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(pixels, 3), w, h),
1113 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(pixels, 4), w, h),
1114 PixelLayout::Gray8 => ModularImage::from_gray8(pixels, w, h),
1115 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(pixels, w, h),
1116 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(pixels, w, h),
1117 PixelLayout::Gray16 => ModularImage::from_gray16_native(pixels, w, h),
1118 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1119 }
1120 .map_err(EncodeError::from)?;
1121
1122 let mut file_header = if image.is_grayscale {
1124 FileHeader::new_gray(self.width, self.height)
1125 } else if image.has_alpha {
1126 FileHeader::new_rgba(self.width, self.height)
1127 } else {
1128 FileHeader::new_rgb(self.width, self.height)
1129 };
1130 if image.bit_depth == 16 {
1131 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
1132 for ec in &mut file_header.metadata.extra_channels {
1133 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
1134 }
1135 }
1136 if let Some(meta) = self.metadata
1137 && meta.icc_profile.is_some()
1138 {
1139 file_header.metadata.color_encoding.want_icc = true;
1140 }
1141
1142 let mut writer = BitWriter::new();
1144 file_header.write(&mut writer).map_err(EncodeError::from)?;
1145 if let Some(meta) = self.metadata
1146 && let Some(icc) = meta.icc_profile
1147 {
1148 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
1149 }
1150 writer.zero_pad_to_byte();
1151
1152 let frame_encoder = FrameEncoder::new(
1154 w,
1155 h,
1156 FrameEncoderOptions {
1157 use_modular: true,
1158 effort: cfg.effort,
1159 use_ans: cfg.use_ans,
1160 use_tree_learning: cfg.tree_learning,
1161 use_squeeze: cfg.squeeze,
1162 have_animation: false,
1163 duration: 0,
1164 is_last: true,
1165 crop: None,
1166 },
1167 );
1168 let color_encoding = ColorEncoding::srgb();
1169 frame_encoder
1170 .encode_modular(&image, &color_encoding, &mut writer)
1171 .map_err(EncodeError::from)?;
1172
1173 let stats = EncodeStats {
1174 mode: EncodeMode::Lossless,
1175 ans: cfg.use_ans,
1176 ..Default::default()
1177 };
1178 Ok((writer.finish_with_padding(), stats))
1179 }
1180
1181 fn encode_lossy(
1184 &self,
1185 cfg: &LossyConfig,
1186 pixels: &[u8],
1187 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1188 let w = self.width as usize;
1189 let h = self.height as usize;
1190
1191 let (linear_rgb, alpha, bit_depth_16) = match self.layout {
1193 PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(pixels, 3), None, false),
1194 PixelLayout::Bgr8 => (
1195 srgb_u8_to_linear_f32(&bgr_to_rgb(pixels, 3), 3),
1196 None,
1197 false,
1198 ),
1199 PixelLayout::Rgba8 => {
1200 let rgb = srgb_u8_to_linear_f32(pixels, 4);
1201 let alpha = extract_alpha(pixels, 4, 3);
1202 (rgb, Some(alpha), false)
1203 }
1204 PixelLayout::Bgra8 => {
1205 let swapped = bgr_to_rgb(pixels, 4);
1206 let rgb = srgb_u8_to_linear_f32(&swapped, 4);
1207 let alpha = extract_alpha(pixels, 4, 3);
1208 (rgb, Some(alpha), false)
1209 }
1210 PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(pixels, 3), None, true),
1211 PixelLayout::Rgba16 => {
1212 let rgb = srgb_u16_to_linear_f32(pixels, 4);
1213 let alpha = extract_alpha_u16(pixels, 4, 3);
1214 (rgb, Some(alpha), true)
1215 }
1216 PixelLayout::RgbLinearF32 => {
1217 let floats: &[f32] = bytemuck::cast_slice(pixels);
1218 (floats.to_vec(), None, false)
1219 }
1220 PixelLayout::Gray8 | PixelLayout::GrayAlpha8 | PixelLayout::Gray16 => {
1221 return Err(EncodeError::UnsupportedPixelLayout(self.layout));
1222 }
1223 };
1224
1225 let mut tiny = crate::vardct::VarDctEncoder::new(cfg.distance);
1226 tiny.use_ans = cfg.use_ans;
1227 tiny.optimize_codes = true;
1228 tiny.custom_orders = true;
1229 tiny.enable_noise = cfg.noise;
1230 tiny.enable_denoise = cfg.denoise;
1231 tiny.enable_gaborish = cfg.gaborish;
1232 tiny.error_diffusion = cfg.error_diffusion;
1233 tiny.pixel_domain_loss = cfg.pixel_domain_loss;
1234 tiny.enable_lz77 = cfg.lz77;
1235 tiny.lz77_method = cfg.lz77_method;
1236 tiny.force_strategy = cfg.force_strategy;
1237 #[cfg(feature = "butteraugli-loop")]
1238 {
1239 tiny.butteraugli_iters = cfg.butteraugli_iters;
1240 }
1241
1242 tiny.bit_depth_16 = bit_depth_16;
1243
1244 if let Some(meta) = self.metadata
1246 && let Some(icc) = meta.icc_profile
1247 {
1248 tiny.icc_profile = Some(icc.to_vec());
1249 }
1250
1251 let output = tiny
1252 .encode(w, h, &linear_rgb, alpha.as_deref())
1253 .map_err(EncodeError::from)?;
1254
1255 #[cfg(feature = "butteraugli-loop")]
1256 let butteraugli_iters_actual = cfg.butteraugli_iters;
1257 #[cfg(not(feature = "butteraugli-loop"))]
1258 let butteraugli_iters_actual = 0u32;
1259
1260 let stats = EncodeStats {
1261 mode: EncodeMode::Lossy,
1262 strategy_counts: output.strategy_counts,
1263 gaborish: cfg.gaborish,
1264 ans: cfg.use_ans,
1265 butteraugli_iters: butteraugli_iters_actual,
1266 pixel_domain_loss: cfg.pixel_domain_loss,
1267 ..Default::default()
1268 };
1269 Ok((output.data, stats))
1270 }
1271}
1272
1273fn validate_animation_input(
1276 width: u32,
1277 height: u32,
1278 layout: PixelLayout,
1279 frames: &[AnimationFrame<'_>],
1280) -> core::result::Result<(), EncodeError> {
1281 if width == 0 || height == 0 {
1282 return Err(EncodeError::InvalidInput {
1283 message: format!("zero dimensions: {width}x{height}"),
1284 });
1285 }
1286 if frames.is_empty() {
1287 return Err(EncodeError::InvalidInput {
1288 message: "animation requires at least one frame".into(),
1289 });
1290 }
1291 let expected_size = (width as usize)
1292 .checked_mul(height as usize)
1293 .and_then(|n| n.checked_mul(layout.bytes_per_pixel()))
1294 .ok_or_else(|| EncodeError::InvalidInput {
1295 message: "image dimensions overflow".into(),
1296 })?;
1297 for (i, frame) in frames.iter().enumerate() {
1298 if frame.pixels.len() != expected_size {
1299 return Err(EncodeError::InvalidInput {
1300 message: format!(
1301 "frame {} pixel buffer size mismatch: expected {expected_size}, got {}",
1302 i,
1303 frame.pixels.len()
1304 ),
1305 });
1306 }
1307 }
1308 Ok(())
1309}
1310
1311fn encode_animation_lossless(
1312 cfg: &LosslessConfig,
1313 width: u32,
1314 height: u32,
1315 layout: PixelLayout,
1316 animation: &AnimationParams,
1317 frames: &[AnimationFrame<'_>],
1318) -> core::result::Result<Vec<u8>, EncodeError> {
1319 use crate::bit_writer::BitWriter;
1320 use crate::headers::file_header::AnimationHeader;
1321 use crate::headers::{ColorEncoding, FileHeader};
1322 use crate::modular::channel::ModularImage;
1323 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
1324
1325 validate_animation_input(width, height, layout, frames)?;
1326
1327 let w = width as usize;
1328 let h = height as usize;
1329 let num_frames = frames.len();
1330
1331 let sample_image = match layout {
1333 PixelLayout::Rgb8 => ModularImage::from_rgb8(frames[0].pixels, w, h),
1334 PixelLayout::Rgba8 => ModularImage::from_rgba8(frames[0].pixels, w, h),
1335 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(frames[0].pixels, 3), w, h),
1336 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(frames[0].pixels, 4), w, h),
1337 PixelLayout::Gray8 => ModularImage::from_gray8(frames[0].pixels, w, h),
1338 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frames[0].pixels, w, h),
1339 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frames[0].pixels, w, h),
1340 PixelLayout::Gray16 => ModularImage::from_gray16_native(frames[0].pixels, w, h),
1341 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1342 }
1343 .map_err(EncodeError::from)?;
1344
1345 let mut file_header = if sample_image.is_grayscale {
1346 FileHeader::new_gray(width, height)
1347 } else if sample_image.has_alpha {
1348 FileHeader::new_rgba(width, height)
1349 } else {
1350 FileHeader::new_rgb(width, height)
1351 };
1352 if sample_image.bit_depth == 16 {
1353 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
1354 for ec in &mut file_header.metadata.extra_channels {
1355 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
1356 }
1357 }
1358 file_header.metadata.animation = Some(AnimationHeader {
1359 tps_numerator: animation.tps_numerator,
1360 tps_denominator: animation.tps_denominator,
1361 num_loops: animation.num_loops,
1362 have_timecodes: false,
1363 });
1364
1365 let mut writer = BitWriter::new();
1367 file_header.write(&mut writer).map_err(EncodeError::from)?;
1368 writer.zero_pad_to_byte();
1369
1370 let color_encoding = ColorEncoding::srgb();
1372 let bpp = layout.bytes_per_pixel();
1373 let mut prev_pixels: Option<&[u8]> = None;
1374
1375 for (i, frame) in frames.iter().enumerate() {
1376 let crop = if let Some(prev) = prev_pixels {
1379 match detect_frame_crop(prev, frame.pixels, w, h, bpp, false) {
1380 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
1381 Some(_) => None, None => {
1383 Some(FrameCrop {
1385 x0: 0,
1386 y0: 0,
1387 width: 1,
1388 height: 1,
1389 })
1390 }
1391 }
1392 } else {
1393 None };
1395
1396 let (frame_w, frame_h, frame_pixels_owned);
1398 let frame_pixels: &[u8] = if let Some(ref crop) = crop {
1399 frame_w = crop.width as usize;
1400 frame_h = crop.height as usize;
1401 frame_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
1402 &frame_pixels_owned
1403 } else {
1404 frame_w = w;
1405 frame_h = h;
1406 frame_pixels_owned = Vec::new();
1407 let _ = &frame_pixels_owned; frame.pixels
1409 };
1410
1411 let image = match layout {
1412 PixelLayout::Rgb8 => ModularImage::from_rgb8(frame_pixels, frame_w, frame_h),
1413 PixelLayout::Rgba8 => ModularImage::from_rgba8(frame_pixels, frame_w, frame_h),
1414 PixelLayout::Bgr8 => {
1415 ModularImage::from_rgb8(&bgr_to_rgb(frame_pixels, 3), frame_w, frame_h)
1416 }
1417 PixelLayout::Bgra8 => {
1418 ModularImage::from_rgba8(&bgr_to_rgb(frame_pixels, 4), frame_w, frame_h)
1419 }
1420 PixelLayout::Gray8 => ModularImage::from_gray8(frame_pixels, frame_w, frame_h),
1421 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frame_pixels, frame_w, frame_h),
1422 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frame_pixels, frame_w, frame_h),
1423 PixelLayout::Gray16 => ModularImage::from_gray16_native(frame_pixels, frame_w, frame_h),
1424 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1425 }
1426 .map_err(EncodeError::from)?;
1427
1428 let frame_encoder = FrameEncoder::new(
1429 frame_w,
1430 frame_h,
1431 FrameEncoderOptions {
1432 use_modular: true,
1433 effort: cfg.effort,
1434 use_ans: cfg.use_ans,
1435 use_tree_learning: cfg.tree_learning,
1436 use_squeeze: cfg.squeeze,
1437 have_animation: true,
1438 duration: frame.duration,
1439 is_last: i == num_frames - 1,
1440 crop,
1441 },
1442 );
1443 frame_encoder
1444 .encode_modular(&image, &color_encoding, &mut writer)
1445 .map_err(EncodeError::from)?;
1446
1447 prev_pixels = Some(frame.pixels);
1448 }
1449
1450 Ok(writer.finish_with_padding())
1451}
1452
1453fn encode_animation_lossy(
1454 cfg: &LossyConfig,
1455 width: u32,
1456 height: u32,
1457 layout: PixelLayout,
1458 animation: &AnimationParams,
1459 frames: &[AnimationFrame<'_>],
1460) -> core::result::Result<Vec<u8>, EncodeError> {
1461 use crate::bit_writer::BitWriter;
1462 use crate::headers::file_header::AnimationHeader;
1463 use crate::headers::frame_header::FrameOptions;
1464
1465 validate_animation_input(width, height, layout, frames)?;
1466
1467 let w = width as usize;
1468 let h = height as usize;
1469 let num_frames = frames.len();
1470
1471 let mut tiny = crate::vardct::VarDctEncoder::new(cfg.distance);
1473 tiny.use_ans = cfg.use_ans;
1474 tiny.optimize_codes = true;
1475 tiny.custom_orders = true;
1476 tiny.enable_noise = cfg.noise;
1477 tiny.enable_denoise = cfg.denoise;
1478 tiny.enable_gaborish = cfg.gaborish;
1479 tiny.error_diffusion = cfg.error_diffusion;
1480 tiny.pixel_domain_loss = cfg.pixel_domain_loss;
1481 tiny.enable_lz77 = cfg.lz77;
1482 tiny.lz77_method = cfg.lz77_method;
1483 tiny.force_strategy = cfg.force_strategy;
1484 #[cfg(feature = "butteraugli-loop")]
1485 {
1486 tiny.butteraugli_iters = cfg.butteraugli_iters;
1487 }
1488
1489 let has_alpha = layout.has_alpha();
1491 let bit_depth_16 = matches!(layout, PixelLayout::Rgb16 | PixelLayout::Rgba16);
1492 tiny.bit_depth_16 = bit_depth_16;
1493
1494 let mut file_header = tiny.build_file_header(w, h, has_alpha);
1497 file_header.metadata.animation = Some(AnimationHeader {
1498 tps_numerator: animation.tps_numerator,
1499 tps_denominator: animation.tps_denominator,
1500 num_loops: animation.num_loops,
1501 have_timecodes: false,
1502 });
1503
1504 let mut writer = BitWriter::with_capacity(w * h * 4);
1505 file_header.write(&mut writer).map_err(EncodeError::from)?;
1506 if let Some(ref icc) = tiny.icc_profile {
1507 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
1508 }
1509 writer.zero_pad_to_byte();
1510
1511 let bpp = layout.bytes_per_pixel();
1513 let mut prev_pixels: Option<&[u8]> = None;
1514
1515 for (i, frame) in frames.iter().enumerate() {
1516 let crop = if let Some(prev) = prev_pixels {
1519 match detect_frame_crop(prev, frame.pixels, w, h, bpp, true) {
1520 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
1521 Some(_) => None, None => {
1523 Some(FrameCrop {
1525 x0: 0,
1526 y0: 0,
1527 width: 8.min(width),
1528 height: 8.min(height),
1529 })
1530 }
1531 }
1532 } else {
1533 None };
1535
1536 let (frame_w, frame_h) = if let Some(ref crop) = crop {
1538 (crop.width as usize, crop.height as usize)
1539 } else {
1540 (w, h)
1541 };
1542
1543 let crop_pixels_owned;
1544 let src_pixels: &[u8] = if let Some(ref crop) = crop {
1545 crop_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
1546 &crop_pixels_owned
1547 } else {
1548 crop_pixels_owned = Vec::new();
1549 let _ = &crop_pixels_owned;
1550 frame.pixels
1551 };
1552
1553 let (linear_rgb, alpha) = match layout {
1554 PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(src_pixels, 3), None),
1555 PixelLayout::Bgr8 => (srgb_u8_to_linear_f32(&bgr_to_rgb(src_pixels, 3), 3), None),
1556 PixelLayout::Rgba8 => {
1557 let rgb = srgb_u8_to_linear_f32(src_pixels, 4);
1558 let alpha = extract_alpha(src_pixels, 4, 3);
1559 (rgb, Some(alpha))
1560 }
1561 PixelLayout::Bgra8 => {
1562 let swapped = bgr_to_rgb(src_pixels, 4);
1563 let rgb = srgb_u8_to_linear_f32(&swapped, 4);
1564 let alpha = extract_alpha(src_pixels, 4, 3);
1565 (rgb, Some(alpha))
1566 }
1567 PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(src_pixels, 3), None),
1568 PixelLayout::Rgba16 => {
1569 let rgb = srgb_u16_to_linear_f32(src_pixels, 4);
1570 let alpha = extract_alpha_u16(src_pixels, 4, 3);
1571 (rgb, Some(alpha))
1572 }
1573 PixelLayout::RgbLinearF32 => {
1574 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
1575 (floats.to_vec(), None)
1576 }
1577 PixelLayout::Gray8 | PixelLayout::GrayAlpha8 | PixelLayout::Gray16 => {
1578 return Err(EncodeError::UnsupportedPixelLayout(layout));
1579 }
1580 };
1581
1582 let frame_options = FrameOptions {
1583 have_animation: true,
1584 have_timecodes: false,
1585 duration: frame.duration,
1586 is_last: i == num_frames - 1,
1587 crop,
1588 };
1589
1590 tiny.encode_frame_to_writer(
1591 frame_w,
1592 frame_h,
1593 &linear_rgb,
1594 alpha.as_deref(),
1595 &frame_options,
1596 &mut writer,
1597 )
1598 .map_err(EncodeError::from)?;
1599
1600 prev_pixels = Some(frame.pixels);
1601 }
1602
1603 Ok(writer.finish_with_padding())
1604}
1605
1606use crate::headers::frame_header::FrameCrop;
1609
1610fn detect_frame_crop(
1618 prev: &[u8],
1619 curr: &[u8],
1620 width: usize,
1621 height: usize,
1622 bytes_per_pixel: usize,
1623 align_to_8x8: bool,
1624) -> Option<FrameCrop> {
1625 let stride = width * bytes_per_pixel;
1626 debug_assert_eq!(prev.len(), height * stride);
1627 debug_assert_eq!(curr.len(), height * stride);
1628
1629 let mut top = height;
1631 let mut bottom = 0;
1632 let mut left = width;
1633 let mut right = 0;
1634
1635 for y in 0..height {
1636 let row_start = y * stride;
1637 let prev_row = &prev[row_start..row_start + stride];
1638 let curr_row = &curr[row_start..row_start + stride];
1639
1640 let (prev_prefix, prev_u64, prev_suffix) = bytemuck::pod_align_to::<u8, u64>(prev_row);
1642 let (curr_prefix, curr_u64, curr_suffix) = bytemuck::pod_align_to::<u8, u64>(curr_row);
1643 if prev_prefix == curr_prefix && prev_u64 == curr_u64 && prev_suffix == curr_suffix {
1644 continue;
1645 }
1646
1647 if top == height {
1649 top = y;
1650 }
1651 bottom = y;
1652
1653 for x in 0..width {
1655 let px_start = x * bytes_per_pixel;
1656 if prev_row[px_start..px_start + bytes_per_pixel]
1657 != curr_row[px_start..px_start + bytes_per_pixel]
1658 {
1659 left = left.min(x);
1660 break;
1661 }
1662 }
1663 for x in (0..width).rev() {
1665 let px_start = x * bytes_per_pixel;
1666 if prev_row[px_start..px_start + bytes_per_pixel]
1667 != curr_row[px_start..px_start + bytes_per_pixel]
1668 {
1669 right = right.max(x);
1670 break;
1671 }
1672 }
1673 }
1674
1675 if top == height {
1676 return None;
1678 }
1679
1680 let mut crop_x = left as i32;
1682 let mut crop_y = top as i32;
1683 let mut crop_w = (right - left + 1) as u32;
1684 let mut crop_h = (bottom - top + 1) as u32;
1685
1686 if align_to_8x8 {
1687 let aligned_x = (crop_x / 8) * 8;
1689 let aligned_y = (crop_y / 8) * 8;
1690 let end_x = (crop_x as u32 + crop_w).div_ceil(8) * 8;
1691 let end_y = (crop_y as u32 + crop_h).div_ceil(8) * 8;
1692 crop_x = aligned_x;
1693 crop_y = aligned_y;
1694 crop_w = end_x.min(width as u32) - aligned_x as u32;
1695 crop_h = end_y.min(height as u32) - aligned_y as u32;
1696 }
1697
1698 Some(FrameCrop {
1699 x0: crop_x,
1700 y0: crop_y,
1701 width: crop_w,
1702 height: crop_h,
1703 })
1704}
1705
1706fn extract_pixel_crop(
1710 pixels: &[u8],
1711 full_width: usize,
1712 crop: &FrameCrop,
1713 bytes_per_pixel: usize,
1714) -> Vec<u8> {
1715 let cx = crop.x0 as usize;
1716 let cy = crop.y0 as usize;
1717 let cw = crop.width as usize;
1718 let ch = crop.height as usize;
1719 let stride = full_width * bytes_per_pixel;
1720
1721 let mut out = Vec::with_capacity(cw * ch * bytes_per_pixel);
1722 for y in cy..cy + ch {
1723 let row_start = y * stride + cx * bytes_per_pixel;
1724 out.extend_from_slice(&pixels[row_start..row_start + cw * bytes_per_pixel]);
1725 }
1726 out
1727}
1728
1729#[inline]
1733fn srgb_to_linear(c: u8) -> f32 {
1734 srgb_to_linear_f(c as f32 / 255.0)
1735}
1736
1737fn srgb_u8_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
1738 data.chunks(channels)
1739 .flat_map(|px| {
1740 [
1741 srgb_to_linear(px[0]),
1742 srgb_to_linear(px[1]),
1743 srgb_to_linear(px[2]),
1744 ]
1745 })
1746 .collect()
1747}
1748
1749fn srgb_u16_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
1751 let pixels: &[u16] = bytemuck::cast_slice(data);
1752 pixels
1753 .chunks(channels)
1754 .flat_map(|px| {
1755 [
1756 srgb_to_linear_f(px[0] as f32 / 65535.0),
1757 srgb_to_linear_f(px[1] as f32 / 65535.0),
1758 srgb_to_linear_f(px[2] as f32 / 65535.0),
1759 ]
1760 })
1761 .collect()
1762}
1763
1764#[inline]
1766fn srgb_to_linear_f(c: f32) -> f32 {
1767 if c <= 0.04045 {
1768 c / 12.92
1769 } else {
1770 ((c + 0.055) / 1.055).powf(2.4)
1771 }
1772}
1773
1774fn extract_alpha_u16(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
1776 let pixels: &[u16] = bytemuck::cast_slice(data);
1777 pixels
1778 .chunks(stride)
1779 .map(|px| (px[alpha_offset] >> 8) as u8)
1780 .collect()
1781}
1782
1783fn bgr_to_rgb(data: &[u8], stride: usize) -> Vec<u8> {
1785 let mut out = data.to_vec();
1786 for chunk in out.chunks_mut(stride) {
1787 chunk.swap(0, 2);
1788 }
1789 out
1790}
1791
1792fn extract_alpha(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
1794 data.chunks(stride).map(|px| px[alpha_offset]).collect()
1795}
1796
1797#[cfg(test)]
1800mod tests {
1801 use super::*;
1802
1803 #[test]
1804 fn test_lossless_config_builder_and_getters() {
1805 let cfg = LosslessConfig::new()
1806 .with_effort(5)
1807 .with_ans(false)
1808 .with_squeeze(true)
1809 .with_tree_learning(true);
1810 assert_eq!(cfg.effort(), 5);
1811 assert!(!cfg.ans());
1812 assert!(cfg.squeeze());
1813 assert!(cfg.tree_learning());
1814 }
1815
1816 #[test]
1817 fn test_lossy_config_builder_and_getters() {
1818 let cfg = LossyConfig::new(2.0)
1819 .with_effort(3)
1820 .with_gaborish(false)
1821 .with_noise(true);
1822 assert_eq!(cfg.distance(), 2.0);
1823 assert_eq!(cfg.effort(), 3);
1824 assert!(!cfg.gaborish());
1825 assert!(cfg.noise());
1826 }
1827
1828 #[test]
1829 fn test_pixel_layout_helpers() {
1830 assert_eq!(PixelLayout::Rgb8.bytes_per_pixel(), 3);
1831 assert_eq!(PixelLayout::Rgba8.bytes_per_pixel(), 4);
1832 assert_eq!(PixelLayout::Bgr8.bytes_per_pixel(), 3);
1833 assert_eq!(PixelLayout::Bgra8.bytes_per_pixel(), 4);
1834 assert_eq!(PixelLayout::Gray8.bytes_per_pixel(), 1);
1835 assert_eq!(PixelLayout::Rgb16.bytes_per_pixel(), 6);
1836 assert_eq!(PixelLayout::Rgba16.bytes_per_pixel(), 8);
1837 assert_eq!(PixelLayout::Gray16.bytes_per_pixel(), 2);
1838 assert!(!PixelLayout::Rgb8.is_linear());
1839 assert!(PixelLayout::RgbLinearF32.is_linear());
1840 assert!(!PixelLayout::Rgb16.is_linear());
1841 assert!(!PixelLayout::Rgb8.has_alpha());
1842 assert!(PixelLayout::Rgba8.has_alpha());
1843 assert!(PixelLayout::Bgra8.has_alpha());
1844 assert!(PixelLayout::GrayAlpha8.has_alpha());
1845 assert!(PixelLayout::Rgba16.has_alpha());
1846 assert!(!PixelLayout::Rgb16.has_alpha());
1847 assert!(PixelLayout::Rgb16.is_16bit());
1848 assert!(PixelLayout::Rgba16.is_16bit());
1849 assert!(PixelLayout::Gray16.is_16bit());
1850 assert!(!PixelLayout::Rgb8.is_16bit());
1851 assert!(PixelLayout::Gray8.is_grayscale());
1852 assert!(PixelLayout::Gray16.is_grayscale());
1853 assert!(!PixelLayout::Rgb16.is_grayscale());
1854 }
1855
1856 #[test]
1857 fn test_quality_to_distance() {
1858 assert!(Quality::Distance(1.0).to_distance().unwrap() == 1.0);
1859 assert!(Quality::Distance(-1.0).to_distance().is_err());
1860 assert!(Quality::Percent(100).to_distance().is_err()); assert!(Quality::Percent(90).to_distance().unwrap() == 1.0);
1862 }
1863
1864 #[test]
1865 fn test_pixel_validation() {
1866 let cfg = LosslessConfig::new();
1867 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
1868 assert!(req.validate_pixels(&[0u8; 12]).is_ok());
1869 }
1870
1871 #[test]
1872 fn test_pixel_validation_wrong_size() {
1873 let cfg = LosslessConfig::new();
1874 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
1875 assert!(req.validate_pixels(&[0u8; 11]).is_err());
1876 }
1877
1878 #[test]
1879 fn test_limits_check() {
1880 let limits = Limits::new().with_max_width(100);
1881 let cfg = LosslessConfig::new();
1882 let req = cfg
1883 .encode_request(200, 100, PixelLayout::Rgb8)
1884 .with_limits(&limits);
1885 assert!(req.check_limits().is_err());
1886 }
1887
1888 #[test]
1889 fn test_lossless_encode_rgb8_small() {
1890 let pixels = [255u8, 0, 0].repeat(16);
1892 let result = LosslessConfig::new()
1893 .encode_request(4, 4, PixelLayout::Rgb8)
1894 .encode(&pixels);
1895 assert!(result.is_ok());
1896 let jxl = result.unwrap();
1897 assert_eq!(&jxl[..2], &[0xFF, 0x0A]); }
1899
1900 #[test]
1901 fn test_lossy_encode_rgb8_small() {
1902 let mut pixels = Vec::with_capacity(8 * 8 * 3);
1904 for y in 0..8u8 {
1905 for x in 0..8u8 {
1906 pixels.push(x * 32);
1907 pixels.push(y * 32);
1908 pixels.push(128);
1909 }
1910 }
1911 let result = LossyConfig::new(2.0)
1912 .with_gaborish(false)
1913 .encode_request(8, 8, PixelLayout::Rgb8)
1914 .encode(&pixels);
1915 assert!(result.is_ok());
1916 let jxl = result.unwrap();
1917 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
1918 }
1919
1920 #[test]
1921 fn test_fluent_lossless() {
1922 let pixels = vec![128u8; 4 * 4 * 3];
1923 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Rgb8);
1924 assert!(result.is_ok());
1925 }
1926
1927 #[test]
1928 fn test_lossy_unsupported_gray() {
1929 let pixels = vec![128u8; 8 * 8];
1930 let result = LossyConfig::new(1.0)
1931 .encode_request(8, 8, PixelLayout::Gray8)
1932 .encode(&pixels);
1933 assert!(matches!(
1934 result.as_ref().map_err(|e| e.error()),
1935 Err(EncodeError::UnsupportedPixelLayout(_))
1936 ));
1937 }
1938
1939 #[test]
1940 fn test_bgra_lossless() {
1941 let pixels = [0u8, 0, 255, 255].repeat(16);
1943 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Bgra8);
1944 assert!(result.is_ok());
1945 let jxl = result.unwrap();
1946 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
1947 }
1948
1949 #[test]
1950 fn test_lossy_alpha_encodes() {
1951 let pixels = [255u8, 0, 0, 255].repeat(64);
1953 let result =
1954 LossyConfig::new(2.0)
1955 .with_gaborish(false)
1956 .encode(&pixels, 8, 8, PixelLayout::Bgra8);
1957 assert!(
1958 result.is_ok(),
1959 "BGRA lossy encode failed: {:?}",
1960 result.err()
1961 );
1962
1963 let result2 = LossyConfig::new(2.0).encode(&pixels, 8, 8, PixelLayout::Rgba8);
1964 assert!(
1965 result2.is_ok(),
1966 "RGBA lossy encode failed: {:?}",
1967 result2.err()
1968 );
1969 }
1970
1971 #[test]
1972 fn test_stop_cancellation() {
1973 use enough::Unstoppable;
1974 let pixels = vec![128u8; 4 * 4 * 3];
1976 let cfg = LosslessConfig::new();
1977 let result = cfg
1978 .encode_request(4, 4, PixelLayout::Rgb8)
1979 .with_stop(&Unstoppable)
1980 .encode(&pixels);
1981 assert!(result.is_ok());
1982 }
1983}