Skip to main content

image_extras/
ora.rs

1//! Decoding of OpenRaster Images (*.ora)
2//!
3//! OpenRaster is an a file format used to communicate layered images; the
4//! decoder provided herein only extracts and displays the final merged raster
5//! image cached by the OpenRaster file, and does not expose the details of
6//! layers (which may be either raster or vector graphics) or render the merged
7//! image itself.
8//!
9//! # Related Links
10//! * <https://en.wikipedia.org/wiki/OpenRaster> - The OpenRaster format on Wikipedia
11//! * <https://www.openraster.org/> - OpenRaster specification
12
13use image::codecs::png::PngDecoder;
14use image::error::{DecodingError, ImageFormatHint, UnsupportedError};
15use image::metadata::Orientation;
16use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult, Limits};
17use ouroboros::self_referencing;
18use std::io::{self, BufReader, Read, Seek};
19use std::marker::PhantomData;
20use zip::read::{ZipArchive, ZipFile};
21
22pub struct OpenRasterDecoder<'a, R>
23where
24    R: Read + Seek + 'a,
25{
26    mergedimg_decoder: PngDecoder<BufReader<SeekableArchiveFile<'a, R>>>,
27}
28
29fn openraster_format_hint() -> ImageFormatHint {
30    ImageFormatHint::Name("OpenRaster".into())
31}
32
33/// Adjust the format of the PngDecoder's errors to indicate OpenRaster instead
34fn set_ora_image_type(err: ImageError) -> ImageError {
35    match err {
36        ImageError::Decoding(e) => {
37            // DecodingError does not directly expose the underlying type,
38            // so nest the error
39            ImageError::Decoding(DecodingError::new(openraster_format_hint(), e))
40        }
41        ImageError::Encoding(_) => {
42            // Should not be encoding any files
43            unreachable!();
44        }
45        ImageError::Parameter(e) => ImageError::Parameter(e),
46        ImageError::Limits(e) => ImageError::Limits(e),
47        ImageError::Unsupported(e) => ImageError::Unsupported(
48            UnsupportedError::from_format_and_kind(openraster_format_hint(), e.kind()),
49        ),
50        ImageError::IoError(e) => ImageError::IoError(e),
51    }
52}
53
54#[self_referencing]
55struct SeekableArchiveCore<'a, R: Read + Seek + 'a> {
56    archive: ZipArchive<R>,
57    #[covariant]
58    #[borrows(mut archive)]
59    file: ZipFile<'this, R>,
60    lifetime_helper: PhantomData<&'a R>,
61}
62
63/// The zip crate does not provide a seekable reader that works on compressed
64/// entries, while png::Decoder requires the Seek bound (but does not currently
65/// use it). This structure implements Seek by reopening and reading the zip
66/// archive entry whenever it seeks backwards.
67struct SeekableArchiveFile<'a, R: Read + Seek + 'a> {
68    core: Option<SeekableArchiveCore<'a, R>>,
69    file_index: usize,
70    position: u64,
71    file_size: u64,
72}
73
74impl<'a, R: Read + Seek + 'a> SeekableArchiveFile<'a, R> {
75    fn new(
76        archive: ZipArchive<R>,
77        file_index: usize,
78    ) -> Result<SeekableArchiveFile<'a, R>, io::Error> {
79        let core = SeekableArchiveCore::try_new(archive, |x| x.by_index(file_index), PhantomData)
80            .map_err(|x| io::Error::other(format!("failed to open: {:?}", x)))?;
81        let file_size = core.with_file(|file| file.size());
82        Ok(SeekableArchiveFile {
83            core: Some(core),
84            file_index,
85            position: 0,
86            file_size,
87        })
88    }
89}
90
91impl<R: Read + Seek> Read for SeekableArchiveFile<'_, R> {
92    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
93        let res = self
94            .core
95            .as_mut()
96            .unwrap()
97            .with_file_mut(|file| file.read(buf));
98        let nread = res?;
99        self.position
100            .checked_add(nread as u64)
101            .ok_or_else(|| io::Error::other("seek position overflow"))?;
102        Ok(nread)
103    }
104}
105
106impl<R: Read + Seek> Seek for SeekableArchiveFile<'_, R> {
107    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
108        let target_pos = match pos {
109            io::SeekFrom::Start(offset) => offset,
110            io::SeekFrom::End(offset) => self
111                .file_size
112                .checked_add_signed(offset)
113                .ok_or_else(|| io::Error::other("seek position over or underflow"))?,
114            io::SeekFrom::Current(offset) => self
115                .position
116                .checked_add_signed(offset)
117                .ok_or_else(|| io::Error::other("seek position over or underflow"))?,
118        };
119
120        if target_pos < self.position {
121            let core = self.core.take();
122            let archive = core.unwrap().into_heads().archive;
123
124            self.core = Some(
125                SeekableArchiveCore::try_new(archive, |x| x.by_index(self.file_index), PhantomData)
126                    .map_err(|x| io::Error::other(format!("failed to reopen: {:?}", x)))?,
127            );
128        }
129        while self.position < target_pos {
130            const TMP_LEN: usize = 1024;
131            let mut tmp = [0_u8; TMP_LEN];
132            let cur_pos = self.position;
133            let nr = self
134                .read(&mut tmp[..std::cmp::min(TMP_LEN as u64, target_pos - cur_pos) as usize])?;
135            if nr == 0 {
136                return Err(io::Error::other("unexpected eof when seeking"));
137            }
138            self.position += nr as u64;
139        }
140
141        Ok(0)
142    }
143}
144
145/// Verify that this _is_ an OpenRaster file, and not some unrelated ZIP archive
146fn verify_archive(archive: &mut ZipArchive<impl Read + Seek>) -> ImageResult<()> {
147    let mimetype_index = archive.index_for_name("mimetype").ok_or_else(|| {
148        ImageError::Decoding(DecodingError::new(
149            openraster_format_hint(),
150            "OpenRaster images should contain a mimetype subfile",
151        ))
152    })?;
153
154    let mut mimetype_file = archive
155        .by_index(mimetype_index)
156        .map_err(|x| ImageError::Decoding(DecodingError::new(openraster_format_hint(), x)))?;
157
158    const EXPECTED_MIMETYPE: &str = "image/openraster";
159    let mut tmp = [0u8; EXPECTED_MIMETYPE.len()];
160
161    mimetype_file.read_exact(&mut tmp)?;
162
163    if tmp != EXPECTED_MIMETYPE.as_bytes() || mimetype_file.size() != EXPECTED_MIMETYPE.len() as u64
164    {
165        return Err(ImageError::Decoding(DecodingError::new(
166            openraster_format_hint(),
167            "Image did not have correct mimetype subentry to be identified as OpenRaster",
168        )));
169    }
170
171    Ok(())
172}
173
174impl<'a, R> OpenRasterDecoder<'a, R>
175where
176    R: Read + Seek + 'a,
177{
178    /// Create a new `OpenRasterDecoder` with the provided limits.
179    ///
180    /// (Limits need to be specified in advance, because determining the
181    /// minimum information needed for the ImageDecoder trait (image size and
182    /// color type) may require reading through and remembering image-dependent
183    /// amount of data.)
184    ///
185    /// Warning: While decoding limits apply to the header parsing and decoding
186    /// of the merged imaged component (a PNG file inside the ZIP archive that
187    /// forms an OpenRaster file), memory constraints on the ZIP file decoding
188    /// process have not yet been implemented; input ZIP files with very many
189    /// entries may require significant amounts of memory to read.
190    pub fn with_limits(r: R, limits: Limits) -> Result<OpenRasterDecoder<'a, R>, ImageError> {
191        let mut archive = ZipArchive::new(r)
192            .map_err(|e| ImageError::Decoding(DecodingError::new(openraster_format_hint(), e)))?;
193
194        verify_archive(&mut archive)?;
195
196        let mergedimage_index = archive.index_for_name("mergedimage.png").ok_or_else(|| {
197            ImageError::Decoding(DecodingError::new(
198                openraster_format_hint(),
199                "OpenRaster image missing mergedimage.png entry",
200            ))
201        })?;
202
203        let file = SeekableArchiveFile::new(archive, mergedimage_index)?;
204        let decoder =
205            PngDecoder::with_limits(BufReader::new(file), limits).map_err(set_ora_image_type)?;
206
207        Ok(OpenRasterDecoder {
208            mergedimg_decoder: decoder,
209        })
210    }
211}
212
213impl<'a, R: Read + Seek + 'a> ImageDecoder for OpenRasterDecoder<'a, R> {
214    fn dimensions(&self) -> (u32, u32) {
215        self.mergedimg_decoder.dimensions()
216    }
217
218    fn color_type(&self) -> ColorType {
219        self.mergedimg_decoder.color_type()
220    }
221
222    fn original_color_type(&self) -> ExtendedColorType {
223        self.mergedimg_decoder.original_color_type()
224    }
225
226    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
227        // Warning: this does not account for any ZIP reading overhead
228        self.mergedimg_decoder.set_limits(limits)
229    }
230
231    fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
232        self.mergedimg_decoder.icc_profile()
233    }
234
235    fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
236        self.mergedimg_decoder.exif_metadata()
237    }
238
239    fn orientation(&mut self) -> ImageResult<Orientation> {
240        self.mergedimg_decoder.orientation()
241    }
242
243    fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
244        self.mergedimg_decoder.read_image(buf)
245    }
246
247    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
248        (*self).read_image(buf)
249    }
250}