Skip to main content

j2k_native/
inspect.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Lightweight JPEG 2000 codestream header inspection.
4
5extern crate alloc;
6
7use alloc::vec::Vec;
8use core::fmt;
9
10use crate::{MAX_J2K_IMAGE_DIMENSION, MAX_J2K_SPEC_COMPONENTS, MAX_J2K_TILE_COUNT};
11
12const MARKER_SOC: u8 = 0x4F;
13const MARKER_CAP: u8 = 0x50;
14const MARKER_SIZ: u8 = 0x51;
15const MARKER_COD: u8 = 0x52;
16const MARKER_SOT: u8 = 0x90;
17const MARKER_SOD: u8 = 0x93;
18const MARKER_EOC: u8 = 0xD9;
19
20/// Parsed JPEG 2000 codestream metadata from the main header.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct J2kCodestreamHeaderMetadata {
23    /// Reference-grid image dimensions derived from SIZ.
24    pub dimensions: (u32, u32),
25    /// Number of codestream components.
26    pub components: u16,
27    /// Maximum component precision in bits.
28    pub bit_depth: u8,
29    /// Reference tile width and height.
30    pub tile_size: (u32, u32),
31    /// Number of reference tiles horizontally and vertically.
32    pub tile_count: (u32, u32),
33    /// Per-component SIZ precision and sampling metadata.
34    pub component_info: Vec<J2kCodestreamComponentHeader>,
35    /// Number of resolution levels from COD.
36    pub resolution_levels: u8,
37    /// Whether COD enables a multi-component transform.
38    pub has_mct: bool,
39    /// Whether COD selects the reversible 5/3 transform.
40    pub reversible: bool,
41    /// Whether the codestream advertises high-throughput block coding.
42    pub high_throughput: bool,
43}
44
45/// Parsed SIZ component metadata.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct J2kCodestreamComponentHeader {
48    /// Component precision in bits.
49    pub bit_depth: u8,
50    /// Whether component samples are signed.
51    pub signed: bool,
52    /// Horizontal SIZ sampling factor (`XRsiz`).
53    pub x_rsiz: u8,
54    /// Vertical SIZ sampling factor (`YRsiz`).
55    pub y_rsiz: u8,
56}
57
58/// Error returned by [`inspect_j2k_codestream_header`].
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum J2kCodestreamHeaderError {
62    /// Input was shorter than the required prefix.
63    TooShort {
64        /// Required byte count.
65        need: usize,
66        /// Available byte count.
67        have: usize,
68    },
69    /// Input ended while reading a marker or marker segment.
70    TruncatedAt {
71        /// Byte offset where the truncated segment begins.
72        offset: usize,
73        /// Segment being read.
74        segment: &'static str,
75    },
76    /// A codestream marker did not start with `0xFF`.
77    InvalidMarker {
78        /// Byte offset of the invalid marker.
79        offset: usize,
80        /// Byte found where the marker code was expected.
81        marker: u8,
82    },
83    /// A required codestream marker was absent.
84    MissingRequiredMarker {
85        /// Missing marker name.
86        marker: &'static str,
87    },
88    /// A generic marker segment was malformed.
89    InvalidSegment {
90        /// Byte offset of the segment length.
91        offset: usize,
92        /// Description of the invalid segment.
93        what: &'static str,
94    },
95    /// The SIZ marker segment was malformed or unsupported.
96    InvalidSiz {
97        /// Description of the invalid SIZ segment.
98        what: &'static str,
99    },
100    /// The COD marker segment was malformed or unsupported.
101    InvalidCod {
102        /// Description of the invalid COD segment.
103        what: &'static str,
104    },
105    /// The header is valid, but outside the public inspection contract.
106    Unsupported {
107        /// Description of the unsupported feature.
108        what: &'static str,
109    },
110    /// The host allocator rejected bounded per-component header metadata.
111    HostAllocationFailed {
112        /// Requested metadata byte count.
113        bytes: usize,
114    },
115}
116
117impl fmt::Display for J2kCodestreamHeaderError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::TooShort { need, have } => {
121                write!(f, "input too short: need {need} bytes, have {have}")
122            }
123            Self::TruncatedAt { offset, segment } => {
124                write!(f, "truncated {segment} at offset {offset}")
125            }
126            Self::InvalidMarker { offset, marker } => {
127                write!(
128                    f,
129                    "invalid codestream marker FF{marker:02X} at offset {offset}"
130                )
131            }
132            Self::MissingRequiredMarker { marker } => {
133                write!(f, "missing required codestream marker {marker}")
134            }
135            Self::InvalidSegment { what, .. } => write!(f, "invalid marker segment: {what}"),
136            Self::InvalidSiz { what } => write!(f, "invalid SIZ segment: {what}"),
137            Self::InvalidCod { what } => write!(f, "invalid COD segment: {what}"),
138            Self::Unsupported { what } => write!(f, "unsupported codestream header: {what}"),
139            Self::HostAllocationFailed { bytes } => {
140                write!(f, "codestream header allocation failed for {bytes} bytes")
141            }
142        }
143    }
144}
145
146impl core::error::Error for J2kCodestreamHeaderError {}
147
148/// Inspect a raw JPEG 2000 codestream main header without decoding tile data.
149///
150/// This helper reads SIZ/COD metadata and stops at SOT/SOD/EOC. It intentionally
151/// does not require full decode headers such as QCD, so callers can inspect the
152/// same lightweight codestreams that later decode construction may reject.
153///
154/// # Errors
155///
156/// Returns an error when required markers are missing, truncated, or inconsistent.
157pub fn inspect_j2k_codestream_header(
158    input: &[u8],
159) -> Result<J2kCodestreamHeaderMetadata, J2kCodestreamHeaderError> {
160    if input.len() < 2 {
161        return Err(J2kCodestreamHeaderError::TooShort {
162            need: 2,
163            have: input.len(),
164        });
165    }
166    if !looks_like_j2k_codestream(input) {
167        return Err(J2kCodestreamHeaderError::InvalidMarker {
168            offset: 0,
169            marker: input[1],
170        });
171    }
172
173    let mut offset = 2usize;
174    let mut siz = None;
175    let mut cod = None;
176    let mut high_throughput_cap = false;
177    let mut terminated = false;
178
179    while offset < input.len() {
180        let marker = read_marker(input, &mut offset)?;
181        match marker {
182            MARKER_SOT | MARKER_SOD | MARKER_EOC => {
183                terminated = true;
184                break;
185            }
186            MARKER_SIZ => {
187                let payload = read_segment_payload(input, &mut offset, "SIZ")?;
188                siz = Some(parse_siz(payload)?);
189            }
190            MARKER_COD => {
191                let payload = read_segment_payload(input, &mut offset, "COD")?;
192                cod = Some(parse_cod(payload)?);
193            }
194            MARKER_CAP => {
195                let _ = read_segment_payload(input, &mut offset, "CAP")?;
196                high_throughput_cap = true;
197            }
198            0x30..=0x3F => {}
199            _ => {
200                let _ = read_segment_payload(input, &mut offset, "segment")?;
201            }
202        }
203    }
204
205    if !terminated {
206        return Err(J2kCodestreamHeaderError::TruncatedAt {
207            offset,
208            segment: "main header terminator",
209        });
210    }
211
212    let siz = siz.ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "SIZ" })?;
213    let cod = cod
214        .ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "COD" })?
215        .with_high_throughput_cap(high_throughput_cap);
216
217    Ok(J2kCodestreamHeaderMetadata {
218        dimensions: siz.dimensions,
219        components: siz.components,
220        bit_depth: siz.bit_depth,
221        tile_size: siz.tile_size,
222        tile_count: siz.tile_count,
223        component_info: siz.component_info,
224        resolution_levels: cod.resolution_levels,
225        has_mct: cod.has_mct,
226        reversible: cod.reversible,
227        high_throughput: cod.high_throughput,
228    })
229}
230
231/// Return whether bytes start with the raw JPEG 2000 SOC marker.
232#[must_use]
233pub fn looks_like_j2k_codestream(input: &[u8]) -> bool {
234    input.len() >= 2 && input[0] == 0xFF && input[1] == MARKER_SOC
235}
236
237#[derive(Debug, Clone)]
238struct ParsedSiz {
239    dimensions: (u32, u32),
240    components: u16,
241    bit_depth: u8,
242    tile_size: (u32, u32),
243    tile_count: (u32, u32),
244    component_info: Vec<J2kCodestreamComponentHeader>,
245}
246
247#[derive(Debug, Clone, Copy)]
248struct ParsedCod {
249    resolution_levels: u8,
250    has_mct: bool,
251    reversible: bool,
252    high_throughput: bool,
253}
254
255impl ParsedCod {
256    const fn with_high_throughput_cap(mut self, high_throughput_cap: bool) -> Self {
257        self.high_throughput |= high_throughput_cap;
258        self
259    }
260}
261
262fn read_marker(input: &[u8], offset: &mut usize) -> Result<u8, J2kCodestreamHeaderError> {
263    if *offset + 2 > input.len() {
264        return Err(J2kCodestreamHeaderError::TruncatedAt {
265            offset: *offset,
266            segment: "marker",
267        });
268    }
269    if input[*offset] != 0xFF {
270        return Err(J2kCodestreamHeaderError::InvalidMarker {
271            offset: *offset,
272            marker: input[*offset],
273        });
274    }
275    let marker = input[*offset + 1];
276    *offset += 2;
277    Ok(marker)
278}
279
280fn read_segment_payload<'a>(
281    input: &'a [u8],
282    offset: &mut usize,
283    segment: &'static str,
284) -> Result<&'a [u8], J2kCodestreamHeaderError> {
285    if *offset + 2 > input.len() {
286        return Err(J2kCodestreamHeaderError::TruncatedAt {
287            offset: *offset,
288            segment,
289        });
290    }
291    let length = u16::from_be_bytes([input[*offset], input[*offset + 1]]) as usize;
292    if length < 2 {
293        return Err(J2kCodestreamHeaderError::InvalidSegment {
294            offset: *offset,
295            what: "segment length smaller than header",
296        });
297    }
298    let start = *offset + 2;
299    let end = *offset + length;
300    if end > input.len() {
301        return Err(J2kCodestreamHeaderError::TruncatedAt {
302            offset: *offset,
303            segment,
304        });
305    }
306    *offset = end;
307    Ok(&input[start..end])
308}
309
310struct SizGeometry {
311    x_size: u32,
312    y_size: u32,
313    x_origin: u32,
314    y_origin: u32,
315    tile_width: u32,
316    tile_height: u32,
317    tile_x_origin: u32,
318    tile_y_origin: u32,
319}
320
321type SizDimensionsAndTileCount = ((u32, u32), (u32, u32));
322
323fn parse_siz(payload: &[u8]) -> Result<ParsedSiz, J2kCodestreamHeaderError> {
324    if payload.len() < 36 {
325        return Err(J2kCodestreamHeaderError::InvalidSiz {
326            what: "payload shorter than fixed SIZ header",
327        });
328    }
329    let geometry = SizGeometry {
330        x_size: read_u32(payload, 2),
331        y_size: read_u32(payload, 6),
332        x_origin: read_u32(payload, 10),
333        y_origin: read_u32(payload, 14),
334        tile_width: read_u32(payload, 18),
335        tile_height: read_u32(payload, 22),
336        tile_x_origin: read_u32(payload, 26),
337        tile_y_origin: read_u32(payload, 30),
338    };
339    let component_count = read_u16(payload, 34);
340
341    let required_len = usize::from(component_count)
342        .checked_mul(3)
343        .and_then(|component_bytes| 36usize.checked_add(component_bytes))
344        .ok_or(J2kCodestreamHeaderError::InvalidSiz {
345            what: "component descriptor length overflows",
346        })?;
347    if payload.len() < required_len {
348        return Err(J2kCodestreamHeaderError::InvalidSiz {
349            what: "component descriptors truncated",
350        });
351    }
352    let ((width, height), (tiles_x, tiles_y)) = validate_siz_geometry(&geometry, component_count)?;
353
354    let mut bit_depth = 0u8;
355    let component_len = usize::from(component_count);
356    let component_bytes = component_len
357        .checked_mul(core::mem::size_of::<J2kCodestreamComponentHeader>())
358        .ok_or(J2kCodestreamHeaderError::HostAllocationFailed { bytes: usize::MAX })?;
359    let mut component_info = Vec::new();
360    component_info
361        .try_reserve_exact(component_len)
362        .map_err(|_| J2kCodestreamHeaderError::HostAllocationFailed {
363            bytes: component_bytes,
364        })?;
365    for idx in 0..component_len {
366        let ssiz = payload[36 + idx * 3];
367        let precision = (ssiz & 0x7F) + 1;
368        let x_rsiz = payload[36 + idx * 3 + 1];
369        let y_rsiz = payload[36 + idx * 3 + 2];
370        if x_rsiz == 0 || y_rsiz == 0 {
371            return Err(J2kCodestreamHeaderError::InvalidSiz {
372                what: "component sampling factors must be non-zero",
373            });
374        }
375        bit_depth = bit_depth.max(precision);
376        component_info.push(J2kCodestreamComponentHeader {
377            bit_depth: precision,
378            signed: ssiz & 0x80 != 0,
379            x_rsiz,
380            y_rsiz,
381        });
382    }
383
384    Ok(ParsedSiz {
385        dimensions: (width, height),
386        components: component_count,
387        bit_depth,
388        tile_size: (geometry.tile_width, geometry.tile_height),
389        tile_count: (tiles_x, tiles_y),
390        component_info,
391    })
392}
393
394fn validate_siz_geometry(
395    geometry: &SizGeometry,
396    component_count: u16,
397) -> Result<SizDimensionsAndTileCount, J2kCodestreamHeaderError> {
398    if component_count == 0 {
399        return Err(J2kCodestreamHeaderError::InvalidSiz {
400            what: "component count must be non-zero",
401        });
402    }
403    if component_count > MAX_J2K_SPEC_COMPONENTS {
404        return Err(J2kCodestreamHeaderError::InvalidSiz {
405            what: "component count exceeds JPEG 2000 limit",
406        });
407    }
408    if geometry.x_size <= geometry.x_origin || geometry.y_size <= geometry.y_origin {
409        return Err(J2kCodestreamHeaderError::InvalidSiz {
410            what: "image origin must be smaller than image size",
411        });
412    }
413    if geometry.tile_width == 0 || geometry.tile_height == 0 {
414        return Err(J2kCodestreamHeaderError::InvalidSiz {
415            what: "tile size must be non-zero",
416        });
417    }
418    if geometry.tile_x_origin >= geometry.x_size || geometry.tile_y_origin >= geometry.y_size {
419        return Err(J2kCodestreamHeaderError::InvalidSiz {
420            what: "tile origin must be within image bounds",
421        });
422    }
423    if geometry.tile_x_origin > geometry.x_origin || geometry.tile_y_origin > geometry.y_origin {
424        return Err(J2kCodestreamHeaderError::InvalidSiz {
425            what: "tile origin must not exceed image origin",
426        });
427    }
428    if geometry
429        .tile_x_origin
430        .checked_add(geometry.tile_width)
431        .ok_or(J2kCodestreamHeaderError::InvalidSiz {
432            what: "tile extent overflows",
433        })?
434        <= geometry.x_origin
435        || geometry
436            .tile_y_origin
437            .checked_add(geometry.tile_height)
438            .ok_or(J2kCodestreamHeaderError::InvalidSiz {
439                what: "tile extent overflows",
440            })?
441            <= geometry.y_origin
442    {
443        return Err(J2kCodestreamHeaderError::InvalidSiz {
444            what: "first tile must overlap image area",
445        });
446    }
447
448    let width = geometry.x_size - geometry.x_origin;
449    let height = geometry.y_size - geometry.y_origin;
450    if width > MAX_J2K_IMAGE_DIMENSION || height > MAX_J2K_IMAGE_DIMENSION {
451        return Err(J2kCodestreamHeaderError::InvalidSiz {
452            what: "image dimensions exceed JPEG 2000 inspect limit",
453        });
454    }
455    let tiles_x = (geometry.x_size - geometry.tile_x_origin).div_ceil(geometry.tile_width);
456    let tiles_y = (geometry.y_size - geometry.tile_y_origin).div_ceil(geometry.tile_height);
457    let tile_count = u64::from(tiles_x) * u64::from(tiles_y);
458    if tile_count > MAX_J2K_TILE_COUNT {
459        return Err(J2kCodestreamHeaderError::InvalidSiz {
460            what: "image has too many tiles",
461        });
462    }
463    Ok(((width, height), (tiles_x, tiles_y)))
464}
465
466fn parse_cod(payload: &[u8]) -> Result<ParsedCod, J2kCodestreamHeaderError> {
467    if payload.len() < 10 {
468        return Err(J2kCodestreamHeaderError::InvalidCod {
469            what: "payload shorter than fixed COD header",
470        });
471    }
472    Ok(ParsedCod {
473        resolution_levels: payload[5].saturating_add(1),
474        has_mct: payload[4] != 0,
475        reversible: payload[9] == 1,
476        high_throughput: payload[8] & 0x40 != 0,
477    })
478}
479
480fn read_u16(bytes: &[u8], offset: usize) -> u16 {
481    u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
482}
483
484fn read_u32(bytes: &[u8], offset: usize) -> u32 {
485    u32::from_be_bytes([
486        bytes[offset],
487        bytes[offset + 1],
488        bytes[offset + 2],
489        bytes[offset + 3],
490    ])
491}
492
493#[cfg(test)]
494mod tests {
495    use super::{inspect_j2k_codestream_header, J2kCodestreamHeaderError};
496    use alloc::{vec, vec::Vec};
497
498    #[test]
499    fn inspect_j2k_codestream_header_accepts_minimal_main_header() {
500        let header = inspect_j2k_codestream_header(&minimal_codestream()).expect("header");
501
502        assert_eq!(header.dimensions, (128, 64));
503        assert_eq!(header.components, 3);
504        assert_eq!(header.bit_depth, 8);
505        assert_eq!(header.tile_size, (64, 64));
506        assert_eq!(header.tile_count, (2, 1));
507        assert_eq!(header.resolution_levels, 6);
508        assert!(header.reversible);
509    }
510
511    #[test]
512    fn inspect_skips_parameterless_reserved_main_header_markers() {
513        let mut bytes = minimal_codestream();
514        let sot = bytes
515            .windows(2)
516            .position(|marker| marker == [0xFF, 0x90])
517            .expect("SOT marker");
518        bytes.splice(sot..sot, [0xFF, 0x30]);
519
520        let header = inspect_j2k_codestream_header(&bytes).expect("header with reserved marker");
521
522        assert_eq!(header.dimensions, (128, 64));
523    }
524
525    #[test]
526    fn inspect_rejects_zero_component_sampling() {
527        let mut bytes = minimal_codestream();
528        rewrite_component_sampling(&mut bytes, 0, 0, 1);
529
530        let err = inspect_j2k_codestream_header(&bytes).expect_err("zero sampling must reject");
531
532        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
533    }
534
535    #[test]
536    fn inspect_rejects_oversized_dimensions() {
537        let mut bytes = minimal_codestream();
538        rewrite_siz_u32(&mut bytes, 2, 60_001);
539
540        let err = inspect_j2k_codestream_header(&bytes).expect_err("oversized width must reject");
541
542        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
543    }
544
545    #[test]
546    fn inspect_rejects_tile_origin_after_image_origin() {
547        let mut bytes = minimal_codestream();
548        rewrite_siz_u32(&mut bytes, 26, 1);
549
550        let err = inspect_j2k_codestream_header(&bytes).expect_err("bad tile origin must reject");
551
552        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
553    }
554
555    #[test]
556    fn inspect_rejects_tile_extent_overflow() {
557        let mut bytes = minimal_codestream();
558        rewrite_siz_u32(&mut bytes, 2, u32::MAX);
559        rewrite_siz_u32(&mut bytes, 10, u32::MAX - 1);
560        rewrite_siz_u32(&mut bytes, 18, 10);
561        rewrite_siz_u32(&mut bytes, 26, u32::MAX - 2);
562
563        let err = inspect_j2k_codestream_header(&bytes).expect_err("overflow must reject");
564
565        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
566    }
567
568    #[test]
569    fn inspect_rejects_excessive_tile_count() {
570        let mut bytes = minimal_codestream();
571        rewrite_siz_u32(&mut bytes, 2, 257);
572        rewrite_siz_u32(&mut bytes, 6, 257);
573        rewrite_siz_u32(&mut bytes, 18, 1);
574        rewrite_siz_u32(&mut bytes, 22, 1);
575
576        let err = inspect_j2k_codestream_header(&bytes).expect_err("tile count must reject");
577
578        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
579    }
580
581    #[test]
582    fn inspect_accepts_legal_38_bit_component_metadata() {
583        let mut bytes = minimal_codestream();
584        rewrite_component_descriptor(&mut bytes, 0, 0x25);
585        rewrite_component_descriptor(&mut bytes, 1, 0x80 | 0x25);
586
587        let header = inspect_j2k_codestream_header(&bytes).expect("legal 38-bit SIZ inspect");
588
589        assert_eq!(header.bit_depth, 38);
590        assert_eq!(header.component_info[0].bit_depth, 38);
591        assert!(!header.component_info[0].signed);
592        assert_eq!(header.component_info[1].bit_depth, 38);
593        assert!(header.component_info[1].signed);
594    }
595
596    fn minimal_codestream() -> Vec<u8> {
597        let mut bytes = vec![0xFF, 0x4F];
598        let mut siz = Vec::new();
599        push_u16(&mut siz, 0);
600        push_u32(&mut siz, 128);
601        push_u32(&mut siz, 64);
602        push_u32(&mut siz, 0);
603        push_u32(&mut siz, 0);
604        push_u32(&mut siz, 64);
605        push_u32(&mut siz, 64);
606        push_u32(&mut siz, 0);
607        push_u32(&mut siz, 0);
608        push_u16(&mut siz, 3);
609        for _ in 0..3 {
610            siz.extend_from_slice(&[0x07, 0x01, 0x01]);
611        }
612        bytes.extend_from_slice(&[0xFF, 0x51]);
613        push_u16(
614            &mut bytes,
615            u16::try_from(siz.len() + 2).expect("test SIZ segment length fits u16"),
616        );
617        bytes.extend_from_slice(&siz);
618
619        let cod = [0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x04, 0x04, 0x00, 0x01];
620        bytes.extend_from_slice(&[0xFF, 0x52]);
621        push_u16(
622            &mut bytes,
623            u16::try_from(cod.len() + 2).expect("test COD segment length fits u16"),
624        );
625        bytes.extend_from_slice(&cod);
626        bytes.extend_from_slice(&[0xFF, 0x90, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
627        bytes
628    }
629
630    fn push_u16(out: &mut Vec<u8>, value: u16) {
631        out.extend_from_slice(&value.to_be_bytes());
632    }
633
634    fn push_u32(out: &mut Vec<u8>, value: u32) {
635        out.extend_from_slice(&value.to_be_bytes());
636    }
637
638    fn rewrite_siz_u32(bytes: &mut [u8], payload_offset: usize, value: u32) {
639        let siz = bytes
640            .windows(2)
641            .position(|marker| marker == [0xFF, 0x51])
642            .expect("SIZ marker");
643        let offset = siz + 4 + payload_offset;
644        bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
645    }
646
647    fn rewrite_component_sampling(bytes: &mut [u8], component: usize, x_rsiz: u8, y_rsiz: u8) {
648        let siz = bytes
649            .windows(2)
650            .position(|marker| marker == [0xFF, 0x51])
651            .expect("SIZ marker");
652        let component_offset = siz + 40 + component * 3;
653        bytes[component_offset + 1] = x_rsiz;
654        bytes[component_offset + 2] = y_rsiz;
655    }
656
657    fn rewrite_component_descriptor(bytes: &mut [u8], component: usize, descriptor: u8) {
658        let siz = bytes
659            .windows(2)
660            .position(|marker| marker == [0xFF, 0x51])
661            .expect("SIZ marker");
662        bytes[siz + 40 + component * 3] = descriptor;
663    }
664}