Skip to main content

hpr_io/grib2/
jpeg2000.rs

1//! JPEG 2000 packing: data representation template 5.40 (WMO-No. 306, Volume I.2, FM 92 GRIB
2//! edition 2, template 5.40 and code table 5.40).
3//!
4//! The field's integers `X` are a greyscale image, one sample per packed value, coded as a JPEG
5//! 2000 codestream (ISO/IEC 15444-1) in section 7; each unpacks as simple packing's do,
6//! `Y = (R + X · 2^E) / 10^D`. The codestream is decoded by `hayro-jpeg2000` (MIT or Apache-2.0),
7//! in strict mode, so a damaged or cut-short codestream is refused rather than filled in.
8//!
9//! The decoder runs the reversible 5/3 wavelet (ISO/IEC 15444-1, Annex F) in `f32`, whose whole
10//! numbers are exact only below `2^24`. The inverse transform adds two neighbouring high-pass
11//! coefficients before each floor, and for `B`-bit samples those sums reach nearly `16 · 2^(B−1)`
12//! (the cascaded 5/3 analysis filters bound a coefficient by about `8.2 · 2^(B−1)`), so every step
13//! stays exact only for `B ≤ 21`: fields of more bits are refused by name. Probes at 24 bits came
14//! back off by one in 121 and 136 of 10,152 values, and one of six at 23 bits in one value, whole
15//! and in range, which no later check could catch.
16//!
17//! Only the codestream NCEP and ecCodes write is read, checked from its main header before any
18//! decoding (ISO/IEC 15444-1, Annex A): one unsigned component, no subsampling or offsets, one
19//! tile, the reversible 5/3 transform without quantization, default precincts, and no marker that
20//! overrides these per component or per tile. Anything else is refused by name, so a hostile
21//! header cannot make the decoder size its buffers past the grid. Lossy coding (code table 5.40's
22//! 1) is refused too.
23
24use hayro_jpeg2000::{DecodeSettings, DecoderContext, Image};
25use serde::{Deserialize, Serialize};
26
27use super::{Grib2Error, be_i16, be_u32, malformed, need, unpack};
28
29/// The most bits per value read: the 5/3 wavelet's sums in `hayro-jpeg2000`'s `f32` stay below
30/// `2^24`, and so exact, for samples of at most 21 bits (see the module's documentation).
31pub const MAX_BITS: u8 = 21;
32
33/// `hayro-jpeg2000` refuses an image wider or taller than this. NCEP codes a field with a bitmap
34/// as one row of its packed values, so such a field of more values is refused by name.
35pub const MAX_SIDE: u32 = 60_000;
36
37/// JPEG 2000 packing's parameters: data representation template 5.40.
38#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
40pub struct Jpeg2000Packing {
41    /// The reference value `R`, as written (a 32-bit float).
42    pub reference: f32,
43    /// The binary scale factor `E`.
44    pub binary_scale: i16,
45    /// The decimal scale factor `D`.
46    pub decimal_scale: i16,
47    /// The image's bits per sample, 0 to [`MAX_BITS`]. A field of 0 bits has no codestream: it is
48    /// `R / 10^D` at every point with a value, by the regulation (not checked against ecCodes,
49    /// which gave `R` for a field of 0 bits with `D = 2`).
50    pub bits: u8,
51    /// How many values are packed: the grid points the bitmap marks, or all of them.
52    pub count: u32,
53}
54
55impl Jpeg2000Packing {
56    /// `Y = (R + X · 2^E) / 10^D` for a packed integer `X`.
57    #[must_use]
58    pub fn unpack(&self, x: u32) -> f64 {
59        unpack(
60            self.reference,
61            self.binary_scale,
62            self.decimal_scale,
63            f64::from(x),
64        )
65    }
66}
67
68/// Section 5 in template 5.40: 23 bytes.
69pub(super) fn read(s: &[u8], message: usize) -> Result<Jpeg2000Packing, Grib2Error> {
70    let s = need(s, 23, "data template 5.40", message)?;
71    let unsupported = |what, value: u8| Grib2Error::Unsupported {
72        message,
73        what,
74        value: value.into(),
75    };
76    let bits = s[19];
77    if bits > MAX_BITS {
78        return Err(unsupported("bits per value in JPEG 2000", bits));
79    }
80    // Table 5.1: 0 floating point, 1 integer. Either unpacks the same way.
81    if s[20] > 1 {
82        return Err(unsupported("type of original field values", s[20]));
83    }
84    // Code table 5.40: 0 lossless, 1 lossy.
85    if s[21] != 0 {
86        return Err(unsupported("type of JPEG 2000 compression", s[21]));
87    }
88    let reference = f32::from_bits(be_u32(&s[11..15]));
89    if !reference.is_finite() {
90        return Err(malformed(message, "the reference value is not finite"));
91    }
92    Ok(Jpeg2000Packing {
93        reference,
94        binary_scale: be_i16(&s[15..17]),
95        decimal_scale: be_i16(&s[17..19]),
96        bits,
97        count: be_u32(&s[5..9]),
98    })
99}
100
101fn settings() -> DecodeSettings {
102    DecodeSettings {
103        // A GRIB2 codestream is raw (no JP2 boxes), so there is no palette to resolve.
104        resolve_palette_indices: false,
105        // Refuse what the lenient mode fills in: a cut-short codestream decodes there to the DC
106        // offset at every point, whole numbers in range.
107        strict: true,
108        ..DecodeSettings::default()
109    }
110}
111
112/// Reads the codestream's main header and checks it holds one sample per packed value, at the
113/// bits section 5 gives, coded as NCEP and ecCodes code it: `parse` calls it, so a field whose
114/// image cannot hold its values, or that the decoder could size past the grid, is refused before
115/// any is read.
116pub(super) fn check(p: &Jpeg2000Packing, data: &[u8], message: usize) -> Result<(), Grib2Error> {
117    if p.bits == 0 {
118        return Ok(());
119    }
120    let header = Header::read(data, message)?;
121    let samples = u64::from(header.width) * u64::from(header.height);
122    if samples != u64::from(p.count) {
123        return Err(malformed(
124            message,
125            format!(
126                "the JPEG 2000 image has {samples} samples ({} by {}) but section 5 packs {} values",
127                header.width, header.height, p.count
128            ),
129        ));
130    }
131    if header.bits != p.bits {
132        return Err(malformed(
133            message,
134            format!(
135                "the JPEG 2000 image has {} bits per sample but section 5 gives {}",
136                header.bits, p.bits
137            ),
138        ));
139    }
140    Ok(())
141}
142
143/// What a coding marker sets.
144enum Coding {
145    /// COD, the default coding style.
146    Style,
147    /// QCD, the default quantization.
148    Quantization,
149    /// COC, QCC or COM.
150    Other,
151}
152
153/// What `check` needs from a codestream's main header (ISO/IEC 15444-1, Annex A).
154struct Header {
155    width: u32,
156    height: u32,
157    bits: u8,
158}
159
160impl Header {
161    /// Walks the main header and the tile-part headers, refusing by name anything but the one
162    /// coding NCEP and ecCodes use.
163    fn read(data: &[u8], message: usize) -> Result<Self, Grib2Error> {
164        let bad = |reason: &str| malformed(message, format!("the JPEG 2000 codestream: {reason}"));
165        let unsupported = |what, value: u64| Grib2Error::Unsupported {
166            message,
167            what,
168            value,
169        };
170        let u16_at = |at: usize| -> Result<u16, Grib2Error> {
171            data.get(at..at.saturating_add(2))
172                .map(|b| u16::from_be_bytes([b[0], b[1]]))
173                .ok_or_else(|| bad("its header is cut short"))
174        };
175        let u32_at = |at: usize| -> Result<u32, Grib2Error> {
176            data.get(at..at.saturating_add(4))
177                .map(be_u32)
178                .ok_or_else(|| bad("its header is cut short"))
179        };
180        let byte = |at: usize| -> Result<u8, Grib2Error> {
181            data.get(at)
182                .copied()
183                .ok_or_else(|| bad("its header is cut short"))
184        };
185        if u16_at(0)? != 0xFF4F || u16_at(2)? != 0xFF51 {
186            return Err(bad("it does not start with SOC and SIZ"));
187        }
188        // SIZ, from byte 2: Lsiz, Rsiz, Xsiz, Ysiz, XOsiz, YOsiz, XTsiz, YTsiz, XTOsiz, YTOsiz,
189        // Csiz, then Ssiz, XRsiz, YRsiz per component.
190        let (x, y) = (u32_at(8)?, u32_at(12)?);
191        if x > MAX_SIDE || y > MAX_SIDE {
192            return Err(unsupported("JPEG 2000 image side", x.max(y).into()));
193        }
194        let (x0, y0) = (u32_at(16)?, u32_at(20)?);
195        let (xt, yt) = (u32_at(24)?, u32_at(28)?);
196        let (xt0, yt0) = (u32_at(32)?, u32_at(36)?);
197        let components = u16_at(40)?;
198        if components != 1 {
199            return Err(unsupported("JPEG 2000 components", components.into()));
200        }
201        let ssiz = byte(42)?;
202        if ssiz & 0x80 != 0 {
203            return Err(unsupported("JPEG 2000 signed samples", ssiz.into()));
204        }
205        let (xr, yr) = (byte(43)?, byte(44)?);
206        if (xr, yr) != (1, 1) {
207            return Err(unsupported(
208                "JPEG 2000 subsampling",
209                u64::from(xr) << 8 | u64::from(yr),
210            ));
211        }
212        if x0 != 0 || y0 != 0 || xt0 != 0 || yt0 != 0 {
213            return Err(unsupported(
214                "JPEG 2000 image or tile offset",
215                u64::from(x0.max(y0).max(xt0).max(yt0)),
216            ));
217        }
218        if xt < x || yt < y {
219            return Err(unsupported(
220                "JPEG 2000 tiles",
221                u64::from(x.div_ceil(xt.max(1))) * u64::from(y.div_ceil(yt.max(1))),
222            ));
223        }
224        // The first marker after SIZ: SIZ's marker at byte 2, then its length.
225        // SIZ, COD, COC and SOT are read field by field by the decoder, not skipped by their
226        // length, so a longer length would hide a segment from this walk that the decoder reads.
227        let fixed = |at: usize, length: u16| -> Result<(), Grib2Error> {
228            let marker = u16_at(at)?;
229            let have = u16_at(at.saturating_add(2))?;
230            if have == length {
231                Ok(())
232            } else {
233                Err(malformed(
234                    message,
235                    format!(
236                        "the JPEG 2000 codestream: marker {marker:#06X} is {have} bytes long, not {length}"
237                    ),
238                ))
239            }
240        };
241        // The markers that set the coding, in the main header or a tile-part's, held to the one
242        // coding read: `None` for any other marker.
243        let coding = |marker: u16, at: usize| -> Result<Option<Coding>, Grib2Error> {
244            // COD's and COC's style byte: bit 0 set when precinct sizes are given.
245            let precincts = |scod: u8| {
246                if scod & 1 == 0 {
247                    Ok(())
248                } else {
249                    Err(unsupported("JPEG 2000 precinct sizes", scod.into()))
250                }
251            };
252            let transform = |t: u8| {
253                if t == 1 {
254                    Ok(())
255                } else {
256                    Err(unsupported("JPEG 2000 wavelet transform", t.into()))
257                }
258            };
259            // Sqcd's and Sqcc's low five bits: the quantization style, 0 for none.
260            let quantization = |sq: u8| {
261                if sq & 0x1F == 0 {
262                    Ok(())
263                } else {
264                    Err(unsupported("JPEG 2000 quantization", (sq & 0x1F).into()))
265                }
266            };
267            Ok(Some(match marker {
268                // COD: Scod, SGcod (progression, layers, color transform), SPcod (levels,
269                // code-block width and height, style, transform).
270                0xFF52 => {
271                    fixed(at, 12)?;
272                    precincts(byte(at + 4)?)?;
273                    transform(byte(at + 13)?)?;
274                    Coding::Style
275                }
276                // COC: Ccoc (one byte, with fewer than 257 components), Scoc, SPcoc.
277                0xFF53 => {
278                    fixed(at, 9)?;
279                    precincts(byte(at + 5)?)?;
280                    transform(byte(at + 10)?)?;
281                    Coding::Other
282                }
283                // QCD: Sqcd.
284                0xFF5C => {
285                    quantization(byte(at + 4)?)?;
286                    Coding::Quantization
287                }
288                // QCC: Cqcc (one byte), Sqcc.
289                0xFF5D => {
290                    quantization(byte(at + 5)?)?;
291                    Coding::Other
292                }
293                // COM.
294                0xFF64 => Coding::Other,
295                _ => return Ok(None),
296            }))
297        };
298        // The first marker after SIZ: SIZ's marker at byte 2, then its length, which for one
299        // component is 41.
300        fixed(2, 41)?;
301        let mut at = 4 + usize::from(u16_at(4)?);
302        let (mut cod, mut qcd) = (false, false);
303        loop {
304            let marker = u16_at(at)?;
305            // SOT: the first tile-part starts.
306            if marker == 0xFF90 {
307                break;
308            }
309            let length = usize::from(u16_at(at.saturating_add(2))?);
310            if length < 2 {
311                return Err(bad("a marker segment is shorter than its length field"));
312            }
313            match coding(marker, at)? {
314                Some(Coding::Style) => cod = true,
315                Some(Coding::Quantization) => qcd = true,
316                Some(Coding::Other) => {}
317                // TLM and PLM carry no coding.
318                None if matches!(marker, 0xFF55 | 0xFF57) => {}
319                None => return Err(unsupported("JPEG 2000 main-header marker", marker.into())),
320            }
321            at = at.saturating_add(2 + length);
322        }
323        if !(cod && qcd) {
324            return Err(bad("its main header has no COD or no QCD"));
325        }
326        // Each tile-part: SOT (Lsot, Isot, Psot, TPsot, TNsot), then markers up to SOD.
327        while u16_at(at)? == 0xFF90 {
328            fixed(at, 10)?;
329            let psot = usize::try_from(u32_at(at.saturating_add(6))?).unwrap_or(usize::MAX);
330            let mut marker_at = at.saturating_add(2 + usize::from(u16_at(at.saturating_add(2))?));
331            loop {
332                let marker = u16_at(marker_at)?;
333                if marker == 0xFF93 {
334                    break;
335                }
336                // PLT carries no coding.
337                if coding(marker, marker_at)?.is_none() && marker != 0xFF58 {
338                    return Err(unsupported("JPEG 2000 tile-part marker", marker.into()));
339                }
340                let length = usize::from(u16_at(marker_at.saturating_add(2))?);
341                if length < 2 {
342                    return Err(bad("a marker segment is shorter than its length field"));
343                }
344                marker_at = marker_at.saturating_add(2 + length);
345            }
346            // Psot 0: the tile-part runs to the end of the codestream.
347            if psot == 0 {
348                break;
349            }
350            if psot < 14 {
351                return Err(bad("a tile-part is shorter than its header"));
352            }
353            at = at.saturating_add(psot);
354            if at.saturating_add(2) > data.len() || u16_at(at)? == 0xFFD9 {
355                break;
356            }
357        }
358        let bits = (ssiz & 0x7F) + 1;
359        Ok(Self {
360            width: x,
361            height: y,
362            bits,
363        })
364    }
365}
366
367/// Every packed integer, in order. `check` has passed on the same bytes.
368pub(super) fn decode(
369    p: &Jpeg2000Packing,
370    data: &[u8],
371    message: usize,
372) -> Result<Vec<u32>, Grib2Error> {
373    // `parse` bounds `count` by the grid's points, which fit a `usize` on every target.
374    let count = usize::try_from(p.count).unwrap_or(0);
375    if p.bits == 0 {
376        return Ok(vec![0; count]);
377    }
378    // `read` caps `bits`; a caller who edited the public packing past it is refused here.
379    if p.bits > MAX_BITS {
380        return Err(Grib2Error::Unsupported {
381            message,
382            what: "bits per value in JPEG 2000",
383            value: p.bits.into(),
384        });
385    }
386    check(p, data, message)?;
387    let codestream = |e| malformed(message, format!("the JPEG 2000 codestream: {e}"));
388    let image = Image::new(data, &settings()).map_err(codestream)?;
389    let mut context = DecoderContext::default();
390    let decoded = image.decode(&mut context).map_err(codestream)?;
391    let [component] = decoded.components() else {
392        return Err(malformed(
393            message,
394            format!(
395                "the JPEG 2000 image has {} components, not one",
396                decoded.components().len()
397            ),
398        ));
399    };
400    let samples = component.samples();
401    if samples.len() != count {
402        return Err(malformed(
403            message,
404            format!(
405                "the JPEG 2000 image decodes to {} samples but section 5 packs {count} values",
406                samples.len()
407            ),
408        ));
409    }
410    let largest = (1_u32 << p.bits) - 1;
411    samples
412        .iter()
413        .map(|&s| {
414            whole(s, largest).ok_or_else(|| {
415                malformed(
416                    message,
417                    format!("a JPEG 2000 sample, {s}, is not a whole number from 0 to {largest}"),
418                )
419            })
420        })
421        .collect()
422}
423
424/// A sample as an integer, when it is a whole number from 0 to `largest` (at most `2^24 − 1`):
425/// a lossless codestream's samples all are, and anything else is a codestream this reader must
426/// not guess at.
427#[allow(
428    clippy::cast_possible_truncation,
429    clippy::cast_sign_loss,
430    clippy::cast_precision_loss,
431    reason = "`largest` is below 2^24, so exact in f32, and `s` is checked whole and in range first"
432)]
433fn whole(s: f32, largest: u32) -> Option<u32> {
434    (s.fract() == 0.0 && (0.0..=largest as f32).contains(&s)).then_some(s as u32)
435}