Skip to main content

pdfluent_jpeg2000/
integration.rs

1//! Integration with the [image] crate
2
3use std::{
4    ffi::OsStr,
5    io::{BufRead, Seek},
6};
7
8use crate::{ColorSpace, DecodeSettings, Image};
9use ::image::error::{DecodingError, ImageFormatHint};
10use ::image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult};
11use image::hooks::{decoding_hook_registered, register_format_detection_hook};
12use moxcms::{CmsError, ColorProfile, Layout, TransformOptions};
13
14const CMYK_PROFILE: &[u8] = include_bytes!("../assets/CGATS001Compat-v2-micro.icc");
15
16impl ImageDecoder for Image<'_> {
17    fn dimensions(&self) -> (u32, u32) {
18        (self.width(), self.height())
19    }
20
21    fn color_type(&self) -> ColorType {
22        let channel_count = self.color_space.num_channels();
23        let has_alpha = self.has_alpha;
24
25        match (channel_count, has_alpha) {
26            (1, false) => ColorType::L8,
27            (1, true) => ColorType::La8,
28            (3, false) => ColorType::Rgb8,
29            (3, true) => ColorType::Rgba8,
30            // We convert CMYK to RGB.
31            (4, false) => ColorType::Rgb8,
32            (4, true) => ColorType::Rgba8,
33            // We have to return something...
34            _ => ColorType::Rgb8,
35        }
36    }
37
38    fn original_color_type(&self) -> ExtendedColorType {
39        let channel_count = self.color_space.num_channels();
40        let has_alpha = self.has_alpha;
41        let depth = self.original_bit_depth();
42        // match logic based on color_type() above
43        match (channel_count, depth, has_alpha) {
44            // Grayscale
45            (1, 1, false) => ExtendedColorType::L1,
46            (1, 1, true) => ExtendedColorType::La1,
47            (1, 2, false) => ExtendedColorType::L2,
48            (1, 2, true) => ExtendedColorType::La2,
49            (1, 4, false) => ExtendedColorType::L4,
50            (1, 4, true) => ExtendedColorType::La4,
51            (1, 8, false) => ExtendedColorType::L8,
52            (1, 8, true) => ExtendedColorType::La8,
53            (1, 16, false) => ExtendedColorType::L8,
54            (1, 16, true) => ExtendedColorType::La8,
55            // RGB
56            (3, 1, false) => ExtendedColorType::Rgb1,
57            (3, 1, true) => ExtendedColorType::Rgba1,
58            (3, 2, false) => ExtendedColorType::Rgb2,
59            (3, 2, true) => ExtendedColorType::Rgba2,
60            (3, 4, false) => ExtendedColorType::Rgb4,
61            (3, 4, true) => ExtendedColorType::Rgba4,
62            (3, 8, false) => ExtendedColorType::Rgb8,
63            (3, 8, true) => ExtendedColorType::Rgba8,
64            (3, 16, false) => ExtendedColorType::Rgb8,
65            (3, 16, true) => ExtendedColorType::Rgba8,
66            // CMYK
67            (4, 8, false) => ExtendedColorType::Cmyk8,
68            (4, 16, false) => ExtendedColorType::Cmyk16,
69            // CMYK with alpha is not representable
70            _ => ExtendedColorType::Unknown(orig_bits_per_pixel(self)),
71        }
72    }
73
74    fn read_image(self, buf: &mut [u8]) -> ImageResult<()>
75    where
76        Self: Sized,
77    {
78        convert_inner(&self, buf)
79    }
80
81    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
82        convert_inner(&self, buf)
83    }
84}
85
86#[doc(hidden)]
87/// JPEG2000 decoder compatible with `image` decoding hook APIs that pass an `impl Read + Seek`
88pub struct Jp2Decoder {
89    // Lots of fields from `crate::Image` are duplicated here;
90    // this is necessary because `crate::Image` borrows a slice and keeping it in the same struct
91    // as `input: Vec<u8>` would create a self-referential struct that Rust cannot easily express.
92    //
93    // This approach is modeled after the integration of early versions of zune-jpeg into image:
94    // https://docs.rs/image/0.25.6/src/image/codecs/jpeg/decoder.rs.html#27-58
95    //
96    // Buffering the entire input in memory is not an issue for lossy formats like JPEG.
97    // The compression ratio is so high that an image that expands to hundreds of MB when decoded
98    // only takes up a single-digit number of MB in a compressed form.
99    input: Vec<u8>,
100    width: u32,
101    height: u32,
102    color_type: ColorType,
103    orig_color_type: ExtendedColorType,
104}
105
106impl Jp2Decoder {
107    /// Create a new decoder that decodes from the stream ```r```
108    pub fn new<R: BufRead + Seek>(r: R) -> ImageResult<Self> {
109        let mut input = Vec::new();
110        let mut r = r;
111        r.read_to_end(&mut input)?;
112        let headers = Image::new(&input, &DecodeSettings::default())?;
113        Ok(Self {
114            width: headers.width(),
115            height: headers.height(),
116            color_type: headers.color_type(),
117            orig_color_type: headers.original_color_type(),
118            input,
119        })
120    }
121}
122
123impl ImageDecoder for Jp2Decoder {
124    fn dimensions(&self) -> (u32, u32) {
125        (self.width, self.height)
126    }
127
128    fn color_type(&self) -> ColorType {
129        self.color_type
130    }
131
132    fn original_color_type(&self) -> ExtendedColorType {
133        self.orig_color_type
134    }
135
136    fn read_image(self, buf: &mut [u8]) -> ImageResult<()>
137    where
138        Self: Sized,
139    {
140        // we can safely .unwrap() because we've already done this on decoder creation and know this works
141        let decoder = Image::new(&self.input, &DecodeSettings::default()).unwrap();
142        decoder.read_image(buf)
143    }
144
145    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
146        // we can safely .unwrap() because we've already done this on decoder creation and know this works
147        let decoder = Image::new(&self.input, &DecodeSettings::default()).unwrap();
148        decoder.read_image(buf)
149    }
150}
151
152/// Private convenience function for `image` integration
153fn orig_bits_per_pixel(img: &Image<'_>) -> u8 {
154    let mut channel_count = img.color_space().num_channels();
155    if img.has_alpha {
156        channel_count += 1;
157    }
158    channel_count * img.original_bit_depth()
159}
160
161fn convert_inner(image: &Image<'_>, buf: &mut [u8]) -> ImageResult<()> {
162    let width = image.width();
163    let height = image.height();
164    let color_space = image.color_space().clone();
165    let has_alpha = image.has_alpha();
166
167    fn from_icc(
168        icc: &[u8],
169        num_channels: u8,
170        has_alpha: bool,
171        width: u32,
172        height: u32,
173        input_data: &[u8],
174    ) -> Result<Vec<u8>, CmsError> {
175        let src_profile = ColorProfile::new_from_slice(icc)?;
176        let dest_profile = ColorProfile::new_srgb();
177
178        let (src_layout, dest_layout, out_channels) = match (num_channels, has_alpha) {
179            (1, false) => (Layout::Gray, Layout::Gray, 1),
180            (1, true) => (Layout::GrayAlpha, Layout::GrayAlpha, 2),
181            (3, false) => (Layout::Rgb, Layout::Rgb, 3),
182            (3, true) => (Layout::Rgba, Layout::Rgba, 4),
183            // CMYK will be converted to RGB.
184            (4, false) => (Layout::Rgba, Layout::Rgb, 3),
185            _ => {
186                return Err(CmsError::UnsupportedChannelConfiguration);
187            }
188        };
189
190        // Same reason as pdf-interpret's ICC transform: moxcms' fixed-point
191        // pipeline is a different implementation per architecture and the
192        // implementations disagree, so an embedded JPEG 2000 profile would
193        // decode to different bytes on x86 and aarch64. A deliberate
194        // divergence from upstream hayro, noted on this crate's entry in
195        // docs/UPSTREAM_FORKS.toml so a future merge does not read it as
196        // upstream work we dropped. (#330)
197        let transform = src_profile.create_transform_8bit(
198            src_layout,
199            &dest_profile,
200            dest_layout,
201            TransformOptions {
202                prefer_fixed_point: false,
203                ..TransformOptions::default()
204            },
205        )?;
206
207        let mut transformed = vec![0; (width * height * out_channels) as usize];
208
209        transform.transform(input_data, &mut transformed)?;
210
211        Ok(transformed)
212    }
213
214    fn process(
215        image: &Image<'_>,
216        buf: &mut [u8],
217        width: u32,
218        height: u32,
219        has_alpha: bool,
220        cs: ColorSpace,
221    ) -> Result<(), ImageError> {
222        let mut decoder_context = crate::DecoderContext::default();
223
224        match (cs, has_alpha) {
225            (ColorSpace::Gray, false) => {
226                image.decode(&mut decoder_context)?.store_u8_into(buf);
227            }
228            (ColorSpace::Gray, true) => {
229                image.decode(&mut decoder_context)?.store_u8_into(buf);
230            }
231            (ColorSpace::RGB, false) => {
232                image.decode(&mut decoder_context)?.store_u8_into(buf);
233            }
234            (ColorSpace::RGB, true) => {
235                image.decode(&mut decoder_context)?.store_u8_into(buf);
236            }
237            (ColorSpace::CMYK, false) => {
238                let decoded = image.decode(&mut decoder_context)?.data_u8();
239                let transformed = from_icc(CMYK_PROFILE, 4, has_alpha, width, height, &decoded)
240                    .map_err(icc_err_to_image)?;
241                buf.copy_from_slice(&transformed);
242            }
243            (ColorSpace::CMYK, true) => {
244                // moxcms doesn't support CMYK interleaved with alpha, so we
245                // need to split it.
246                let decoded = image.decode(&mut decoder_context)?.data_u8();
247                let mut cmyk = vec![];
248                let mut alpha = vec![];
249
250                for sample in decoded.chunks_exact(5) {
251                    cmyk.extend_from_slice(&sample[..4]);
252                    alpha.push(sample[4]);
253                }
254
255                let rgb = from_icc(CMYK_PROFILE, 4, false, width, height, &cmyk)
256                    .map_err(icc_err_to_image)?;
257                for (out, pixel) in buf.chunks_exact_mut(4).zip(
258                    rgb.chunks_exact(3)
259                        .zip(alpha)
260                        .map(|(rgb, alpha)| [rgb[0], rgb[1], rgb[2], alpha]),
261                ) {
262                    out.copy_from_slice(&pixel);
263                }
264            }
265            (
266                ColorSpace::Icc {
267                    profile,
268                    num_channels: num_components,
269                },
270                has_alpha,
271            ) => {
272                let decoded = image.decode(&mut decoder_context)?.data_u8();
273
274                let transformed =
275                    from_icc(&profile, num_components, has_alpha, width, height, &decoded);
276
277                if let Ok(transformed) = transformed {
278                    buf.copy_from_slice(&transformed);
279                } else {
280                    match num_components {
281                        1 => process(image, buf, width, height, has_alpha, ColorSpace::Gray)?,
282                        3 => process(image, buf, width, height, has_alpha, ColorSpace::RGB)?,
283                        4 => process(image, buf, width, height, has_alpha, ColorSpace::CMYK)?,
284                        _ => {
285                            return Err(unsupported_color_error(image.original_color_type()));
286                        }
287                    }
288                };
289            }
290            (ColorSpace::Unknown { .. }, _) => {
291                return Err(unsupported_color_error(image.original_color_type()));
292            }
293        };
294
295        Ok(())
296    }
297
298    process(image, buf, width, height, has_alpha, color_space)
299}
300
301impl From<crate::DecodeError> for DecodingError {
302    fn from(value: crate::DecodeError) -> Self {
303        let format = ImageFormatHint::Name("JPEG2000".to_owned());
304        Self::new(format, value)
305    }
306}
307
308impl From<crate::DecodeError> for ImageError {
309    fn from(value: crate::DecodeError) -> Self {
310        Self::Decoding(value.into())
311    }
312}
313
314fn icc_err_to_image(err: CmsError) -> ImageError {
315    let format = ImageFormatHint::Name("JPEG2000".to_owned());
316    ImageError::Decoding(DecodingError::new(format, err))
317}
318
319fn unsupported_color_error(color: ExtendedColorType) -> ImageError {
320    ImageError::Unsupported(image::error::UnsupportedError::from_format_and_kind(
321        ImageFormatHint::Name("JPEG2000".to_owned()),
322        image::error::UnsupportedErrorKind::Color(color),
323    ))
324}
325
326/// Registers the decoder with the `image` crate so that non-format-specific calls such as
327/// `ImageReader::open("image.jp2")?.decode()?;` work with JPEG2000 files.
328///
329/// Returns `true` on success, or `false` if the hook for JPEG2000 is already registered.
330pub fn register_decoding_hook() -> bool {
331    if decoding_hook_registered(OsStr::new("jp2")) {
332        return false;
333    }
334
335    for extension in ["jp2", "jpg2", "j2k", "jpf"] {
336        image::hooks::register_decoding_hook(
337            extension.into(),
338            Box::new(|r| Ok(Box::new(Jp2Decoder::new(r)?))),
339        );
340        register_format_detection_hook(extension.into(), crate::JP2_MAGIC, None);
341    }
342
343    for extension in ["j2c", "jpc"] {
344        image::hooks::register_decoding_hook(
345            extension.into(),
346            Box::new(|r| Ok(Box::new(Jp2Decoder::new(r)?))),
347        );
348        register_format_detection_hook(extension.into(), crate::CODESTREAM_MAGIC, None);
349    }
350
351    true
352}