1use core::f64::consts::PI;
4use core::fmt;
5use std::sync::LazyLock;
6
7pub(crate) const fn low_len(sample_len: usize) -> usize {
8 sample_len.div_ceil(2)
9}
10
11pub(crate) const fn high_len(sample_len: usize) -> usize {
12 sample_len / 2
13}
14
15pub(crate) fn idct8_basis(sample_idx: usize, freq: usize) -> f64 {
16 debug_assert!(sample_idx < 8);
17 debug_assert!(freq < 8);
18
19 idct8_basis_table()[sample_idx][freq]
20}
21
22pub(crate) fn idct8_basis_table() -> &'static [[f64; 8]; 8] {
23 static BASIS: LazyLock<[[f64; 8]; 8]> = LazyLock::new(|| {
24 let mut basis = [[0.0; 8]; 8];
25 for (sample_idx, row) in basis.iter_mut().enumerate() {
26 for (freq, value) in row.iter_mut().enumerate() {
27 *value = idct8_basis_uncached(sample_idx, freq);
28 }
29 }
30 basis
31 });
32 &BASIS
33}
34
35#[expect(
36 clippy::cast_precision_loss,
37 reason = "DCT basis indices are debug-asserted below eight and exactly representable in f64"
38)]
39fn idct8_basis_uncached(sample_idx: usize, freq: usize) -> f64 {
40 let scale = if freq == 0 {
41 (1.0_f64 / 8.0).sqrt()
42 } else {
43 (2.0_f64 / 8.0).sqrt()
44 };
45 scale * (((sample_idx as f64 + 0.5) * freq as f64 * PI) / 8.0).cos()
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct DctGridError {
51 block_count: usize,
52 block_cols: usize,
53 block_rows: usize,
54 width: usize,
55 height: usize,
56}
57
58impl DctGridError {
59 const fn new(
60 block_count: usize,
61 block_cols: usize,
62 block_rows: usize,
63 width: usize,
64 height: usize,
65 ) -> Self {
66 Self {
67 block_count,
68 block_cols,
69 block_rows,
70 width,
71 height,
72 }
73 }
74
75 #[must_use]
77 pub const fn block_count(self) -> usize {
78 self.block_count
79 }
80
81 #[must_use]
83 pub const fn block_cols(self) -> usize {
84 self.block_cols
85 }
86
87 #[must_use]
89 pub const fn block_rows(self) -> usize {
90 self.block_rows
91 }
92
93 #[must_use]
95 pub const fn width(self) -> usize {
96 self.width
97 }
98
99 #[must_use]
101 pub const fn height(self) -> usize {
102 self.height
103 }
104}
105
106impl fmt::Display for DctGridError {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(
109 f,
110 "DCT grid has {} blocks for {}x{} grid covering requested {}x{} samples",
111 self.block_count, self.block_cols, self.block_rows, self.width, self.height
112 )
113 }
114}
115
116impl std::error::Error for DctGridError {}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum DctTransformError {
126 Grid(DctGridError),
128 InvalidSamplePlaneDimensions {
130 width: usize,
132 height: usize,
134 },
135 SamplePlaneLengthMismatch {
137 sample_count: usize,
139 width: usize,
141 height: usize,
143 },
144 SymbolicWeightIndexOutOfRange {
146 sample_len: usize,
148 output_index: usize,
150 high_pass: bool,
152 },
153 MemoryCapExceeded {
155 requested: usize,
157 cap: usize,
159 },
160 HostAllocationFailed {
162 bytes: usize,
164 },
165}
166
167impl From<DctGridError> for DctTransformError {
168 fn from(value: DctGridError) -> Self {
169 Self::Grid(value)
170 }
171}
172
173impl fmt::Display for DctTransformError {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 Self::Grid(error) => error.fmt(f),
177 Self::InvalidSamplePlaneDimensions { width, height } => {
178 write!(
179 f,
180 "sample plane dimensions must be non-zero, got {width}x{height}"
181 )
182 }
183 Self::SamplePlaneLengthMismatch {
184 sample_count,
185 width,
186 height,
187 } => write!(
188 f,
189 "sample plane has {sample_count} values for declared {width}x{height} dimensions"
190 ),
191 Self::SymbolicWeightIndexOutOfRange {
192 sample_len,
193 output_index,
194 high_pass,
195 } => write!(
196 f,
197 "symbolic 5/3 row {output_index} is outside the {}-pass extent for {sample_len} samples",
198 if *high_pass { "high" } else { "low" }
199 ),
200 Self::MemoryCapExceeded { requested, cap } => write!(
201 f,
202 "DCT transform workspace requires {requested} bytes, exceeding the {cap}-byte cap"
203 ),
204 Self::HostAllocationFailed { bytes } => {
205 write!(f, "DCT transform host allocation failed for {bytes} bytes")
206 }
207 }
208 }
209}
210
211impl std::error::Error for DctTransformError {
212 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
213 match self {
214 Self::Grid(error) => Some(error),
215 Self::InvalidSamplePlaneDimensions { .. }
216 | Self::SamplePlaneLengthMismatch { .. }
217 | Self::SymbolicWeightIndexOutOfRange { .. }
218 | Self::MemoryCapExceeded { .. }
219 | Self::HostAllocationFailed { .. } => None,
220 }
221 }
222}
223
224pub(crate) fn validate_dct_block_grid(
225 block_count: usize,
226 block_cols: usize,
227 block_rows: usize,
228 width: usize,
229 height: usize,
230) -> Result<(), DctGridError> {
231 let expected_blocks = block_cols
232 .checked_mul(block_rows)
233 .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;
234 let covered_width = block_cols
235 .checked_mul(8)
236 .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;
237 let covered_height = block_rows
238 .checked_mul(8)
239 .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;
240
241 if block_count != expected_blocks
242 || width == 0
243 || height == 0
244 || width > covered_width
245 || height > covered_height
246 {
247 return Err(DctGridError::new(
248 block_count,
249 block_cols,
250 block_rows,
251 width,
252 height,
253 ));
254 }
255 Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260 use std::error::Error as _;
261
262 use super::{
263 high_len, idct8_basis, low_len, validate_dct_block_grid, DctGridError, DctTransformError,
264 };
265
266 #[test]
267 fn band_lengths_split_even_and_odd_samples() {
268 assert_eq!((low_len(0), high_len(0)), (0, 0));
269 assert_eq!((low_len(1), high_len(1)), (1, 0));
270 assert_eq!((low_len(8), high_len(8)), (4, 4));
271 assert_eq!((low_len(9), high_len(9)), (5, 4));
272 }
273
274 #[test]
275 fn idct8_basis_is_orthonormal_for_dc_and_first_ac() {
276 assert!((idct8_basis(0, 0) - (1.0_f64 / 8.0).sqrt()).abs() < 1e-12);
277 assert!((idct8_basis(0, 1) - 0.490_392_640_201_615_2).abs() < 1e-12);
278 }
279
280 #[test]
281 fn validates_non_empty_grid_covered_by_blocks() {
282 assert_eq!(validate_dct_block_grid(6, 3, 2, 24, 16), Ok(()));
283 }
284
285 #[test]
286 fn rejects_overflowing_grid_dimensions() {
287 let err = validate_dct_block_grid(1, usize::MAX, usize::MAX, 8, 8)
288 .expect_err("overflowing dimensions must fail");
289 assert_eq!(err.block_count(), 1);
290 assert_eq!(err.block_cols(), usize::MAX);
291 assert_eq!(err.block_rows(), usize::MAX);
292 }
293
294 #[test]
295 fn rejects_zero_image_extent() {
296 assert!(validate_dct_block_grid(1, 1, 1, 0, 8).is_err());
297 assert!(validate_dct_block_grid(1, 1, 1, 8, 0).is_err());
298 }
299
300 #[test]
301 fn transform_error_conversion_display_and_sources_preserve_typed_context() {
302 let grid =
303 validate_dct_block_grid(1, 2, 1, 16, 8).expect_err("block count mismatch must fail");
304 let cases = [
305 (
306 DctTransformError::from(grid),
307 "DCT grid has 1 blocks for 2x1 grid covering requested 16x8 samples",
308 ),
309 (
310 DctTransformError::InvalidSamplePlaneDimensions {
311 width: 0,
312 height: 8,
313 },
314 "sample plane dimensions must be non-zero, got 0x8",
315 ),
316 (
317 DctTransformError::SamplePlaneLengthMismatch {
318 sample_count: 7,
319 width: 2,
320 height: 4,
321 },
322 "sample plane has 7 values for declared 2x4 dimensions",
323 ),
324 (
325 DctTransformError::SymbolicWeightIndexOutOfRange {
326 sample_len: 5,
327 output_index: 3,
328 high_pass: true,
329 },
330 "symbolic 5/3 row 3 is outside the high-pass extent for 5 samples",
331 ),
332 (
333 DctTransformError::MemoryCapExceeded {
334 requested: 17,
335 cap: 16,
336 },
337 "DCT transform workspace requires 17 bytes, exceeding the 16-byte cap",
338 ),
339 (
340 DctTransformError::HostAllocationFailed { bytes: 32 },
341 "DCT transform host allocation failed for 32 bytes",
342 ),
343 ];
344
345 for (index, (error, expected)) in cases.into_iter().enumerate() {
346 assert_eq!(error.to_string(), expected);
347 if index == 0 {
348 assert_eq!(
349 error
350 .source()
351 .and_then(|source| source.downcast_ref::<DctGridError>()),
352 Some(&grid)
353 );
354 } else {
355 assert!(error.source().is_none());
356 }
357 }
358 }
359}