wbraster 0.2.1

A pure-Rust library for reading and writing raster GIS formats
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! JPEG 2000 JP2 file format box (superbox) parsing and writing.
//!
//! The JP2 file format wraps a JPEG 2000 codestream in a series of typed
//! length-prefixed boxes:
//!
//! ```text
//! [LBox(4)] [TBox(4)] [XLBox(8, optional)] [data…]
//! ```
//!
//! - `LBox = 0`  → box extends to end of file
//! - `LBox = 1`  → `XLBox` follows and contains the true 64-bit length
//! - `LBox ≥ 8`  → total box length including header
//!
//! Standard JP2 box sequence:
//! 1. Signature box  (`jP  ` / `6A502020`)
//! 2. File type box  (`ftyp`)
//! 3. JP2 Header box (`jp2h`) — superbox containing `ihdr`, `colr`, optionally `pclr`, `cmap`, `res `
//! 4. UUID box       (`uuid`) — GeoJP2 geolocation metadata (optional)
//! 5. XML box        (`xml `) — GML/arbitrary XML metadata (optional)
//! 6. Contiguous Codestream box (`jp2c`) — the actual JPEG 2000 codestream

use super::error::{Jp2Error, Result};
use std::io::{Read, Seek, SeekFrom, Write};

// ── Box type constants ────────────────────────────────────────────────────────

pub mod box_type {
    pub const SIGNATURE: [u8; 4] = *b"jP  ";
    pub const FILE_TYPE: [u8; 4] = *b"ftyp";
    pub const JP2_HEADER: [u8; 4] = *b"jp2h";
    pub const IMAGE_HEADER: [u8; 4] = *b"ihdr";
    pub const COLOUR_SPEC: [u8; 4] = *b"colr";
    pub const PALETTE: [u8; 4] = *b"pclr";
    pub const COMP_MAP: [u8; 4] = *b"cmap";
    pub const CHAN_DEF: [u8; 4] = *b"cdef";
    pub const RESOLUTION: [u8; 4] = *b"res ";
    pub const CODESTREAM: [u8; 4] = *b"jp2c";
    pub const UUID: [u8; 4] = *b"uuid";
    pub const UUID_INFO: [u8; 4] = *b"uinf";
    pub const XML: [u8; 4] = *b"xml ";
    pub const INTELLECTUAL: [u8; 4] = *b"jp2i";
}

/// UUID identifying the GeoJP2 box (contains embedded GeoTIFF metadata bytes).
pub const GEOJP2_UUID: [u8; 16] = [
    0xb1, 0x4b, 0xf8, 0xbd, 0x08, 0x3d, 0x4b, 0x43, 0xa5, 0xae, 0x8c, 0xd7, 0xd5, 0xa6, 0xce, 0x03,
];

/// UUID identifying a World File box (alternate geolocation, less common).
pub const WORLD_FILE_UUID: [u8; 16] = [
    0x96, 0xa9, 0xf1, 0xf1, 0xdc, 0x98, 0x40, 0x2d, 0xa7, 0xae, 0xd6, 0x8e, 0x34, 0x45, 0x18, 0x09,
];

// ── RawBox ────────────────────────────────────────────────────────────────────

/// A fully-read JP2 box: header + payload bytes.
#[derive(Debug, Clone)]
pub struct RawBox {
    /// 4-byte box type.
    pub box_type: [u8; 4],
    /// File offset of the first byte of the box header.
    pub file_offset: u64,
    /// Payload bytes (excludes the LBox/TBox/XLBox header).
    pub data: Vec<u8>,
}

impl RawBox {
    /// Box type as a UTF-8 string (lossy) for error messages.
    pub fn type_str(&self) -> String {
        String::from_utf8_lossy(&self.box_type).into_owned()
    }

    /// Whether this box's type matches the given 4-byte constant.
    pub fn is(&self, t: [u8; 4]) -> bool {
        self.box_type == t
    }
}

// ── BoxReader ─────────────────────────────────────────────────────────────────

/// Reads JP2 boxes sequentially from any `Read + Seek` source.
pub struct BoxReader<R: Read + Seek> {
    inner: R,
    file_len: u64,
}

impl<R: Read + Seek> BoxReader<R> {
    /// Wrap a reader. `file_len` is used to handle LBox=0 (to-EOF) boxes.
    pub fn new(mut inner: R) -> Result<Self> {
        let file_len = inner.seek(SeekFrom::End(0)).map_err(Jp2Error::Io)?;
        inner.seek(SeekFrom::Start(0)).map_err(Jp2Error::Io)?;
        Ok(Self { inner, file_len })
    }

    /// Read the next box from the current stream position.
    /// Returns `None` at EOF.
    pub fn next_box(&mut self) -> Result<Option<RawBox>> {
        let file_offset = self.inner.stream_position().map_err(Jp2Error::Io)?;
        if file_offset >= self.file_len {
            return Ok(None);
        }

        let mut hdr = [0u8; 8];
        match self.inner.read_exact(&mut hdr) {
            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
            Err(e) => return Err(Jp2Error::Io(e)),
            Ok(_) => {}
        }

        let lbox = u32::from_be_bytes(hdr[0..4].try_into().unwrap());
        let box_type: [u8; 4] = hdr[4..8].try_into().unwrap();

        let (header_size, payload_len): (u64, u64) = match lbox {
            0 => {
                // Box extends to end of file
                (8, self.file_len.saturating_sub(file_offset + 8))
            }
            1 => {
                // XLBox follows — 8-byte true length
                let mut xlbox = [0u8; 8];
                self.inner.read_exact(&mut xlbox).map_err(Jp2Error::Io)?;
                let total = u64::from_be_bytes(xlbox);
                if total < 16 {
                    return Err(Jp2Error::InvalidBox {
                        box_type: String::from_utf8_lossy(&box_type).into_owned(),
                        message: format!("XLBox value {} is too small", total),
                    });
                }
                (16, total - 16)
            }
            n if n < 8 => {
                return Err(Jp2Error::InvalidBox {
                    box_type: String::from_utf8_lossy(&box_type).into_owned(),
                    message: format!("LBox value {} is invalid (must be 0, 1, or ≥ 8)", n),
                });
            }
            n => (8, (n as u64) - 8),
        };

        let mut data = vec![0u8; payload_len as usize];
        self.inner.read_exact(&mut data).map_err(Jp2Error::Io)?;

        Ok(Some(RawBox {
            box_type,
            file_offset,
            data,
        }))
    }

    /// Collect all top-level boxes into a Vec.
    pub fn read_all(&mut self) -> Result<Vec<RawBox>> {
        let mut boxes = Vec::new();
        while let Some(b) = self.next_box()? {
            boxes.push(b);
        }
        Ok(boxes)
    }

    /// Read all sub-boxes from a superbox's payload (e.g. `jp2h`).
    pub fn sub_boxes(data: &[u8]) -> Result<Vec<RawBox>> {
        let mut cur = std::io::Cursor::new(data);
        let mut reader = BoxReader::new(&mut cur)?;
        reader.read_all()
    }
}

// ── Box writers ───────────────────────────────────────────────────────────────

/// Write a JP2 box to `w`.  If `payload` is empty, only the 8-byte header is written.
pub fn write_box<W: Write>(w: &mut W, box_type: [u8; 4], payload: &[u8]) -> std::io::Result<()> {
    let total = 8u64 + payload.len() as u64;
    if total <= u32::MAX as u64 {
        w.write_all(&(total as u32).to_be_bytes())?;
        w.write_all(&box_type)?;
    } else {
        // XLBox form
        w.write_all(&1u32.to_be_bytes())?;
        w.write_all(&box_type)?;
        w.write_all(&total.to_be_bytes())?;
    }
    w.write_all(payload)
}

/// Write a superbox (a box whose payload consists of child boxes).
pub fn write_super_box<W: Write>(
    w: &mut W,
    box_type: [u8; 4],
    children: &[u8],
) -> std::io::Result<()> {
    write_box(w, box_type, children)
}

// ── Signature box ─────────────────────────────────────────────────────────────

/// Verify and skip (or write) the JP2 12-byte signature box.
///
/// The signature box contains `0x0D0A870A` as its payload.
pub const JP2_SIGNATURE_PAYLOAD: [u8; 4] = [0x0D, 0x0A, 0x87, 0x0A];

pub fn validate_signature(b: &RawBox) -> Result<()> {
    if !b.is(box_type::SIGNATURE) {
        return Err(Jp2Error::NotJp2(format!(
            "Expected signature box 'jP  ', found '{}'",
            b.type_str()
        )));
    }
    if b.data != JP2_SIGNATURE_PAYLOAD {
        return Err(Jp2Error::NotJp2(
            "Signature box payload does not match JP2 magic bytes".into(),
        ));
    }
    Ok(())
}

pub fn write_signature<W: Write>(w: &mut W) -> std::io::Result<()> {
    write_box(w, box_type::SIGNATURE, &JP2_SIGNATURE_PAYLOAD)
}

// ── File type box ─────────────────────────────────────────────────────────────

/// JP2 `ftyp` box: brand `jp2 `, minor version 0, compatibility list `jp2 `.
pub fn write_file_type<W: Write>(w: &mut W) -> std::io::Result<()> {
    let mut payload = Vec::new();
    payload.extend_from_slice(b"jp2 "); // BR (brand)
    payload.extend_from_slice(&0u32.to_be_bytes()); // MinV
    payload.extend_from_slice(b"jp2 "); // CL (compatibility list)
    write_box(w, box_type::FILE_TYPE, &payload)
}

// ── Image header box (ihdr) ───────────────────────────────────────────────────

/// Parse the `ihdr` box payload.
#[derive(Debug, Clone)]
pub struct ImageHeader {
    pub height: u32,
    pub width: u32,
    pub components: u16,
    /// Bit depth minus 1 (per component; we use the first component value).
    pub bpc: u8,
    /// Compression type: 7 = JP2.
    pub c: u8,
    /// Colourspace unknown flag.
    pub unk_c: u8,
    /// Intellectual property flag.
    pub ipr: u8,
}

impl ImageHeader {
    pub fn parse(data: &[u8]) -> Result<Self> {
        if data.len() < 14 {
            return Err(Jp2Error::InvalidBox {
                box_type: "ihdr".into(),
                message: format!("payload too short: {} bytes", data.len()),
            });
        }
        Ok(Self {
            height: u32::from_be_bytes(data[0..4].try_into().unwrap()),
            width: u32::from_be_bytes(data[4..8].try_into().unwrap()),
            components: u16::from_be_bytes(data[8..10].try_into().unwrap()),
            bpc: data[10],
            c: data[11],
            unk_c: data[12],
            ipr: data[13],
        })
    }

    /// Actual bits per component (bpc field is value-1, or 0xFF for variable).
    pub fn bits_per_component(&self) -> u8 {
        if self.bpc == 0xFF {
            0
        } else {
            (self.bpc & 0x7F) + 1
        }
    }

    /// Whether samples are signed (MSB of bpc).
    pub fn is_signed(&self) -> bool {
        self.bpc & 0x80 != 0
    }

    pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
        let mut payload = Vec::new();
        payload.extend_from_slice(&self.height.to_be_bytes());
        payload.extend_from_slice(&self.width.to_be_bytes());
        payload.extend_from_slice(&self.components.to_be_bytes());
        payload.push(self.bpc);
        payload.push(self.c);
        payload.push(self.unk_c);
        payload.push(self.ipr);
        write_box(w, box_type::IMAGE_HEADER, &payload)
    }
}

// ── Colour spec box (colr) ────────────────────────────────────────────────────

#[derive(Debug, Clone)]
pub struct ColourSpec {
    /// Method: 1 = enumerated colourspace, 2 = ICC profile.
    pub meth: u8,
    pub prec: i8,
    pub approx: u8,
    /// Enumerated colourspace (if meth=1).
    pub enumcs: Option<u32>,
    /// ICC profile bytes (if meth=2).
    pub icc_profile: Option<Vec<u8>>,
}

impl ColourSpec {
    pub fn enumerated(cs: u32) -> Self {
        Self {
            meth: 1,
            prec: 0,
            approx: 0,
            enumcs: Some(cs),
            icc_profile: None,
        }
    }

    pub fn parse(data: &[u8]) -> Result<Self> {
        if data.len() < 3 {
            return Err(Jp2Error::InvalidBox {
                box_type: "colr".into(),
                message: "payload too short".into(),
            });
        }
        let meth = data[0];
        let prec = data[1] as i8;
        let approx = data[2];
        let (enumcs, icc_profile) = match meth {
            1 => {
                if data.len() < 7 {
                    return Err(Jp2Error::InvalidBox {
                        box_type: "colr".into(),
                        message: "enumerated colourspace truncated".into(),
                    });
                }
                (
                    Some(u32::from_be_bytes(data[3..7].try_into().unwrap())),
                    None,
                )
            }
            2 => (None, Some(data[3..].to_vec())),
            _ => (None, None),
        };
        Ok(Self {
            meth,
            prec,
            approx,
            enumcs,
            icc_profile,
        })
    }

    pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
        let mut payload = vec![self.meth, self.prec as u8, self.approx];
        if let Some(cs) = self.enumcs {
            payload.extend_from_slice(&cs.to_be_bytes());
        }
        if let Some(ref icc) = self.icc_profile {
            payload.extend_from_slice(icc);
        }
        write_box(w, box_type::COLOUR_SPEC, &payload)
    }
}

// ── UUID box (GeoJP2) ─────────────────────────────────────────────────────────

/// Write a GeoJP2 UUID box containing embedded GeoTIFF metadata bytes.
///
/// The payload is: 16-byte UUID + GeoTIFF tag data (identical to what would
/// appear inside a GeoTIFF file's IFD region).
pub fn write_uuid_box<W: Write>(w: &mut W, uuid: &[u8; 16], data: &[u8]) -> std::io::Result<()> {
    let mut payload = Vec::with_capacity(16 + data.len());
    payload.extend_from_slice(uuid);
    payload.extend_from_slice(data);
    write_box(w, box_type::UUID, &payload)
}

/// Parse a UUID box, returning `(uuid, payload_after_uuid)`.
pub fn parse_uuid_box(b: &RawBox) -> Result<([u8; 16], &[u8])> {
    if b.data.len() < 16 {
        return Err(Jp2Error::InvalidBox {
            box_type: "uuid".into(),
            message: "UUID box too short".into(),
        });
    }
    let mut uuid = [0u8; 16];
    uuid.copy_from_slice(&b.data[..16]);
    Ok((uuid, &b.data[16..]))
}

// ── XML box ───────────────────────────────────────────────────────────────────

pub fn write_xml_box<W: Write>(w: &mut W, xml: &str) -> std::io::Result<()> {
    write_box(w, box_type::XML, xml.as_bytes())
}

// ── Resolution box ────────────────────────────────────────────────────────────

/// Capture resolution sub-box (`resc`) payload.
/// Numerators/denominators for vertical and horizontal resolution.
#[derive(Debug, Clone)]
pub struct ResolutionBox {
    pub vr_n: u16,
    pub vr_d: u16,
    pub hr_n: u16,
    pub hr_d: u16,
    pub vr_e: i8,
    pub hr_e: i8,
}

impl ResolutionBox {
    /// Write a `res ` superbox containing a `resc` capture-resolution sub-box.
    pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
        let mut resc = Vec::new();
        resc.extend_from_slice(&self.vr_n.to_be_bytes());
        resc.extend_from_slice(&self.vr_d.to_be_bytes());
        resc.extend_from_slice(&self.hr_n.to_be_bytes());
        resc.extend_from_slice(&self.hr_d.to_be_bytes());
        resc.push(self.vr_e as u8);
        resc.push(self.hr_e as u8);

        let mut resc_box = Vec::new();
        write_box(&mut resc_box, *b"resc", &resc)?;

        write_super_box(w, box_type::RESOLUTION, &resc_box)
    }
}