1use super::ac_strategy::{AcStrategyMap, adjust_quant_field_with_distance, compute_ac_strategy};
8use super::adaptive_quant::{compute_mask1x1, compute_quant_field_float, quantize_quant_field};
9use super::chroma_from_luma::{CflMap, compute_cfl_map};
10use super::common::*;
11use super::frame::{DistanceParams, write_toc};
12use super::gaborish::gaborish_inverse;
13use super::noise::{denoise_xyb, estimate_noise_params, noise_quality_coef};
14use super::static_codes::{get_ac_entropy_code, get_dc_entropy_code};
15use crate::bit_writer::BitWriter;
16#[cfg(feature = "debug-tokens")]
17use crate::debug_log;
18use crate::entropy_coding::encode::{
19 OwnedAnsEntropyCode, OwnedEntropyCode, write_entropy_code_ans, write_tokens, write_tokens_ans,
20};
21use crate::entropy_coding::token::Token;
22use crate::error::Result;
23use crate::headers::frame_header::FrameHeader;
24
25pub(crate) fn force_strategy_map(
27 xsize_blocks: usize,
28 ysize_blocks: usize,
29 raw_strategy: u8,
30) -> AcStrategyMap {
31 AcStrategyMap::force_strategy(xsize_blocks, ysize_blocks, raw_strategy)
32}
33
34pub enum BuiltEntropyCode<'a> {
36 StaticHuffman(crate::entropy_coding::encode::EntropyCode<'a>),
38 Huffman(OwnedEntropyCode),
40 Ans(OwnedAnsEntropyCode),
42}
43
44impl<'a> BuiltEntropyCode<'a> {
45 pub fn write_header(&self, writer: &mut BitWriter) -> Result<()> {
47 match self {
48 BuiltEntropyCode::StaticHuffman(code) => {
49 crate::entropy_coding::encode::write_entropy_code(code, writer)
50 }
51 BuiltEntropyCode::Huffman(code) => {
52 crate::entropy_coding::encode::write_entropy_code(&code.as_entropy_code(), writer)
53 }
54 BuiltEntropyCode::Ans(code) => write_entropy_code_ans(code, writer),
55 }
56 }
57
58 pub fn write_tokens(
60 &self,
61 tokens: &[Token],
62 lz77: Option<&crate::entropy_coding::lz77::Lz77Params>,
63 writer: &mut BitWriter,
64 ) -> Result<()> {
65 match self {
66 BuiltEntropyCode::StaticHuffman(code) => write_tokens(tokens, code, lz77, writer),
67 BuiltEntropyCode::Huffman(code) => {
68 write_tokens(tokens, &code.as_entropy_code(), lz77, writer)
69 }
70 BuiltEntropyCode::Ans(code) => write_tokens_ans(tokens, code, lz77, writer),
71 }
72 }
73
74 pub fn as_huffman(&self) -> crate::entropy_coding::encode::EntropyCode<'_> {
78 match self {
79 BuiltEntropyCode::StaticHuffman(code) => *code,
80 BuiltEntropyCode::Huffman(code) => code.as_entropy_code(),
81 BuiltEntropyCode::Ans(_) => {
82 panic!("ANS codes cannot be used with streaming encoder")
83 }
84 }
85 }
86
87 #[allow(dead_code)]
88 pub fn num_contexts(&self) -> usize {
90 match self {
91 BuiltEntropyCode::StaticHuffman(code) => code.num_contexts,
92 BuiltEntropyCode::Huffman(code) => code.context_map.len(),
93 BuiltEntropyCode::Ans(code) => code.context_map.len(),
94 }
95 }
96
97 #[allow(dead_code)]
98 pub fn num_histograms(&self) -> usize {
100 match self {
101 BuiltEntropyCode::StaticHuffman(code) => code.num_prefix_codes,
102 BuiltEntropyCode::Huffman(code) => code.prefix_codes.len(),
103 BuiltEntropyCode::Ans(code) => code.histograms.len(),
104 }
105 }
106}
107
108pub struct VarDctOutput {
110 pub data: Vec<u8>,
112 pub strategy_counts: [u32; 19],
114}
115
116pub struct VarDctEncoder {
124 pub distance: f32,
126 pub optimize_codes: bool,
130 pub enhanced_clustering: bool,
137 pub use_ans: bool,
141 pub cfl_enabled: bool,
145 pub ac_strategy_enabled: bool,
149 pub custom_orders: bool,
154 pub force_strategy: Option<u8>,
158 pub enable_noise: bool,
163 pub enable_denoise: bool,
169 pub enable_gaborish: bool,
175 pub error_diffusion: bool,
180 pub pixel_domain_loss: bool,
187 pub enable_lz77: bool,
193 pub lz77_method: crate::entropy_coding::lz77::Lz77Method,
200 pub dc_tree_learning: bool,
208 #[cfg(feature = "butteraugli-loop")]
218 pub butteraugli_iters: u32,
219 pub bit_depth_16: bool,
224 pub icc_profile: Option<Vec<u8>>,
227}
228
229impl Default for VarDctEncoder {
230 fn default() -> Self {
231 Self {
232 distance: 1.0,
233 optimize_codes: true,
234 enhanced_clustering: true, use_ans: true, cfl_enabled: true,
237 ac_strategy_enabled: true,
238 custom_orders: true,
239 force_strategy: None,
240 enable_noise: false,
241 enable_denoise: false,
242 enable_gaborish: true,
243 error_diffusion: true, pixel_domain_loss: true, enable_lz77: false, lz77_method: crate::entropy_coding::lz77::Lz77Method::Greedy, dc_tree_learning: false, #[cfg(feature = "butteraugli-loop")]
249 butteraugli_iters: 0, bit_depth_16: false,
251 icc_profile: None,
252 }
253 }
254}
255
256impl VarDctEncoder {
257 pub fn new(distance: f32) -> Self {
259 Self {
260 distance,
261 optimize_codes: true,
262 enhanced_clustering: true, use_ans: true, cfl_enabled: true,
265 ac_strategy_enabled: true,
266 custom_orders: true,
267 force_strategy: None,
268 enable_noise: false,
269 enable_denoise: false,
270 enable_gaborish: true,
271 error_diffusion: true, pixel_domain_loss: true, enable_lz77: false, lz77_method: crate::entropy_coding::lz77::Lz77Method::Greedy, dc_tree_learning: false, #[cfg(feature = "butteraugli-loop")]
277 butteraugli_iters: 0, bit_depth_16: false,
279 icc_profile: None,
280 }
281 }
282
283 pub fn encode(
291 &self,
292 width: usize,
293 height: usize,
294 linear_rgb: &[f32],
295 alpha: Option<&[u8]>,
296 ) -> Result<VarDctOutput> {
297 assert_eq!(linear_rgb.len(), width * height * 3);
298 if let Some(a) = alpha {
299 assert_eq!(a.len(), width * height);
300 }
301
302 let xsize_blocks = div_ceil(width, BLOCK_DIM);
304 let ysize_blocks = div_ceil(height, BLOCK_DIM);
305 let xsize_groups = div_ceil(width, GROUP_DIM);
306 let ysize_groups = div_ceil(height, GROUP_DIM);
307 let xsize_dc_groups = div_ceil(width, DC_GROUP_DIM);
308 let ysize_dc_groups = div_ceil(height, DC_GROUP_DIM);
309 let num_groups = xsize_groups * ysize_groups;
310 let num_dc_groups = xsize_dc_groups * ysize_dc_groups;
311
312 let num_sections = 2 + num_dc_groups + num_groups;
314
315 let padded_width = xsize_blocks * BLOCK_DIM;
317 let padded_height = ysize_blocks * BLOCK_DIM;
318
319 let (mut xyb_x, mut xyb_y, mut xyb_b) =
322 self.convert_to_xyb_padded(width, height, padded_width, padded_height, linear_rgb);
323
324 let noise_params = if self.enable_noise {
327 let quality_coef = noise_quality_coef(self.distance);
328 let params = estimate_noise_params(
329 &xyb_x,
330 &xyb_y,
331 &xyb_b,
332 padded_width,
333 padded_height,
334 quality_coef,
335 );
336
337 if self.enable_denoise
341 && let Some(ref p) = params
342 {
343 denoise_xyb(
344 &mut xyb_x,
345 &mut xyb_y,
346 &mut xyb_b,
347 padded_width,
348 padded_height,
349 p,
350 quality_coef,
351 );
352 }
353
354 params
355 } else {
356 None
357 };
358
359 let pixel_stats = super::frame::PixelStatsForChromacityAdjustment::calc(
362 &xyb_x,
363 &xyb_y,
364 &xyb_b,
365 padded_width,
366 padded_height,
367 );
368 let chromacity_x = pixel_stats.how_much_is_x_channel_pixelized();
369 let chromacity_b = pixel_stats.how_much_is_b_channel_pixelized();
370
371 if self.enable_gaborish {
374 gaborish_inverse(
375 &mut xyb_x,
376 &mut xyb_y,
377 &mut xyb_b,
378 padded_width,
379 padded_height,
380 );
381 }
382
383 let distance_for_iqf = if self.enable_gaborish {
389 self.distance
390 } else {
391 self.distance * 0.62
392 };
393
394 let (quant_field_float, masking) = compute_quant_field_float(
396 &xyb_x,
397 &xyb_y,
398 &xyb_b,
399 padded_width,
400 padded_height,
401 xsize_blocks,
402 ysize_blocks,
403 distance_for_iqf,
404 );
405
406 let mut params =
410 DistanceParams::compute_from_quant_field(self.distance, &quant_field_float);
411
412 params.apply_chromacity_adjustment(chromacity_x, chromacity_b);
414
415 let mut quant_field = quantize_quant_field(&quant_field_float, params.inv_scale);
417
418 let cfl_map = if self.cfl_enabled {
420 compute_cfl_map(
421 &xyb_x,
422 &xyb_y,
423 &xyb_b,
424 padded_width,
425 padded_height,
426 xsize_blocks,
427 ysize_blocks,
428 )
429 } else {
430 CflMap::zeros(
431 div_ceil(xsize_blocks, TILE_DIM_IN_BLOCKS),
432 div_ceil(ysize_blocks, TILE_DIM_IN_BLOCKS),
433 )
434 };
435
436 let mask1x1 = if self.ac_strategy_enabled && self.pixel_domain_loss {
439 Some(compute_mask1x1(&xyb_y, padded_width, padded_height))
440 } else {
441 None
442 };
443
444 let ac_strategy = if let Some(forced) = self.force_strategy {
446 force_strategy_map(xsize_blocks, ysize_blocks, forced)
448 } else if !self.ac_strategy_enabled {
449 AcStrategyMap::new_dct8(xsize_blocks, ysize_blocks)
450 } else {
451 compute_ac_strategy(
452 &xyb_x,
453 &xyb_y,
454 &xyb_b,
455 padded_width,
456 padded_height,
457 xsize_blocks,
458 ysize_blocks,
459 self.distance,
460 &quant_field_float,
461 &masking,
462 &cfl_map,
463 mask1x1.as_deref(),
464 padded_width,
465 )
466 };
467
468 #[cfg(feature = "debug-ac-strategy")]
470 {
471 eprintln!(
472 "AC strategy mode: {}",
473 if mask1x1.is_some() {
474 "pixel-domain"
475 } else {
476 "coefficient-domain"
477 }
478 );
479 ac_strategy.print_histogram();
480 }
481
482 adjust_quant_field_with_distance(&ac_strategy, &mut quant_field, self.distance);
485
486 #[cfg(feature = "butteraugli-loop")]
489 if self.butteraugli_iters > 0 {
490 let initial_quant_field = quant_field.clone();
491 self.butteraugli_refine_quant_field(
492 linear_rgb,
493 width,
494 height,
495 &xyb_x,
496 &xyb_y,
497 &xyb_b,
498 padded_width,
499 padded_height,
500 xsize_blocks,
501 ysize_blocks,
502 ¶ms,
503 &mut quant_field,
504 &initial_quant_field,
505 &cfl_map,
506 &ac_strategy,
507 );
508 }
509
510 let transform_out = self.transform_and_quantize(
512 &xyb_x,
513 &xyb_y,
514 &xyb_b,
515 padded_width,
516 xsize_blocks,
517 ysize_blocks,
518 ¶ms,
519 &mut quant_field,
520 &cfl_map,
521 &ac_strategy,
522 );
523 let quant_dc = &transform_out.quant_dc;
524 let quant_ac = &transform_out.quant_ac;
525 let nzeros = &transform_out.nzeros;
526 let raw_nzeros = &transform_out.raw_nzeros;
527
528 let sharpness_map = if params.epf_iters > 0 && self.distance >= 0.5 {
530 let mask = mask1x1.unwrap_or_else(|| {
531 super::adaptive_quant::compute_mask1x1(&xyb_y, padded_width, padded_height)
532 });
533 Some(super::epf::compute_epf_sharpness(
534 [&xyb_x, &xyb_y, &xyb_b],
535 quant_dc,
536 quant_ac,
537 &quant_field,
538 &mask,
539 ¶ms,
540 &cfl_map,
541 &ac_strategy,
542 self.enable_gaborish,
543 xsize_blocks,
544 ysize_blocks,
545 ))
546 } else {
547 None
548 };
549
550 if self.optimize_codes {
552 let strategy_counts = ac_strategy.strategy_histogram();
553 let data = self.encode_two_pass(
554 width,
555 height,
556 ¶ms,
557 xsize_blocks,
558 ysize_blocks,
559 xsize_groups,
560 ysize_groups,
561 xsize_dc_groups,
562 ysize_dc_groups,
563 num_groups,
564 num_dc_groups,
565 num_sections,
566 quant_dc,
567 quant_ac,
568 nzeros,
569 raw_nzeros,
570 &quant_field,
571 &cfl_map,
572 &ac_strategy,
573 &noise_params,
574 sharpness_map.as_deref(),
575 alpha,
576 )?;
577 return Ok(VarDctOutput {
578 data,
579 strategy_counts,
580 });
581 }
582
583 let dc_code = BuiltEntropyCode::StaticHuffman(get_dc_entropy_code());
585 let ac_code = BuiltEntropyCode::StaticHuffman(get_ac_entropy_code());
586
587 let mut writer = BitWriter::with_capacity(width * height * 4);
589
590 self.write_file_header_and_pad(width, height, false, &mut writer)?;
593 #[cfg(feature = "debug-tokens")]
594 debug_log!(
595 "After file header: bit {} (byte {})",
596 writer.bits_written(),
597 writer.bits_written() / 8
598 );
599
600 {
602 let mut fh = FrameHeader::lossy();
603 fh.x_qm_scale = params.x_qm_scale;
604 fh.b_qm_scale = params.b_qm_scale;
605 fh.epf_iters = params.epf_iters;
606 fh.gaborish = self.enable_gaborish;
607 if noise_params.is_some() {
608 fh.flags |= 0x01; }
610 fh.write(&mut writer)?;
612 }
613 #[cfg(feature = "debug-tokens")]
614 debug_log!(
615 "After frame header: bit {} (byte {})",
616 writer.bits_written(),
617 writer.bits_written() / 8
618 );
619
620 if num_sections == 4 {
623 let block_ctx_map = super::ac_context::BlockCtxMap::default();
625 let num_blocks = xsize_blocks * ysize_blocks;
626 let mut dc_global = BitWriter::with_capacity(4096);
627 self.write_dc_global(
628 ¶ms,
629 num_dc_groups,
630 &dc_code,
631 &noise_params,
632 None,
633 &block_ctx_map,
634 None, &mut dc_global,
636 )?;
637
638 let dc_huffman = dc_code.as_huffman();
640 let ac_huffman = ac_code.as_huffman();
641
642 let mut dc_group = BitWriter::with_capacity(num_blocks * 10);
643 self.write_dc_group(
644 0,
645 quant_dc,
646 xsize_blocks,
647 ysize_blocks,
648 xsize_dc_groups,
649 &quant_field,
650 &cfl_map,
651 &ac_strategy,
652 None, &dc_huffman,
654 &mut dc_group,
655 )?;
656
657 let mut ac_global = BitWriter::with_capacity(4096);
658 self.write_ac_global(num_groups, &ac_code, 0, None, None, &mut ac_global)?;
659
660 let mut ac_group_writer = BitWriter::with_capacity(num_blocks * 100);
661 self.write_ac_group(
662 0,
663 quant_ac,
664 nzeros,
665 raw_nzeros,
666 xsize_blocks,
667 ysize_blocks,
668 xsize_groups,
669 &quant_field,
670 &ac_strategy,
671 &block_ctx_map,
672 &ac_huffman,
673 &mut ac_group_writer,
674 )?;
675
676 #[cfg(feature = "debug-tokens")]
677 {
678 debug_log!(
679 "Section bit counts: DC_global={}, DC_group={}, AC_global={}, AC_group={}",
680 dc_global.bits_written(),
681 dc_group.bits_written(),
682 ac_global.bits_written(),
683 ac_group_writer.bits_written()
684 );
685 }
686
687 let mut combined = dc_global;
689 #[cfg(feature = "debug-tokens")]
690 debug_log!("After DC_global: {} bits", combined.bits_written());
691 combined.append_unaligned(&dc_group)?;
692 #[cfg(feature = "debug-tokens")]
693 debug_log!("After DC_group: {} bits", combined.bits_written());
694 combined.append_unaligned(&ac_global)?;
695 #[cfg(feature = "debug-tokens")]
696 debug_log!("After AC_global: {} bits", combined.bits_written());
697 combined.append_unaligned(&ac_group_writer)?;
698 #[cfg(feature = "debug-tokens")]
699 debug_log!("After AC_group: {} bits", combined.bits_written());
700 combined.zero_pad_to_byte();
701 let combined_bytes = combined.finish();
702
703 #[cfg(feature = "debug-tokens")]
704 {
705 debug_log!("Combined section size: {} bytes", combined_bytes.len());
706 debug_log!(
707 "Before TOC: bit {} (byte {})",
708 writer.bits_written(),
709 writer.bits_written() / 8
710 );
711 }
712 write_toc(&[combined_bytes.len()], &mut writer)?;
713 #[cfg(feature = "debug-tokens")]
714 debug_log!(
715 "After TOC: bit {} (byte {})",
716 writer.bits_written(),
717 writer.bits_written() / 8
718 );
719 writer.append_bytes(&combined_bytes)?;
720 } else {
721 let mut sections: Vec<Vec<u8>> = Vec::with_capacity(num_sections);
723 let dc_huffman = dc_code.as_huffman();
724 let ac_huffman = ac_code.as_huffman();
725
726 let block_ctx_map = super::ac_context::BlockCtxMap::default();
728 let mut dc_global = BitWriter::with_capacity(4096);
729 self.write_dc_global(
730 ¶ms,
731 num_dc_groups,
732 &dc_code,
733 &noise_params,
734 None,
735 &block_ctx_map,
736 None, &mut dc_global,
738 )?;
739 dc_global.zero_pad_to_byte();
740 sections.push(dc_global.finish());
741
742 let blocks_per_dc_group = (256 / 8) * (256 / 8); for dc_group_idx in 0..num_dc_groups {
745 let mut dc_group = BitWriter::with_capacity(blocks_per_dc_group * 10);
746 self.write_dc_group(
747 dc_group_idx,
748 quant_dc,
749 xsize_blocks,
750 ysize_blocks,
751 xsize_dc_groups,
752 &quant_field,
753 &cfl_map,
754 &ac_strategy,
755 None, &dc_huffman,
757 &mut dc_group,
758 )?;
759 dc_group.zero_pad_to_byte();
760 sections.push(dc_group.finish());
761 }
762
763 let mut ac_global = BitWriter::with_capacity(4096);
765 self.write_ac_global(num_groups, &ac_code, 0, None, None, &mut ac_global)?;
766 ac_global.zero_pad_to_byte();
767 sections.push(ac_global.finish());
768
769 let blocks_per_ac_group = (256 / 8) * (256 / 8); for group_idx in 0..num_groups {
772 let mut ac_group_writer = BitWriter::with_capacity(blocks_per_ac_group * 100);
773 self.write_ac_group(
774 group_idx,
775 quant_ac,
776 nzeros,
777 raw_nzeros,
778 xsize_blocks,
779 ysize_blocks,
780 xsize_groups,
781 &quant_field,
782 &ac_strategy,
783 &block_ctx_map,
784 &ac_huffman,
785 &mut ac_group_writer,
786 )?;
787 ac_group_writer.zero_pad_to_byte();
788 sections.push(ac_group_writer.finish());
789 }
790
791 let section_sizes: Vec<usize> = sections.iter().map(|s| s.len()).collect();
792 write_toc(§ion_sizes, &mut writer)?;
793 for section in sections {
794 writer.append_bytes(§ion)?;
795 }
796 }
797
798 let strategy_counts = ac_strategy.strategy_histogram();
799 Ok(VarDctOutput {
800 data: writer.finish_with_padding(),
801 strategy_counts,
802 })
803 }
804
805 #[cfg(feature = "butteraugli-loop")]
820 #[allow(clippy::too_many_arguments)]
821 pub(crate) fn butteraugli_refine_quant_field(
822 &self,
823 linear_rgb: &[f32],
824 width: usize,
825 height: usize,
826 xyb_x: &[f32],
827 xyb_y: &[f32],
828 xyb_b: &[f32],
829 padded_width: usize,
830 padded_height: usize,
831 xsize_blocks: usize,
832 ysize_blocks: usize,
833 params: &DistanceParams,
834 quant_field: &mut [u8],
835 initial_quant_field: &[u8],
836 cfl_map: &CflMap,
837 ac_strategy: &AcStrategyMap,
838 ) {
839 use super::epf;
840 use super::reconstruct::{gab_smooth, reconstruct_xyb, xyb_to_linear_rgb_planar};
841
842 let target_distance = self.distance;
843 let num_blocks = xsize_blocks * ysize_blocks;
844 let padded_pixels = padded_width * padded_height;
845
846 let butteraugli_params = butteraugli::ButteraugliParams::new()
850 .with_intensity_target(80.0)
851 .with_compute_diffmap(true);
852 let reference = match butteraugli::ButteraugliReference::new_linear(
853 linear_rgb,
854 width,
855 height,
856 butteraugli_params,
857 ) {
858 Ok(r) => r,
859 Err(_) => return, };
861
862 let mut qf_float: Vec<f32> = quant_field.iter().map(|&v| v as f32).collect();
865 let initial_qf_float: Vec<f32> = initial_quant_field.iter().map(|&v| v as f32).collect();
866
867 let initial_qf_min = initial_qf_float
871 .iter()
872 .copied()
873 .reduce(f32::min)
874 .unwrap_or(1.0)
875 .max(1.0);
876 let initial_qf_max = initial_qf_float
877 .iter()
878 .copied()
879 .reduce(f32::max)
880 .unwrap_or(255.0);
881 let initial_qf_ratio = initial_qf_max / initial_qf_min;
882 let qf_max_deviation_low = (250.0 / initial_qf_ratio).sqrt();
883 let asymmetry = 2.0f32.min(qf_max_deviation_low);
884 let qf_lower = (initial_qf_min / (asymmetry * qf_max_deviation_low)).max(1.0);
885 let qf_higher = (initial_qf_max * (qf_max_deviation_low / asymmetry)).min(255.0);
886
887 let mut qf_copy = vec![0u8; quant_field.len()];
889 let sharpness = vec![4u8; num_blocks];
890 let mut tile_dist = vec![0.0f32; num_blocks];
891 let mut recon_r = vec![0.0f32; padded_pixels];
893 let mut recon_g = vec![0.0f32; padded_pixels];
894 let mut recon_b = vec![0.0f32; padded_pixels];
895 let mut transform_out = super::transform::TransformOutput::new(xsize_blocks, ysize_blocks);
896
897 for iter in 0..self.butteraugli_iters {
898 for (dst, &src) in qf_copy.iter_mut().zip(qf_float.iter()) {
900 *dst = (src.round() as u8).clamp(1, 255);
901 }
902 self.transform_and_quantize_into(
903 xyb_x,
904 xyb_y,
905 xyb_b,
906 padded_width,
907 xsize_blocks,
908 ysize_blocks,
909 params,
910 &mut qf_copy,
911 cfl_map,
912 ac_strategy,
913 &mut transform_out,
914 );
915
916 let mut planes = reconstruct_xyb(
918 &transform_out.quant_dc,
919 &transform_out.quant_ac,
920 params,
921 &qf_copy,
922 cfl_map,
923 ac_strategy,
924 xsize_blocks,
925 ysize_blocks,
926 );
927
928 if self.enable_gaborish {
930 gab_smooth(&mut planes, padded_width, padded_height);
931 }
932
933 if params.epf_iters > 0 {
935 epf::apply_epf(
936 &mut planes,
937 &qf_copy,
938 &sharpness,
939 params.scale,
940 params.epf_iters,
941 xsize_blocks,
942 ysize_blocks,
943 padded_width,
944 padded_height,
945 );
946 }
947
948 xyb_to_linear_rgb_planar(
950 &planes[0],
951 &planes[1],
952 &planes[2],
953 &mut recon_r,
954 &mut recon_g,
955 &mut recon_b,
956 padded_pixels,
957 );
958
959 let result =
963 match reference.compare_linear_planar(&recon_r, &recon_g, &recon_b, padded_width) {
964 Ok(r) => r,
965 Err(_) => return,
966 };
967
968 let diffmap = match result.diffmap {
969 Some(dm) => dm,
970 None => return,
971 };
972
973 const K_TILE_NORM: f32 = 1.2;
976 let diffmap_buf = diffmap.buf();
977 tile_dist.fill(0.0);
978 for by in 0..ysize_blocks {
979 for bx in 0..xsize_blocks {
980 if !ac_strategy.is_first(bx, by) {
981 continue;
982 }
983 let covered_x = ac_strategy.covered_blocks_x(bx, by);
984 let covered_y = ac_strategy.covered_blocks_y(bx, by);
985 let px_start_x = bx * BLOCK_DIM;
986 let px_start_y = by * BLOCK_DIM;
987 let px_end_x = ((bx + covered_x) * BLOCK_DIM).min(width);
988 let px_end_y = ((by + covered_y) * BLOCK_DIM).min(height);
989 if px_start_x >= width || px_start_y >= height {
990 continue;
991 }
992 let mut dist_norm = 0.0f64;
993 let mut pixels = 0.0f64;
994 for py in px_start_y..px_end_y {
995 for px in px_start_x..px_end_x {
996 let v = diffmap_buf[py * width + px] as f64;
997 let v2 = v * v;
999 let v4 = v2 * v2;
1000 let v8 = v4 * v4;
1001 let v16 = v8 * v8;
1002 dist_norm += v16;
1003 pixels += 1.0;
1004 }
1005 }
1006 if pixels == 0.0 {
1007 pixels = 1.0;
1008 }
1009 let td = K_TILE_NORM * (dist_norm / pixels).sqrt().sqrt().sqrt().sqrt() as f32;
1011 for sy in 0..covered_y {
1013 for sx in 0..covered_x {
1014 tile_dist[(by + sy) * xsize_blocks + (bx + sx)] = td;
1015 }
1016 }
1017 }
1018 }
1019
1020 if iter == 1 {
1032 const K_INIT_MUL: f64 = 0.6;
1033 const K_ONE_MINUS_INIT_MUL: f64 = 1.0 - K_INIT_MUL;
1034 for bi in 0..num_blocks {
1035 let init_qf = initial_qf_float[bi] as f64;
1036 let cur_qf = qf_float[bi] as f64;
1037 let clamp_val = K_ONE_MINUS_INIT_MUL * cur_qf + K_INIT_MUL * init_qf;
1038 if cur_qf < clamp_val {
1039 qf_float[bi] = (clamp_val as f32).clamp(qf_lower, qf_higher);
1040 }
1041 }
1042 }
1043
1044 let cur_pow: f64 = if iter < 2 {
1048 0.2 + (target_distance as f64 - 1.0) * 0.0 } else {
1050 0.0
1051 };
1052
1053 for bi in 0..num_blocks {
1054 let diff = tile_dist[bi] / target_distance;
1055 let old_qf = qf_float[bi];
1056
1057 if diff <= 1.0 {
1058 if cur_pow != 0.0 {
1060 qf_float[bi] = old_qf * (diff as f64).powf(cur_pow) as f32;
1062 }
1063 } else {
1065 qf_float[bi] = old_qf * diff;
1067 if qf_float[bi].round() as u8 == old_qf.round() as u8 {
1069 qf_float[bi] = old_qf + 1.0;
1070 }
1071 }
1072 qf_float[bi] = qf_float[bi].clamp(qf_lower, qf_higher);
1074 }
1075
1076 eprintln!(
1077 " Butteraugli iter {}: score={:.3} (target={:.3})",
1078 iter, result.score, target_distance,
1079 );
1080 }
1081
1082 for (dst, &src) in quant_field.iter_mut().zip(qf_float.iter()) {
1084 *dst = (src.round() as u8).clamp(1, 255);
1085 }
1086 }
1087
1088 #[cfg(feature = "rate-control")]
1103 pub fn encode_with_rate_control(
1104 &self,
1105 width: usize,
1106 height: usize,
1107 linear_rgb: &[f32],
1108 ) -> Result<Vec<u8>> {
1109 let config = super::rate_control::RateControlConfig::default();
1110 let (encoded, _iters) =
1111 self.encode_with_rate_control_config(width, height, linear_rgb, &config)?;
1112 Ok(encoded)
1113 }
1114
1115 #[cfg(feature = "rate-control")]
1121 pub fn encode_with_rate_control_config(
1122 &self,
1123 width: usize,
1124 height: usize,
1125 linear_rgb: &[f32],
1126 config: &super::rate_control::RateControlConfig,
1127 ) -> Result<(Vec<u8>, usize)> {
1128 let precomputed = super::precomputed::EncoderPrecomputed::compute(
1130 width,
1131 height,
1132 linear_rgb,
1133 self.distance,
1134 self.cfl_enabled,
1135 self.ac_strategy_enabled,
1136 self.pixel_domain_loss,
1137 self.enable_noise,
1138 self.enable_denoise,
1139 self.enable_gaborish,
1140 self.force_strategy,
1141 );
1142
1143 super::rate_control::encode_with_rate_control(self, &precomputed, config)
1145 }
1146
1147 #[cfg(feature = "rate-control")]
1155 pub fn encode_from_precomputed(
1156 &self,
1157 precomputed: &super::precomputed::EncoderPrecomputed,
1158 quant_field: &[u8],
1159 ) -> Result<Vec<u8>> {
1160 let width = precomputed.width;
1161 let height = precomputed.height;
1162 let xsize_blocks = precomputed.xsize_blocks;
1163 let ysize_blocks = precomputed.ysize_blocks;
1164 let padded_width = precomputed.padded_width;
1165
1166 let xsize_groups = div_ceil(width, GROUP_DIM);
1168 let ysize_groups = div_ceil(height, GROUP_DIM);
1169 let xsize_dc_groups = div_ceil(width, DC_GROUP_DIM);
1170 let ysize_dc_groups = div_ceil(height, DC_GROUP_DIM);
1171 let num_groups = xsize_groups * ysize_groups;
1172 let num_dc_groups = xsize_dc_groups * ysize_dc_groups;
1173 let num_sections = 2 + num_dc_groups + num_groups;
1174
1175 let mut quant_field = quant_field.to_vec();
1177 adjust_quant_field_with_distance(&precomputed.ac_strategy, &mut quant_field, self.distance);
1178
1179 let mut params =
1181 DistanceParams::compute_from_quant_field(self.distance, &precomputed.quant_field_float);
1182
1183 params.apply_chromacity_adjustment(
1185 precomputed.chromacity_x_pixelized,
1186 precomputed.chromacity_b_pixelized,
1187 );
1188
1189 let transform_out = self.transform_and_quantize(
1191 &precomputed.xyb_x,
1192 &precomputed.xyb_y,
1193 &precomputed.xyb_b,
1194 padded_width,
1195 xsize_blocks,
1196 ysize_blocks,
1197 ¶ms,
1198 &mut quant_field,
1199 &precomputed.cfl_map,
1200 &precomputed.ac_strategy,
1201 );
1202 let quant_dc = &transform_out.quant_dc;
1203 let quant_ac = &transform_out.quant_ac;
1204 let nzeros = &transform_out.nzeros;
1205 let raw_nzeros = &transform_out.raw_nzeros;
1206
1207 self.encode_two_pass(
1209 width,
1210 height,
1211 ¶ms,
1212 xsize_blocks,
1213 ysize_blocks,
1214 xsize_groups,
1215 ysize_groups,
1216 xsize_dc_groups,
1217 ysize_dc_groups,
1218 num_groups,
1219 num_dc_groups,
1220 num_sections,
1221 quant_dc,
1222 quant_ac,
1223 nzeros,
1224 raw_nzeros,
1225 &quant_field,
1226 &precomputed.cfl_map,
1227 &precomputed.ac_strategy,
1228 &precomputed.noise_params,
1229 None, None, )
1232 }
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237 use super::*;
1238
1239 #[test]
1240 fn test_encoder_creation() {
1241 let encoder = VarDctEncoder::new(1.0);
1242 assert_eq!(encoder.distance, 1.0);
1243
1244 let encoder_default = VarDctEncoder::default();
1245 assert_eq!(encoder_default.distance, 1.0);
1246 }
1247
1248 #[test]
1249 fn test_encode_small_image() {
1250 let encoder = VarDctEncoder::new(1.0);
1251
1252 let width = 8;
1254 let height = 8;
1255 let mut linear_rgb = vec![0.0f32; width * height * 3];
1256 for y in 0..height {
1257 for x in 0..width {
1258 let idx = (y * width + x) * 3;
1259 linear_rgb[idx] = 1.0; linear_rgb[idx + 1] = 0.0; linear_rgb[idx + 2] = 0.0; }
1263 }
1264
1265 let result = encoder.encode(width, height, &linear_rgb, None);
1267 assert!(result.is_ok());
1269 let output = result.unwrap();
1270 assert!(output.data.len() > 2);
1271 assert_eq!(output.data[0], 0xFF);
1272 assert_eq!(output.data[1], 0x0A);
1273 }
1274
1275 #[test]
1276 fn test_convert_to_xyb_padded() {
1277 let encoder = VarDctEncoder::new(1.0);
1278
1279 let linear_rgb = vec![0.5, 0.5, 0.5];
1281 let (x, y, b) = encoder.convert_to_xyb_padded(1, 1, 8, 8, &linear_rgb);
1282
1283 assert_eq!(x.len(), 64);
1285 assert_eq!(y.len(), 64);
1286 assert_eq!(b.len(), 64);
1287
1288 assert!(x[0].abs() < 0.01, "X should be near zero for gray");
1290 assert!(y[0] > 0.0, "Y should be positive");
1291 assert!(b[0] > 0.0, "B should be positive");
1292
1293 for i in 0..64 {
1295 assert!((x[i] - x[0]).abs() < 1e-6, "All padded X should match");
1296 assert!((y[i] - y[0]).abs() < 1e-6, "All padded Y should match");
1297 assert!((b[i] - b[0]).abs() < 1e-6, "All padded B should match");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_encode_16x16_red_image() {
1303 let encoder = VarDctEncoder::new(1.0);
1305
1306 let width = 16;
1307 let height = 16;
1308 let mut linear_rgb = vec![0.0f32; width * height * 3];
1309 for y in 0..height {
1310 for x in 0..width {
1311 let idx = (y * width + x) * 3;
1312 linear_rgb[idx] = 1.0; linear_rgb[idx + 1] = 0.0; linear_rgb[idx + 2] = 0.0; }
1316 }
1317
1318 let result = encoder.encode(width, height, &linear_rgb, None);
1319 assert!(result.is_ok());
1320 let output = result.unwrap();
1321
1322 eprintln!("Output file size: {} bytes", output.data.len());
1323 eprintln!(
1324 "First 32 bytes: {:02x?}",
1325 &output.data[..32.min(output.data.len())]
1326 );
1327
1328 std::fs::write(std::env::temp_dir().join("our_16x16.jxl"), &output.data).unwrap();
1330
1331 assert_eq!(output.data[0], 0xFF);
1340 assert_eq!(output.data[1], 0x0A);
1341 }
1342
1343 fn hash_bytes(bytes: &[u8]) -> u64 {
1345 use std::hash::{Hash, Hasher};
1346 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1347 bytes.hash(&mut hasher);
1348 hasher.finish()
1349 }
1350
1351 #[test]
1355 #[cfg(target_arch = "x86_64")]
1356 fn test_hash_lock_8x8_gradient() {
1357 let encoder = VarDctEncoder::new(1.0);
1358 let width = 8;
1359 let height = 8;
1360 let mut linear_rgb = vec![0.0f32; width * height * 3];
1361
1362 for y in 0..height {
1364 for x in 0..width {
1365 let idx = (y * width + x) * 3;
1366 linear_rgb[idx] = x as f32 / 7.0; linear_rgb[idx + 1] = y as f32 / 7.0; linear_rgb[idx + 2] = 0.5; }
1370 }
1371
1372 let bytes = encoder
1373 .encode(width, height, &linear_rgb, None)
1374 .unwrap()
1375 .data;
1376 let hash = hash_bytes(&bytes);
1377
1378 const EXPECTED_HASH: u64 = 0x1578d7de62b7489d;
1381 assert_eq!(
1382 hash,
1383 EXPECTED_HASH,
1384 "8x8 gradient hash mismatch: got {:#x}, expected {:#x}. \
1385 Output size: {} bytes. If intentional, update EXPECTED_HASH.",
1386 hash,
1387 EXPECTED_HASH,
1388 bytes.len()
1389 );
1390 }
1391
1392 #[test]
1395 #[cfg(target_arch = "x86_64")]
1396 fn test_hash_lock_16x16_solid() {
1397 let encoder = VarDctEncoder::new(1.0);
1398 let width = 16;
1399 let height = 16;
1400 let linear_rgb = vec![0.3f32; width * height * 3]; let bytes = encoder
1403 .encode(width, height, &linear_rgb, None)
1404 .unwrap()
1405 .data;
1406 let hash = hash_bytes(&bytes);
1407
1408 const EXPECTED_HASH: u64 = 0x2cf2e7aae4f14de7;
1410 assert_eq!(
1411 hash,
1412 EXPECTED_HASH,
1413 "16x16 solid hash mismatch: got {:#x}, expected {:#x}. \
1414 Output size: {} bytes. If intentional, update EXPECTED_HASH.",
1415 hash,
1416 EXPECTED_HASH,
1417 bytes.len()
1418 );
1419 }
1420
1421 #[test]
1424 #[cfg(target_arch = "x86_64")]
1425 fn test_hash_lock_64x64_checkerboard() {
1426 let encoder = VarDctEncoder::new(1.0);
1427 let width = 64;
1428 let height = 64;
1429 let mut linear_rgb = vec![0.0f32; width * height * 3];
1430
1431 for y in 0..height {
1433 for x in 0..width {
1434 let idx = (y * width + x) * 3;
1435 let checker = ((x / 8) + (y / 8)) % 2 == 0;
1436 let val = if checker { 0.8 } else { 0.2 };
1437 linear_rgb[idx] = val;
1438 linear_rgb[idx + 1] = val;
1439 linear_rgb[idx + 2] = val;
1440 }
1441 }
1442
1443 let bytes = encoder
1444 .encode(width, height, &linear_rgb, None)
1445 .unwrap()
1446 .data;
1447 let hash = hash_bytes(&bytes);
1448
1449 const EXPECTED_HASH: u64 = 0xd91c3989788e5448;
1451 assert_eq!(
1452 hash,
1453 EXPECTED_HASH,
1454 "64x64 checkerboard hash mismatch: got {:#x}, expected {:#x}. \
1455 Output size: {} bytes. If intentional, update EXPECTED_HASH.",
1456 hash,
1457 EXPECTED_HASH,
1458 bytes.len()
1459 );
1460 }
1461
1462 #[test]
1465 #[cfg(target_arch = "x86_64")]
1466 fn test_hash_lock_13x17_noise() {
1467 let encoder = VarDctEncoder::new(1.0);
1468 let width = 13;
1469 let height = 17;
1470 let mut linear_rgb = vec![0.0f32; width * height * 3];
1471
1472 let mut seed = 12345u64;
1474 for val in &mut linear_rgb {
1475 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
1476 *val = ((seed >> 32) as f32) / (u32::MAX as f32);
1477 }
1478
1479 let bytes = encoder
1480 .encode(width, height, &linear_rgb, None)
1481 .unwrap()
1482 .data;
1483 let hash = hash_bytes(&bytes);
1484
1485 const EXPECTED_HASH: u64 = 0x5324c4f675e42ff7;
1487 assert_eq!(
1488 hash,
1489 EXPECTED_HASH,
1490 "13x17 noise hash mismatch: got {:#x}, expected {:#x}. \
1491 Output size: {} bytes. If intentional, update EXPECTED_HASH.",
1492 hash,
1493 EXPECTED_HASH,
1494 bytes.len()
1495 );
1496 }
1497
1498 #[test]
1507 fn test_roundtrip_non_8_aligned() {
1508 for &(w, h) in &[(100, 75), (13, 17), (33, 49), (7, 9)] {
1509 let mut linear_rgb = vec![0.0f32; w * h * 3];
1510
1511 for y in 0..h {
1513 for x in 0..w {
1514 let idx = (y * w + x) * 3;
1515 linear_rgb[idx] = x as f32 / w.max(1) as f32;
1516 linear_rgb[idx + 1] = y as f32 / h.max(1) as f32;
1517 linear_rgb[idx + 2] = 0.3;
1518 }
1519 }
1520
1521 let encoder = VarDctEncoder::new(1.0);
1522 let bytes = encoder
1523 .encode(w, h, &linear_rgb, None)
1524 .unwrap_or_else(|e| panic!("encode {}x{} failed: {}", w, h, e))
1525 .data;
1526
1527 assert_eq!(bytes[0], 0xFF, "{}x{}: bad signature byte 0", w, h);
1529 assert_eq!(bytes[1], 0x0A, "{}x{}: bad signature byte 1", w, h);
1530
1531 let image = jxl_oxide::JxlImage::builder()
1533 .read(std::io::Cursor::new(&bytes))
1534 .unwrap_or_else(|e| panic!("jxl-oxide decode {}x{} failed: {}", w, h, e));
1535 assert_eq!(
1536 image.width(),
1537 w as u32,
1538 "{}x{}: decoded width mismatch",
1539 w,
1540 h
1541 );
1542 assert_eq!(
1543 image.height(),
1544 h as u32,
1545 "{}x{}: decoded height mismatch",
1546 w,
1547 h
1548 );
1549
1550 let render = image
1552 .render_frame(0)
1553 .unwrap_or_else(|e| panic!("jxl-oxide render {}x{} failed: {}", w, h, e));
1554 let _pixels = render.image_all_channels();
1555 }
1556 }
1557
1558 #[test]
1560 fn test_dc_tree_learning() {
1561 let width = 64;
1562 let height = 64;
1563
1564 let mut linear_rgb = vec![0.0f32; width * height * 3];
1566 for y in 0..height {
1567 for x in 0..width {
1568 let idx = (y * width + x) * 3;
1569 linear_rgb[idx] = x as f32 / width as f32;
1570 linear_rgb[idx + 1] = y as f32 / height as f32;
1571 linear_rgb[idx + 2] = 0.5;
1572 }
1573 }
1574
1575 let mut encoder_baseline = VarDctEncoder::new(1.0);
1577 encoder_baseline.dc_tree_learning = false;
1578 let bytes_baseline = encoder_baseline
1579 .encode(width, height, &linear_rgb, None)
1580 .expect("baseline encode failed")
1581 .data;
1582
1583 let mut encoder_learned = VarDctEncoder::new(1.0);
1585 encoder_learned.dc_tree_learning = true;
1586 std::fs::write(
1587 std::env::temp_dir().join("dc_baseline_test.jxl"),
1588 &bytes_baseline,
1589 )
1590 .unwrap();
1591 let bytes_learned = encoder_learned
1592 .encode(width, height, &linear_rgb, None)
1593 .expect("learned encode failed")
1594 .data;
1595 std::fs::write(
1596 std::env::temp_dir().join("dc_learned_test.jxl"),
1597 &bytes_learned,
1598 )
1599 .unwrap();
1600
1601 eprintln!(
1602 "DC tree learning: baseline={} bytes, learned={} bytes (delta={:.2}%)",
1603 bytes_baseline.len(),
1604 bytes_learned.len(),
1605 (bytes_learned.len() as f64 / bytes_baseline.len() as f64 - 1.0) * 100.0
1606 );
1607
1608 assert_eq!(bytes_baseline[0], 0xFF);
1610 assert_eq!(bytes_baseline[1], 0x0A);
1611 assert_eq!(bytes_learned[0], 0xFF);
1612 assert_eq!(bytes_learned[1], 0x0A);
1613
1614 {
1616 let image = jxl_oxide::JxlImage::builder()
1617 .read(std::io::Cursor::new(&bytes_baseline))
1618 .expect("jxl-oxide parse of baseline failed");
1619 let render = image
1620 .render_frame(0)
1621 .expect("jxl-oxide render of baseline failed");
1622 let _pixels = render.image_all_channels();
1623 eprintln!("Baseline ANS decodes OK ({} bytes)", bytes_baseline.len());
1624 }
1625
1626 let image = jxl_oxide::JxlImage::builder()
1628 .read(std::io::Cursor::new(&bytes_learned))
1629 .expect("jxl-oxide decode of learned version failed");
1630 assert_eq!(image.width(), width as u32);
1631 assert_eq!(image.height(), height as u32);
1632
1633 let render = image
1635 .render_frame(0)
1636 .expect("jxl-oxide render of learned version failed");
1637 let _pixels = render.image_all_channels();
1638 eprintln!("Learned ANS decodes OK ({} bytes)", bytes_learned.len());
1639
1640 std::fs::write(
1642 std::env::temp_dir().join("dc_learned_test.jxl"),
1643 &bytes_learned,
1644 )
1645 .unwrap();
1646 }
1647
1648 #[cfg(feature = "butteraugli-loop")]
1650 #[test]
1651 fn test_butteraugli_loop_basic() {
1652 let width = 64;
1654 let height = 64;
1655 let mut linear_rgb = vec![0.0f32; width * height * 3];
1656 for y in 0..height {
1657 for x in 0..width {
1658 let idx = (y * width + x) * 3;
1659 let fx = x as f32 / width as f32;
1660 let fy = y as f32 / height as f32;
1661 linear_rgb[idx] = fx * 0.8; linear_rgb[idx + 1] = fy * 0.6; linear_rgb[idx + 2] = (1.0 - fx) * 0.4; }
1665 }
1666
1667 let mut encoder_baseline = VarDctEncoder::new(2.0);
1669 encoder_baseline.butteraugli_iters = 0;
1670 let bytes_baseline = encoder_baseline
1671 .encode(width, height, &linear_rgb, None)
1672 .expect("baseline encode failed")
1673 .data;
1674
1675 let mut encoder_loop = VarDctEncoder::new(2.0);
1677 encoder_loop.butteraugli_iters = 2;
1678 let bytes_loop = encoder_loop
1679 .encode(width, height, &linear_rgb, None)
1680 .expect("butteraugli loop encode failed")
1681 .data;
1682
1683 assert_eq!(bytes_baseline[0], 0xFF);
1685 assert_eq!(bytes_baseline[1], 0x0A);
1686 assert_eq!(bytes_loop[0], 0xFF);
1687 assert_eq!(bytes_loop[1], 0x0A);
1688
1689 eprintln!(
1691 "Baseline: {} bytes, Butteraugli loop (2 iters): {} bytes",
1692 bytes_baseline.len(),
1693 bytes_loop.len()
1694 );
1695
1696 let image = jxl_oxide::JxlImage::builder()
1698 .read(std::io::Cursor::new(&bytes_loop))
1699 .expect("jxl-oxide decode of butteraugli loop output failed");
1700 assert_eq!(image.width(), width as u32);
1701 assert_eq!(image.height(), height as u32);
1702
1703 let render = image
1704 .render_frame(0)
1705 .expect("jxl-oxide render of butteraugli loop output failed");
1706 let _pixels = render.image_all_channels();
1707 eprintln!("Butteraugli loop output decodes OK");
1708 }
1709}