Skip to main content

justjp2/
lib.rs

1pub mod bio;
2pub mod dwt;
3pub mod error;
4pub mod htj2k;
5pub mod j2k;
6pub mod jp2;
7pub mod jp2_box;
8pub mod marker;
9pub mod mct;
10pub mod mqc;
11pub mod pi;
12pub mod quantize;
13pub mod simd;
14pub mod stream;
15pub mod t1;
16pub mod t2;
17pub mod tgt;
18pub mod tcd;
19pub mod types;
20
21// ── Phase 11: Public API ──
22
23pub use error::{Jp2Error, Result};
24pub use types::CodecFormat;
25
26use tcd::{TcdComponent, TcdParams};
27
28/// An image with one or more components.
29#[derive(Debug, Clone)]
30pub struct Image {
31    pub width: u32,
32    pub height: u32,
33    pub components: Vec<Component>,
34}
35
36/// A single image component.
37#[derive(Debug, Clone)]
38pub struct Component {
39    pub data: Vec<i32>,
40    pub width: u32,
41    pub height: u32,
42    pub precision: u32,
43    pub signed: bool,
44    pub dx: u32,
45    pub dy: u32,
46}
47
48/// Encoding parameters.
49#[derive(Debug, Clone)]
50pub struct EncodeParams {
51    pub lossless: bool,
52    pub num_decomp_levels: u32,
53    pub cblk_width: u32,
54    pub cblk_height: u32,
55    pub format: CodecFormat,
56}
57
58impl Default for EncodeParams {
59    fn default() -> Self {
60        Self {
61            lossless: true,
62            num_decomp_levels: 5,
63            cblk_width: 64,
64            cblk_height: 64,
65            format: CodecFormat::Jp2,
66        }
67    }
68}
69
70/// Decode a JPEG 2000 file (auto-detects J2K vs JP2 format).
71pub fn decode(data: &[u8]) -> Result<Image> {
72    decode_with_reduce(data, 0)
73}
74
75/// Decode a JPEG 2000 file at a reduced resolution.
76///
77/// # Arguments
78/// * `data` - The encoded JPEG 2000 data
79/// * `reduce` - Number of resolution levels to discard (0 = full resolution)
80pub fn decode_with_reduce(data: &[u8], reduce: u32) -> Result<Image> {
81    if data.len() < 4 {
82        return Err(Jp2Error::InvalidData(
83            "data too short to detect format".to_string(),
84        ));
85    }
86
87    let format = detect_format(data)?;
88
89    let (components_data, comp_info) = match format {
90        CodecFormat::Jp2 => {
91            if reduce > 0 {
92                // For JP2, extract the J2K codestream and decode with reduce
93                // For now, only J2K format supports reduce directly
94                jp2::jp2_decode(data)?
95            } else {
96                jp2::jp2_decode(data)?
97            }
98        }
99        CodecFormat::J2k => j2k::j2k_decode_with_reduce(data, reduce)?,
100    };
101
102    // Derive image dimensions from the first component (reference grid)
103    let width = comp_info[0].width;
104    let height = comp_info[0].height;
105
106    let components = components_data
107        .into_iter()
108        .zip(comp_info.iter())
109        .map(|(data, ci)| Component {
110            data,
111            width: ci.width,
112            height: ci.height,
113            precision: ci.precision,
114            signed: ci.signed,
115            dx: ci.dx,
116            dy: ci.dy,
117        })
118        .collect();
119
120    Ok(Image {
121        width,
122        height,
123        components,
124    })
125}
126
127/// Decode only a specific rectangular region of the image.
128///
129/// The region is specified in pixel coordinates: (x0, y0) is the top-left
130/// corner (inclusive) and (x1, y1) is the bottom-right corner (exclusive).
131///
132/// Returns an `Image` containing only the requested region.
133///
134/// # Errors
135/// Returns an error if the region is invalid (empty, or extends beyond the
136/// image bounds) or if the underlying decode fails.
137pub fn decode_region(data: &[u8], x0: u32, y0: u32, x1: u32, y1: u32) -> Result<Image> {
138    if x0 >= x1 || y0 >= y1 {
139        return Err(Jp2Error::InvalidData(
140            "decode_region: empty region (x0 >= x1 or y0 >= y1)".to_string(),
141        ));
142    }
143
144    // Decode the full image first, then crop.
145    let full = decode(data)?;
146
147    if x1 > full.width || y1 > full.height {
148        return Err(Jp2Error::InvalidData(format!(
149            "decode_region: region ({x0},{y0})-({x1},{y1}) exceeds image bounds {}x{}",
150            full.width, full.height
151        )));
152    }
153
154    let region_w = x1 - x0;
155    let region_h = y1 - y0;
156
157    let components = full
158        .components
159        .iter()
160        .map(|comp| {
161            // Handle sub-sampled components: adjust region coordinates by dx/dy
162            let cx0 = x0 / comp.dx;
163            let cy0 = y0 / comp.dy;
164            let cx1 = ((x1 + comp.dx - 1) / comp.dx).min(comp.width);
165            let cy1 = ((y1 + comp.dy - 1) / comp.dy).min(comp.height);
166            let cw = cx1 - cx0;
167            let ch = cy1 - cy0;
168
169            let mut region_data = vec![0i32; (cw * ch) as usize];
170            for y in 0..ch {
171                for x in 0..cw {
172                    let src_idx = ((cy0 + y) * comp.width + (cx0 + x)) as usize;
173                    let dst_idx = (y * cw + x) as usize;
174                    region_data[dst_idx] = comp.data[src_idx];
175                }
176            }
177
178            Component {
179                data: region_data,
180                width: cw,
181                height: ch,
182                precision: comp.precision,
183                signed: comp.signed,
184                dx: comp.dx,
185                dy: comp.dy,
186            }
187        })
188        .collect();
189
190    Ok(Image {
191        width: region_w,
192        height: region_h,
193        components,
194    })
195}
196
197/// Encode an image as JPEG 2000.
198pub fn encode(image: &Image, params: &EncodeParams) -> Result<Vec<u8>> {
199    if image.components.is_empty() {
200        return Err(Jp2Error::InvalidData(
201            "image has no components".to_string(),
202        ));
203    }
204
205    let comp_info: Vec<TcdComponent> = image
206        .components
207        .iter()
208        .map(|c| TcdComponent {
209            width: c.width,
210            height: c.height,
211            precision: c.precision,
212            signed: c.signed,
213            dx: c.dx,
214            dy: c.dy,
215        })
216        .collect();
217
218    let components_data: Vec<Vec<i32>> = image
219        .components
220        .iter()
221        .map(|c| c.data.clone())
222        .collect();
223
224    let use_mct = image.components.len() >= 3;
225
226    let tcd_params = TcdParams {
227        num_res: params.num_decomp_levels + 1,
228        cblk_w: params.cblk_width,
229        cblk_h: params.cblk_height,
230        reversible: params.lossless,
231        num_layers: 1,
232        use_mct,
233        reduce: 0,
234        max_bytes: None,
235    };
236
237    match params.format {
238        CodecFormat::Jp2 => jp2::jp2_encode(&components_data, &comp_info, &tcd_params),
239        CodecFormat::J2k => j2k::j2k_encode(&components_data, &comp_info, &tcd_params),
240    }
241}
242
243/// Auto-detect whether data is JP2 or raw J2K.
244fn detect_format(data: &[u8]) -> Result<CodecFormat> {
245    if data.len() < 4 {
246        return Err(Jp2Error::InvalidData(
247            "data too short to detect format".to_string(),
248        ));
249    }
250
251    // Check for JP2: first 4 bytes are a box length (typically 0x0000000C = 12),
252    // followed by the JP2 signature box type 0x6A502020.
253    if data.len() >= 12 {
254        let box_type = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
255        if box_type == jp2_box::JP2_JP {
256            return Ok(CodecFormat::Jp2);
257        }
258    }
259
260    // Check for raw J2K: starts with SOC marker 0xFF4F
261    if data[0] == 0xFF && data[1] == 0x4F {
262        return Ok(CodecFormat::J2k);
263    }
264
265    Err(Jp2Error::InvalidData(
266        "unrecognized format: not JP2 or J2K".to_string(),
267    ))
268}