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 mut columns = [0_i64; 64];
117
118 for column in 0..8 {
119 let input: [i64; 8] =
120 std::array::from_fn(|row| i64::from(*coefficients.get(column + row * 8).unwrap_or(&0)));
121
122 let [dc, rest @ ..] = input;
125 if rest.iter().all(|&value| value == 0) {
126 let flat = ((dc << SCALE) + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
127 for row in 0..8 {
128 if let Some(slot) = columns.get_mut(column + row * 8) {
129 *slot = flat;
130 }
131 }
132 continue;
133 }
134
135 let output = transform(input);
136 for (row, value) in output.into_iter().enumerate() {
137 if let Some(slot) = columns.get_mut(column + row * 8) {
138 *slot = (value + rounding(COLUMN_SHIFT)) >> COLUMN_SHIFT;
139 }
140 }
141 }
142
143 for row in 0..8 {
144 let input: [i64; 8] =
145 std::array::from_fn(|column| *columns.get(column + row * 8).unwrap_or(&0));
146 let output = transform(input);
147
148 let Some(target) = out.get_mut(offset + row * stride..) else {
149 continue;
150 };
151 for (slot, value) in target.iter_mut().take(8).zip(output) {
152 let shifted = (value + rounding(ROW_SHIFT)) >> ROW_SHIFT;
156 *slot = (shifted + 128).clamp(0, 255) as u8;
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
178#[non_exhaustive]
179pub enum Scale {
180 Eighth,
182 Quarter,
184 Half,
186 #[default]
188 Full,
189}
190
191impl Scale {
192 pub const ALL: [Self; 4] = [Self::Eighth, Self::Quarter, Self::Half, Self::Full];
194
195 #[must_use]
197 pub const fn block_size(self) -> u32 {
198 match self {
199 Self::Eighth => 1,
200 Self::Quarter => 2,
201 Self::Half => 4,
202 Self::Full => 8,
203 }
204 }
205
206 #[must_use]
211 pub const fn apply(self, source: u32) -> u32 {
212 let scaled = source.div_ceil(8 / self.block_size());
213 if scaled == 0 { 1 } else { scaled }
215 }
216
217 #[must_use]
223 pub fn fitting(source: (u32, u32), target: (u32, u32)) -> Self {
224 Self::ALL
225 .into_iter()
226 .find(|scale| scale.apply(source.0) >= target.0 && scale.apply(source.1) >= target.1)
227 .unwrap_or(Self::Full)
228 }
229}
230
231const BOX_1: [[i64; 1]; 8] = [[2896], [0], [0], [0], [0], [0], [0], [0]];
258
259const BOX_2: [[i64; 2]; 8] = [
261 [2896, 2896],
262 [2624, -2624],
263 [0, 0],
264 [-922, 922],
265 [0, 0],
266 [616, -616],
267 [0, 0],
268 [-522, 522],
269];
270
271const BOX_4: [[i64; 4]; 8] = [
273 [2896, 2896, 2896, 2896],
274 [3711, 1537, -1537, -3711],
275 [2676, -2676, -2676, 2676],
276 [1303, -3146, 3146, -1303],
277 [0, 0, 0, 0],
278 [-871, 2102, -2102, 871],
279 [-1108, 1108, 1108, -1108],
280 [-738, -306, 306, 738],
281];
282
283fn reduced<const M: usize>(
290 coefficients: &[i32; 64],
291 basis: &[[i64; M]; 8],
292 out: &mut [u8],
293 offset: usize,
294 stride: usize,
295) {
296 let mut rows = [[0_i64; M]; 8];
300 for (v, row) in rows.iter_mut().enumerate() {
301 for (u, weights) in basis.iter().enumerate() {
302 let coefficient = i64::from(coefficients.get(v * 8 + u).copied().unwrap_or(0));
305 if coefficient == 0 {
306 continue;
307 }
308 for (slot, &weight) in row.iter_mut().zip(weights.iter()) {
309 *slot += coefficient * weight;
310 }
311 }
312 }
313
314 for y in 0..M {
315 let Some(target) = out.get_mut(offset + y * stride..) else {
316 continue;
317 };
318 for (x, slot) in target.iter_mut().take(M).enumerate() {
319 let mut sum = 0_i64;
320 for (v, weights) in basis.iter().enumerate() {
321 let value = rows.get(v).and_then(|row| row.get(x)).copied().unwrap_or(0);
322 if value != 0 {
323 sum += value * weights.get(y).copied().unwrap_or(0);
324 }
325 }
326 let shifted = (sum + rounding(2 * SCALE + 2)) >> (2 * SCALE + 2);
327 *slot = (shifted + 128).clamp(0, 255) as u8;
328 }
329 }
330}
331
332pub fn scaled_block(
338 coefficients: &[i32; 64],
339 scale: Scale,
340 out: &mut [u8],
341 offset: usize,
342 stride: usize,
343) {
344 match scale {
345 Scale::Eighth => reduced(coefficients, &BOX_1, out, offset, stride),
346 Scale::Quarter => reduced(coefficients, &BOX_2, out, offset, stride),
347 Scale::Half => reduced(coefficients, &BOX_4, out, offset, stride),
348 Scale::Full => block(coefficients, out, offset, stride),
351 }
352}
353
354#[cfg(test)]
355#[allow(
356 clippy::unwrap_used,
357 clippy::indexing_slicing,
358 reason = "tests operate on known-good values and assert shapes directly"
359)]
360mod tests {
361 use super::*;
362
363 fn reference(coefficients: &[i32; 64]) -> [f64; 64] {
365 let mut out = [0.0; 64];
366 for y in 0..8 {
367 for x in 0..8 {
368 let mut sum = 0.0;
369 for v in 0..8 {
370 for u in 0..8 {
371 let cu = if u == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
372 let cv = if v == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
373 sum += cu
374 * cv
375 * f64::from(coefficients[v * 8 + u])
376 * (((2 * x + 1) as f64 * u as f64 * std::f64::consts::PI) / 16.0).cos()
377 * (((2 * y + 1) as f64 * v as f64 * std::f64::consts::PI) / 16.0).cos();
378 }
379 }
380 out[y * 8 + x] = sum / 4.0 + 128.0;
381 }
382 }
383 out
384 }
385
386 fn decode(coefficients: &[i32; 64]) -> Vec<u8> {
387 let mut out = vec![0_u8; 64];
388 block(coefficients, &mut out, 0, 8);
389 out
390 }
391
392 #[test]
393 fn a_dc_only_block_is_a_flat_grey() {
394 let mut coefficients = [0_i32; 64];
396 coefficients[0] = 8 * 16;
397 assert!(decode(&coefficients).iter().all(|&v| v == 144));
398
399 assert!(decode(&[0; 64]).iter().all(|&v| v == 128));
401 }
402
403 #[test]
404 fn output_matches_the_reference_idct_within_one_step() {
405 let mut cases: Vec<[i32; 64]> = Vec::new();
407 for index in [0, 1, 8, 9, 27, 63] {
408 let mut block = [0_i32; 64];
409 block[index] = 200;
410 cases.push(block);
411 let mut negative = [0_i32; 64];
412 negative[index] = -300;
413 cases.push(negative);
414 }
415 let mut ramp = [0_i32; 64];
416 for (index, slot) in ramp.iter_mut().enumerate() {
417 *slot = (index as i32 % 7) * 20 - 60;
418 }
419 ramp[0] = 400;
420 cases.push(ramp);
421
422 let mut noisy = [0_i32; 64];
423 let mut state = 12_345_u32;
424 for slot in &mut noisy {
425 state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
426 *slot = ((state >> 16) as i32 % 512) - 256;
427 }
428 cases.push(noisy);
429
430 for coefficients in cases {
431 let ours = decode(&coefficients);
432 let theirs = reference(&coefficients);
433 for (index, (&got, &want)) in ours.iter().zip(theirs.iter()).enumerate() {
434 let want = want.clamp(0.0, 255.0);
435 assert!(
436 (f64::from(got) - want).abs() <= 1.0,
437 "sample {index}: got {got}, reference {want:.3}"
438 );
439 }
440 }
441 }
442
443 #[test]
444 fn the_flat_column_shortcut_agrees_with_the_full_transform() {
445 let mut coefficients = [0_i32; 64];
449 coefficients[0] = 300;
450 coefficients[3] = -120;
451 let ours = decode(&coefficients);
452 let theirs = reference(&coefficients);
453 for (&got, &want) in ours.iter().zip(theirs.iter()) {
454 assert!((f64::from(got) - want.clamp(0.0, 255.0)).abs() <= 1.0);
455 }
456 }
457
458 #[test]
459 fn extreme_coefficients_clamp_instead_of_wrapping() {
460 let mut coefficients = [0_i32; 64];
463 coefficients[0] = -32_768;
464 assert!(decode(&coefficients).iter().all(|&v| v == 0));
465 coefficients[0] = 32_767;
466 assert!(decode(&coefficients).iter().all(|&v| v == 255));
467 }
468
469 #[test]
470 fn the_basis_tables_match_the_averages_they_stand_for() {
471 let expect = |m: usize, u: usize, x: usize| -> i64 {
475 let group = 8 / m;
476 let c = if u == 0 { 1.0 / 2.0_f64.sqrt() } else { 1.0 };
477 let sum: f64 = (0..group)
478 .map(|k| {
479 let t = x * group + k;
480 (((2 * t + 1) as f64 * u as f64 * std::f64::consts::PI) / 16.0).cos()
481 })
482 .sum();
483 (c * (m as f64 / 8.0) * sum * f64::from(1_i32 << SCALE)).round() as i64
484 };
485 for (u, row) in BOX_1.iter().enumerate() {
486 for (x, &value) in row.iter().enumerate() {
487 assert_eq!(value, expect(1, u, x), "BOX_1[{u}][{x}]");
488 }
489 }
490 for (u, row) in BOX_2.iter().enumerate() {
491 for (x, &value) in row.iter().enumerate() {
492 assert_eq!(value, expect(2, u, x), "BOX_2[{u}][{x}]");
493 }
494 }
495 for (u, row) in BOX_4.iter().enumerate() {
496 for (x, &value) in row.iter().enumerate() {
497 assert_eq!(value, expect(4, u, x), "BOX_4[{u}][{x}]");
498 }
499 }
500 }
501
502 #[test]
503 fn a_scaled_block_averages_to_what_the_full_block_averages_to() {
504 let mut state = 5_150_u32;
509 for _ in 0..40 {
510 let mut coefficients = [0_i32; 64];
511 for slot in &mut coefficients {
512 state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
513 *slot = ((state >> 20) as i32 % 50) - 25;
514 }
515 coefficients[0] = 200;
516
517 let mut full = vec![0_u8; 64];
518 block(&coefficients, &mut full, 0, 8);
519 assert!(
524 full.iter().all(|&v| v > 0 && v < 255),
525 "the test block clamped; the mean comparison would be vacuous"
526 );
527 let mean = full.iter().map(|&v| u32::from(v)).sum::<u32>() as f64 / 64.0;
528
529 for scale in [Scale::Eighth, Scale::Quarter, Scale::Half] {
530 let m = scale.block_size() as usize;
531 let mut small = vec![0_u8; m * m];
532 scaled_block(&coefficients, scale, &mut small, 0, m);
533 let got = small.iter().map(|&v| u32::from(v)).sum::<u32>() as f64 / (m * m) as f64;
534 assert!(
535 (got - mean).abs() <= 1.5,
536 "{scale:?}: mean {got:.2} against the full block's {mean:.2}"
537 );
538 }
539 }
540 }
541
542 #[test]
543 fn a_flat_block_stays_flat_at_every_scale() {
544 let mut coefficients = [0_i32; 64];
545 coefficients[0] = 8 * 16;
546 for scale in Scale::ALL {
547 let m = scale.block_size() as usize;
548 let mut out = vec![0_u8; m * m];
549 scaled_block(&coefficients, scale, &mut out, 0, m);
550 assert!(
551 out.iter().all(|&v| v == 144),
552 "{scale:?}: {out:?} is not a flat 144"
553 );
554 }
555 }
556
557 #[test]
558 fn a_scaled_block_is_a_box_downsample_of_the_full_one() {
559 let mut coefficients = [0_i32; 64];
565 coefficients[0] = 400;
566 coefficients[1] = -180;
567 coefficients[8] = 120;
568 coefficients[9] = 60;
569 coefficients[2] = 40;
570 coefficients[16] = -30;
571
572 let mut full = vec![0_u8; 64];
573 block(&coefficients, &mut full, 0, 8);
574
575 for scale in [Scale::Quarter, Scale::Half] {
576 let m = scale.block_size() as usize;
577 let factor = 8 / m;
578 let mut small = vec![0_u8; m * m];
579 scaled_block(&coefficients, scale, &mut small, 0, m);
580
581 for y in 0..m {
582 for x in 0..m {
583 let mut sum = 0_u32;
584 for dy in 0..factor {
585 for dx in 0..factor {
586 sum += u32::from(full[(y * factor + dy) * 8 + x * factor + dx]);
587 }
588 }
589 let boxed = sum as f64 / (factor * factor) as f64;
590 let got = f64::from(small[y * m + x]);
591 assert!(
592 (got - boxed).abs() <= 1.5,
593 "{scale:?} at ({x},{y}): {got} against box average {boxed:.1}"
594 );
595 }
596 }
597 }
598 }
599
600 #[test]
601 fn scales_map_sizes_the_way_the_format_counts_blocks() {
602 assert_eq!(Scale::Eighth.apply(64), 8);
603 assert_eq!(Scale::Quarter.apply(64), 16);
604 assert_eq!(Scale::Half.apply(64), 32);
605 assert_eq!(Scale::Full.apply(64), 64);
606 assert_eq!(Scale::Eighth.apply(61), 8);
608 assert_eq!(Scale::Quarter.apply(61), 16);
609 assert_eq!(Scale::Eighth.apply(1), 1);
611 assert_eq!(Scale::Eighth.apply(3), 1);
612 }
613
614 #[test]
615 fn fitting_never_decodes_below_the_target() {
616 let source = (4000_u32, 3000_u32);
617 assert_eq!(Scale::fitting(source, (200, 150)), Scale::Eighth);
619 assert_eq!(Scale::fitting(source, (600, 450)), Scale::Quarter);
621 assert_eq!(Scale::fitting(source, (1200, 900)), Scale::Half);
622 assert_eq!(Scale::fitting(source, (3000, 2250)), Scale::Full);
623 assert_eq!(Scale::fitting(source, (9000, 9000)), Scale::Full);
626
627 assert_eq!(Scale::fitting(source, (200, 400)), Scale::Quarter);
629 }
630
631 #[test]
632 fn blocks_write_at_a_stride_and_clip_at_the_end_of_the_buffer() {
633 let mut coefficients = [0_i32; 64];
634 coefficients[0] = 8 * 16;
635
636 let mut plane = vec![0_u8; 16 * 8];
638 block(&coefficients, &mut plane, 8, 16);
639 for row in 0..8 {
640 assert_eq!(&plane[row * 16..row * 16 + 8], &[0_u8; 8]);
641 assert_eq!(&plane[row * 16 + 8..row * 16 + 16], &[144_u8; 8]);
642 }
643
644 let mut short = vec![0_u8; 8 * 3];
646 block(&coefficients, &mut short, 0, 8);
647 assert!(short.iter().all(|&v| v == 144));
648 }
649}