Skip to main content

otf_pixels_codec_jpeg/
idct.rs

1//! The inverse DCT, in fixed point.
2//!
3//! # Why fixed point
4//!
5//! ADR-0011: integer arithmetic gives the same answer on every machine, which
6//! is what lets the engine promise byte-identical output run to run and across
7//! thread counts. A float IDCT would be a per-target result, and "the same
8//! image twice" would stop being a testable property.
9//!
10//! The transform is the usual even/odd decomposition of the 8-point IDCT
11//! (Loeffler-Ligtenberg-Moerkotte), applied to columns then rows. Constants
12//! are scaled by 2^12; the column pass keeps two extra bits of precision for
13//! the row pass to spend, so the passes shift right by 10 and 17. The two
14//! constant scales and the two shifts leave a net 2^-3, which is exactly the
15//! 1/8 a normalized 2-D IDCT ends on.
16//!
17//! Accuracy is within the ±1 tolerance the JPEG conformance spec allows, which
18//! is why decoded output is compared against reference decoders with a
19//! tolerance rather than for equality — no two IDCTs agree exactly, and the
20//! format does not require them to.
21
22/// Fixed-point scale for the transform constants.
23///
24/// Shared with the forward transform in [`crate::fdct`]: the two directions
25/// are built from the same eight constants, and letting them drift apart
26/// would be a silent accuracy bug in whichever one was not being tested.
27pub(crate) const SCALE: u32 = 12;
28
29/// Right shift applied after the column pass, keeping two bits of headroom.
30const COLUMN_SHIFT: u32 = SCALE - 2;
31
32/// Right shift applied after the row pass, spending that headroom.
33const ROW_SHIFT: u32 = SCALE + 5;
34
35/// The rounding term for a right shift of `bits`.
36pub(crate) const fn rounding(bits: u32) -> i64 {
37    1 << (bits - 1)
38}
39
40/// Round a float constant into [`SCALE`]-bit fixed point.
41const fn fixed(value: f64) -> i64 {
42    (value * (1_i64 << SCALE) as f64 + 0.5) as i64
43}
44
45// The eight distinct constants the decomposition needs.
46pub(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
59/// One 8-point inverse DCT, returning values scaled by 2^`SCALE`.
60///
61/// Splitting even and odd inputs is what makes this eleven multiplies instead
62/// of sixty-four: the even half is a 4-point transform, and the odd half's
63/// four outputs share three of their products.
64fn transform([s0, s1, s2, s3, s4, s5, s6, s7]: [i64; 8]) -> [i64; 8] {
65    // Even part: inputs 0, 2, 4, 6.
66    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    // Odd part: inputs 1, 3, 5, 7. The four cross sums below are each used
78    // twice, which is the saving the decomposition exists for.
79    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
107/// Inverse-transform one dequantized block into 8 rows of `out`, level-shifted
108/// and clamped to `u8`.
109///
110/// `out` is written at `offset` with `stride` bytes between rows, so blocks
111/// land directly in a component plane with no intermediate copy. Rows that
112/// fall outside `out` are dropped rather than being an error: the bottom and
113/// right edges of an image are padded to whole blocks by the format, and that
114/// padding has nowhere to go.
115pub 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        // An all-zero AC column — the common case in flat areas — is a
123        // constant, so the whole transform collapses to one broadcast.
124        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            // Undo the column pass's headroom and both constant scales, then
153            // re-centre: JPEG codes samples as signed values around zero, so
154            // the 128 the encoder subtracted comes back here.
155            let shifted = (value + rounding(ROW_SHIFT)) >> ROW_SHIFT;
156            *slot = (shifted + 128).clamp(0, 255) as u8;
157        }
158    }
159}
160
161/// How much of an image a decode produces, as eighths of full size.
162///
163/// # What it buys
164///
165/// A downsampled block can be produced directly from its coefficients, so a
166/// thumbnail never materializes the full-resolution image for the rest of the
167/// pipeline to carry: at 1/8 the resize, colour conversion and every later op
168/// see one sixty-fourth of the pixels. The reduced transform is also cheaper
169/// than the full one at 1/8 and 1/4, though at 1/2 it is not — it folds all
170/// sixty-four coefficients into sixteen outputs. The saving that matters is
171/// downstream, not in the transform.
172///
173/// This is a *resolution* reduction, not random access: rows still arrive top
174/// to bottom, and every coefficient is still entropy-decoded. A scaled decode
175/// is therefore a decoder configuration rather than a
176/// [`DecodeCapability`](otf_pixels_core::DecodeCapability).
177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
178#[non_exhaustive]
179pub enum Scale {
180    /// 1/8 scale: the DC coefficient alone.
181    Eighth,
182    /// 1/4 scale.
183    Quarter,
184    /// 1/2 scale.
185    Half,
186    /// Full resolution.
187    #[default]
188    Full,
189}
190
191impl Scale {
192    /// Every scale, coarsest first.
193    pub const ALL: [Self; 4] = [Self::Eighth, Self::Quarter, Self::Half, Self::Full];
194
195    /// The side of the block this scale produces, in samples: `M` of `M/8`.
196    #[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    /// The size `source` decodes to at this scale.
207    ///
208    /// Rounded up, as the format requires: an image 61 pixels wide is 8 blocks
209    /// across, and at 1/8 scale those 8 blocks are 8 samples.
210    #[must_use]
211    pub const fn apply(self, source: u32) -> u32 {
212        let scaled = source.div_ceil(8 / self.block_size());
213        // A one-pixel image still has a block, and so still has a sample.
214        if scaled == 0 { 1 } else { scaled }
215    }
216
217    /// The coarsest scale still at least `target` in both axes.
218    ///
219    /// Never returns a scale that would decode *below* the target: enlarging
220    /// afterwards would throw away detail and then invent it back, which is
221    /// worse than decoding one step larger and shrinking properly.
222    #[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
231/// Basis tables for the reduced transforms: `BOX_M[u][x]` maps coefficient
232/// `u` onto reduced output `x`, scaled by `2^SCALE`.
233///
234/// # What these are
235///
236/// Each entry is `C(u) * (M/8) * sum over the full-resolution samples that
237/// output `x` covers, of cos((2t+1)*u*pi/16)`. That makes the reduced
238/// transform *exactly* the box average of the full one — the average of the
239/// `8/M` samples it replaces — rather than an approximation of it.
240///
241/// The obvious alternative, inverse-transforming only the top-left `MxM`
242/// coefficients, is not the same thing and is measurably worse: it discards
243/// the high-frequency coefficients outright instead of folding their
244/// contribution into the average, so detailed images come out visibly wrong.
245/// Against libjpeg's own scaled decode on a noise fixture at 1/2, truncation
246/// was ten times further from the true downsample than this is.
247///
248/// Note `u = 4` contributing nothing at 1/2 scale, and every odd-indexed
249/// coefficient contributing nothing at 1/8: those are the terms that average
250/// to zero over their group. libjpeg's reduced transforms drop exactly the
251/// same terms, which is a useful sign that both are computing the same thing.
252///
253/// Hardcoded rather than computed, because `cos` is not available in a `const`
254/// context and because a value computed at run time could differ in its last
255/// bit between targets — which ADR-0011 forbids. The tests check every entry
256/// against the real cosines, so a transcription slip fails loudly.
257const BOX_1: [[i64; 1]; 8] = [[2896], [0], [0], [0], [0], [0], [0], [0]];
258
259/// The 1/4-scale basis.
260const 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
271/// The 1/2-scale basis.
272const 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
283/// Inverse-transform a block at a reduced scale, into `MxM` samples.
284///
285/// Both passes multiply by a `2^SCALE` constant and the transform carries the
286/// 1/4 every 2-D IDCT ends on, so the result is descaled by `2 * SCALE + 2` —
287/// the same total the full transform applies, which is what makes the two
288/// agree on a flat block.
289fn 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    // Rows first. Every coefficient row contributes, unlike a truncating
297    // transform: that is the whole difference between this and a crop of the
298    // low frequencies.
299    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            // Most coefficients in a real block are zero, and skipping them
303            // is worth more than any arrangement of the loop below.
304            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
332/// Inverse-transform one dequantized block at `scale`.
333///
334/// Writes `scale.block_size()` rows of that many samples at `offset`, so a
335/// caller's plane geometry scales with the same factor and nothing else in the
336/// decoder needs to know a reduced transform happened.
337pub 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        // The full transform is separable and specialized; the general form
349        // above would be correct but several times slower.
350        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    /// The textbook floating-point IDCT, to check the fixed-point one against.
364    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        // DC of 8*16 is a level of 16 above the 128 mid-point.
395        let mut coefficients = [0_i32; 64];
396        coefficients[0] = 8 * 16;
397        assert!(decode(&coefficients).iter().all(|&v| v == 144));
398
399        // A zero block is the mid-grey the level shift centres on.
400        assert!(decode(&[0; 64]).iter().all(|&v| v == 128));
401    }
402
403    #[test]
404    fn output_matches_the_reference_idct_within_one_step() {
405        // A spread of blocks: single frequencies, a ramp, and a noisy one.
406        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        // Only the DC and one AC row are set, so most columns take the
446        // shortcut and one does not. Both must land on the same answer as the
447        // reference transform.
448        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        // A maximally negative and a maximally positive DC, which without
461        // clamping would wrap around the byte range.
462        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        // The tables are hardcoded because `cos` is not const and because a
472        // value computed at run time could differ in its last bit between
473        // targets. This is what stops that from being a place a typo hides.
474        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        // The defining property of a reduced IDCT: it is the full block
505        // low-pass filtered, so the mean must survive. A scaling error shows
506        // up here immediately, because the mean is carried entirely by the DC
507        // coefficient the reduced transform keeps.
508        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            // Clamping would break the premise rather than the transform: a
520            // sample pinned at 0 or 255 has had energy discarded, and the
521            // means would then legitimately differ. The AC range above keeps
522            // the block clear of both ends, and this is what proves it.
523            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        // Not "close to" but *is*: the basis is defined as the average over
560        // the samples each reduced output replaces, so the only difference
561        // from box-averaging the full transform is fixed-point rounding in
562        // two places. This is the property the whole design rests on, and the
563        // one a truncating transform would fail outright.
564        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        // Rounded up: 61 pixels is 8 blocks, and 8 blocks is 8 samples at 1/8.
607        assert_eq!(Scale::Eighth.apply(61), 8);
608        assert_eq!(Scale::Quarter.apply(61), 16);
609        // Never zero, however small the source.
610        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        // A 200x150 thumbnail fits inside the 1/8 decode (500x375).
618        assert_eq!(Scale::fitting(source, (200, 150)), Scale::Eighth);
619        // 600 wide does not fit 1/8's 500, so the next step up is used.
620        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        // A target larger than the source cannot be met by any scale, so the
624        // most detailed one is the right answer rather than an error.
625        assert_eq!(Scale::fitting(source, (9000, 9000)), Scale::Full);
626
627        // Both axes have to fit, not just one.
628        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        // A 16-wide plane, block written to the right half.
637        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        // A plane with only three rows: the rest of the block is dropped.
645        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}