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::DimensionOverflow {
90 width,
91 height,
92 channels,
93 } => Self::InvalidInput {
94 message: format!("dimension overflow: {width}x{height}x{channels} exceeds usize"),
95 },
96 crate::error::Error::InvalidInput(msg) => Self::InvalidInput { message: msg },
97 crate::error::Error::OutOfMemory(e) => Self::Oom(e),
98 #[cfg(feature = "std")]
99 crate::error::Error::IoError(e) => Self::Io(e),
100 crate::error::Error::Cancelled => Self::Cancelled,
101 other => Self::Internal {
102 message: format!("{other}"),
103 },
104 }
105 }
106}
107
108#[cfg(feature = "std")]
109impl From<std::io::Error> for EncodeError {
110 fn from(e: std::io::Error) -> Self {
111 Self::Io(e)
112 }
113}
114
115impl From<enough::StopReason> for EncodeError {
116 fn from(_: enough::StopReason) -> Self {
117 Self::Cancelled
118 }
119}
120
121pub type Result<T> = core::result::Result<T, At<EncodeError>>;
126
127#[derive(Clone, Debug)]
135pub struct EncodeResult {
136 data: Option<Vec<u8>>,
137 stats: EncodeStats,
138}
139
140impl EncodeResult {
141 pub fn data(&self) -> Option<&[u8]> {
143 self.data.as_deref()
144 }
145
146 pub fn take_data(&mut self) -> Option<Vec<u8>> {
148 self.data.take()
149 }
150
151 pub fn stats(&self) -> &EncodeStats {
153 &self.stats
154 }
155}
156
157#[derive(Clone, Debug, Default)]
159#[non_exhaustive]
160pub struct EncodeStats {
161 codestream_size: usize,
162 output_size: usize,
163 mode: EncodeMode,
164 strategy_counts: [u32; 19],
166 gaborish: bool,
167 ans: bool,
168 butteraugli_iters: u32,
169 pixel_domain_loss: bool,
170}
171
172impl EncodeStats {
173 pub fn codestream_size(&self) -> usize {
175 self.codestream_size
176 }
177
178 pub fn output_size(&self) -> usize {
180 self.output_size
181 }
182
183 pub fn mode(&self) -> EncodeMode {
185 self.mode
186 }
187
188 pub fn strategy_counts(&self) -> &[u32; 19] {
190 &self.strategy_counts
191 }
192
193 pub fn gaborish(&self) -> bool {
195 self.gaborish
196 }
197
198 pub fn ans(&self) -> bool {
200 self.ans
201 }
202
203 pub fn butteraugli_iters(&self) -> u32 {
205 self.butteraugli_iters
206 }
207
208 pub fn pixel_domain_loss(&self) -> bool {
210 self.pixel_domain_loss
211 }
212}
213
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum EncodeMode {
217 #[default]
219 Lossy,
220 Lossless,
222}
223
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228#[non_exhaustive]
229pub enum PixelLayout {
230 Rgb8,
232 Rgba8,
234 Bgr8,
236 Bgra8,
238 Gray8,
240 GrayAlpha8,
242 Rgb16,
244 Rgba16,
246 Gray16,
248 GrayAlpha16,
250 RgbLinearF32,
252 RgbaLinearF32,
254 GrayLinearF32,
256 GrayAlphaLinearF32,
258}
259
260impl PixelLayout {
261 pub const fn bytes_per_pixel(self) -> usize {
263 match self {
264 Self::Rgb8 | Self::Bgr8 => 3,
265 Self::Rgba8 | Self::Bgra8 => 4,
266 Self::Gray8 => 1,
267 Self::GrayAlpha8 => 2,
268 Self::Rgb16 => 6,
269 Self::Rgba16 => 8,
270 Self::Gray16 => 2,
271 Self::GrayAlpha16 => 4,
272 Self::RgbLinearF32 => 12,
273 Self::RgbaLinearF32 => 16,
274 Self::GrayLinearF32 => 4,
275 Self::GrayAlphaLinearF32 => 8,
276 }
277 }
278
279 pub const fn is_linear(self) -> bool {
281 matches!(
282 self,
283 Self::RgbLinearF32
284 | Self::RgbaLinearF32
285 | Self::GrayLinearF32
286 | Self::GrayAlphaLinearF32
287 )
288 }
289
290 pub const fn is_16bit(self) -> bool {
292 matches!(
293 self,
294 Self::Rgb16 | Self::Rgba16 | Self::Gray16 | Self::GrayAlpha16
295 )
296 }
297
298 pub const fn is_f32(self) -> bool {
300 matches!(
301 self,
302 Self::RgbLinearF32
303 | Self::RgbaLinearF32
304 | Self::GrayLinearF32
305 | Self::GrayAlphaLinearF32
306 )
307 }
308
309 pub const fn has_alpha(self) -> bool {
311 matches!(
312 self,
313 Self::Rgba8
314 | Self::Bgra8
315 | Self::GrayAlpha8
316 | Self::Rgba16
317 | Self::GrayAlpha16
318 | Self::RgbaLinearF32
319 | Self::GrayAlphaLinearF32
320 )
321 }
322
323 pub const fn is_grayscale(self) -> bool {
325 matches!(
326 self,
327 Self::Gray8
328 | Self::GrayAlpha8
329 | Self::Gray16
330 | Self::GrayAlpha16
331 | Self::GrayLinearF32
332 | Self::GrayAlphaLinearF32
333 )
334 }
335}
336
337#[derive(Clone, Copy, Debug)]
341#[non_exhaustive]
342pub enum Quality {
343 Distance(f32),
345 Percent(u32),
347}
348
349impl Quality {
350 fn to_distance(self) -> core::result::Result<f32, EncodeError> {
352 match self {
353 Self::Distance(d) => {
354 if d <= 0.0 {
355 return Err(EncodeError::InvalidConfig {
356 message: format!("lossy distance must be > 0.0, got {d}"),
357 });
358 }
359 Ok(d)
360 }
361 Self::Percent(q) => {
362 if q >= 100 {
363 return Err(EncodeError::InvalidConfig {
364 message: "quality 100 is lossless; use LosslessConfig instead".into(),
365 });
366 }
367 Ok(percent_to_distance(q))
368 }
369 }
370 }
371}
372
373fn percent_to_distance(quality: u32) -> f32 {
374 if quality >= 100 {
375 0.0
376 } else if quality >= 90 {
377 (100 - quality) as f32 / 10.0
378 } else if quality >= 70 {
379 1.0 + (90 - quality) as f32 / 20.0
380 } else {
381 2.0 + (70 - quality) as f32 / 10.0
382 }
383}
384
385#[must_use]
392pub fn quality_to_distance(quality: f32) -> f32 {
393 let q = quality.clamp(0.0, 100.0);
394 if q >= 100.0 {
395 0.0
396 } else if q >= 90.0 {
397 (100.0 - q) / 10.0
398 } else if q >= 70.0 {
399 1.0 + (90.0 - q) / 20.0
400 } else {
401 2.0 + (70.0 - q) / 10.0
402 }
403}
404
405#[must_use]
411pub fn calibrated_jxl_quality(generic_q: f32) -> f32 {
412 let clamped = generic_q.clamp(0.0, 100.0);
413 const TABLE: &[(f32, f32)] = &[
414 (5.0, 5.0),
415 (10.0, 5.0),
416 (15.0, 5.0),
417 (20.0, 5.0),
418 (25.0, 9.3),
419 (30.0, 22.7),
420 (35.0, 33.0),
421 (40.0, 38.8),
422 (45.0, 43.8),
423 (50.0, 48.5),
424 (55.0, 51.9),
425 (60.0, 55.1),
426 (65.0, 58.0),
427 (70.0, 61.3),
428 (72.0, 63.2),
429 (75.0, 65.5),
430 (78.0, 67.9),
431 (80.0, 69.1),
432 (82.0, 71.8),
433 (85.0, 76.1),
434 (87.0, 79.3),
435 (90.0, 84.2),
436 (92.0, 86.9),
437 (95.0, 91.2),
438 (97.0, 92.8),
439 (99.0, 93.8),
440 ];
441 interp_quality(TABLE, clamped)
442}
443
444fn interp_quality(table: &[(f32, f32)], x: f32) -> f32 {
446 if x <= table[0].0 {
447 return table[0].1;
448 }
449 if x >= table[table.len() - 1].0 {
450 return table[table.len() - 1].1;
451 }
452 for i in 1..table.len() {
453 if x <= table[i].0 {
454 let (x0, y0) = table[i - 1];
455 let (x1, y1) = table[i];
456 let t = (x - x0) / (x1 - x0);
457 return y0 + t * (y1 - y0);
458 }
459 }
460 table[table.len() - 1].1
461}
462
463#[derive(Clone, Debug, Default)]
467pub struct ImageMetadata<'a> {
468 icc_profile: Option<&'a [u8]>,
469 exif: Option<&'a [u8]>,
470 xmp: Option<&'a [u8]>,
471 intensity_target: Option<f32>,
473 min_nits: Option<f32>,
475 intrinsic_size: Option<(u32, u32)>,
477}
478
479impl<'a> ImageMetadata<'a> {
480 pub fn new() -> Self {
482 Self::default()
483 }
484
485 pub fn with_icc_profile(mut self, data: &'a [u8]) -> Self {
487 self.icc_profile = Some(data);
488 self
489 }
490
491 pub fn with_exif(mut self, data: &'a [u8]) -> Self {
493 self.exif = Some(data);
494 self
495 }
496
497 pub fn with_xmp(mut self, data: &'a [u8]) -> Self {
499 self.xmp = Some(data);
500 self
501 }
502
503 pub fn icc_profile(&self) -> Option<&[u8]> {
505 self.icc_profile
506 }
507
508 pub fn exif(&self) -> Option<&[u8]> {
510 self.exif
511 }
512
513 pub fn xmp(&self) -> Option<&[u8]> {
515 self.xmp
516 }
517
518 pub fn with_intensity_target(mut self, nits: f32) -> Self {
523 self.intensity_target = Some(nits);
524 self
525 }
526
527 pub fn with_min_nits(mut self, nits: f32) -> Self {
532 self.min_nits = Some(nits);
533 self
534 }
535
536 pub fn intensity_target(&self) -> Option<f32> {
538 self.intensity_target
539 }
540
541 pub fn min_nits(&self) -> Option<f32> {
543 self.min_nits
544 }
545
546 pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
551 self.intrinsic_size = Some((width, height));
552 self
553 }
554
555 pub fn intrinsic_size(&self) -> Option<(u32, u32)> {
557 self.intrinsic_size
558 }
559}
560
561#[derive(Clone, Debug, Default)]
563pub struct Limits {
564 max_width: Option<u64>,
565 max_height: Option<u64>,
566 max_pixels: Option<u64>,
567 max_memory_bytes: Option<u64>,
568}
569
570impl Limits {
571 pub fn new() -> Self {
573 Self::default()
574 }
575
576 pub fn with_max_width(mut self, w: u64) -> Self {
578 self.max_width = Some(w);
579 self
580 }
581
582 pub fn with_max_height(mut self, h: u64) -> Self {
584 self.max_height = Some(h);
585 self
586 }
587
588 pub fn with_max_pixels(mut self, p: u64) -> Self {
590 self.max_pixels = Some(p);
591 self
592 }
593
594 pub fn with_max_memory_bytes(mut self, bytes: u64) -> Self {
596 self.max_memory_bytes = Some(bytes);
597 self
598 }
599
600 pub fn max_width(&self) -> Option<u64> {
602 self.max_width
603 }
604
605 pub fn max_height(&self) -> Option<u64> {
607 self.max_height
608 }
609
610 pub fn max_pixels(&self) -> Option<u64> {
612 self.max_pixels
613 }
614
615 pub fn max_memory_bytes(&self) -> Option<u64> {
617 self.max_memory_bytes
618 }
619}
620
621#[derive(Clone, Debug)]
625pub struct AnimationParams {
626 pub tps_numerator: u32,
628 pub tps_denominator: u32,
630 pub num_loops: u32,
632}
633
634impl Default for AnimationParams {
635 fn default() -> Self {
636 Self {
637 tps_numerator: 100,
638 tps_denominator: 1,
639 num_loops: 0,
640 }
641 }
642}
643
644pub struct AnimationFrame<'a> {
646 pub pixels: &'a [u8],
648 pub duration: u32,
650}
651
652#[derive(Clone, Debug)]
658pub struct LosslessConfig {
659 effort: u8,
660 mode: EncoderMode,
661 use_ans: bool,
662 squeeze: bool,
663 tree_learning: bool,
664 lz77: bool,
665 lz77_method: Lz77Method,
666 patches: bool,
667 lossy_palette: bool,
668 threads: usize,
669 profile_override: Option<crate::effort::EffortProfile>,
673}
674
675impl Default for LosslessConfig {
676 fn default() -> Self {
677 Self::with_effort_level(7)
678 }
679}
680
681impl LosslessConfig {
682 fn with_effort_level(effort: u8) -> Self {
683 let profile = crate::effort::EffortProfile::lossless(effort, EncoderMode::Reference);
684 Self {
685 effort: profile.effort,
686 mode: EncoderMode::Reference,
687 use_ans: profile.use_ans,
688 tree_learning: profile.tree_learning,
689 squeeze: false, lz77: profile.lz77,
691 lz77_method: profile.lz77_method,
692 patches: profile.patches,
693 lossy_palette: false,
694 threads: 0,
695 profile_override: None,
696 }
697 }
698
699 pub(crate) fn effective_profile(&self) -> crate::effort::EffortProfile {
702 self.profile_override
703 .clone()
704 .unwrap_or_else(|| crate::effort::EffortProfile::lossless(self.effort, self.mode))
705 }
706
707 #[cfg(feature = "__expert")]
724 #[doc(hidden)]
725 pub fn with_internal_params(mut self, params: crate::effort::LosslessInternalParams) -> Self {
726 let mut profile = crate::effort::EffortProfile::lossless(self.effort, self.mode);
727 params.apply_to(&mut profile);
728 self.profile_override = Some(profile);
729 self
730 }
731
732 pub fn new() -> Self {
734 Self::default()
735 }
736
737 pub fn with_effort(self, effort: u8) -> Self {
748 let mut new = Self::with_effort_level(effort);
749 new.mode = self.mode;
751 new.squeeze = self.squeeze;
752 new.profile_override = self.profile_override;
753 new
754 }
755
756 pub fn with_mode(mut self, mode: EncoderMode) -> Self {
761 self.mode = mode;
762 self
763 }
764
765 pub fn mode(&self) -> EncoderMode {
767 self.mode
768 }
769
770 pub fn with_patches(mut self, enable: bool) -> Self {
773 self.patches = enable;
774 self
775 }
776
777 pub fn with_ans(mut self, enable: bool) -> Self {
779 self.use_ans = enable;
780 self
781 }
782
783 pub fn with_squeeze(mut self, enable: bool) -> Self {
789 self.squeeze = enable;
790 self
791 }
792
793 pub fn with_tree_learning(mut self, enable: bool) -> Self {
795 self.tree_learning = enable;
796 self
797 }
798
799 pub fn with_lz77(mut self, enable: bool) -> Self {
801 self.lz77 = enable;
802 self
803 }
804
805 pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
807 self.lz77_method = method;
808 self
809 }
810
811 pub fn with_lossy_palette(mut self, enable: bool) -> Self {
818 self.lossy_palette = enable;
819 self
820 }
821
822 pub fn with_threads(mut self, threads: usize) -> Self {
832 self.threads = threads;
833 self
834 }
835
836 pub fn effort(&self) -> u8 {
840 self.effort
841 }
842
843 pub fn ans(&self) -> bool {
845 self.use_ans
846 }
847
848 pub fn squeeze(&self) -> bool {
850 self.squeeze
851 }
852
853 pub fn tree_learning(&self) -> bool {
855 self.tree_learning
856 }
857
858 pub fn lz77(&self) -> bool {
860 self.lz77
861 }
862
863 pub fn lz77_method(&self) -> Lz77Method {
865 self.lz77_method
866 }
867
868 pub fn patches(&self) -> bool {
870 self.patches
871 }
872
873 pub fn lossy_palette(&self) -> bool {
875 self.lossy_palette
876 }
877
878 pub fn threads(&self) -> usize {
880 self.threads
881 }
882
883 #[cfg(feature = "__expert")]
886 pub(crate) fn profile_override_ref(&self) -> Option<&crate::effort::EffortProfile> {
887 self.profile_override.as_ref()
888 }
889
890 pub fn encode_request(
896 &self,
897 width: u32,
898 height: u32,
899 layout: PixelLayout,
900 ) -> EncodeRequest<'_> {
901 EncodeRequest {
902 config: ConfigRef::Lossless(self),
903 width,
904 height,
905 layout,
906 metadata: None,
907 limits: None,
908 stop: None,
909 source_gamma: None,
910 color_encoding: None,
911 }
912 }
913
914 #[track_caller]
923 pub fn encode(
924 &self,
925 pixels: &[u8],
926 width: u32,
927 height: u32,
928 layout: PixelLayout,
929 ) -> Result<Vec<u8>> {
930 self.encode_request(width, height, layout).encode(pixels)
931 }
932
933 #[track_caller]
935 pub fn encode_into(
936 &self,
937 pixels: &[u8],
938 width: u32,
939 height: u32,
940 layout: PixelLayout,
941 out: &mut Vec<u8>,
942 ) -> Result<()> {
943 self.encode_request(width, height, layout)
944 .encode_into(pixels, out)
945 .map(|_| ())
946 }
947
948 #[track_caller]
953 pub fn encode_animation(
954 &self,
955 width: u32,
956 height: u32,
957 layout: PixelLayout,
958 animation: &AnimationParams,
959 frames: &[AnimationFrame<'_>],
960 ) -> Result<Vec<u8>> {
961 encode_animation_lossless(self, width, height, layout, animation, frames).map_err(at)
962 }
963}
964
965#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
974pub enum EncoderMode {
975 #[default]
981 Reference,
982
983 Experimental,
989}
990
991#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1003pub enum ProgressiveMode {
1004 #[default]
1006 Single,
1007 QuantizedAcFullAc,
1014 DcVlfLfAc,
1022}
1023
1024#[derive(Clone, Debug)]
1030pub struct LossyConfig {
1031 distance: f32,
1032 effort: u8,
1033 mode: EncoderMode,
1034 use_ans: bool,
1035 gaborish: bool,
1036 noise: bool,
1037 denoise: bool,
1038 error_diffusion: bool,
1039 pixel_domain_loss: bool,
1040 lz77: bool,
1041 lz77_method: Lz77Method,
1042 force_strategy: Option<u8>,
1043 max_strategy_size: Option<u8>,
1044 patches: bool,
1045 splines: Option<Vec<crate::vardct::splines::Spline>>,
1046 progressive: ProgressiveMode,
1047 lf_frame: bool,
1048 #[cfg(feature = "butteraugli-loop")]
1049 butteraugli_iters: u32,
1050 #[cfg(feature = "butteraugli-loop")]
1051 butteraugli_iters_explicit: bool,
1052 #[cfg(feature = "ssim2-loop")]
1053 ssim2_iters: u32,
1054 #[cfg(feature = "zensim-loop")]
1055 zensim_iters: u32,
1056 threads: usize,
1057 profile_override: Option<crate::effort::EffortProfile>,
1061}
1062
1063impl LossyConfig {
1064 pub fn new(distance: f32) -> Self {
1066 Self::new_with_effort(distance, 7)
1067 }
1068
1069 fn new_with_effort(distance: f32, effort: u8) -> Self {
1070 let profile = crate::effort::EffortProfile::lossy(effort, EncoderMode::Reference);
1071 Self {
1072 distance,
1073 effort: profile.effort,
1074 mode: EncoderMode::Reference,
1075 use_ans: profile.use_ans,
1076 gaborish: profile.gaborish,
1077 noise: false,
1078 denoise: false,
1079 error_diffusion: profile.error_diffusion,
1080 pixel_domain_loss: profile.pixel_domain_loss,
1081 lz77: profile.lz77,
1082 lz77_method: profile.lz77_method,
1083 force_strategy: None,
1084 max_strategy_size: None,
1085 patches: profile.patches,
1086 splines: None,
1087 progressive: ProgressiveMode::Single,
1088 lf_frame: false,
1089 #[cfg(feature = "butteraugli-loop")]
1090 butteraugli_iters: profile.butteraugli_iters,
1091 #[cfg(feature = "butteraugli-loop")]
1092 butteraugli_iters_explicit: false,
1093 #[cfg(feature = "ssim2-loop")]
1094 ssim2_iters: 0,
1095 #[cfg(feature = "zensim-loop")]
1096 zensim_iters: 0,
1097 threads: 0,
1098 profile_override: None,
1099 }
1100 }
1101
1102 pub(crate) fn effective_profile(&self) -> crate::effort::EffortProfile {
1105 self.profile_override
1106 .clone()
1107 .unwrap_or_else(|| crate::effort::EffortProfile::lossy(self.effort, self.mode))
1108 }
1109
1110 #[cfg(feature = "__expert")]
1127 #[doc(hidden)]
1128 pub fn with_internal_params(mut self, params: crate::effort::LossyInternalParams) -> Self {
1129 let mut profile = crate::effort::EffortProfile::lossy(self.effort, self.mode);
1130 params.apply_to(&mut profile);
1131 self.profile_override = Some(profile);
1132 self
1133 }
1134
1135 pub fn from_quality(quality: Quality) -> core::result::Result<Self, EncodeError> {
1137 let distance = quality.to_distance()?;
1138 Ok(Self::new(distance))
1139 }
1140
1141 pub fn with_effort(self, effort: u8) -> Self {
1154 let mut new = Self::new_with_effort(self.distance, effort);
1155 new.mode = self.mode;
1157 new.noise = self.noise;
1158 new.denoise = self.denoise;
1159 new.force_strategy = self.force_strategy;
1160 new.max_strategy_size = self.max_strategy_size;
1161 new.splines = self.splines;
1162 new.progressive = self.progressive;
1163 #[cfg(feature = "butteraugli-loop")]
1165 if self.butteraugli_iters_explicit {
1166 new.butteraugli_iters = self.butteraugli_iters;
1167 new.butteraugli_iters_explicit = true;
1168 }
1169 #[cfg(feature = "ssim2-loop")]
1170 {
1171 new.ssim2_iters = self.ssim2_iters;
1172 }
1173 #[cfg(feature = "zensim-loop")]
1174 {
1175 new.zensim_iters = self.zensim_iters;
1176 }
1177 new.profile_override = self.profile_override;
1178 new
1179 }
1180
1181 pub fn with_mode(mut self, mode: EncoderMode) -> Self {
1186 self.mode = mode;
1187 self
1188 }
1189
1190 pub fn mode(&self) -> EncoderMode {
1192 self.mode
1193 }
1194
1195 pub fn with_ans(mut self, enable: bool) -> Self {
1197 self.use_ans = enable;
1198 self
1199 }
1200
1201 pub fn with_gaborish(mut self, enable: bool) -> Self {
1203 self.gaborish = enable;
1204 self
1205 }
1206
1207 pub fn with_noise(mut self, enable: bool) -> Self {
1209 self.noise = enable;
1210 self
1211 }
1212
1213 pub fn with_denoise(mut self, enable: bool) -> Self {
1215 self.denoise = enable;
1216 if enable {
1217 self.noise = true;
1218 }
1219 self
1220 }
1221
1222 pub fn with_error_diffusion(mut self, enable: bool) -> Self {
1231 self.error_diffusion = enable;
1232 self
1233 }
1234
1235 pub fn with_pixel_domain_loss(mut self, enable: bool) -> Self {
1237 self.pixel_domain_loss = enable;
1238 self
1239 }
1240
1241 pub fn with_lz77(mut self, enable: bool) -> Self {
1243 self.lz77 = enable;
1244 self
1245 }
1246
1247 pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
1249 self.lz77_method = method;
1250 self
1251 }
1252
1253 pub fn with_force_strategy(mut self, strategy: Option<u8>) -> Self {
1255 self.force_strategy = strategy;
1256 self
1257 }
1258
1259 pub fn with_max_strategy_size(mut self, size: Option<u8>) -> Self {
1270 self.max_strategy_size = size;
1271 self
1272 }
1273
1274 pub fn with_patches(mut self, enable: bool) -> Self {
1277 self.patches = enable;
1278 self
1279 }
1280
1281 pub fn with_splines(mut self, splines: Vec<crate::vardct::splines::Spline>) -> Self {
1288 self.splines = Some(splines);
1289 self
1290 }
1291
1292 pub fn with_progressive(mut self, mode: ProgressiveMode) -> Self {
1297 self.progressive = mode;
1298 self
1299 }
1300
1301 pub fn with_lf_frame(mut self, enable: bool) -> Self {
1306 self.lf_frame = enable;
1307 self
1308 }
1309
1310 #[cfg(feature = "butteraugli-loop")]
1315 pub fn with_butteraugli_iters(mut self, n: u32) -> Self {
1316 self.butteraugli_iters = n;
1317 self.butteraugli_iters_explicit = true;
1318 self
1319 }
1320
1321 #[cfg(feature = "ssim2-loop")]
1326 pub fn with_ssim2_iters(mut self, n: u32) -> Self {
1327 self.ssim2_iters = n;
1328 self
1329 }
1330
1331 #[cfg(feature = "zensim-loop")]
1339 pub fn with_zensim_iters(mut self, n: u32) -> Self {
1340 self.zensim_iters = n;
1341 self
1342 }
1343
1344 pub fn with_threads(mut self, threads: usize) -> Self {
1354 self.threads = threads;
1355 self
1356 }
1357
1358 pub fn distance(&self) -> f32 {
1362 self.distance
1363 }
1364
1365 pub fn effort(&self) -> u8 {
1367 self.effort
1368 }
1369
1370 pub fn ans(&self) -> bool {
1372 self.use_ans
1373 }
1374
1375 pub fn gaborish(&self) -> bool {
1377 self.gaborish
1378 }
1379
1380 pub fn noise(&self) -> bool {
1382 self.noise
1383 }
1384
1385 pub fn denoise(&self) -> bool {
1387 self.denoise
1388 }
1389
1390 pub fn error_diffusion(&self) -> bool {
1392 self.error_diffusion
1393 }
1394
1395 pub fn pixel_domain_loss(&self) -> bool {
1397 self.pixel_domain_loss
1398 }
1399
1400 pub fn lz77(&self) -> bool {
1402 self.lz77
1403 }
1404
1405 pub fn lz77_method(&self) -> Lz77Method {
1407 self.lz77_method
1408 }
1409
1410 pub fn force_strategy(&self) -> Option<u8> {
1412 self.force_strategy
1413 }
1414
1415 pub fn max_strategy_size(&self) -> Option<u8> {
1417 self.max_strategy_size
1418 }
1419
1420 pub fn progressive(&self) -> ProgressiveMode {
1422 self.progressive
1423 }
1424
1425 pub fn lf_frame(&self) -> bool {
1427 self.lf_frame
1428 }
1429
1430 #[cfg(feature = "butteraugli-loop")]
1432 pub fn butteraugli_iters(&self) -> u32 {
1433 self.butteraugli_iters
1434 }
1435
1436 #[cfg(feature = "ssim2-loop")]
1438 pub(crate) fn ssim2_iters_value(&self) -> u32 {
1439 self.ssim2_iters
1440 }
1441
1442 #[cfg(feature = "zensim-loop")]
1444 pub(crate) fn zensim_iters_value(&self) -> u32 {
1445 self.zensim_iters
1446 }
1447
1448 #[cfg(feature = "__expert")]
1451 pub(crate) fn profile_override_ref(&self) -> Option<&crate::effort::EffortProfile> {
1452 self.profile_override.as_ref()
1453 }
1454
1455 pub fn threads(&self) -> usize {
1457 self.threads
1458 }
1459
1460 pub fn encode_request(
1466 &self,
1467 width: u32,
1468 height: u32,
1469 layout: PixelLayout,
1470 ) -> EncodeRequest<'_> {
1471 EncodeRequest {
1472 config: ConfigRef::Lossy(self),
1473 width,
1474 height,
1475 layout,
1476 metadata: None,
1477 limits: None,
1478 stop: None,
1479 source_gamma: None,
1480 color_encoding: None,
1481 }
1482 }
1483
1484 #[track_caller]
1493 pub fn encode(
1494 &self,
1495 pixels: &[u8],
1496 width: u32,
1497 height: u32,
1498 layout: PixelLayout,
1499 ) -> Result<Vec<u8>> {
1500 self.encode_request(width, height, layout).encode(pixels)
1501 }
1502
1503 #[track_caller]
1505 pub fn encode_into(
1506 &self,
1507 pixels: &[u8],
1508 width: u32,
1509 height: u32,
1510 layout: PixelLayout,
1511 out: &mut Vec<u8>,
1512 ) -> Result<()> {
1513 self.encode_request(width, height, layout)
1514 .encode_into(pixels, out)
1515 .map(|_| ())
1516 }
1517
1518 #[track_caller]
1523 pub fn encode_animation(
1524 &self,
1525 width: u32,
1526 height: u32,
1527 layout: PixelLayout,
1528 animation: &AnimationParams,
1529 frames: &[AnimationFrame<'_>],
1530 ) -> Result<Vec<u8>> {
1531 encode_animation_lossy(self, width, height, layout, animation, frames).map_err(at)
1532 }
1533}
1534
1535#[derive(Clone, Copy, Debug)]
1539enum ConfigRef<'a> {
1540 Lossless(&'a LosslessConfig),
1541 Lossy(&'a LossyConfig),
1542}
1543
1544pub struct EncodeRequest<'a> {
1548 config: ConfigRef<'a>,
1549 width: u32,
1550 height: u32,
1551 layout: PixelLayout,
1552 metadata: Option<&'a ImageMetadata<'a>>,
1553 limits: Option<&'a Limits>,
1554 stop: Option<&'a dyn Stop>,
1555 source_gamma: Option<f32>,
1556 color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
1557}
1558
1559impl<'a> EncodeRequest<'a> {
1560 pub fn with_metadata(mut self, meta: &'a ImageMetadata<'a>) -> Self {
1562 self.metadata = Some(meta);
1563 self
1564 }
1565
1566 pub fn with_limits(mut self, limits: &'a Limits) -> Self {
1568 self.limits = Some(limits);
1569 self
1570 }
1571
1572 pub fn with_stop(mut self, stop: &'a dyn Stop) -> Self {
1577 self.stop = Some(stop);
1578 self
1579 }
1580
1581 pub fn with_source_gamma(mut self, gamma: f32) -> Self {
1589 self.source_gamma = Some(gamma);
1590 self
1591 }
1592
1593 pub fn with_color_encoding(
1605 mut self,
1606 ce: crate::headers::color_encoding::ColorEncoding,
1607 ) -> Self {
1608 self.color_encoding = Some(ce);
1609 self
1610 }
1611
1612 #[track_caller]
1614 pub fn encode(self, pixels: &[u8]) -> Result<Vec<u8>> {
1615 self.encode_inner(pixels)
1616 .map(|mut r| r.take_data().unwrap())
1617 .map_err(at)
1618 }
1619
1620 #[track_caller]
1622 pub fn encode_with_stats(self, pixels: &[u8]) -> Result<EncodeResult> {
1623 self.encode_inner(pixels).map_err(at)
1624 }
1625
1626 #[track_caller]
1628 pub fn encode_into(self, pixels: &[u8], out: &mut Vec<u8>) -> Result<EncodeResult> {
1629 let mut result = self.encode_inner(pixels).map_err(at)?;
1630 if let Some(data) = result.data.take() {
1631 out.extend_from_slice(&data);
1632 }
1633 Ok(result)
1634 }
1635
1636 #[cfg(feature = "std")]
1638 #[track_caller]
1639 pub fn encode_to(self, pixels: &[u8], mut dest: impl std::io::Write) -> Result<EncodeResult> {
1640 let mut result = self.encode_inner(pixels).map_err(at)?;
1641 if let Some(data) = result.data.take() {
1642 dest.write_all(&data)
1643 .map_err(|e| at(EncodeError::from(e)))?;
1644 }
1645 Ok(result)
1646 }
1647
1648 fn encode_inner(&self, pixels: &[u8]) -> core::result::Result<EncodeResult, EncodeError> {
1649 self.validate_pixels(pixels)?;
1650 self.check_limits()?;
1651
1652 let threads = match self.config {
1653 ConfigRef::Lossless(cfg) => cfg.threads,
1654 ConfigRef::Lossy(cfg) => cfg.threads,
1655 };
1656
1657 let (codestream, mut stats) = run_with_threads(threads, || match self.config {
1658 ConfigRef::Lossless(cfg) => self.encode_lossless(cfg, pixels),
1659 ConfigRef::Lossy(cfg) => self.encode_lossy(cfg, pixels),
1660 })?;
1661
1662 stats.codestream_size = codestream.len();
1663
1664 let output = if let Some(meta) = self.metadata
1666 && (meta.exif.is_some() || meta.xmp.is_some())
1667 {
1668 crate::container::wrap_in_container(&codestream, meta.exif, meta.xmp)
1669 } else {
1670 codestream
1671 };
1672
1673 stats.output_size = output.len();
1674
1675 Ok(EncodeResult {
1676 data: Some(output),
1677 stats,
1678 })
1679 }
1680
1681 fn validate_pixels(&self, pixels: &[u8]) -> core::result::Result<(), EncodeError> {
1682 let w = self.width as usize;
1683 let h = self.height as usize;
1684 if w == 0 || h == 0 {
1685 return Err(EncodeError::InvalidInput {
1686 message: format!("zero dimensions: {w}x{h}"),
1687 });
1688 }
1689 const MAX_JXL_DIM: u32 = 1 << 30;
1691 if self.width > MAX_JXL_DIM || self.height > MAX_JXL_DIM {
1692 return Err(EncodeError::LimitExceeded {
1693 message: format!(
1694 "image {}x{} exceeds JXL spec maximum of {MAX_JXL_DIM} per dimension",
1695 self.width, self.height
1696 ),
1697 });
1698 }
1699 let expected = w
1700 .checked_mul(h)
1701 .and_then(|n| n.checked_mul(self.layout.bytes_per_pixel()));
1702 match expected {
1703 Some(expected) if pixels.len() == expected => Ok(()),
1704 Some(expected) => Err(EncodeError::InvalidInput {
1705 message: format!(
1706 "pixel buffer size mismatch: expected {expected} bytes for {w}x{h} {:?}, got {}",
1707 self.layout,
1708 pixels.len()
1709 ),
1710 }),
1711 None => Err(EncodeError::InvalidInput {
1712 message: "image dimensions overflow".into(),
1713 }),
1714 }
1715 }
1716
1717 fn check_limits(&self) -> core::result::Result<(), EncodeError> {
1718 let Some(limits) = self.limits else {
1719 return Ok(());
1720 };
1721 let w = self.width as u64;
1722 let h = self.height as u64;
1723 if let Some(max_w) = limits.max_width
1724 && w > max_w
1725 {
1726 return Err(EncodeError::LimitExceeded {
1727 message: format!("width {w} > max {max_w}"),
1728 });
1729 }
1730 if let Some(max_h) = limits.max_height
1731 && h > max_h
1732 {
1733 return Err(EncodeError::LimitExceeded {
1734 message: format!("height {h} > max {max_h}"),
1735 });
1736 }
1737 if let Some(max_px) = limits.max_pixels
1738 && w * h > max_px
1739 {
1740 return Err(EncodeError::LimitExceeded {
1741 message: format!("pixels {}x{} = {} > max {max_px}", w, h, w * h),
1742 });
1743 }
1744 if let Some(max_mem) = limits.max_memory_bytes {
1745 let estimated = w.saturating_mul(h).saturating_mul(40);
1748 if estimated > max_mem {
1749 return Err(EncodeError::LimitExceeded {
1750 message: format!(
1751 "estimated memory {estimated} bytes > max {max_mem} bytes \
1752 (for {w}x{h} image)"
1753 ),
1754 });
1755 }
1756 }
1757 Ok(())
1758 }
1759
1760 fn encode_lossless(
1763 &self,
1764 cfg: &LosslessConfig,
1765 pixels: &[u8],
1766 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1767 use crate::bit_writer::BitWriter;
1768 use crate::headers::color_encoding::ColorSpace;
1769 use crate::headers::{ColorEncoding, FileHeader};
1770 use crate::modular::channel::ModularImage;
1771 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
1772
1773 let w = self.width as usize;
1774 let h = self.height as usize;
1775
1776 let rgb_pixels;
1778 let detection_pixels: &[u8] = match self.layout {
1779 PixelLayout::Bgr8 => {
1780 rgb_pixels = bgr_to_rgb(pixels, 3);
1781 &rgb_pixels
1782 }
1783 PixelLayout::Bgra8 => {
1784 rgb_pixels = bgr_to_rgb(pixels, 4);
1785 &rgb_pixels
1786 }
1787 _ => {
1788 rgb_pixels = Vec::new();
1789 let _ = &rgb_pixels;
1790 pixels
1791 }
1792 };
1793
1794 let mut image = match self.layout {
1796 PixelLayout::Rgb8 => ModularImage::from_rgb8(pixels, w, h),
1797 PixelLayout::Rgba8 => ModularImage::from_rgba8(pixels, w, h),
1798 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(pixels, 3), w, h),
1799 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(pixels, 4), w, h),
1800 PixelLayout::Gray8 => ModularImage::from_gray8(pixels, w, h),
1801 PixelLayout::GrayAlpha8 => ModularImage::from_grayalpha8(pixels, w, h),
1802 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(pixels, w, h),
1803 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(pixels, w, h),
1804 PixelLayout::Gray16 => ModularImage::from_gray16_native(pixels, w, h),
1805 PixelLayout::GrayAlpha16 => ModularImage::from_grayalpha16_native(pixels, w, h),
1806 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
1807 }
1808 .map_err(EncodeError::from)?;
1809
1810 let num_channels = self.layout.bytes_per_pixel();
1812 let can_use_patches =
1813 cfg.patches && !image.is_grayscale && image.bit_depth <= 8 && num_channels >= 3;
1814 let patches_data = if can_use_patches {
1815 crate::vardct::patches::find_and_build_lossless(
1816 detection_pixels,
1817 w,
1818 h,
1819 num_channels,
1820 image.bit_depth,
1821 )
1822 } else {
1823 None
1824 };
1825
1826 let mut file_header = if image.is_grayscale {
1828 FileHeader::new_gray(self.width, self.height)
1829 } else if image.has_alpha {
1830 FileHeader::new_rgba(self.width, self.height)
1831 } else {
1832 FileHeader::new_rgb(self.width, self.height)
1833 };
1834 if image.bit_depth == 16 {
1835 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
1836 for ec in &mut file_header.metadata.extra_channels {
1837 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
1838 }
1839 }
1840 if let Some(meta) = self.metadata {
1841 if meta.icc_profile.is_some() {
1842 file_header.metadata.color_encoding.want_icc = true;
1843 }
1844 if let Some(it) = meta.intensity_target {
1845 file_header.metadata.intensity_target = it;
1846 }
1847 if let Some(mn) = meta.min_nits {
1848 file_header.metadata.min_nits = mn;
1849 }
1850 if let Some((w, h)) = meta.intrinsic_size {
1851 file_header.metadata.have_intrinsic_size = true;
1852 file_header.metadata.intrinsic_width = w;
1853 file_header.metadata.intrinsic_height = h;
1854 }
1855 }
1856
1857 let mut writer = BitWriter::new();
1859 file_header.write(&mut writer).map_err(EncodeError::from)?;
1860 if let Some(meta) = self.metadata
1861 && let Some(icc) = meta.icc_profile
1862 {
1863 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
1864 }
1865 writer.zero_pad_to_byte();
1866
1867 if let Some(ref pd) = patches_data {
1869 let lossless_profile = cfg.effective_profile();
1870 crate::vardct::patches::encode_reference_frame_rgb(
1871 pd,
1872 image.bit_depth,
1873 cfg.use_ans,
1874 lossless_profile.patch_ref_tree_learning,
1875 &mut writer,
1876 )
1877 .map_err(EncodeError::from)?;
1878 writer.zero_pad_to_byte();
1879 let bd = image.bit_depth;
1880 crate::vardct::patches::subtract_patches_modular(&mut image, pd, bd);
1881 }
1882
1883 let use_tree_learning = cfg.tree_learning;
1885 let frame_encoder = FrameEncoder::new(
1886 w,
1887 h,
1888 FrameEncoderOptions {
1889 use_modular: true,
1890 effort: cfg.effort,
1891 use_ans: cfg.use_ans,
1892 use_tree_learning,
1893 use_squeeze: cfg.squeeze,
1894 enable_lz77: cfg.lz77,
1895 lz77_method: cfg.lz77_method,
1896 lossy_palette: cfg.lossy_palette,
1897 encoder_mode: cfg.mode,
1898 profile: cfg.effective_profile(),
1899 have_animation: false,
1900 duration: 0,
1901 is_last: true,
1902 crop: None,
1903 skip_rct: false,
1904 },
1905 );
1906 let color_encoding = if let Some(ce) = self.color_encoding.clone() {
1907 if image.is_grayscale && ce.color_space != ColorSpace::Gray {
1910 ColorEncoding {
1911 color_space: ColorSpace::Gray,
1912 ..ce
1913 }
1914 } else {
1915 ce
1916 }
1917 } else if let Some(gamma) = self.source_gamma {
1918 if image.is_grayscale {
1919 ColorEncoding::gray_with_gamma(gamma)
1920 } else {
1921 ColorEncoding::with_gamma(gamma)
1922 }
1923 } else if image.is_grayscale {
1924 ColorEncoding::gray()
1925 } else {
1926 ColorEncoding::srgb()
1927 };
1928 frame_encoder
1929 .encode_modular_with_patches(
1930 &image,
1931 &color_encoding,
1932 &mut writer,
1933 patches_data.as_ref(),
1934 )
1935 .map_err(EncodeError::from)?;
1936
1937 let stats = EncodeStats {
1938 mode: EncodeMode::Lossless,
1939 ans: cfg.use_ans,
1940 ..Default::default()
1941 };
1942 Ok((writer.finish_with_padding(), stats))
1943 }
1944
1945 fn encode_lossy(
1948 &self,
1949 cfg: &LossyConfig,
1950 pixels: &[u8],
1951 ) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
1952 let w = self.width as usize;
1953 let h = self.height as usize;
1954
1955 let gamma = self.source_gamma;
1959 let (linear_rgb, alpha, bit_depth_16) = match self.layout {
1960 PixelLayout::Rgb8 => {
1961 let linear = if let Some(g) = gamma {
1962 gamma_u8_to_linear_f32(pixels, 3, g)
1963 } else {
1964 srgb_u8_to_linear_f32(pixels, 3)
1965 };
1966 (linear, None, false)
1967 }
1968 PixelLayout::Bgr8 => {
1969 let rgb = bgr_to_rgb(pixels, 3);
1970 let linear = if let Some(g) = gamma {
1971 gamma_u8_to_linear_f32(&rgb, 3, g)
1972 } else {
1973 srgb_u8_to_linear_f32(&rgb, 3)
1974 };
1975 (linear, None, false)
1976 }
1977 PixelLayout::Rgba8 => {
1978 let rgb = if let Some(g) = gamma {
1979 gamma_u8_to_linear_f32(pixels, 4, g)
1980 } else {
1981 srgb_u8_to_linear_f32(pixels, 4)
1982 };
1983 let alpha = extract_alpha(pixels, 4, 3);
1984 (rgb, Some(alpha), false)
1985 }
1986 PixelLayout::Bgra8 => {
1987 let swapped = bgr_to_rgb(pixels, 4);
1988 let rgb = if let Some(g) = gamma {
1989 gamma_u8_to_linear_f32(&swapped, 4, g)
1990 } else {
1991 srgb_u8_to_linear_f32(&swapped, 4)
1992 };
1993 let alpha = extract_alpha(pixels, 4, 3);
1994 (rgb, Some(alpha), false)
1995 }
1996 PixelLayout::Gray8 => {
1997 let rgb = if let Some(g) = gamma {
1998 gamma_gray_u8_to_linear_f32_rgb(pixels, 1, g)
1999 } else {
2000 gray_u8_to_linear_f32_rgb(pixels, 1)
2001 };
2002 (rgb, None, false)
2003 }
2004 PixelLayout::GrayAlpha8 => {
2005 let rgb = if let Some(g) = gamma {
2006 gamma_gray_u8_to_linear_f32_rgb(pixels, 2, g)
2007 } else {
2008 gray_u8_to_linear_f32_rgb(pixels, 2)
2009 };
2010 let alpha = extract_alpha(pixels, 2, 1);
2011 (rgb, Some(alpha), false)
2012 }
2013 PixelLayout::Rgb16 => {
2014 let linear = if let Some(g) = gamma {
2015 gamma_u16_to_linear_f32(pixels, 3, g)
2016 } else {
2017 srgb_u16_to_linear_f32(pixels, 3)
2018 };
2019 (linear, None, true)
2020 }
2021 PixelLayout::Rgba16 => {
2022 let rgb = if let Some(g) = gamma {
2023 gamma_u16_to_linear_f32(pixels, 4, g)
2024 } else {
2025 srgb_u16_to_linear_f32(pixels, 4)
2026 };
2027 let alpha = extract_alpha_u16(pixels, 4, 3);
2028 (rgb, Some(alpha), true)
2029 }
2030 PixelLayout::Gray16 => {
2031 let rgb = if let Some(g) = gamma {
2032 gamma_gray_u16_to_linear_f32_rgb(pixels, 1, g)
2033 } else {
2034 gray_u16_to_linear_f32_rgb(pixels, 1)
2035 };
2036 (rgb, None, true)
2037 }
2038 PixelLayout::GrayAlpha16 => {
2039 let rgb = if let Some(g) = gamma {
2040 gamma_gray_u16_to_linear_f32_rgb(pixels, 2, g)
2041 } else {
2042 gray_u16_to_linear_f32_rgb(pixels, 2)
2043 };
2044 let alpha = extract_alpha_u16(pixels, 2, 1);
2045 (rgb, Some(alpha), true)
2046 }
2047 PixelLayout::RgbLinearF32 => {
2048 let floats: &[f32] = bytemuck::cast_slice(pixels);
2049 (floats.to_vec(), None, false)
2050 }
2051 PixelLayout::RgbaLinearF32 => {
2052 let floats: &[f32] = bytemuck::cast_slice(pixels);
2053 let rgb: Vec<f32> = floats
2054 .chunks(4)
2055 .flat_map(|px| [px[0], px[1], px[2]])
2056 .collect();
2057 let alpha = extract_alpha_f32(floats, 4, 3);
2058 (rgb, Some(alpha), false)
2059 }
2060 PixelLayout::GrayLinearF32 => {
2061 let floats: &[f32] = bytemuck::cast_slice(pixels);
2062 (gray_f32_to_linear_f32_rgb(floats, 1), None, false)
2063 }
2064 PixelLayout::GrayAlphaLinearF32 => {
2065 let floats: &[f32] = bytemuck::cast_slice(pixels);
2066 let rgb = gray_f32_to_linear_f32_rgb(floats, 2);
2067 let alpha = extract_alpha_f32(floats, 2, 1);
2068 (rgb, Some(alpha), false)
2069 }
2070 };
2071
2072 let mut profile = cfg.effective_profile();
2073
2074 if let Some(max_size) = cfg.max_strategy_size {
2076 if max_size < 16 {
2077 profile.try_dct16 = false;
2078 }
2079 if max_size < 32 {
2080 profile.try_dct32 = false;
2081 }
2082 if max_size < 64 {
2083 profile.try_dct64 = false;
2084 }
2085 }
2086
2087 let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
2088 enc.effort = cfg.effort;
2089 enc.profile = profile;
2090 enc.use_ans = cfg.use_ans;
2091 enc.optimize_codes = enc.profile.optimize_codes;
2092 enc.custom_orders = enc.profile.custom_orders;
2093 enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
2094 enc.enable_noise = cfg.noise;
2095 enc.enable_denoise = cfg.denoise;
2096 enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
2098 enc.error_diffusion = cfg.error_diffusion;
2099 enc.pixel_domain_loss = cfg.pixel_domain_loss;
2100 enc.enable_lz77 = cfg.lz77;
2101 enc.lz77_method = cfg.lz77_method;
2102 enc.force_strategy = cfg.force_strategy;
2103 enc.enable_patches = cfg.patches;
2104 enc.encoder_mode = cfg.mode;
2105 enc.splines = cfg.splines.clone();
2106 enc.is_grayscale = self.layout.is_grayscale();
2107 enc.progressive = cfg.progressive;
2108 enc.use_lf_frame = cfg.lf_frame;
2109 #[cfg(feature = "butteraugli-loop")]
2110 {
2111 enc.butteraugli_iters = cfg.butteraugli_iters;
2112 }
2113 #[cfg(feature = "ssim2-loop")]
2114 {
2115 enc.ssim2_iters = cfg.ssim2_iters;
2116 }
2117 #[cfg(feature = "zensim-loop")]
2118 {
2119 enc.zensim_iters = cfg.zensim_iters;
2120 }
2121
2122 enc.bit_depth_16 = bit_depth_16;
2123 enc.source_gamma = self.source_gamma;
2124 enc.color_encoding = self.color_encoding.clone();
2125
2126 if let Some(meta) = self.metadata {
2128 if let Some(it) = meta.intensity_target {
2129 enc.intensity_target = it;
2130 }
2131 if let Some(mn) = meta.min_nits {
2132 enc.min_nits = mn;
2133 }
2134 if meta.intrinsic_size.is_some() {
2135 enc.intrinsic_size = meta.intrinsic_size;
2136 }
2137 }
2138
2139 if let Some(meta) = self.metadata
2141 && let Some(icc) = meta.icc_profile
2142 {
2143 enc.icc_profile = Some(icc.to_vec());
2144 }
2145
2146 let output = enc
2147 .encode(w, h, &linear_rgb, alpha.as_deref())
2148 .map_err(EncodeError::from)?;
2149
2150 #[cfg(feature = "butteraugli-loop")]
2151 let butteraugli_iters_actual = cfg.butteraugli_iters;
2152 #[cfg(not(feature = "butteraugli-loop"))]
2153 let butteraugli_iters_actual = 0u32;
2154
2155 let stats = EncodeStats {
2156 mode: EncodeMode::Lossy,
2157 strategy_counts: output.strategy_counts,
2158 gaborish: cfg.gaborish,
2159 ans: cfg.use_ans,
2160 butteraugli_iters: butteraugli_iters_actual,
2161 pixel_domain_loss: cfg.pixel_domain_loss,
2162 ..Default::default()
2163 };
2164 Ok((output.data, stats))
2165 }
2166}
2167
2168pub struct LossyEncoder {
2194 cfg: LossyConfig,
2195 width: u32,
2196 height: u32,
2197 layout: PixelLayout,
2198 rows_pushed: u32,
2199 linear_rgb: Vec<f32>,
2200 alpha: Option<Vec<u8>>,
2201 bit_depth_16: bool,
2202 icc_profile: Option<Vec<u8>>,
2203 exif: Option<Vec<u8>>,
2204 xmp: Option<Vec<u8>>,
2205 source_gamma: Option<f32>,
2206 color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
2207 intensity_target: f32,
2208 min_nits: f32,
2209 intrinsic_size: Option<(u32, u32)>,
2210}
2211
2212impl LossyEncoder {
2213 pub fn with_icc_profile(mut self, data: &[u8]) -> Self {
2215 self.icc_profile = Some(data.to_vec());
2216 self
2217 }
2218
2219 pub fn with_exif(mut self, data: &[u8]) -> Self {
2221 self.exif = Some(data.to_vec());
2222 self
2223 }
2224
2225 pub fn with_xmp(mut self, data: &[u8]) -> Self {
2227 self.xmp = Some(data.to_vec());
2228 self
2229 }
2230
2231 pub fn with_source_gamma(mut self, gamma: f32) -> Self {
2233 self.source_gamma = Some(gamma);
2234 self
2235 }
2236
2237 pub fn with_color_encoding(
2239 mut self,
2240 ce: crate::headers::color_encoding::ColorEncoding,
2241 ) -> Self {
2242 self.color_encoding = Some(ce);
2243 self
2244 }
2245
2246 pub fn with_intensity_target(mut self, nits: f32) -> Self {
2248 self.intensity_target = nits;
2249 self
2250 }
2251
2252 pub fn with_min_nits(mut self, nits: f32) -> Self {
2254 self.min_nits = nits;
2255 self
2256 }
2257
2258 pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
2260 self.intrinsic_size = Some((width, height));
2261 self
2262 }
2263
2264 pub fn rows_pushed(&self) -> u32 {
2266 self.rows_pushed
2267 }
2268
2269 pub fn height(&self) -> u32 {
2271 self.height
2272 }
2273
2274 #[track_caller]
2280 pub fn push_rows(&mut self, pixels: &[u8], num_rows: u32) -> Result<()> {
2281 self.push_rows_inner(pixels, num_rows).map_err(at)
2282 }
2283
2284 fn push_rows_inner(
2285 &mut self,
2286 pixels: &[u8],
2287 num_rows: u32,
2288 ) -> core::result::Result<(), EncodeError> {
2289 if num_rows == 0 {
2290 return Ok(());
2291 }
2292 let remaining = self.height - self.rows_pushed;
2293 if num_rows > remaining {
2294 return Err(EncodeError::InvalidInput {
2295 message: format!(
2296 "push_rows: {num_rows} rows would exceed image height \
2297 ({} pushed + {num_rows} > {})",
2298 self.rows_pushed, self.height
2299 ),
2300 });
2301 }
2302 let w = self.width as usize;
2303 let n = num_rows as usize;
2304 let expected = w
2305 .checked_mul(n)
2306 .and_then(|wn| wn.checked_mul(self.layout.bytes_per_pixel()));
2307 match expected {
2308 Some(expected) if pixels.len() == expected => {}
2309 Some(expected) => {
2310 return Err(EncodeError::InvalidInput {
2311 message: format!(
2312 "push_rows: expected {expected} bytes for {w}x{n} {:?}, got {}",
2313 self.layout,
2314 pixels.len()
2315 ),
2316 });
2317 }
2318 None => {
2319 return Err(EncodeError::InvalidInput {
2320 message: "push_rows: row dimensions overflow".into(),
2321 });
2322 }
2323 }
2324
2325 let gamma = self.source_gamma;
2326
2327 let new_linear: Vec<f32> = match self.layout {
2329 PixelLayout::Rgb8 => {
2330 if let Some(g) = gamma {
2331 gamma_u8_to_linear_f32(pixels, 3, g)
2332 } else {
2333 srgb_u8_to_linear_f32(pixels, 3)
2334 }
2335 }
2336 PixelLayout::Bgr8 => {
2337 let rgb = bgr_to_rgb(pixels, 3);
2338 if let Some(g) = gamma {
2339 gamma_u8_to_linear_f32(&rgb, 3, g)
2340 } else {
2341 srgb_u8_to_linear_f32(&rgb, 3)
2342 }
2343 }
2344 PixelLayout::Rgba8 => {
2345 if let Some(g) = gamma {
2346 gamma_u8_to_linear_f32(pixels, 4, g)
2347 } else {
2348 srgb_u8_to_linear_f32(pixels, 4)
2349 }
2350 }
2351 PixelLayout::Bgra8 => {
2352 let swapped = bgr_to_rgb(pixels, 4);
2353 if let Some(g) = gamma {
2354 gamma_u8_to_linear_f32(&swapped, 4, g)
2355 } else {
2356 srgb_u8_to_linear_f32(&swapped, 4)
2357 }
2358 }
2359 PixelLayout::Gray8 => {
2360 if let Some(g) = gamma {
2361 gamma_gray_u8_to_linear_f32_rgb(pixels, 1, g)
2362 } else {
2363 gray_u8_to_linear_f32_rgb(pixels, 1)
2364 }
2365 }
2366 PixelLayout::GrayAlpha8 => {
2367 if let Some(g) = gamma {
2368 gamma_gray_u8_to_linear_f32_rgb(pixels, 2, g)
2369 } else {
2370 gray_u8_to_linear_f32_rgb(pixels, 2)
2371 }
2372 }
2373 PixelLayout::Rgb16 => {
2374 if let Some(g) = gamma {
2375 gamma_u16_to_linear_f32(pixels, 3, g)
2376 } else {
2377 srgb_u16_to_linear_f32(pixels, 3)
2378 }
2379 }
2380 PixelLayout::Rgba16 => {
2381 if let Some(g) = gamma {
2382 gamma_u16_to_linear_f32(pixels, 4, g)
2383 } else {
2384 srgb_u16_to_linear_f32(pixels, 4)
2385 }
2386 }
2387 PixelLayout::Gray16 => {
2388 if let Some(g) = gamma {
2389 gamma_gray_u16_to_linear_f32_rgb(pixels, 1, g)
2390 } else {
2391 gray_u16_to_linear_f32_rgb(pixels, 1)
2392 }
2393 }
2394 PixelLayout::GrayAlpha16 => {
2395 if let Some(g) = gamma {
2396 gamma_gray_u16_to_linear_f32_rgb(pixels, 2, g)
2397 } else {
2398 gray_u16_to_linear_f32_rgb(pixels, 2)
2399 }
2400 }
2401 PixelLayout::RgbLinearF32 => {
2402 let floats: &[f32] = bytemuck::cast_slice(pixels);
2403 floats.to_vec()
2404 }
2405 PixelLayout::RgbaLinearF32 => {
2406 let floats: &[f32] = bytemuck::cast_slice(pixels);
2407 floats
2408 .chunks(4)
2409 .flat_map(|px| [px[0], px[1], px[2]])
2410 .collect()
2411 }
2412 PixelLayout::GrayLinearF32 => {
2413 let floats: &[f32] = bytemuck::cast_slice(pixels);
2414 gray_f32_to_linear_f32_rgb(floats, 1)
2415 }
2416 PixelLayout::GrayAlphaLinearF32 => {
2417 let floats: &[f32] = bytemuck::cast_slice(pixels);
2418 gray_f32_to_linear_f32_rgb(floats, 2)
2419 }
2420 };
2421 self.linear_rgb.extend_from_slice(&new_linear);
2422
2423 match self.layout {
2425 PixelLayout::Rgba8 | PixelLayout::Bgra8 => {
2426 let new_alpha = extract_alpha(pixels, 4, 3);
2427 self.alpha
2428 .get_or_insert_with(Vec::new)
2429 .extend_from_slice(&new_alpha);
2430 }
2431 PixelLayout::GrayAlpha8 => {
2432 let new_alpha = extract_alpha(pixels, 2, 1);
2433 self.alpha
2434 .get_or_insert_with(Vec::new)
2435 .extend_from_slice(&new_alpha);
2436 }
2437 PixelLayout::Rgba16 => {
2438 let new_alpha = extract_alpha_u16(pixels, 4, 3);
2439 self.alpha
2440 .get_or_insert_with(Vec::new)
2441 .extend_from_slice(&new_alpha);
2442 }
2443 PixelLayout::GrayAlpha16 => {
2444 let new_alpha = extract_alpha_u16(pixels, 2, 1);
2445 self.alpha
2446 .get_or_insert_with(Vec::new)
2447 .extend_from_slice(&new_alpha);
2448 }
2449 PixelLayout::RgbaLinearF32 => {
2450 let floats: &[f32] = bytemuck::cast_slice(pixels);
2451 let new_alpha = extract_alpha_f32(floats, 4, 3);
2452 self.alpha
2453 .get_or_insert_with(Vec::new)
2454 .extend_from_slice(&new_alpha);
2455 }
2456 PixelLayout::GrayAlphaLinearF32 => {
2457 let floats: &[f32] = bytemuck::cast_slice(pixels);
2458 let new_alpha = extract_alpha_f32(floats, 2, 1);
2459 self.alpha
2460 .get_or_insert_with(Vec::new)
2461 .extend_from_slice(&new_alpha);
2462 }
2463 _ => {}
2464 }
2465
2466 self.rows_pushed += num_rows;
2467 Ok(())
2468 }
2469
2470 #[track_caller]
2475 pub fn finish(self) -> Result<Vec<u8>> {
2476 self.finish_inner()
2477 .map(|mut r| r.take_data().unwrap())
2478 .map_err(at)
2479 }
2480
2481 #[track_caller]
2483 pub fn finish_with_stats(self) -> Result<EncodeResult> {
2484 self.finish_inner().map_err(at)
2485 }
2486
2487 #[track_caller]
2489 pub fn finish_into(self, out: &mut Vec<u8>) -> Result<EncodeResult> {
2490 let mut result = self.finish_inner().map_err(at)?;
2491 if let Some(data) = result.data.take() {
2492 out.extend_from_slice(&data);
2493 }
2494 Ok(result)
2495 }
2496
2497 #[cfg(feature = "std")]
2499 #[track_caller]
2500 pub fn finish_to(self, mut dest: impl std::io::Write) -> Result<EncodeResult> {
2501 let mut result = self.finish_inner().map_err(at)?;
2502 if let Some(data) = result.data.take() {
2503 dest.write_all(&data)
2504 .map_err(|e| at(EncodeError::from(e)))?;
2505 }
2506 Ok(result)
2507 }
2508
2509 fn finish_inner(self) -> core::result::Result<EncodeResult, EncodeError> {
2510 if self.rows_pushed != self.height {
2511 return Err(EncodeError::InvalidInput {
2512 message: format!(
2513 "incomplete image: {} of {} rows pushed",
2514 self.rows_pushed, self.height
2515 ),
2516 });
2517 }
2518
2519 let cfg = &self.cfg;
2520 let w = self.width as usize;
2521 let h = self.height as usize;
2522 let linear_rgb = self.linear_rgb;
2523 let alpha = self.alpha;
2524
2525 let (codestream, mut stats) = run_with_threads(cfg.threads, || {
2526 let mut profile = cfg.effective_profile();
2527 if let Some(max_size) = cfg.max_strategy_size {
2528 if max_size < 16 {
2529 profile.try_dct16 = false;
2530 }
2531 if max_size < 32 {
2532 profile.try_dct32 = false;
2533 }
2534 if max_size < 64 {
2535 profile.try_dct64 = false;
2536 }
2537 }
2538
2539 let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
2540 enc.effort = cfg.effort;
2541 enc.profile = profile;
2542 enc.use_ans = cfg.use_ans;
2543 enc.optimize_codes = enc.profile.optimize_codes;
2544 enc.custom_orders = enc.profile.custom_orders;
2545 enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
2546 enc.enable_noise = cfg.noise;
2547 enc.enable_denoise = cfg.denoise;
2548 enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
2549 enc.error_diffusion = cfg.error_diffusion;
2550 enc.pixel_domain_loss = cfg.pixel_domain_loss;
2551 enc.enable_lz77 = cfg.lz77;
2552 enc.lz77_method = cfg.lz77_method;
2553 enc.force_strategy = cfg.force_strategy;
2554 enc.enable_patches = cfg.patches;
2555 enc.encoder_mode = cfg.mode;
2556 enc.splines = cfg.splines.clone();
2557 enc.is_grayscale = self.layout.is_grayscale();
2558 enc.progressive = cfg.progressive;
2559 enc.use_lf_frame = cfg.lf_frame;
2560 #[cfg(feature = "butteraugli-loop")]
2561 {
2562 enc.butteraugli_iters = cfg.butteraugli_iters;
2563 }
2564 enc.bit_depth_16 = self.bit_depth_16;
2565 enc.source_gamma = self.source_gamma;
2566 enc.color_encoding = self.color_encoding.clone();
2567 enc.intensity_target = self.intensity_target;
2568 enc.min_nits = self.min_nits;
2569 enc.intrinsic_size = self.intrinsic_size;
2570 if let Some(ref icc) = self.icc_profile {
2571 enc.icc_profile = Some(icc.clone());
2572 }
2573
2574 let output = enc
2575 .encode(w, h, &linear_rgb, alpha.as_deref())
2576 .map_err(EncodeError::from)?;
2577
2578 #[cfg(feature = "butteraugli-loop")]
2579 let butteraugli_iters_actual = cfg.butteraugli_iters;
2580 #[cfg(not(feature = "butteraugli-loop"))]
2581 let butteraugli_iters_actual = 0u32;
2582
2583 let stats = EncodeStats {
2584 mode: EncodeMode::Lossy,
2585 strategy_counts: output.strategy_counts,
2586 gaborish: cfg.gaborish,
2587 ans: cfg.use_ans,
2588 butteraugli_iters: butteraugli_iters_actual,
2589 pixel_domain_loss: cfg.pixel_domain_loss,
2590 ..Default::default()
2591 };
2592 Ok::<_, EncodeError>((output.data, stats))
2593 })?;
2594
2595 stats.codestream_size = codestream.len();
2596
2597 let output = if self.exif.is_some() || self.xmp.is_some() {
2598 crate::container::wrap_in_container(
2599 &codestream,
2600 self.exif.as_deref(),
2601 self.xmp.as_deref(),
2602 )
2603 } else {
2604 codestream
2605 };
2606
2607 stats.output_size = output.len();
2608 Ok(EncodeResult {
2609 data: Some(output),
2610 stats,
2611 })
2612 }
2613}
2614
2615impl LossyConfig {
2616 #[track_caller]
2622 pub fn encoder(&self, width: u32, height: u32, layout: PixelLayout) -> Result<LossyEncoder> {
2623 if width == 0 || height == 0 {
2624 return Err(at(EncodeError::InvalidInput {
2625 message: format!("zero dimensions: {width}x{height}"),
2626 }));
2627 }
2628 let w = width as usize;
2629 let h = height as usize;
2630 let rgb_capacity = w.checked_mul(h).and_then(|n| n.checked_mul(3));
2631 let Some(rgb_capacity) = rgb_capacity else {
2632 return Err(at(EncodeError::InvalidInput {
2633 message: "image dimensions overflow".into(),
2634 }));
2635 };
2636
2637 let bit_depth_16 = layout.is_16bit();
2638 let has_alpha = layout.has_alpha();
2639 let alpha = if has_alpha {
2640 let mut v = Vec::new();
2641 v.try_reserve(w * h)
2642 .map_err(|e| at(EncodeError::from(crate::error::Error::from(e))))?;
2643 Some(v)
2644 } else {
2645 None
2646 };
2647
2648 let mut linear_rgb = Vec::new();
2649 linear_rgb
2650 .try_reserve(rgb_capacity)
2651 .map_err(|e| at(EncodeError::from(crate::error::Error::from(e))))?;
2652
2653 Ok(LossyEncoder {
2654 cfg: self.clone(),
2655 width,
2656 height,
2657 layout,
2658 rows_pushed: 0,
2659 linear_rgb,
2660 alpha,
2661 bit_depth_16,
2662 icc_profile: None,
2663 exif: None,
2664 xmp: None,
2665 source_gamma: None,
2666 color_encoding: None,
2667 intensity_target: 255.0,
2668 min_nits: 0.0,
2669 intrinsic_size: None,
2670 })
2671 }
2672}
2673
2674pub struct LosslessEncoder {
2697 cfg: LosslessConfig,
2698 width: u32,
2699 height: u32,
2700 layout: PixelLayout,
2701 rows_pushed: u32,
2702 channels: Vec<crate::modular::channel::Channel>,
2703 num_source_channels: usize,
2704 bit_depth: u32,
2705 is_grayscale: bool,
2706 has_alpha: bool,
2707 icc_profile: Option<Vec<u8>>,
2708 exif: Option<Vec<u8>>,
2709 xmp: Option<Vec<u8>>,
2710 source_gamma: Option<f32>,
2711 color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
2712 intensity_target: f32,
2713 min_nits: f32,
2714 intrinsic_size: Option<(u32, u32)>,
2715}
2716
2717impl LosslessEncoder {
2718 pub fn with_icc_profile(mut self, data: &[u8]) -> Self {
2720 self.icc_profile = Some(data.to_vec());
2721 self
2722 }
2723
2724 pub fn with_exif(mut self, data: &[u8]) -> Self {
2726 self.exif = Some(data.to_vec());
2727 self
2728 }
2729
2730 pub fn with_xmp(mut self, data: &[u8]) -> Self {
2732 self.xmp = Some(data.to_vec());
2733 self
2734 }
2735
2736 pub fn with_source_gamma(mut self, gamma: f32) -> Self {
2738 self.source_gamma = Some(gamma);
2739 self
2740 }
2741
2742 pub fn with_color_encoding(
2744 mut self,
2745 ce: crate::headers::color_encoding::ColorEncoding,
2746 ) -> Self {
2747 self.color_encoding = Some(ce);
2748 self
2749 }
2750
2751 pub fn with_intensity_target(mut self, nits: f32) -> Self {
2753 self.intensity_target = nits;
2754 self
2755 }
2756
2757 pub fn with_min_nits(mut self, nits: f32) -> Self {
2759 self.min_nits = nits;
2760 self
2761 }
2762
2763 pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
2765 self.intrinsic_size = Some((width, height));
2766 self
2767 }
2768
2769 pub fn rows_pushed(&self) -> u32 {
2771 self.rows_pushed
2772 }
2773
2774 pub fn height(&self) -> u32 {
2776 self.height
2777 }
2778
2779 #[track_caller]
2785 pub fn push_rows(&mut self, pixels: &[u8], num_rows: u32) -> Result<()> {
2786 self.push_rows_inner(pixels, num_rows).map_err(at)
2787 }
2788
2789 fn push_rows_inner(
2790 &mut self,
2791 pixels: &[u8],
2792 num_rows: u32,
2793 ) -> core::result::Result<(), EncodeError> {
2794 if num_rows == 0 {
2795 return Ok(());
2796 }
2797 let remaining = self.height - self.rows_pushed;
2798 if num_rows > remaining {
2799 return Err(EncodeError::InvalidInput {
2800 message: format!(
2801 "push_rows: {num_rows} rows would exceed image height \
2802 ({} pushed + {num_rows} > {})",
2803 self.rows_pushed, self.height
2804 ),
2805 });
2806 }
2807 let w = self.width as usize;
2808 let n = num_rows as usize;
2809 let bpp = self.layout.bytes_per_pixel();
2810 let expected = w.checked_mul(n).and_then(|wn| wn.checked_mul(bpp));
2811 match expected {
2812 Some(expected) if pixels.len() == expected => {}
2813 Some(expected) => {
2814 return Err(EncodeError::InvalidInput {
2815 message: format!(
2816 "push_rows: expected {expected} bytes for {w}x{n} {:?}, got {}",
2817 self.layout,
2818 pixels.len()
2819 ),
2820 });
2821 }
2822 None => {
2823 return Err(EncodeError::InvalidInput {
2824 message: "push_rows: row dimensions overflow".into(),
2825 });
2826 }
2827 }
2828
2829 let y_start = self.rows_pushed as usize;
2830 let nc = self.num_source_channels;
2831
2832 match self.layout {
2833 PixelLayout::Rgb8 | PixelLayout::Bgr8 => {
2834 let is_bgr = matches!(self.layout, PixelLayout::Bgr8);
2835 for y in 0..n {
2836 let row_offset = y * w * 3;
2837 let dst_y = y_start + y;
2838 for x in 0..w {
2839 let src = row_offset + x * 3;
2840 let (r, g, b) = if is_bgr {
2841 (pixels[src + 2], pixels[src + 1], pixels[src])
2842 } else {
2843 (pixels[src], pixels[src + 1], pixels[src + 2])
2844 };
2845 self.channels[0].set(x, dst_y, r as i32);
2846 self.channels[1].set(x, dst_y, g as i32);
2847 self.channels[2].set(x, dst_y, b as i32);
2848 }
2849 }
2850 }
2851 PixelLayout::Rgba8 | PixelLayout::Bgra8 => {
2852 let is_bgr = matches!(self.layout, PixelLayout::Bgra8);
2853 for y in 0..n {
2854 let row_offset = y * w * 4;
2855 let dst_y = y_start + y;
2856 for x in 0..w {
2857 let src = row_offset + x * 4;
2858 let (r, g, b) = if is_bgr {
2859 (pixels[src + 2], pixels[src + 1], pixels[src])
2860 } else {
2861 (pixels[src], pixels[src + 1], pixels[src + 2])
2862 };
2863 self.channels[0].set(x, dst_y, r as i32);
2864 self.channels[1].set(x, dst_y, g as i32);
2865 self.channels[2].set(x, dst_y, b as i32);
2866 self.channels[3].set(x, dst_y, pixels[src + 3] as i32);
2867 }
2868 }
2869 }
2870 PixelLayout::Gray8 => {
2871 for y in 0..n {
2872 let row_offset = y * w;
2873 let dst_y = y_start + y;
2874 for x in 0..w {
2875 self.channels[0].set(x, dst_y, pixels[row_offset + x] as i32);
2876 }
2877 }
2878 }
2879 PixelLayout::GrayAlpha8 => {
2880 for y in 0..n {
2881 let row_offset = y * w * 2;
2882 let dst_y = y_start + y;
2883 for x in 0..w {
2884 let src = row_offset + x * 2;
2885 self.channels[0].set(x, dst_y, pixels[src] as i32);
2886 self.channels[1].set(x, dst_y, pixels[src + 1] as i32);
2887 }
2888 }
2889 }
2890 PixelLayout::Rgb16
2891 | PixelLayout::Rgba16
2892 | PixelLayout::Gray16
2893 | PixelLayout::GrayAlpha16 => {
2894 let pixels_u16: &[u16] = bytemuck::cast_slice(pixels);
2895 for y in 0..n {
2896 let row_offset = y * w * nc;
2897 let dst_y = y_start + y;
2898 for x in 0..w {
2899 let src = row_offset + x * nc;
2900 for c in 0..nc {
2901 self.channels[c].set(x, dst_y, pixels_u16[src + c] as i32);
2902 }
2903 }
2904 }
2905 }
2906 _ => {
2907 return Err(EncodeError::UnsupportedPixelLayout(self.layout));
2908 }
2909 }
2910
2911 self.rows_pushed += num_rows;
2912 Ok(())
2913 }
2914
2915 #[track_caller]
2920 pub fn finish(self) -> Result<Vec<u8>> {
2921 self.finish_inner()
2922 .map(|mut r| r.take_data().unwrap())
2923 .map_err(at)
2924 }
2925
2926 #[track_caller]
2928 pub fn finish_with_stats(self) -> Result<EncodeResult> {
2929 self.finish_inner().map_err(at)
2930 }
2931
2932 #[track_caller]
2934 pub fn finish_into(self, out: &mut Vec<u8>) -> Result<EncodeResult> {
2935 let mut result = self.finish_inner().map_err(at)?;
2936 if let Some(data) = result.data.take() {
2937 out.extend_from_slice(&data);
2938 }
2939 Ok(result)
2940 }
2941
2942 #[cfg(feature = "std")]
2944 #[track_caller]
2945 pub fn finish_to(self, mut dest: impl std::io::Write) -> Result<EncodeResult> {
2946 let mut result = self.finish_inner().map_err(at)?;
2947 if let Some(data) = result.data.take() {
2948 dest.write_all(&data)
2949 .map_err(|e| at(EncodeError::from(e)))?;
2950 }
2951 Ok(result)
2952 }
2953
2954 fn finish_inner(self) -> core::result::Result<EncodeResult, EncodeError> {
2955 use crate::bit_writer::BitWriter;
2956 use crate::headers::color_encoding::ColorSpace;
2957 use crate::headers::{ColorEncoding, FileHeader};
2958 use crate::modular::channel::ModularImage;
2959 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
2960
2961 if self.rows_pushed != self.height {
2962 return Err(EncodeError::InvalidInput {
2963 message: format!(
2964 "incomplete image: {} of {} rows pushed",
2965 self.rows_pushed, self.height
2966 ),
2967 });
2968 }
2969
2970 let cfg = &self.cfg;
2971 let w = self.width as usize;
2972 let h = self.height as usize;
2973
2974 let mut image = ModularImage {
2975 channels: self.channels,
2976 bit_depth: self.bit_depth,
2977 is_grayscale: self.is_grayscale,
2978 has_alpha: self.has_alpha,
2979 };
2980
2981 let (codestream, mut stats) = run_with_threads(cfg.threads, || {
2982 let num_channels = self.layout.bytes_per_pixel();
2984 let can_use_patches =
2985 cfg.patches && !image.is_grayscale && image.bit_depth <= 8 && num_channels >= 3;
2986 let patches_data = if can_use_patches {
2987 let mut detection_pixels = vec![0u8; w * h * num_channels];
2988 let nc = core::cmp::min(num_channels, image.channels.len());
2989 for y in 0..h {
2990 for x in 0..w {
2991 for c in 0..nc {
2992 detection_pixels[(y * w + x) * num_channels + c] =
2993 image.channels[c].get(x, y) as u8;
2994 }
2995 for c in nc..num_channels {
2997 if c < image.channels.len() {
2998 detection_pixels[(y * w + x) * num_channels + c] =
2999 image.channels[c].get(x, y) as u8;
3000 }
3001 }
3002 }
3003 }
3004 crate::vardct::patches::find_and_build_lossless(
3005 &detection_pixels,
3006 w,
3007 h,
3008 num_channels,
3009 image.bit_depth,
3010 )
3011 } else {
3012 None
3013 };
3014
3015 let mut file_header = if image.is_grayscale {
3017 FileHeader::new_gray(self.width, self.height)
3018 } else if image.has_alpha {
3019 FileHeader::new_rgba(self.width, self.height)
3020 } else {
3021 FileHeader::new_rgb(self.width, self.height)
3022 };
3023 if image.bit_depth == 16 {
3024 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
3025 for ec in &mut file_header.metadata.extra_channels {
3026 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
3027 }
3028 }
3029 if self.icc_profile.is_some() {
3030 file_header.metadata.color_encoding.want_icc = true;
3031 }
3032 file_header.metadata.intensity_target = self.intensity_target;
3033 file_header.metadata.min_nits = self.min_nits;
3034 if let Some((w, h)) = self.intrinsic_size {
3035 file_header.metadata.have_intrinsic_size = true;
3036 file_header.metadata.intrinsic_width = w;
3037 file_header.metadata.intrinsic_height = h;
3038 }
3039
3040 let mut writer = BitWriter::new();
3041 file_header.write(&mut writer).map_err(EncodeError::from)?;
3042 if let Some(ref icc) = self.icc_profile {
3043 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
3044 }
3045 writer.zero_pad_to_byte();
3046
3047 if let Some(ref pd) = patches_data {
3049 let lossless_profile = cfg.effective_profile();
3050 crate::vardct::patches::encode_reference_frame_rgb(
3051 pd,
3052 image.bit_depth,
3053 cfg.use_ans,
3054 lossless_profile.patch_ref_tree_learning,
3055 &mut writer,
3056 )
3057 .map_err(EncodeError::from)?;
3058 writer.zero_pad_to_byte();
3059 let bd = image.bit_depth;
3060 crate::vardct::patches::subtract_patches_modular(&mut image, pd, bd);
3061 }
3062
3063 let frame_encoder = FrameEncoder::new(
3065 w,
3066 h,
3067 FrameEncoderOptions {
3068 use_modular: true,
3069 effort: cfg.effort,
3070 use_ans: cfg.use_ans,
3071 use_tree_learning: cfg.tree_learning,
3072 use_squeeze: cfg.squeeze,
3073 enable_lz77: cfg.lz77,
3074 lz77_method: cfg.lz77_method,
3075 lossy_palette: cfg.lossy_palette,
3076 encoder_mode: cfg.mode,
3077 profile: cfg.effective_profile(),
3078 have_animation: false,
3079 duration: 0,
3080 is_last: true,
3081 crop: None,
3082 skip_rct: false,
3083 },
3084 );
3085 let color_encoding = if let Some(ce) = self.color_encoding.clone() {
3086 if image.is_grayscale && ce.color_space != ColorSpace::Gray {
3087 ColorEncoding {
3088 color_space: ColorSpace::Gray,
3089 ..ce
3090 }
3091 } else {
3092 ce
3093 }
3094 } else if let Some(gamma) = self.source_gamma {
3095 if image.is_grayscale {
3096 ColorEncoding::gray_with_gamma(gamma)
3097 } else {
3098 ColorEncoding::with_gamma(gamma)
3099 }
3100 } else if image.is_grayscale {
3101 ColorEncoding::gray()
3102 } else {
3103 ColorEncoding::srgb()
3104 };
3105 frame_encoder
3106 .encode_modular_with_patches(
3107 &image,
3108 &color_encoding,
3109 &mut writer,
3110 patches_data.as_ref(),
3111 )
3112 .map_err(EncodeError::from)?;
3113
3114 let stats = EncodeStats {
3115 mode: EncodeMode::Lossless,
3116 ans: cfg.use_ans,
3117 ..Default::default()
3118 };
3119 Ok::<_, EncodeError>((writer.finish_with_padding(), stats))
3120 })?;
3121
3122 stats.codestream_size = codestream.len();
3123
3124 let output = if self.exif.is_some() || self.xmp.is_some() {
3125 crate::container::wrap_in_container(
3126 &codestream,
3127 self.exif.as_deref(),
3128 self.xmp.as_deref(),
3129 )
3130 } else {
3131 codestream
3132 };
3133
3134 stats.output_size = output.len();
3135 Ok(EncodeResult {
3136 data: Some(output),
3137 stats,
3138 })
3139 }
3140}
3141
3142impl LosslessConfig {
3143 #[track_caller]
3149 pub fn encoder(&self, width: u32, height: u32, layout: PixelLayout) -> Result<LosslessEncoder> {
3150 use crate::modular::channel::Channel;
3151
3152 if width == 0 || height == 0 {
3153 return Err(at(EncodeError::InvalidInput {
3154 message: format!("zero dimensions: {width}x{height}"),
3155 }));
3156 }
3157
3158 let w = width as usize;
3159 let h = height as usize;
3160
3161 let (num_channels, bit_depth, is_grayscale, has_alpha) = match layout {
3162 PixelLayout::Rgb8 | PixelLayout::Bgr8 => (3, 8u32, false, false),
3163 PixelLayout::Rgba8 | PixelLayout::Bgra8 => (4, 8, false, true),
3164 PixelLayout::Gray8 => (1, 8, true, false),
3165 PixelLayout::GrayAlpha8 => (2, 8, true, true),
3166 PixelLayout::Rgb16 => (3, 16, false, false),
3167 PixelLayout::Rgba16 => (4, 16, false, true),
3168 PixelLayout::Gray16 => (1, 16, true, false),
3169 PixelLayout::GrayAlpha16 => (2, 16, true, true),
3170 other => return Err(at(EncodeError::UnsupportedPixelLayout(other))),
3171 };
3172
3173 let mut channels = Vec::with_capacity(num_channels);
3174 for _ in 0..num_channels {
3175 channels.push(Channel::new(w, h).map_err(|e| at(EncodeError::from(e)))?);
3176 }
3177
3178 Ok(LosslessEncoder {
3179 cfg: self.clone(),
3180 width,
3181 height,
3182 layout,
3183 rows_pushed: 0,
3184 channels,
3185 num_source_channels: num_channels,
3186 bit_depth,
3187 is_grayscale,
3188 has_alpha,
3189 icc_profile: None,
3190 exif: None,
3191 xmp: None,
3192 source_gamma: None,
3193 color_encoding: None,
3194 intensity_target: 255.0,
3195 min_nits: 0.0,
3196 intrinsic_size: None,
3197 })
3198 }
3199}
3200
3201#[cfg(feature = "parallel")]
3211fn run_with_threads<T>(threads: usize, f: impl FnOnce() -> T + Send) -> T
3212where
3213 T: Send,
3214{
3215 if threads <= 1 {
3216 return f();
3217 }
3218 match rayon::ThreadPoolBuilder::new().num_threads(threads).build() {
3219 Ok(pool) => pool.install(f),
3220 Err(_) => f(),
3221 }
3222}
3223
3224#[cfg(not(feature = "parallel"))]
3225fn run_with_threads<T>(_threads: usize, f: impl FnOnce() -> T) -> T {
3226 f()
3227}
3228
3229fn validate_animation_input(
3232 width: u32,
3233 height: u32,
3234 layout: PixelLayout,
3235 frames: &[AnimationFrame<'_>],
3236) -> core::result::Result<(), EncodeError> {
3237 if width == 0 || height == 0 {
3238 return Err(EncodeError::InvalidInput {
3239 message: format!("zero dimensions: {width}x{height}"),
3240 });
3241 }
3242 if frames.is_empty() {
3243 return Err(EncodeError::InvalidInput {
3244 message: "animation requires at least one frame".into(),
3245 });
3246 }
3247 let expected_size = (width as usize)
3248 .checked_mul(height as usize)
3249 .and_then(|n| n.checked_mul(layout.bytes_per_pixel()))
3250 .ok_or_else(|| EncodeError::InvalidInput {
3251 message: "image dimensions overflow".into(),
3252 })?;
3253 for (i, frame) in frames.iter().enumerate() {
3254 if frame.pixels.len() != expected_size {
3255 return Err(EncodeError::InvalidInput {
3256 message: format!(
3257 "frame {} pixel buffer size mismatch: expected {expected_size}, got {}",
3258 i,
3259 frame.pixels.len()
3260 ),
3261 });
3262 }
3263 }
3264 Ok(())
3265}
3266
3267fn encode_animation_lossless(
3268 cfg: &LosslessConfig,
3269 width: u32,
3270 height: u32,
3271 layout: PixelLayout,
3272 animation: &AnimationParams,
3273 frames: &[AnimationFrame<'_>],
3274) -> core::result::Result<Vec<u8>, EncodeError> {
3275 use crate::bit_writer::BitWriter;
3276 use crate::headers::file_header::AnimationHeader;
3277 use crate::headers::{ColorEncoding, FileHeader};
3278 use crate::modular::channel::ModularImage;
3279 use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
3280
3281 validate_animation_input(width, height, layout, frames)?;
3282
3283 let w = width as usize;
3284 let h = height as usize;
3285 let num_frames = frames.len();
3286
3287 let sample_image = match layout {
3289 PixelLayout::Rgb8 => ModularImage::from_rgb8(frames[0].pixels, w, h),
3290 PixelLayout::Rgba8 => ModularImage::from_rgba8(frames[0].pixels, w, h),
3291 PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(frames[0].pixels, 3), w, h),
3292 PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(frames[0].pixels, 4), w, h),
3293 PixelLayout::Gray8 => ModularImage::from_gray8(frames[0].pixels, w, h),
3294 PixelLayout::GrayAlpha8 => ModularImage::from_grayalpha8(frames[0].pixels, w, h),
3295 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frames[0].pixels, w, h),
3296 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frames[0].pixels, w, h),
3297 PixelLayout::Gray16 => ModularImage::from_gray16_native(frames[0].pixels, w, h),
3298 PixelLayout::GrayAlpha16 => ModularImage::from_grayalpha16_native(frames[0].pixels, w, h),
3299 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
3300 }
3301 .map_err(EncodeError::from)?;
3302
3303 let mut file_header = if sample_image.is_grayscale {
3304 FileHeader::new_gray(width, height)
3305 } else if sample_image.has_alpha {
3306 FileHeader::new_rgba(width, height)
3307 } else {
3308 FileHeader::new_rgb(width, height)
3309 };
3310 if sample_image.bit_depth == 16 {
3311 file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
3312 for ec in &mut file_header.metadata.extra_channels {
3313 ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
3314 }
3315 }
3316 file_header.metadata.animation = Some(AnimationHeader {
3317 tps_numerator: animation.tps_numerator,
3318 tps_denominator: animation.tps_denominator,
3319 num_loops: animation.num_loops,
3320 have_timecodes: false,
3321 });
3322
3323 let mut writer = BitWriter::new();
3325 file_header.write(&mut writer).map_err(EncodeError::from)?;
3326 writer.zero_pad_to_byte();
3327
3328 let color_encoding = ColorEncoding::srgb();
3330 let bpp = layout.bytes_per_pixel();
3331 let mut prev_pixels: Option<&[u8]> = None;
3332
3333 for (i, frame) in frames.iter().enumerate() {
3334 let crop = if let Some(prev) = prev_pixels {
3337 match detect_frame_crop(prev, frame.pixels, w, h, bpp, false) {
3338 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
3339 Some(_) => None, None => {
3341 Some(FrameCrop {
3343 x0: 0,
3344 y0: 0,
3345 width: 1,
3346 height: 1,
3347 })
3348 }
3349 }
3350 } else {
3351 None };
3353
3354 let (frame_w, frame_h, frame_pixels_owned);
3356 let frame_pixels: &[u8] = if let Some(ref crop) = crop {
3357 frame_w = crop.width as usize;
3358 frame_h = crop.height as usize;
3359 frame_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
3360 &frame_pixels_owned
3361 } else {
3362 frame_w = w;
3363 frame_h = h;
3364 frame_pixels_owned = Vec::new();
3365 let _ = &frame_pixels_owned; frame.pixels
3367 };
3368
3369 let image = match layout {
3370 PixelLayout::Rgb8 => ModularImage::from_rgb8(frame_pixels, frame_w, frame_h),
3371 PixelLayout::Rgba8 => ModularImage::from_rgba8(frame_pixels, frame_w, frame_h),
3372 PixelLayout::Bgr8 => {
3373 ModularImage::from_rgb8(&bgr_to_rgb(frame_pixels, 3), frame_w, frame_h)
3374 }
3375 PixelLayout::Bgra8 => {
3376 ModularImage::from_rgba8(&bgr_to_rgb(frame_pixels, 4), frame_w, frame_h)
3377 }
3378 PixelLayout::Gray8 => ModularImage::from_gray8(frame_pixels, frame_w, frame_h),
3379 PixelLayout::GrayAlpha8 => {
3380 ModularImage::from_grayalpha8(frame_pixels, frame_w, frame_h)
3381 }
3382 PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frame_pixels, frame_w, frame_h),
3383 PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frame_pixels, frame_w, frame_h),
3384 PixelLayout::Gray16 => ModularImage::from_gray16_native(frame_pixels, frame_w, frame_h),
3385 PixelLayout::GrayAlpha16 => {
3386 ModularImage::from_grayalpha16_native(frame_pixels, frame_w, frame_h)
3387 }
3388 other => return Err(EncodeError::UnsupportedPixelLayout(other)),
3389 }
3390 .map_err(EncodeError::from)?;
3391
3392 let use_tree_learning = cfg.tree_learning;
3393 let frame_encoder = FrameEncoder::new(
3394 frame_w,
3395 frame_h,
3396 FrameEncoderOptions {
3397 use_modular: true,
3398 effort: cfg.effort,
3399 use_ans: cfg.use_ans,
3400 use_tree_learning,
3401 use_squeeze: cfg.squeeze,
3402 enable_lz77: cfg.lz77,
3403 lz77_method: cfg.lz77_method,
3404 lossy_palette: cfg.lossy_palette,
3405 encoder_mode: cfg.mode,
3406 profile: cfg.effective_profile(),
3407 have_animation: true,
3408 duration: frame.duration,
3409 is_last: i == num_frames - 1,
3410 crop,
3411 skip_rct: false,
3412 },
3413 );
3414 frame_encoder
3415 .encode_modular(&image, &color_encoding, &mut writer)
3416 .map_err(EncodeError::from)?;
3417
3418 prev_pixels = Some(frame.pixels);
3419 }
3420
3421 Ok(writer.finish_with_padding())
3422}
3423
3424fn encode_animation_lossy(
3425 cfg: &LossyConfig,
3426 width: u32,
3427 height: u32,
3428 layout: PixelLayout,
3429 animation: &AnimationParams,
3430 frames: &[AnimationFrame<'_>],
3431) -> core::result::Result<Vec<u8>, EncodeError> {
3432 use crate::bit_writer::BitWriter;
3433 use crate::headers::file_header::AnimationHeader;
3434 use crate::headers::frame_header::FrameOptions;
3435
3436 validate_animation_input(width, height, layout, frames)?;
3437
3438 let w = width as usize;
3439 let h = height as usize;
3440 let num_frames = frames.len();
3441
3442 let mut profile = cfg.effective_profile();
3444
3445 if let Some(max_size) = cfg.max_strategy_size {
3447 if max_size < 16 {
3448 profile.try_dct16 = false;
3449 }
3450 if max_size < 32 {
3451 profile.try_dct32 = false;
3452 }
3453 if max_size < 64 {
3454 profile.try_dct64 = false;
3455 }
3456 }
3457
3458 let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
3459 enc.effort = cfg.effort;
3460 enc.profile = profile;
3461 enc.use_ans = cfg.use_ans;
3462 enc.optimize_codes = enc.profile.optimize_codes;
3463 enc.custom_orders = enc.profile.custom_orders;
3464 enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
3465 enc.enable_noise = cfg.noise;
3466 enc.enable_denoise = cfg.denoise;
3467 enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
3469 enc.error_diffusion = cfg.error_diffusion;
3470 enc.pixel_domain_loss = cfg.pixel_domain_loss;
3471 enc.enable_lz77 = cfg.lz77;
3472 enc.lz77_method = cfg.lz77_method;
3473 enc.force_strategy = cfg.force_strategy;
3474 enc.progressive = cfg.progressive;
3475 enc.use_lf_frame = cfg.lf_frame;
3476 #[cfg(feature = "butteraugli-loop")]
3477 {
3478 enc.butteraugli_iters = cfg.butteraugli_iters;
3479 }
3480 #[cfg(feature = "ssim2-loop")]
3481 {
3482 enc.ssim2_iters = cfg.ssim2_iters;
3483 }
3484 #[cfg(feature = "zensim-loop")]
3485 {
3486 enc.zensim_iters = cfg.zensim_iters;
3487 }
3488
3489 let has_alpha = layout.has_alpha();
3491 let bit_depth_16 = matches!(layout, PixelLayout::Rgb16 | PixelLayout::Rgba16);
3492 enc.bit_depth_16 = bit_depth_16;
3493
3494 let mut file_header = enc.build_file_header(w, h, has_alpha);
3497 file_header.metadata.animation = Some(AnimationHeader {
3498 tps_numerator: animation.tps_numerator,
3499 tps_denominator: animation.tps_denominator,
3500 num_loops: animation.num_loops,
3501 have_timecodes: false,
3502 });
3503
3504 let mut writer = BitWriter::with_capacity(w * h * 4);
3505 file_header.write(&mut writer).map_err(EncodeError::from)?;
3506 if let Some(ref icc) = enc.icc_profile {
3507 crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
3508 }
3509 writer.zero_pad_to_byte();
3510
3511 let bpp = layout.bytes_per_pixel();
3513 let mut prev_pixels: Option<&[u8]> = None;
3514
3515 for (i, frame) in frames.iter().enumerate() {
3516 let crop = if let Some(prev) = prev_pixels {
3519 match detect_frame_crop(prev, frame.pixels, w, h, bpp, true) {
3520 Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
3521 Some(_) => None, None => {
3523 Some(FrameCrop {
3525 x0: 0,
3526 y0: 0,
3527 width: 8.min(width),
3528 height: 8.min(height),
3529 })
3530 }
3531 }
3532 } else {
3533 None };
3535
3536 let (frame_w, frame_h) = if let Some(ref crop) = crop {
3538 (crop.width as usize, crop.height as usize)
3539 } else {
3540 (w, h)
3541 };
3542
3543 let crop_pixels_owned;
3544 let src_pixels: &[u8] = if let Some(ref crop) = crop {
3545 crop_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
3546 &crop_pixels_owned
3547 } else {
3548 crop_pixels_owned = Vec::new();
3549 let _ = &crop_pixels_owned;
3550 frame.pixels
3551 };
3552
3553 let (linear_rgb, alpha) = match layout {
3554 PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(src_pixels, 3), None),
3555 PixelLayout::Bgr8 => (srgb_u8_to_linear_f32(&bgr_to_rgb(src_pixels, 3), 3), None),
3556 PixelLayout::Rgba8 => {
3557 let rgb = srgb_u8_to_linear_f32(src_pixels, 4);
3558 let alpha = extract_alpha(src_pixels, 4, 3);
3559 (rgb, Some(alpha))
3560 }
3561 PixelLayout::Bgra8 => {
3562 let swapped = bgr_to_rgb(src_pixels, 4);
3563 let rgb = srgb_u8_to_linear_f32(&swapped, 4);
3564 let alpha = extract_alpha(src_pixels, 4, 3);
3565 (rgb, Some(alpha))
3566 }
3567 PixelLayout::Gray8 => (gray_u8_to_linear_f32_rgb(src_pixels, 1), None),
3568 PixelLayout::GrayAlpha8 => {
3569 let rgb = gray_u8_to_linear_f32_rgb(src_pixels, 2);
3570 let alpha = extract_alpha(src_pixels, 2, 1);
3571 (rgb, Some(alpha))
3572 }
3573 PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(src_pixels, 3), None),
3574 PixelLayout::Rgba16 => {
3575 let rgb = srgb_u16_to_linear_f32(src_pixels, 4);
3576 let alpha = extract_alpha_u16(src_pixels, 4, 3);
3577 (rgb, Some(alpha))
3578 }
3579 PixelLayout::Gray16 => (gray_u16_to_linear_f32_rgb(src_pixels, 1), None),
3580 PixelLayout::GrayAlpha16 => {
3581 let rgb = gray_u16_to_linear_f32_rgb(src_pixels, 2);
3582 let alpha = extract_alpha_u16(src_pixels, 2, 1);
3583 (rgb, Some(alpha))
3584 }
3585 PixelLayout::RgbLinearF32 => {
3586 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
3587 (floats.to_vec(), None)
3588 }
3589 PixelLayout::RgbaLinearF32 => {
3590 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
3591 let rgb: Vec<f32> = floats
3592 .chunks(4)
3593 .flat_map(|px| [px[0], px[1], px[2]])
3594 .collect();
3595 let alpha = extract_alpha_f32(floats, 4, 3);
3596 (rgb, Some(alpha))
3597 }
3598 PixelLayout::GrayLinearF32 => {
3599 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
3600 (gray_f32_to_linear_f32_rgb(floats, 1), None)
3601 }
3602 PixelLayout::GrayAlphaLinearF32 => {
3603 let floats: &[f32] = bytemuck::cast_slice(src_pixels);
3604 let rgb = gray_f32_to_linear_f32_rgb(floats, 2);
3605 let alpha = extract_alpha_f32(floats, 2, 1);
3606 (rgb, Some(alpha))
3607 }
3608 };
3609
3610 let frame_options = FrameOptions {
3611 have_animation: true,
3612 have_timecodes: false,
3613 duration: frame.duration,
3614 is_last: i == num_frames - 1,
3615 crop,
3616 };
3617
3618 enc.encode_frame_to_writer(
3619 frame_w,
3620 frame_h,
3621 &linear_rgb,
3622 alpha.as_deref(),
3623 &frame_options,
3624 &mut writer,
3625 )
3626 .map_err(EncodeError::from)?;
3627
3628 prev_pixels = Some(frame.pixels);
3629 }
3630
3631 Ok(writer.finish_with_padding())
3632}
3633
3634use crate::headers::frame_header::FrameCrop;
3637
3638fn detect_frame_crop(
3646 prev: &[u8],
3647 curr: &[u8],
3648 width: usize,
3649 height: usize,
3650 bytes_per_pixel: usize,
3651 align_to_8x8: bool,
3652) -> Option<FrameCrop> {
3653 let stride = width * bytes_per_pixel;
3654 debug_assert_eq!(prev.len(), height * stride);
3655 debug_assert_eq!(curr.len(), height * stride);
3656
3657 let mut top = height;
3659 let mut bottom = 0;
3660 let mut left = width;
3661 let mut right = 0;
3662
3663 for y in 0..height {
3664 let row_start = y * stride;
3665 let prev_row = &prev[row_start..row_start + stride];
3666 let curr_row = &curr[row_start..row_start + stride];
3667
3668 let (prev_prefix, prev_u64, prev_suffix) = bytemuck::pod_align_to::<u8, u64>(prev_row);
3670 let (curr_prefix, curr_u64, curr_suffix) = bytemuck::pod_align_to::<u8, u64>(curr_row);
3671 if prev_prefix == curr_prefix && prev_u64 == curr_u64 && prev_suffix == curr_suffix {
3672 continue;
3673 }
3674
3675 if top == height {
3677 top = y;
3678 }
3679 bottom = y;
3680
3681 for x in 0..width {
3683 let px_start = x * bytes_per_pixel;
3684 if prev_row[px_start..px_start + bytes_per_pixel]
3685 != curr_row[px_start..px_start + bytes_per_pixel]
3686 {
3687 left = left.min(x);
3688 break;
3689 }
3690 }
3691 for x in (0..width).rev() {
3693 let px_start = x * bytes_per_pixel;
3694 if prev_row[px_start..px_start + bytes_per_pixel]
3695 != curr_row[px_start..px_start + bytes_per_pixel]
3696 {
3697 right = right.max(x);
3698 break;
3699 }
3700 }
3701 }
3702
3703 if top == height {
3704 return None;
3706 }
3707
3708 let mut crop_x = left as i32;
3710 let mut crop_y = top as i32;
3711 let mut crop_w = (right - left + 1) as u32;
3712 let mut crop_h = (bottom - top + 1) as u32;
3713
3714 if align_to_8x8 {
3715 let aligned_x = (crop_x / 8) * 8;
3717 let aligned_y = (crop_y / 8) * 8;
3718 let end_x = (crop_x as u32 + crop_w).div_ceil(8) * 8;
3719 let end_y = (crop_y as u32 + crop_h).div_ceil(8) * 8;
3720 crop_x = aligned_x;
3721 crop_y = aligned_y;
3722 crop_w = end_x.min(width as u32) - aligned_x as u32;
3723 crop_h = end_y.min(height as u32) - aligned_y as u32;
3724 }
3725
3726 Some(FrameCrop {
3727 x0: crop_x,
3728 y0: crop_y,
3729 width: crop_w,
3730 height: crop_h,
3731 })
3732}
3733
3734fn extract_pixel_crop(
3738 pixels: &[u8],
3739 full_width: usize,
3740 crop: &FrameCrop,
3741 bytes_per_pixel: usize,
3742) -> Vec<u8> {
3743 let cx = crop.x0 as usize;
3744 let cy = crop.y0 as usize;
3745 let cw = crop.width as usize;
3746 let ch = crop.height as usize;
3747 let stride = full_width * bytes_per_pixel;
3748
3749 let mut out = Vec::with_capacity(cw * ch * bytes_per_pixel);
3750 for y in cy..cy + ch {
3751 let row_start = y * stride + cx * bytes_per_pixel;
3752 out.extend_from_slice(&pixels[row_start..row_start + cw * bytes_per_pixel]);
3753 }
3754 out
3755}
3756
3757const SRGB_U8_TO_LINEAR: [f32; 256] = {
3762 let mut table = [0.0f32; 256];
3763 let mut i = 0u16;
3764 while i < 256 {
3765 let c = i as f64 / 255.0;
3766 table[i as usize] = if c <= 0.04045 {
3770 (c / 12.92) as f32
3771 } else {
3772 let base = (c + 0.055) / 1.055;
3779 let x2 = base * base;
3781 let x4 = x2 * x2;
3782 let x8 = x4 * x4;
3783 let x12 = x8 * x4;
3784 let mut y = base * base; let mut iter = 0;
3790 while iter < 8 {
3791 let y2 = y * y;
3792 let y4 = y2 * y2;
3793 y = (4.0 * y + x12 / y4) / 5.0;
3794 iter += 1;
3795 }
3796 y as f32
3797 };
3798 i += 1;
3799 }
3800 table
3801};
3802
3803#[inline]
3805fn srgb_to_linear(c: u8) -> f32 {
3806 SRGB_U8_TO_LINEAR[c as usize]
3807}
3808
3809fn srgb_u8_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
3810 let num_pixels = data.len() / channels;
3811 let mut out = vec![0.0f32; num_pixels * 3];
3812 let lut = &SRGB_U8_TO_LINEAR;
3813 for (px, rgb) in data.chunks_exact(channels).zip(out.chunks_exact_mut(3)) {
3815 rgb[0] = lut[px[0] as usize];
3816 rgb[1] = lut[px[1] as usize];
3817 rgb[2] = lut[px[2] as usize];
3818 }
3819 out
3820}
3821
3822fn srgb_u16_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
3824 let pixels: &[u16] = bytemuck::cast_slice(data);
3825 pixels
3826 .chunks(channels)
3827 .flat_map(|px| {
3828 [
3829 srgb_to_linear_f(px[0] as f32 / 65535.0),
3830 srgb_to_linear_f(px[1] as f32 / 65535.0),
3831 srgb_to_linear_f(px[2] as f32 / 65535.0),
3832 ]
3833 })
3834 .collect()
3835}
3836
3837#[inline]
3839fn srgb_to_linear_f(c: f32) -> f32 {
3840 if c <= 0.04045 {
3841 c / 12.92
3842 } else {
3843 jxl_simd::fast_powf((c + 0.055) / 1.055, 2.4)
3844 }
3845}
3846
3847fn gamma_u8_to_linear_f32(data: &[u8], channels: usize, gamma: f32) -> Vec<f32> {
3849 let inv_gamma = 1.0 / gamma;
3851 let lut: [f32; 256] =
3852 core::array::from_fn(|i| jxl_simd::fast_powf(i as f32 / 255.0, inv_gamma));
3853 data.chunks(channels)
3854 .flat_map(|px| {
3855 [
3856 lut[px[0] as usize],
3857 lut[px[1] as usize],
3858 lut[px[2] as usize],
3859 ]
3860 })
3861 .collect()
3862}
3863
3864fn gamma_u16_to_linear_f32(data: &[u8], channels: usize, gamma: f32) -> Vec<f32> {
3866 let inv_gamma = 1.0 / gamma;
3867 let pixels: &[u16] = bytemuck::cast_slice(data);
3868 pixels
3869 .chunks(channels)
3870 .flat_map(|px| {
3871 [
3872 jxl_simd::fast_powf(px[0] as f32 / 65535.0, inv_gamma),
3873 jxl_simd::fast_powf(px[1] as f32 / 65535.0, inv_gamma),
3874 jxl_simd::fast_powf(px[2] as f32 / 65535.0, inv_gamma),
3875 ]
3876 })
3877 .collect()
3878}
3879
3880fn gamma_gray_u8_to_linear_f32_rgb(data: &[u8], stride: usize, gamma: f32) -> Vec<f32> {
3882 let inv_gamma = 1.0 / gamma;
3883 let lut: [f32; 256] =
3884 core::array::from_fn(|i| jxl_simd::fast_powf(i as f32 / 255.0, inv_gamma));
3885 data.chunks(stride)
3886 .flat_map(|px| {
3887 let v = lut[px[0] as usize];
3888 [v, v, v]
3889 })
3890 .collect()
3891}
3892
3893fn gamma_gray_u16_to_linear_f32_rgb(data: &[u8], stride: usize, gamma: f32) -> Vec<f32> {
3895 let inv_gamma = 1.0 / gamma;
3896 let pixels: &[u16] = bytemuck::cast_slice(data);
3897 pixels
3898 .chunks(stride)
3899 .flat_map(|px| {
3900 let v = jxl_simd::fast_powf(px[0] as f32 / 65535.0, inv_gamma);
3901 [v, v, v]
3902 })
3903 .collect()
3904}
3905
3906fn extract_alpha_u16(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
3908 let pixels: &[u16] = bytemuck::cast_slice(data);
3909 pixels
3910 .chunks(stride)
3911 .map(|px| (px[alpha_offset] >> 8) as u8)
3912 .collect()
3913}
3914
3915fn bgr_to_rgb(data: &[u8], stride: usize) -> Vec<u8> {
3917 let mut out = data.to_vec();
3918 for chunk in out.chunks_mut(stride) {
3919 chunk.swap(0, 2);
3920 }
3921 out
3922}
3923
3924fn extract_alpha(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
3926 data.chunks(stride).map(|px| px[alpha_offset]).collect()
3927}
3928
3929fn extract_alpha_f32(data: &[f32], stride: usize, alpha_offset: usize) -> Vec<u8> {
3931 data.chunks(stride)
3932 .map(|px| (px[alpha_offset].clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
3933 .collect()
3934}
3935
3936fn gray_u8_to_linear_f32_rgb(data: &[u8], stride: usize) -> Vec<f32> {
3938 data.chunks(stride)
3939 .flat_map(|px| {
3940 let v = srgb_to_linear(px[0]);
3941 [v, v, v]
3942 })
3943 .collect()
3944}
3945
3946fn gray_u16_to_linear_f32_rgb(data: &[u8], stride: usize) -> Vec<f32> {
3948 let pixels: &[u16] = bytemuck::cast_slice(data);
3949 pixels
3950 .chunks(stride)
3951 .flat_map(|px| {
3952 let v = srgb_to_linear_f(px[0] as f32 / 65535.0);
3953 [v, v, v]
3954 })
3955 .collect()
3956}
3957
3958fn gray_f32_to_linear_f32_rgb(data: &[f32], stride: usize) -> Vec<f32> {
3960 data.chunks(stride)
3961 .flat_map(|px| {
3962 let v = px[0];
3963 [v, v, v]
3964 })
3965 .collect()
3966}
3967
3968#[cfg(test)]
3971mod tests {
3972 use super::*;
3973
3974 #[test]
3975 fn test_lossless_config_builder_and_getters() {
3976 let cfg = LosslessConfig::new()
3977 .with_effort(5)
3978 .with_ans(false)
3979 .with_squeeze(true)
3980 .with_tree_learning(true);
3981 assert_eq!(cfg.effort(), 5);
3982 assert!(!cfg.ans());
3983 assert!(cfg.squeeze());
3984 assert!(cfg.tree_learning());
3985 }
3986
3987 #[test]
3988 fn test_lossy_config_builder_and_getters() {
3989 let cfg = LossyConfig::new(2.0)
3990 .with_effort(3)
3991 .with_gaborish(false)
3992 .with_noise(true);
3993 assert_eq!(cfg.distance(), 2.0);
3994 assert_eq!(cfg.effort(), 3);
3995 assert!(!cfg.gaborish());
3996 assert!(cfg.noise());
3997 }
3998
3999 #[test]
4000 fn test_pixel_layout_helpers() {
4001 assert_eq!(PixelLayout::Rgb8.bytes_per_pixel(), 3);
4002 assert_eq!(PixelLayout::Rgba8.bytes_per_pixel(), 4);
4003 assert_eq!(PixelLayout::Bgr8.bytes_per_pixel(), 3);
4004 assert_eq!(PixelLayout::Bgra8.bytes_per_pixel(), 4);
4005 assert_eq!(PixelLayout::Gray8.bytes_per_pixel(), 1);
4006 assert_eq!(PixelLayout::GrayAlpha8.bytes_per_pixel(), 2);
4007 assert_eq!(PixelLayout::Rgb16.bytes_per_pixel(), 6);
4008 assert_eq!(PixelLayout::Rgba16.bytes_per_pixel(), 8);
4009 assert_eq!(PixelLayout::Gray16.bytes_per_pixel(), 2);
4010 assert_eq!(PixelLayout::GrayAlpha16.bytes_per_pixel(), 4);
4011 assert_eq!(PixelLayout::RgbLinearF32.bytes_per_pixel(), 12);
4012 assert_eq!(PixelLayout::RgbaLinearF32.bytes_per_pixel(), 16);
4013 assert_eq!(PixelLayout::GrayLinearF32.bytes_per_pixel(), 4);
4014 assert_eq!(PixelLayout::GrayAlphaLinearF32.bytes_per_pixel(), 8);
4015 assert!(!PixelLayout::Rgb8.is_linear());
4017 assert!(PixelLayout::RgbLinearF32.is_linear());
4018 assert!(PixelLayout::RgbaLinearF32.is_linear());
4019 assert!(PixelLayout::GrayLinearF32.is_linear());
4020 assert!(PixelLayout::GrayAlphaLinearF32.is_linear());
4021 assert!(!PixelLayout::Rgb16.is_linear());
4022 assert!(!PixelLayout::Rgb8.has_alpha());
4024 assert!(PixelLayout::Rgba8.has_alpha());
4025 assert!(PixelLayout::Bgra8.has_alpha());
4026 assert!(PixelLayout::GrayAlpha8.has_alpha());
4027 assert!(PixelLayout::Rgba16.has_alpha());
4028 assert!(PixelLayout::GrayAlpha16.has_alpha());
4029 assert!(PixelLayout::RgbaLinearF32.has_alpha());
4030 assert!(PixelLayout::GrayAlphaLinearF32.has_alpha());
4031 assert!(!PixelLayout::Rgb16.has_alpha());
4032 assert!(!PixelLayout::RgbLinearF32.has_alpha());
4033 assert!(PixelLayout::Rgb16.is_16bit());
4035 assert!(PixelLayout::Rgba16.is_16bit());
4036 assert!(PixelLayout::Gray16.is_16bit());
4037 assert!(PixelLayout::GrayAlpha16.is_16bit());
4038 assert!(!PixelLayout::Rgb8.is_16bit());
4039 assert!(!PixelLayout::RgbLinearF32.is_16bit());
4040 assert!(PixelLayout::RgbLinearF32.is_f32());
4042 assert!(PixelLayout::RgbaLinearF32.is_f32());
4043 assert!(PixelLayout::GrayLinearF32.is_f32());
4044 assert!(PixelLayout::GrayAlphaLinearF32.is_f32());
4045 assert!(!PixelLayout::Rgb8.is_f32());
4046 assert!(!PixelLayout::Rgb16.is_f32());
4047 assert!(PixelLayout::Gray8.is_grayscale());
4049 assert!(PixelLayout::GrayAlpha8.is_grayscale());
4050 assert!(PixelLayout::Gray16.is_grayscale());
4051 assert!(PixelLayout::GrayAlpha16.is_grayscale());
4052 assert!(PixelLayout::GrayLinearF32.is_grayscale());
4053 assert!(PixelLayout::GrayAlphaLinearF32.is_grayscale());
4054 assert!(!PixelLayout::Rgb16.is_grayscale());
4055 assert!(!PixelLayout::RgbLinearF32.is_grayscale());
4056 }
4057
4058 #[test]
4059 fn test_quality_to_distance() {
4060 assert!(Quality::Distance(1.0).to_distance().unwrap() == 1.0);
4061 assert!(Quality::Distance(-1.0).to_distance().is_err());
4062 assert!(Quality::Percent(100).to_distance().is_err()); assert!(Quality::Percent(90).to_distance().unwrap() == 1.0);
4064 }
4065
4066 #[test]
4067 fn test_pixel_validation() {
4068 let cfg = LosslessConfig::new();
4069 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
4070 assert!(req.validate_pixels(&[0u8; 12]).is_ok());
4071 }
4072
4073 #[test]
4074 fn test_pixel_validation_wrong_size() {
4075 let cfg = LosslessConfig::new();
4076 let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
4077 assert!(req.validate_pixels(&[0u8; 11]).is_err());
4078 }
4079
4080 #[test]
4081 fn test_limits_check() {
4082 let limits = Limits::new().with_max_width(100);
4083 let cfg = LosslessConfig::new();
4084 let req = cfg
4085 .encode_request(200, 100, PixelLayout::Rgb8)
4086 .with_limits(&limits);
4087 assert!(req.check_limits().is_err());
4088 }
4089
4090 #[test]
4091 fn test_lossless_encode_rgb8_small() {
4092 let pixels = [255u8, 0, 0].repeat(16);
4094 let result = LosslessConfig::new()
4095 .encode_request(4, 4, PixelLayout::Rgb8)
4096 .encode(&pixels);
4097 assert!(result.is_ok());
4098 let jxl = result.unwrap();
4099 assert_eq!(&jxl[..2], &[0xFF, 0x0A]); }
4101
4102 #[test]
4103 fn test_lossy_encode_rgb8_small() {
4104 let mut pixels = Vec::with_capacity(8 * 8 * 3);
4106 for y in 0..8u8 {
4107 for x in 0..8u8 {
4108 pixels.push(x * 32);
4109 pixels.push(y * 32);
4110 pixels.push(128);
4111 }
4112 }
4113 let result = LossyConfig::new(2.0)
4114 .with_gaborish(false)
4115 .encode_request(8, 8, PixelLayout::Rgb8)
4116 .encode(&pixels);
4117 assert!(result.is_ok());
4118 let jxl = result.unwrap();
4119 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
4120 }
4121
4122 #[test]
4123 fn test_fluent_lossless() {
4124 let pixels = vec![128u8; 4 * 4 * 3];
4125 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Rgb8);
4126 assert!(result.is_ok());
4127 }
4128
4129 #[test]
4130 fn test_lossy_gray8() {
4131 let pixels = vec![128u8; 8 * 8];
4133 let result = LossyConfig::new(2.0)
4134 .with_gaborish(false)
4135 .encode_request(8, 8, PixelLayout::Gray8)
4136 .encode(&pixels);
4137 assert!(result.is_ok(), "lossy Gray8 should encode: {result:?}");
4138 }
4139
4140 #[test]
4141 fn test_lossy_gray_alpha8() {
4142 let pixels: Vec<u8> = (0..8 * 8).flat_map(|_| [128u8, 255]).collect();
4143 let result = LossyConfig::new(2.0)
4144 .with_gaborish(false)
4145 .encode_request(8, 8, PixelLayout::GrayAlpha8)
4146 .encode(&pixels);
4147 assert!(result.is_ok(), "lossy GrayAlpha8 should encode: {result:?}");
4148 }
4149
4150 #[test]
4151 fn test_lossy_gray16() {
4152 let pixels_u16: Vec<u16> = (0..8 * 8).map(|_| 32768u16).collect();
4153 let pixels: &[u8] = bytemuck::cast_slice(&pixels_u16);
4154 let result = LossyConfig::new(2.0)
4155 .with_gaborish(false)
4156 .encode_request(8, 8, PixelLayout::Gray16)
4157 .encode(pixels);
4158 assert!(result.is_ok(), "lossy Gray16 should encode: {result:?}");
4159 }
4160
4161 #[test]
4162 fn test_lossy_rgba_linear_f32() {
4163 let pixels_f32: Vec<f32> = (0..8 * 8).flat_map(|_| [0.5f32, 0.3, 0.7, 1.0]).collect();
4164 let pixels: &[u8] = bytemuck::cast_slice(&pixels_f32);
4165 let result = LossyConfig::new(2.0)
4166 .with_gaborish(false)
4167 .encode_request(8, 8, PixelLayout::RgbaLinearF32)
4168 .encode(pixels);
4169 assert!(
4170 result.is_ok(),
4171 "lossy RgbaLinearF32 should encode: {result:?}"
4172 );
4173 }
4174
4175 #[test]
4176 fn test_lossy_gray_linear_f32() {
4177 let pixels_f32: Vec<f32> = (0..8 * 8).map(|_| 0.5f32).collect();
4178 let pixels: &[u8] = bytemuck::cast_slice(&pixels_f32);
4179 let result = LossyConfig::new(2.0)
4180 .with_gaborish(false)
4181 .encode_request(8, 8, PixelLayout::GrayLinearF32)
4182 .encode(pixels);
4183 assert!(
4184 result.is_ok(),
4185 "lossy GrayLinearF32 should encode: {result:?}"
4186 );
4187 }
4188
4189 #[test]
4190 fn test_lossless_grayalpha8() {
4191 let pixels: Vec<u8> = (0..8 * 8).flat_map(|_| [200u8, 255]).collect();
4192 let result = LosslessConfig::new().encode(&pixels, 8, 8, PixelLayout::GrayAlpha8);
4193 assert!(
4194 result.is_ok(),
4195 "lossless GrayAlpha8 should encode: {result:?}"
4196 );
4197 }
4198
4199 #[test]
4200 fn test_lossless_grayalpha16() {
4201 let pixels_u16: Vec<u16> = (0..8 * 8).flat_map(|_| [32768u16, 65535]).collect();
4202 let pixels: &[u8] = bytemuck::cast_slice(&pixels_u16);
4203 let result = LosslessConfig::new().encode(pixels, 8, 8, PixelLayout::GrayAlpha16);
4204 assert!(
4205 result.is_ok(),
4206 "lossless GrayAlpha16 should encode: {result:?}"
4207 );
4208 }
4209
4210 #[test]
4211 fn test_bgra_lossless() {
4212 let pixels = [0u8, 0, 255, 255].repeat(16);
4214 let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Bgra8);
4215 assert!(result.is_ok());
4216 let jxl = result.unwrap();
4217 assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
4218 }
4219
4220 #[test]
4221 fn test_lossy_alpha_encodes() {
4222 let pixels = [255u8, 0, 0, 255].repeat(64);
4224 let result =
4225 LossyConfig::new(2.0)
4226 .with_gaborish(false)
4227 .encode(&pixels, 8, 8, PixelLayout::Bgra8);
4228 assert!(
4229 result.is_ok(),
4230 "BGRA lossy encode failed: {:?}",
4231 result.err()
4232 );
4233
4234 let result2 = LossyConfig::new(2.0).encode(&pixels, 8, 8, PixelLayout::Rgba8);
4235 assert!(
4236 result2.is_ok(),
4237 "RGBA lossy encode failed: {:?}",
4238 result2.err()
4239 );
4240 }
4241
4242 #[test]
4243 fn test_stop_cancellation() {
4244 use enough::Unstoppable;
4245 let pixels = vec![128u8; 4 * 4 * 3];
4247 let cfg = LosslessConfig::new();
4248 let result = cfg
4249 .encode_request(4, 4, PixelLayout::Rgb8)
4250 .with_stop(&Unstoppable)
4251 .encode(&pixels);
4252 assert!(result.is_ok());
4253 }
4254
4255 #[test]
4256 fn test_lossy_palette_encode() {
4257 let colors = [[255u8, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]];
4259 let mut pixels = Vec::with_capacity(16 * 16 * 3);
4260 for y in 0..16u8 {
4261 for x in 0..16u8 {
4262 let ci = ((y / 4) * 4 + x / 4) as usize % 4;
4263 let noise = ((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 5) as i16 - 2;
4264 for &channel in &colors[ci][..3] {
4265 let v = (channel as i16 + noise).clamp(0, 255) as u8;
4266 pixels.push(v);
4267 }
4268 }
4269 }
4270 let cfg = LosslessConfig::new()
4271 .with_lossy_palette(true)
4272 .with_ans(true);
4273 let result = cfg.encode(&pixels, 16, 16, PixelLayout::Rgb8);
4274 assert!(
4275 result.is_ok(),
4276 "lossy palette encode failed: {:?}",
4277 result.err()
4278 );
4279 let jxl = result.unwrap();
4280 assert_eq!(&jxl[..2], &[0xFF, 0x0A], "JXL signature");
4281
4282 let cursor = std::io::Cursor::new(&jxl);
4284 let reader = std::io::BufReader::new(cursor);
4285 let image = jxl_oxide::JxlImage::builder()
4286 .read(reader)
4287 .expect("jxl-oxide parse");
4288 assert!(
4289 image.width() > 0,
4290 "decoded image should have non-zero width"
4291 );
4292 }
4293
4294 #[test]
4295 fn test_lossy_palette_multi_group() {
4296 let colors = [
4298 [255u8, 0, 0],
4299 [0, 255, 0],
4300 [0, 0, 255],
4301 [255, 255, 0],
4302 [255, 0, 255],
4303 [0, 255, 255],
4304 [128, 128, 128],
4305 [64, 64, 64],
4306 ];
4307 let mut pixels = Vec::with_capacity(300 * 300 * 3);
4308 for y in 0..300u32 {
4309 for x in 0..300u32 {
4310 let ci = ((y / 40) * 8 + x / 40) as usize % colors.len();
4311 let noise = ((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 7) as i16 - 3;
4312 for &channel in &colors[ci][..3] {
4313 let v = (channel as i16 + noise).clamp(0, 255) as u8;
4314 pixels.push(v);
4315 }
4316 }
4317 }
4318
4319 let cfg = LosslessConfig::new()
4321 .with_lossy_palette(true)
4322 .with_ans(true);
4323 let jxl = cfg
4324 .encode(&pixels, 300, 300, PixelLayout::Rgb8)
4325 .expect("lossy palette multi-group encode");
4326 assert_eq!(&jxl[..2], &[0xFF, 0x0A], "JXL signature");
4327 assert!(jxl.len() < 300 * 300 * 3, "should compress");
4328
4329 let out = crate::test_helpers::output_dir("lossy_palette");
4331 let jxl_out = out.join("lossy_palette_multi.jxl");
4332 let png_out = out.join("lossy_palette_multi.png");
4333 std::fs::write(&jxl_out, &jxl).ok();
4334 eprintln!(
4335 "LOSSY_PALETTE_MULTI test: encoded {} bytes ({}x{})",
4336 jxl.len(),
4337 300,
4338 300
4339 );
4340
4341 let djxl_result = std::process::Command::new("djxl")
4343 .args([jxl_out.to_str().unwrap(), png_out.to_str().unwrap()])
4344 .output();
4345 if let Ok(output) = djxl_result {
4346 eprintln!(
4347 "djxl: status={}, stderr={}",
4348 output.status,
4349 String::from_utf8_lossy(&output.stderr)
4350 );
4351 }
4352
4353 let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl).expect("jxl-rs decode failed");
4355 assert_eq!(decoded.width, 300);
4356 assert_eq!(decoded.height, 300);
4357 assert_eq!(decoded.channels, 3);
4358
4359 let mut max_error = 0i32;
4362 let mut error_pos = (0, 0, 0);
4363 for (i, (&orig, &dec)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
4364 let dec_u8 = (dec * 255.0).round().clamp(0.0, 255.0) as u8;
4365 let diff = (orig as i32 - dec_u8 as i32).abs();
4366 if diff > max_error {
4367 max_error = diff;
4368 let pixel = i / 3;
4369 error_pos = (pixel % 300, pixel / 300, i % 3);
4370 }
4371 }
4372 let err_idx = error_pos.1 * 300 * 3 + error_pos.0 * 3 + error_pos.2;
4373 let dec_u8 = (decoded.pixels[err_idx] * 255.0).round().clamp(0.0, 255.0) as u8;
4374 eprintln!(
4375 "max_error={} at ({},{}) ch={}, orig={} decoded={}",
4376 max_error, error_pos.0, error_pos.1, error_pos.2, pixels[err_idx], dec_u8,
4377 );
4378 assert!(
4379 max_error <= 80,
4380 "lossy palette max error {} too large (expected <= 80)",
4381 max_error
4382 );
4383 }
4384
4385 #[test]
4386 fn test_palette_256_colors_regression() {
4387 use crate::modular::channel::{Channel, ModularImage};
4395 use crate::modular::encode::write_modular_stream_with_palette;
4396
4397 let mut pixels = Vec::with_capacity(32 * 32 * 3);
4399 for i in 0..1024u32 {
4400 let idx = (i / 4) as u8;
4401 pixels.push(idx);
4402 pixels.push(((idx as u32 * 7 + 13) & 0xFF) as u8);
4403 pixels.push(((idx as u32 * 31 + 97) & 0xFF) as u8);
4404 }
4405 let cfg = LosslessConfig::new().with_ans(true);
4406 let jxl = cfg
4407 .encode(&pixels, 32, 32, PixelLayout::Rgb8)
4408 .expect("palette 256-colors encode");
4409 let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl).expect("jxl-rs decode failed");
4410 for (i, (&orig, &dec)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
4411 let dec_u8 = (dec * 255.0).round().clamp(0.0, 255.0) as u8;
4412 assert_eq!(
4413 orig, dec_u8,
4414 "32x32: mismatch at byte {}: orig={} decoded={}",
4415 i, orig, dec_u8
4416 );
4417 }
4418
4419 let mut channels = Vec::new();
4421 for c in 0..3 {
4422 let mut ch = Channel::new(16, 16).unwrap();
4423 for y in 0..16 {
4424 for x in 0..16 {
4425 let idx = y * 16 + x;
4426 let val = match c {
4427 0 => idx as i32,
4428 1 => ((idx * 3 + 17) & 0xFF) as i32,
4429 2 => (255 - idx) as i32,
4430 _ => 0,
4431 };
4432 ch.set(x, y, val);
4433 }
4434 }
4435 channels.push(ch);
4436 }
4437 let image = ModularImage {
4438 channels,
4439 bit_depth: 8,
4440 is_grayscale: false,
4441 has_alpha: false,
4442 };
4443 let mut writer = crate::bit_writer::BitWriter::new();
4444 write_modular_stream_with_palette(&image, &mut writer, true, 0, 3)
4445 .expect("palette encode with 256 unique colors must not fail");
4446 }
4447
4448 #[test]
4449 fn test_16bit_tree_learning() {
4450 for &(w, h, layout, label) in &[
4452 (32u32, 32u32, PixelLayout::Rgb16, "32x32 RGB16"),
4453 (8, 8, PixelLayout::Rgba16, "8x8 RGBA16"),
4454 (8, 8, PixelLayout::Rgb16, "8x8 RGB16"),
4455 (16, 16, PixelLayout::Gray16, "16x16 Gray16"),
4456 ] {
4457 let nc = layout.bytes_per_pixel()
4458 / if layout.is_16bit() {
4459 2
4460 } else if layout.is_f32() {
4461 4
4462 } else {
4463 1
4464 };
4465 let mut pixels = vec![0u16; (w * h) as usize * nc];
4466 for y in 0..h {
4467 for x in 0..w {
4468 let idx = ((y * w + x) as usize) * nc;
4469 pixels[idx] = (x * 2048) as u16;
4470 if nc >= 2 {
4471 pixels[idx + 1] = (y * 2048) as u16;
4472 }
4473 if nc >= 3 {
4474 pixels[idx + 2] = ((x + y) * 1024) as u16;
4475 }
4476 if nc >= 4 {
4477 pixels[idx + 3] = 65535; }
4479 }
4480 }
4481 let bytes: Vec<u8> = pixels.iter().flat_map(|v| v.to_ne_bytes()).collect();
4482
4483 let cfg = LosslessConfig::new().with_effort(7).with_ans(true);
4484 let jxl = cfg
4485 .encode(&bytes, w, h, layout)
4486 .unwrap_or_else(|e| panic!("{}: encode failed: {}", label, e));
4487
4488 let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl)
4489 .unwrap_or_else(|e| panic!("{}: jxl-rs decode failed: {}", label, e));
4490 assert_eq!(decoded.width, w as usize, "{}: width", label);
4491 assert_eq!(decoded.height, h as usize, "{}: height", label);
4492
4493 let scale = 65535.0;
4494 let mut mismatches = 0;
4495 for (i, (&orig, &dec_f)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
4496 let dec = (dec_f * scale).round().clamp(0.0, scale) as u16;
4497 if orig != dec && mismatches < 3 {
4498 eprintln!("{}: mismatch[{}]: orig={} dec={}", label, i, orig, dec);
4499 mismatches += 1;
4500 }
4501 }
4502 assert_eq!(mismatches, 0, "{}: {} mismatches", label, mismatches);
4503 eprintln!("{}: PASS ({} bytes)", label, jxl.len());
4504 }
4505 }
4506
4507 #[test]
4508 fn test_srgb_lut_matches_powf() {
4509 for i in 0u16..256 {
4510 let lut_val = SRGB_U8_TO_LINEAR[i as usize];
4511 let fast_val = srgb_to_linear_f(i as f32 / 255.0);
4512 let diff = (lut_val - fast_val).abs();
4513 let tol = fast_val.abs() * 5e-5 + 1e-7;
4515 assert!(
4516 diff <= tol,
4517 "sRGB LUT mismatch at {i}: LUT={lut_val}, fast={fast_val}, diff={diff}"
4518 );
4519 }
4520 }
4521
4522 #[test]
4523 fn test_quality_to_distance_f32_mapping() {
4524 assert_eq!(quality_to_distance(100.0), 0.0);
4526 assert_eq!(quality_to_distance(90.0), 1.0); assert_eq!(quality_to_distance(80.0), 1.5);
4528 assert_eq!(quality_to_distance(70.0), 2.0);
4529 assert_eq!(quality_to_distance(50.0), 4.0);
4530 assert_eq!(quality_to_distance(0.0), 9.0);
4531 assert_eq!(quality_to_distance(110.0), 0.0);
4533 }
4534
4535 #[test]
4536 fn test_calibrated_jxl_quality() {
4537 assert_eq!(calibrated_jxl_quality(0.0), 5.0);
4539 assert_eq!(calibrated_jxl_quality(100.0), 93.8);
4541 assert_eq!(calibrated_jxl_quality(90.0), 84.2);
4543 let mid = calibrated_jxl_quality(52.5);
4545 let expected = 48.5 + 0.5 * (51.9 - 48.5);
4546 assert!(
4547 (mid - expected).abs() < 0.01,
4548 "expected {expected}, got {mid}"
4549 );
4550 }
4551
4552 #[test]
4553 fn test_interp_quality_edge_cases() {
4554 let table = &[(10.0f32, 20.0f32), (20.0, 40.0), (30.0, 60.0)];
4555 assert_eq!(interp_quality(table, 5.0), 20.0);
4557 assert_eq!(interp_quality(table, 35.0), 60.0);
4559 assert_eq!(interp_quality(table, 20.0), 40.0);
4561 assert!((interp_quality(table, 15.0) - 30.0).abs() < 0.001);
4563 }
4564
4565 #[cfg(feature = "__expert")]
4570 mod internal_params {
4571 use super::*;
4572 use crate::effort::{LosslessInternalParams, LossyInternalParams};
4573
4574 fn pseudo_random_rgb8(w: u32, h: u32) -> Vec<u8> {
4578 let mut out = Vec::with_capacity((w * h * 3) as usize);
4579 let mut state: u32 = 0xDEAD_BEEF;
4580 for _ in 0..(w * h) {
4581 let r = state.wrapping_mul(1664525).wrapping_add(1013904223);
4582 state = r;
4583 let g = state.wrapping_mul(1664525).wrapping_add(1013904223);
4584 state = g;
4585 let b = state.wrapping_mul(1664525).wrapping_add(1013904223);
4586 state = b;
4587 out.push((r >> 24) as u8);
4588 out.push((g >> 24) as u8);
4589 out.push((b >> 24) as u8);
4590 }
4591 out
4592 }
4593
4594 #[test]
4595 fn lossless_internal_params_changes_bitstream() {
4596 let params = LosslessInternalParams {
4599 tree_max_buckets: Some(16),
4600 tree_num_properties: Some(3),
4601 nb_rcts_to_try: Some(0),
4602 ..Default::default()
4603 };
4604
4605 let cfg_override = LosslessConfig::new()
4606 .with_effort(7)
4607 .with_internal_params(params)
4608 .with_threads(1);
4609 let cfg_default = LosslessConfig::new().with_effort(7).with_threads(1);
4610
4611 let pixels = pseudo_random_rgb8(64, 64);
4612 let bytes_a = cfg_override
4613 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4614 .expect("override encode");
4615 let bytes_b = cfg_default
4616 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4617 .expect("default encode");
4618
4619 assert_eq!(&bytes_a[..2], &crate::JXL_SIGNATURE);
4620 assert_eq!(&bytes_b[..2], &crate::JXL_SIGNATURE);
4621 assert_ne!(
4622 bytes_a, bytes_b,
4623 "internal_params override should produce different bitstream"
4624 );
4625 }
4626
4627 #[test]
4628 fn lossy_internal_params_changes_bitstream() {
4629 let mut entropy = crate::effort::EntropyMulTable::reference();
4630 entropy.dct8 = 0.95;
4631 let params = LossyInternalParams {
4632 try_dct16: Some(false),
4633 try_dct32: Some(false),
4634 try_dct64: Some(false),
4635 try_dct4x8_afv: Some(false),
4636 k_info_loss_mul_base: Some(1.5),
4637 entropy_mul_table: Some(entropy),
4638 ..Default::default()
4639 };
4640
4641 let cfg_override = LossyConfig::new(2.0)
4642 .with_effort(7)
4643 .with_internal_params(params)
4644 .with_threads(1);
4645 let cfg_default = LossyConfig::new(2.0).with_effort(7).with_threads(1);
4646
4647 let pixels = pseudo_random_rgb8(64, 64);
4648 let bytes_a = cfg_override
4649 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4650 .expect("override encode");
4651 let bytes_b = cfg_default
4652 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4653 .expect("default encode");
4654
4655 assert_eq!(&bytes_a[..2], &crate::JXL_SIGNATURE);
4656 assert_eq!(&bytes_b[..2], &crate::JXL_SIGNATURE);
4657 assert_ne!(
4658 bytes_a, bytes_b,
4659 "internal_params override should produce different bitstream"
4660 );
4661 }
4662
4663 #[test]
4664 fn lossless_internal_params_persist_across_with_effort() {
4665 let params = LosslessInternalParams {
4668 tree_max_buckets: Some(16),
4669 ..Default::default()
4670 };
4671
4672 let cfg = LosslessConfig::new()
4673 .with_internal_params(params)
4674 .with_effort(9) .with_threads(1);
4676
4677 let pixels = pseudo_random_rgb8(64, 64);
4678 let bytes_with_override = cfg
4679 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4680 .expect("encode");
4681 let bytes_e9_plain = LosslessConfig::new()
4682 .with_effort(9)
4683 .with_threads(1)
4684 .encode(&pixels, 64, 64, PixelLayout::Rgb8)
4685 .expect("encode");
4686
4687 assert_ne!(
4688 bytes_with_override, bytes_e9_plain,
4689 "override should persist across with_effort()"
4690 );
4691 }
4692 }
4693}