Skip to main content

document_svg/document/
jpeg2000.rs

1//! Bounded standalone JPEG 2000 / JP2 image conversion.
2//!
3//! PDF and DICOM already use the shared JPEG 2000 header validator. This
4//! adapter exposes the same safe subset as a normal raster document: one
5//! unsigned grayscale, gray+alpha, RGB, or RGB+alpha image per SVG page.
6
7use std::path::Path;
8
9use base64::Engine;
10use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
11use jpeg2k::{ColorSpace, Image};
12
13use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
14use crate::error::{Error, Result};
15use crate::ir::{IDENTITY, Node, Page, SourceMeta};
16
17const MAX_JPEG2000_BYTES: u64 = 128 * 1024 * 1024;
18const MAX_JPEG2000_PIXELS: u64 = 20_000_000;
19const MAX_JPEG2000_DIMENSION: u32 = 100_000;
20const MAX_JPEG2000_DECODED_BYTES: usize = 128 * 1024 * 1024;
21const MAX_JPEG2000_DATA_URI_BYTES: usize = 192 * 1024 * 1024;
22
23pub(crate) fn convert(
24    path: &Path,
25    options: &ConvertOptions,
26    sink: &mut dyn PageConsumer,
27) -> Result<Vec<String>> {
28    let bytes = read_limited_file(
29        path,
30        options.max_input_bytes.min(MAX_JPEG2000_BYTES),
31        "JPEG 2000 input",
32    )?;
33    let (png, width, height, mut warnings) = decode_png(&bytes)?;
34    let data_uri = format!("data:image/png;base64,{}", BASE64_STANDARD.encode(png));
35    if data_uri.len() > MAX_JPEG2000_DATA_URI_BYTES {
36        return Err(Error::LimitExceeded(format!(
37            "JPEG 2000 PNG data URI exceeds {MAX_JPEG2000_DATA_URI_BYTES} bytes"
38        )));
39    }
40    let mut page = Page::new(1, f64::from(width), f64::from(height), "jpeg2000");
41    page.title = "JPEG 2000 image".into();
42    page.description = format!("JPEG 2000 image {width}×{height}");
43    for warning in &warnings {
44        page.warn(warning.clone());
45    }
46    page.nodes.push(Node::Image {
47        id: "jpeg2000-image-1".into(),
48        href: data_uri,
49        x: 0.0,
50        y: 0.0,
51        width: f64::from(width),
52        height: f64::from(height),
53        transform: IDENTITY,
54        opacity: 1.0,
55        clip_id: None,
56        meta: SourceMeta {
57            semantic_role: "jpeg2000:image".into(),
58            ..Default::default()
59        },
60    });
61    sink.consume(page)?;
62    warnings.sort();
63    warnings.dedup();
64    Ok(warnings)
65}
66
67fn decode_png(bytes: &[u8]) -> Result<(Vec<u8>, u32, u32, Vec<String>)> {
68    let header = crate::jpeg2000::parse_jpx_header(bytes)?;
69    if header.width == 0
70        || header.height == 0
71        || header.width > MAX_JPEG2000_DIMENSION
72        || header.height > MAX_JPEG2000_DIMENSION
73    {
74        return Err(Error::LimitExceeded(format!(
75            "JPEG 2000 dimensions {}×{} exceed the supported range",
76            header.width, header.height
77        )));
78    }
79    let pixels = u64::from(header.width)
80        .checked_mul(u64::from(header.height))
81        .ok_or_else(|| Error::LimitExceeded("JPEG 2000 pixel count overflowed".into()))?;
82    if pixels > MAX_JPEG2000_PIXELS {
83        return Err(Error::LimitExceeded(format!(
84            "JPEG 2000 image contains {pixels} pixels; maximum is {MAX_JPEG2000_PIXELS}"
85        )));
86    }
87    let decoded_sample_bytes = usize::try_from(pixels)
88        .ok()
89        .and_then(|count| count.checked_mul(header.components.len()))
90        .and_then(|count| count.checked_mul(std::mem::size_of::<i32>()))
91        .ok_or_else(|| Error::LimitExceeded("JPEG 2000 decoded sample size overflowed".into()))?;
92    if decoded_sample_bytes > MAX_JPEG2000_DECODED_BYTES {
93        return Err(Error::LimitExceeded(format!(
94            "JPEG 2000 decoded samples need {decoded_sample_bytes} bytes; maximum is {MAX_JPEG2000_DECODED_BYTES}"
95        )));
96    }
97    let image = Image::from_bytes(bytes)
98        .map_err(|error| Error::InvalidInput(format!("JPEG 2000 decode failed: {error}")))?;
99    if image.width() != header.width || image.height() != header.height {
100        return Err(Error::InvalidInput(
101            "JPEG 2000 decoder dimensions do not match the validated header".into(),
102        ));
103    }
104    let components = image.components();
105    if components.len() != header.components.len() {
106        return Err(Error::InvalidInput(
107            "JPEG 2000 decoder component count does not match the validated header".into(),
108        ));
109    }
110    let precision = header.components[0].precision;
111    if !(1..=16).contains(&precision)
112        || header.components.iter().any(|component| {
113            component.precision != precision
114                || component.signed
115                || component.width != header.width
116                || component.height != header.height
117        })
118    {
119        return Err(Error::Unsupported(
120            "JPEG 2000 requires unsigned components with a shared 1–16-bit precision".into(),
121        ));
122    }
123    let mut warnings = Vec::new();
124    let color_space = image.color_space();
125    let alpha_index = components
126        .iter()
127        .enumerate()
128        .filter_map(|(index, component)| component.is_alpha().then_some(index))
129        .collect::<Vec<_>>();
130    let alpha = match alpha_index.as_slice() {
131        [] => match components.len() {
132            2 if matches!(
133                color_space,
134                ColorSpace::Gray | ColorSpace::Unknown | ColorSpace::Unspecified
135            ) =>
136            {
137                warnings.push(
138                    "JPEG 2000 two-component image was interpreted as grayscale plus alpha".into(),
139                );
140                Some(1)
141            }
142            4 if matches!(
143                color_space,
144                ColorSpace::SRGB | ColorSpace::Unknown | ColorSpace::Unspecified
145            ) =>
146            {
147                warnings.push(
148                    "JPEG 2000 four-component image was interpreted as RGB plus alpha".into(),
149                );
150                Some(3)
151            }
152            _ => None,
153        },
154        [index] => Some(*index),
155        _ => {
156            return Err(Error::Unsupported(
157                "JPEG 2000 images with multiple alpha components are unsupported".into(),
158            ));
159        }
160    };
161    let color_indices = (0..components.len())
162        .filter(|index| Some(*index) != alpha)
163        .collect::<Vec<_>>();
164    let png_color = match (color_indices.len(), alpha.is_some()) {
165        (1, false) => png::ColorType::Grayscale,
166        (1, true) => png::ColorType::GrayscaleAlpha,
167        (3, false) => png::ColorType::Rgb,
168        (3, true) => png::ColorType::Rgba,
169        _ => {
170            return Err(Error::Unsupported(format!(
171                "JPEG 2000 component count {} is unsupported; expected grayscale or RGB with optional alpha",
172                components.len()
173            )));
174        }
175    };
176    let output_channels = png_color.samples();
177    let output_len = usize::try_from(pixels)
178        .ok()
179        .and_then(|count| count.checked_mul(output_channels))
180        .ok_or_else(|| Error::LimitExceeded("JPEG 2000 PNG size overflowed".into()))?;
181    if output_len > MAX_JPEG2000_DECODED_BYTES {
182        return Err(Error::LimitExceeded(format!(
183            "JPEG 2000 PNG samples need {output_len} bytes; maximum is {MAX_JPEG2000_DECODED_BYTES}"
184        )));
185    }
186    let samples = components
187        .iter()
188        .map(|component| component.data())
189        .collect::<Vec<_>>();
190    let maximum = (1u32 << precision) - 1;
191    let mut output = Vec::with_capacity(output_len);
192    for pixel in 0..usize::try_from(pixels).unwrap_or(0) {
193        for index in &color_indices {
194            output.push(scale_sample(samples[*index].get(pixel).copied(), maximum)?);
195        }
196        if let Some(alpha_index) = alpha {
197            output.push(scale_sample(
198                samples[alpha_index].get(pixel).copied(),
199                maximum,
200            )?);
201        }
202    }
203    let mut png_bytes = Vec::new();
204    {
205        let mut encoder = png::Encoder::new(&mut png_bytes, header.width, header.height);
206        encoder.set_color(png_color);
207        encoder.set_depth(png::BitDepth::Eight);
208        let mut writer = encoder.write_header().map_err(|error| {
209            Error::InvalidInput(format!("could not encode JPEG 2000 PNG header: {error}"))
210        })?;
211        writer.write_image_data(&output).map_err(|error| {
212            Error::InvalidInput(format!("could not encode JPEG 2000 PNG data: {error}"))
213        })?;
214        writer.finish().map_err(|error| {
215            Error::InvalidInput(format!("could not finish JPEG 2000 PNG image: {error}"))
216        })?;
217    }
218    if precision != 8 {
219        warnings.push(format!(
220            "JPEG 2000 {precision}-bit samples were reduced to 8-bit PNG"
221        ));
222    }
223    if !matches!(
224        color_space,
225        ColorSpace::Gray | ColorSpace::SRGB | ColorSpace::Unknown | ColorSpace::Unspecified
226    ) {
227        warnings.push("JPEG 2000 color space metadata was not applied".into());
228    }
229    Ok((png_bytes, header.width, header.height, warnings))
230}
231
232fn scale_sample(sample: Option<i32>, maximum: u32) -> Result<u8> {
233    let sample = sample.ok_or_else(|| {
234        Error::InvalidInput("JPEG 2000 decoder returned an incomplete component".into())
235    })?;
236    if sample < 0 || u32::try_from(sample).unwrap_or(u32::MAX) > maximum {
237        return Err(Error::InvalidInput(
238            "JPEG 2000 sample is outside its declared precision".into(),
239        ));
240    }
241    Ok(
242        ((u64::try_from(sample).unwrap_or(0) * 255 + u64::from(maximum) / 2) / u64::from(maximum))
243            as u8,
244    )
245}