1pub(crate) const SCALE: u32 = 12;
28
29const COLUMN_SHIFT: u32 = SCALE - 2;
31
32const ROW_SHIFT: u32 = SCALE + 5;
34
35pub(crate) const fn rounding(bits: u32) -> i64 {
37 1 << (bits - 1)
38}
39
40const fn fixed(value: f64) -> i64 {
42 (value * (1_i64 << SCALE) as f64 + 0.5) as i64
43}
44
45pub(crate) const C_0_298: i64 = fixed(0.298_631_336);
47pub(crate) const C_0_390: i64 = fixed(0.390_180_644);
48pub(crate) const C_0_541: i64 = fixed(0.541_196_100);
49pub(crate) const C_0_765: i64 = fixed(0.765_366_865);
50pub(crate) const C_0_899: i64 = fixed(0.899_976_223);
51pub(crate) const C_1_175: i64 = fixed(1.175_875_602);
52pub(crate) const C_1_501: i64 = fixed(1.501_321_110);
53pub(crate) const C_1_847: i64 = fixed(1.847_759_065);
54pub(crate) const C_1_961: i64 = fixed(1.961_570_560);
55pub(crate) const C_2_053: i64 = fixed(2.053_119_869);
56pub(crate) const C_2_562: i64 = fixed(2.562_915_447);
57pub(crate) const C_3_072: i64 = fixed(3.072_711_026);
58
59fn transform([s0, s1, s2, s3, s4, s5, s6, s7]: [i64; 8]) -> [i64; 8] {
65 let shared = (s2 + s6) * C_0_541;
67 let even2 = shared - (s6 * C_1_847);
68 let even3 = shared + (s2 * C_0_765);
69 let sum = (s0 + s4) << SCALE;
70 let difference = (s0 - s4) << SCALE;
71
72 let x0 = sum + even3;
73 let x3 = sum - even3;
74 let x1 = difference + even2;
75 let x2 = difference - even2;
76
77 let a = s7 + s3;
80 let b = s5 + s1;
81 let c = s7 + s1;
82 let d = s5 + s3;
83 let common = (c + d) * C_1_175;
84
85 let p1 = common - (c * C_0_899);
86 let p2 = common - (d * C_2_562);
87 let p3 = -(a * C_1_961);
88 let p4 = -(b * C_0_390);
89
90 let y0 = (s7 * C_0_298) + p1 + p3;
91 let y1 = (s5 * C_2_053) + p2 + p4;
92 let y2 = (s3 * C_3_072) + p2 + p3;
93 let y3 = (s1 * C_1_501) + p1 + p4;
94
95 [
96 x0 + y3,
97 x1 + y2,
98 x2 + y1,
99 x3 + y0,
100 x3 - y0,
101 x2 - y1,
102 x1 - y2,
103 x0 - y3,
104 ]
105}
106
107pub fn block(coefficients: &[i32; 64], out: &mut [u8], offset: usize, stride: usize) {
116 let [dc, ac @ ..] = coefficients;
121 if ac.iter().all(|&value| value == 0) {
122 let flat = ((i64::from(*dc) << SCALE) + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
123 let shifted = ((flat << SCALE) + rounding(ROW_SHIFT)) >> ROW_SHIFT;
124 let value = (shifted + 128).clamp(0, 255) as u8;
125 for row in 0..8 {
126 let start = offset + row * stride;
128 if let Some(target) = out.get_mut(start..(start + 8).min(out.len())) {
129 target.fill(value);
130 }
131 }
132 return;
133 }
134
135 let mut columns = [0_i64; 64];
136
137 for column in 0..8 {
138 let input: [i64; 8] =
139 std::array::from_fn(|row| i64::from(*coefficients.get(column + row * 8).unwrap_or(&0)));
140
141 let [dc, rest @ ..] = input;
144 if rest.iter().all(|&value| value == 0) {
145 let flat = ((dc << SCALE) + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
146 for row in 0..8 {
147 if let Some(slot) = columns.get_mut(column + row * 8) {
148 *slot = flat;
149 }
150 }
151 continue;
152 }
153
154 let output = transform(input);
155 for (row, value) in output.into_iter().enumerate() {
156 if let Some(slot) = columns.get_mut(column + row * 8) {
157 *slot = (value + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
158 }
159 }
160 }
161
162 for row in 0..8 {
163 let input: [i64; 8] =
164 std::array::from_fn(|column| *columns.get(column + row * 8).unwrap_or(&0));
165 let [first, rest @ ..] = input;
167 let output = if rest.iter().all(|&value| value == 0) {
168 [first << SCALE; 8]
169 } else {
170 transform(input)
171 };
172
173 let Some(target) = out.get_mut(offset + row * stride..) else {
174 continue;
175 };
176 for (slot, value) in target.iter_mut().take(8).zip(output) {
177 let shifted = (value + rounding(ROW_SHIFT)) >> ROW_SHIFT;
181 *slot = (shifted + 128).clamp(0, 255) as u8;
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
203#[non_exhaustive]
204pub enum Scale {
205 Eighth,
207 Quarter,
209 Half,
211 #[default]
213 Full,
214}
215
216impl Scale {
217 pub const ALL: [Self; 4] = [Self::Eighth, Self::Quarter, Self::Half, Self::Full];
219
220 #[must_use]
222 pub const fn block_size(self) -> u32 {
223 match self {
224 Self::Eighth => 1,
225 Self::Quarter => 2,
226 Self::Half => 4,
227 Self::Full => 8,
228 }
229 }
230
231 #[must_use]
236 pub const fn apply(self, source: u32) -> u32 {
237 let scaled = source.div_ceil(8 / self.block_size());
238 if scaled == 0 { 1 } else { scaled }
240 }
241
242 #[must_use]
248 pub fn fitting(source: (u32, u32), target: (u32, u32)) -> Self {
249 Self::ALL
250 .into_iter()
251 .find(|scale| scale.apply(source.0) >= target.0 && scale.apply(source.1) >= target.1)
252 .unwrap_or(Self::Full)
253 }
254}
255
256const BOX_1: [[i64; 1]; 8] = [[2896], [0], [0], [0], [0], [0], [0], [0]];
283
284const BOX_2: [[i64; 2]; 8] = [
286 [2896, 2896],
287 [2624, -2624],
288 [0, 0],
289 [-922, 922],
290 [0, 0],
291 [616, -616],
292 [0, 0],
293 [-522, 522],
294];
295
296const BOX_4: [[i64; 4]; 8] = [
298 [2896, 2896, 2896, 2896],
299 [3711, 1537, -1537, -3711],
300 [2676, -2676, -2676, 2676],
301 [1303, -3146, 3146, -1303],
302 [0, 0, 0, 0],
303 [-871, 2102, -2102, 871],
304 [-1108, 1108, 1108, -1108],
305 [-738, -306, 306, 738],
306];
307
308fn reduced<const M: usize>(
315 coefficients: &[i32; 64],
316 basis: &[[i64; M]; 8],
317 out: &mut [u8],
318 offset: usize,
319 stride: usize,
320) {
321 let mut rows = [[0_i64; M]; 8];
325 for (v, row) in rows.iter_mut().enumerate() {
326 for (u, weights) in basis.iter().enumerate() {
327 let coefficient = i64::from(coefficients.get(v * 8 + u).copied().unwrap_or(0));
330 if coefficient == 0 {
331 continue;
332 }
333 for (slot, &weight) in row.iter_mut().zip(weights.iter()) {
334 *slot += coefficient * weight;
335 }
336 }
337 }
338
339 for y in 0..M {
340 let Some(target) = out.get_mut(offset + y * stride..) else {
341 continue;
342 };
343 for (x, slot) in target.iter_mut().take(M).enumerate() {
344 let mut sum = 0_i64;
345 for (v, weights) in basis.iter().enumerate() {
346 let value = rows.get(v).and_then(|row| row.get(x)).copied().unwrap_or(0);
347 if value != 0 {
348 sum += value * weights.get(y).copied().unwrap_or(0);
349 }
350 }
351 let shifted = (sum + rounding(2 * SCALE + 2)) >> (2 * SCALE + 2);
352 *slot = (shifted + 128).clamp(0, 255) as u8;
353 }
354 }
355}
356
357pub fn scaled_block(
363 coefficients: &[i32; 64],
364 scale: Scale,
365 out: &mut [u8],
366 offset: usize,
367 stride: usize,
368) {
369 match scale {
370 Scale::Eighth => reduced(coefficients, &BOX_1, out, offset, stride),
371 Scale::Quarter => reduced(coefficients, &BOX_2, out, offset, stride),
372 Scale::Half => reduced(coefficients, &BOX_4, out, offset, stride),
373 Scale::Full => block(coefficients, out, offset, stride),
376 }
377}
378
379#[cfg(test)]
380#[allow(
381 clippy::unwrap_used,
382 clippy::indexing_slicing,
383 reason = "tests operate on known-good values and assert shapes directly"
384)]
385mod tests {
386 use super::*;
387
388 fn reference(coefficients: &[i32; 64]) -> [f64; 64] {
390 let mut out = [0.0; 64];
391 for y in 0..8 {
392 for x in 0..8 {
393 let mut sum = 0.0;
394 for v in 0..8 {
395 for u in 0..8 {
396 let cu = if u == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
397 let cv = if v == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
398 sum += cu
399 * cv
400 * f64::from(coefficients[v * 8 + u])
401 * (((2 * x + 1) as f64 * u as f64 * std::f64::consts::PI) / 16.0).cos()
402 * (((2 * y + 1) as f64 * v as f64 * std::f64::consts::PI) / 16.0).cos();
403 }
404 }
405 out[y * 8 + x] = sum / 4.0 + 128.0;
406 }
407 }
408 out
409 }
410
411 fn without_shortcuts(coefficients: &[i32; 64]) -> Vec<u8> {
414 let mut columns = [0_i64; 64];
415 for column in 0..8 {
416 let input: [i64; 8] =
417 std::array::from_fn(|row| i64::from(coefficients[column + row * 8]));
418 for (row, value) in transform(input).into_iter().enumerate() {
419 columns[column + row * 8] = (value + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
420 }
421 }
422 let mut out = vec![0_u8; 64];
423 for row in 0..8 {
424 let input: [i64; 8] = std::array::from_fn(|column| columns[column + row * 8]);
425 for (column, value) in transform(input).into_iter().enumerate() {
426 let shifted = (value + rounding(ROW_SHIFT)) >> ROW_SHIFT;
427 out[row * 8 + column] = (shifted + 128).clamp(0, 255) as u8;
428 }
429 }
430 out
431 }
432
433 #[test]
434 fn the_shortcuts_change_no_output() {
435 for dc in -20_000..=20_000 {
437 let mut coefficients = [0_i32; 64];
438 coefficients[0] = dc;
439 assert_eq!(
440 decode(&coefficients),
441 without_shortcuts(&coefficients),
442 "dc {dc}"
443 );
444 }
445 let mut state = 0x9E37_79B9_u32;
448 for _ in 0..20_000 {
449 let mut coefficients = [0_i32; 64];
450 for _ in 0..(state % 6) {
451 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
452 let at = (state >> 8) as usize % 64;
453 coefficients[at] = ((state >> 16) % 2001) as i32 - 1000;
454 }
455 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
456 assert_eq!(
457 decode(&coefficients),
458 without_shortcuts(&coefficients),
459 "{coefficients:?}"
460 );
461 }
462 }
463
464 fn decode(coefficients: &[i32; 64]) -> Vec<u8> {
465 let mut out = vec![0_u8; 64];
466 block(coefficients, &mut out, 0, 8);
467 out
468 }
469
470 #[test]
471 fn a_dc_only_block_is_a_flat_grey() {
472 let mut coefficients = [0_i32; 64];
474 coefficients[0] = 8 * 16;
475 assert!(decode(&coefficients).iter().all(|&v| v == 144));
476
477 assert!(decode(&[0; 64]).iter().all(|&v| v == 128));
479 }
480
481 #[test]
482 fn output_matches_the_reference_idct_within_one_step() {
483 let mut cases: Vec<[i32; 64]> = Vec::new();
485 for index in [0, 1, 8, 9, 27, 63] {
486 let mut block = [0_i32; 64];
487 block[index] = 200;
488 cases.push(block);
489 let mut negative = [0_i32; 64];
490 negative[index] = -300;
491 cases.push(negative);
492 }
493 let mut ramp = [0_i32; 64];
494 for (index, slot) in ramp.iter_mut().enumerate() {
495 *slot = (index as i32 % 7) * 20 - 60;
496 }
497 ramp[0] = 400;
498 cases.push(ramp);
499
500 let mut noisy = [0_i32; 64];
501 let mut state = 12_345_u32;
502 for slot in &mut noisy {
503 state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
504 *slot = ((state >> 16) as i32 % 512) - 256;
505 }
506 cases.push(noisy);
507
508 for coefficients in cases {
509 let ours = decode(&coefficients);
510 let theirs = reference(&coefficients);
511 for (index, (&got, &want)) in ours.iter().zip(theirs.iter()).enumerate() {
512 let want = want.clamp(0.0, 255.0);
513 assert!(
514 (f64::from(got) - want).abs() <= 1.0,
515 "sample {index}: got {got}, reference {want:.3}"
516 );
517 }
518 }
519 }
520
521 #[test]
522 fn the_flat_column_shortcut_agrees_with_the_full_transform() {
523 let mut coefficients = [0_i32; 64];
527 coefficients[0] = 300;
528 coefficients[3] = -120;
529 let ours = decode(&coefficients);
530 let theirs = reference(&coefficients);
531 for (&got, &want) in ours.iter().zip(theirs.iter()) {
532 assert!((f64::from(got) - want.clamp(0.0, 255.0)).abs() <= 1.0);
533 }
534 }
535
536 #[test]
537 fn extreme_coefficients_clamp_instead_of_wrapping() {
538 let mut coefficients = [0_i32; 64];
541 coefficients[0] = -32_768;
542 assert!(decode(&coefficients).iter().all(|&v| v == 0));
543 coefficients[0] = 32_767;
544 assert!(decode(&coefficients).iter().all(|&v| v == 255));
545 }
546
547 #[test]
548 fn the_basis_tables_match_the_averages_they_stand_for() {
549 let expect = |m: usize, u: usize, x: usize| -> i64 {
553 let group = 8 / m;
554 let c = if u == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
555 let sum: f64 = (0..group)
556 .map(|k| {
557 let t = x * group + k;
558 (((2 * t + 1) as f64 * u as f64 * std::f64::consts::PI) / 16.0).cos()
559 })
560 .sum();
561 (c * (m as f64 / 8.0) * sum * f64::from(1_i32 << SCALE)).round() as i64
562 };
563 for (u, row) in BOX_1.iter().enumerate() {
564 for (x, &value) in row.iter().enumerate() {
565 assert_eq!(value, expect(1, u, x), "BOX_1[{u}][{x}]");
566 }
567 }
568 for (u, row) in BOX_2.iter().enumerate() {
569 for (x, &value) in row.iter().enumerate() {
570 assert_eq!(value, expect(2, u, x), "BOX_2[{u}][{x}]");
571 }
572 }
573 for (u, row) in BOX_4.iter().enumerate() {
574 for (x, &value) in row.iter().enumerate() {
575 assert_eq!(value, expect(4, u, x), "BOX_4[{u}][{x}]");
576 }
577 }
578 }
579
580 #[test]
581 fn a_scaled_block_averages_to_what_the_full_block_averages_to() {
582 let mut state = 5_150_u32;
587 for _ in 0..40 {
588 let mut coefficients = [0_i32; 64];
589 for slot in &mut coefficients {
590 state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
591 *slot = ((state >> 20) as i32 % 50) - 25;
592 }
593 coefficients[0] = 200;
594
595 let mut full = vec![0_u8; 64];
596 block(&coefficients, &mut full, 0, 8);
597 assert!(
602 full.iter().all(|&v| v > 0 && v < 255),
603 "the test block clamped; the mean comparison would be vacuous"
604 );
605 let mean = full.iter().map(|&v| u32::from(v)).sum::<u32>() as f64 / 64.0;
606
607 for scale in [Scale::Eighth, Scale::Quarter, Scale::Half] {
608 let m = scale.block_size() as usize;
609 let mut small = vec![0_u8; m * m];
610 scaled_block(&coefficients, scale, &mut small, 0, m);
611 let got = small.iter().map(|&v| u32::from(v)).sum::<u32>() as f64 / (m * m) as f64;
612 assert!(
613 (got - mean).abs() <= 1.5,
614 "{scale:?}: mean {got:.2} against the full block's {mean:.2}"
615 );
616 }
617 }
618 }
619
620 #[test]
621 fn a_flat_block_stays_flat_at_every_scale() {
622 let mut coefficients = [0_i32; 64];
623 coefficients[0] = 8 * 16;
624 for scale in Scale::ALL {
625 let m = scale.block_size() as usize;
626 let mut out = vec![0_u8; m * m];
627 scaled_block(&coefficients, scale, &mut out, 0, m);
628 assert!(
629 out.iter().all(|&v| v == 144),
630 "{scale:?}: {out:?} is not a flat 144"
631 );
632 }
633 }
634
635 #[test]
636 fn a_scaled_block_is_a_box_downsample_of_the_full_one() {
637 let mut coefficients = [0_i32; 64];
643 coefficients[0] = 400;
644 coefficients[1] = -180;
645 coefficients[8] = 120;
646 coefficients[9] = 60;
647 coefficients[2] = 40;
648 coefficients[16] = -30;
649
650 let mut full = vec![0_u8; 64];
651 block(&coefficients, &mut full, 0, 8);
652
653 for scale in [Scale::Quarter, Scale::Half] {
654 let m = scale.block_size() as usize;
655 let factor = 8 / m;
656 let mut small = vec![0_u8; m * m];
657 scaled_block(&coefficients, scale, &mut small, 0, m);
658
659 for y in 0..m {
660 for x in 0..m {
661 let mut sum = 0_u32;
662 for dy in 0..factor {
663 for dx in 0..factor {
664 sum += u32::from(full[(y * factor + dy) * 8 + x * factor + dx]);
665 }
666 }
667 let boxed = sum as f64 / (factor * factor) as f64;
668 let got = f64::from(small[y * m + x]);
669 assert!(
670 (got - boxed).abs() <= 1.5,
671 "{scale:?} at ({x},{y}): {got} against box average {boxed:.1}"
672 );
673 }
674 }
675 }
676 }
677
678 #[test]
679 fn scales_map_sizes_the_way_the_format_counts_blocks() {
680 assert_eq!(Scale::Eighth.apply(64), 8);
681 assert_eq!(Scale::Quarter.apply(64), 16);
682 assert_eq!(Scale::Half.apply(64), 32);
683 assert_eq!(Scale::Full.apply(64), 64);
684 assert_eq!(Scale::Eighth.apply(61), 8);
686 assert_eq!(Scale::Quarter.apply(61), 16);
687 assert_eq!(Scale::Eighth.apply(1), 1);
689 assert_eq!(Scale::Eighth.apply(3), 1);
690 }
691
692 #[test]
693 fn fitting_never_decodes_below_the_target() {
694 let source = (4000_u32, 3000_u32);
695 assert_eq!(Scale::fitting(source, (200, 150)), Scale::Eighth);
697 assert_eq!(Scale::fitting(source, (600, 450)), Scale::Quarter);
699 assert_eq!(Scale::fitting(source, (1200, 900)), Scale::Half);
700 assert_eq!(Scale::fitting(source, (3000, 2250)), Scale::Full);
701 assert_eq!(Scale::fitting(source, (9000, 9000)), Scale::Full);
704
705 assert_eq!(Scale::fitting(source, (200, 400)), Scale::Quarter);
707 }
708
709 #[test]
710 fn blocks_write_at_a_stride_and_clip_at_the_end_of_the_buffer() {
711 let mut coefficients = [0_i32; 64];
712 coefficients[0] = 8 * 16;
713
714 let mut plane = vec![0_u8; 16 * 8];
716 block(&coefficients, &mut plane, 8, 16);
717 for row in 0..8 {
718 assert_eq!(&plane[row * 16..row * 16 + 8], &[0_u8; 8]);
719 assert_eq!(&plane[row * 16 + 8..row * 16 + 16], &[144_u8; 8]);
720 }
721
722 let mut short = vec![0_u8; 8 * 3];
724 block(&coefficients, &mut short, 0, 8);
725 assert!(short.iter().all(|&v| v == 144));
726 }
727}