Skip to content

Commit c1e7f13

Browse files
committed
Add OpenRaster (*.ora) decoder
1 parent 560d79c commit c1e7f13

File tree

9 files changed

+301
-3
lines changed

9 files changed

+301
-3
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ publish = false
88
include = ["src", "tests/reference.rs"]
99

1010
[features]
11-
default = ["pcx"]
11+
default = ["pcx", "ora"]
1212
pcx = ["dep:pcx"]
13+
ora = ["image/png", "dep:zip", "dep:ouroboros"]
1314

1415
[dependencies]
1516
image = { version = "0.25.8", default-features = false }
1617
pcx = { version = "0.2.4", optional = true }
18+
# OpenRaster only requires DEFLATED and STORED modes
19+
zip = { version = "5.1.1", default-features = false, features = ["deflate"], optional = true }
20+
ouroboros = { version = "0.18.5", optional = true }
1721

1822
[dev-dependencies]
1923
image = { version = "0.25.8", default-features = false, features = ["png"] }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Decoding support for additional image formats beyond those provided by the [`ima
66
| Extension | File Format Description |
77
| --------- | -------------------- |
88
| PCX | [Wikipedia](https://en.wikipedia.org/wiki/PCX#PCX_file_format) |
9+
| ORA | [Wikipedia](https://en.wikipedia.org/wiki/OpenRaster) |
910

1011
## New Formats
1112

deny.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ allow = [
2121
"MIT",
2222
"MIT-0",
2323
"MPL-2.0",
24+
"Unicode-3.0",
2425
"Unicode-DFS-2016",
26+
"Zlib",
2527
]
2628

2729

fuzz/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ members = ["."]
2525
[[bin]]
2626
name = "fuzzer_script_pcx"
2727
path = "fuzzers/fuzzer_script_pcx.rs"
28+
29+
[[bin]]
30+
name = "fuzzer_script_ora"
31+
path = "fuzzers/fuzzer_script_ora.rs"

fuzz/fuzzers/fuzzer_script_ora.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#![no_main]
2+
#[macro_use]
3+
extern crate libfuzzer_sys;
4+
5+
use image::{ImageDecoder, Limits};
6+
use std::io::Cursor;
7+
8+
fuzz_target!(|data: &[u8]| {
9+
let reader = Cursor::new(data);
10+
let Ok(mut decoder) =
11+
image_extras::ora::OpenRasterDecoder::with_limits(reader, Limits::no_limits())
12+
else {
13+
return;
14+
};
15+
let mut limits = image::Limits::default();
16+
limits.max_alloc = Some(1024 * 1024); // 1 MiB
17+
if limits.reserve(decoder.total_bytes()).is_err() {
18+
return;
19+
}
20+
if decoder.set_limits(limits).is_err() {
21+
return;
22+
}
23+
let _ = std::hint::black_box(image::DynamicImage::from_decoder(decoder));
24+
});

src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,33 @@
1414
//! // Now you can use the image crate as normal
1515
//! let img = image::open("path/to/image.pcx").unwrap();
1616
//! ```
17+
use image::Limits;
1718

1819
#[cfg(feature = "pcx")]
1920
pub mod pcx;
2021

22+
#[cfg(feature = "ora")]
23+
pub mod ora;
24+
2125
/// Register all enabled extra formats with the image crate.
2226
pub fn register() {
23-
let just_registered = image::hooks::register_decoding_hook(
27+
let just_registered_pcx = image::hooks::register_decoding_hook(
2428
"pcx".into(),
2529
Box::new(|r| Ok(Box::new(pcx::PCXDecoder::new(r)?))),
2630
);
27-
if just_registered {
31+
if just_registered_pcx {
2832
image::hooks::register_format_detection_hook("pcx".into(), &[0x0a, 0x0], Some(b"\xFF\xF8"));
2933
}
34+
35+
// OpenRaster images are ZIP files and have no simple signature to distinguish them
36+
// from ZIP files containing other content
37+
image::hooks::register_decoding_hook(
38+
"ora".into(),
39+
Box::new(|r| {
40+
Ok(Box::new(ora::OpenRasterDecoder::with_limits(
41+
r,
42+
Limits::no_limits(),
43+
)?))
44+
}),
45+
);
3046
}

src/ora.rs

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
#![forbid(unsafe_code)]
13+
use image::codecs::png::PngDecoder;
14+
use image::error::{DecodingError, ImageFormatHint, UnsupportedError};
15+
use image::metadata::Orientation;
16+
use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult, Limits};
17+
use ouroboros::self_referencing;
18+
use std::io::{self, BufReader, Read, Seek};
19+
use std::marker::PhantomData;
20+
use zip::read::{ZipArchive, ZipFile};
21+
22+
pub struct OpenRasterDecoder<'a, R>
23+
where
24+
R: Read + Seek + 'a,
25+
{
26+
mergedimg_decoder: PngDecoder<BufReader<SeekableArchiveFile<'a, R>>>,
27+
}
28+
29+
fn openraster_format_hint() -> ImageFormatHint {
30+
ImageFormatHint::Name("OpenRaster".into())
31+
}
32+
33+
/// Adjust the format of the PngDecoder's errors to indicate OpenRaster instead
34+
fn 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]
55+
struct 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.
67+
struct 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+
74+
impl<'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+
91+
impl<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+
106+
impl<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+
impl<'a, R> OpenRasterDecoder<'a, R>
146+
where
147+
R: Read + Seek + 'a,
148+
{
149+
/// Create a new `OpenRasterDecoder` with the provided limits.
150+
///
151+
/// (Limits need to be specified in advance, because determining the
152+
/// minimum information needed for the ImageDecoder trait (image size and
153+
/// color type) may require reading through and remembering image-dependent
154+
/// amount of data.)
155+
///
156+
/// Warning: While decoding limits apply to the header parsing and decoding
157+
/// of the merged imaged component (a PNG file inside the ZIP archive that
158+
/// forms an OpenRaster file), memory constraints on the ZIP file decoding
159+
/// process have not yet been implemented; input ZIP files with very many
160+
/// entries may require significant amounts of memory to read.
161+
pub fn with_limits(r: R, limits: Limits) -> Result<OpenRasterDecoder<'a, R>, ImageError> {
162+
let mut archive = ZipArchive::new(r)
163+
.map_err(|e| ImageError::Decoding(DecodingError::new(openraster_format_hint(), e)))?;
164+
165+
/* Verify that this _is_ an OpenRaster file, and not some unrelated ZIP archive */
166+
let mimetype_index = archive.index_for_name("mimetype").ok_or_else(|| {
167+
ImageError::Decoding(DecodingError::new(
168+
openraster_format_hint(),
169+
"OpenRaster images should contain a mimetype subfile",
170+
))
171+
})?;
172+
173+
let mut mimetype_file = archive
174+
.by_index(mimetype_index)
175+
.map_err(|x| ImageError::Decoding(DecodingError::new(openraster_format_hint(), x)))?;
176+
177+
const EXPECTED_MIMETYPE: &str = "image/openraster";
178+
let mut tmp = [0u8; EXPECTED_MIMETYPE.len()];
179+
180+
mimetype_file.read_exact(&mut tmp)?;
181+
182+
if tmp != EXPECTED_MIMETYPE.as_bytes()
183+
|| mimetype_file.size() != EXPECTED_MIMETYPE.len() as u64
184+
{
185+
return Err(ImageError::Decoding(DecodingError::new(
186+
openraster_format_hint(),
187+
"Image did not have correct mimetype subentry to be identified as OpenRaster",
188+
)));
189+
}
190+
191+
drop(mimetype_file);
192+
193+
let mergedimage_index = archive.index_for_name("mergedimage.png").ok_or_else(|| {
194+
ImageError::Decoding(DecodingError::new(
195+
openraster_format_hint(),
196+
"OpenRaster image missing mergedimage.png entry",
197+
))
198+
})?;
199+
200+
let file = SeekableArchiveFile::new(archive, mergedimage_index)?;
201+
let decoder =
202+
PngDecoder::with_limits(BufReader::new(file), limits).map_err(set_ora_image_type)?;
203+
204+
Ok(OpenRasterDecoder {
205+
mergedimg_decoder: decoder,
206+
})
207+
}
208+
}
209+
210+
impl<'a, R: Read + Seek + 'a> ImageDecoder for OpenRasterDecoder<'a, R> {
211+
fn dimensions(&self) -> (u32, u32) {
212+
self.mergedimg_decoder.dimensions()
213+
}
214+
215+
fn color_type(&self) -> ColorType {
216+
self.mergedimg_decoder.color_type()
217+
}
218+
219+
fn original_color_type(&self) -> ExtendedColorType {
220+
self.mergedimg_decoder.original_color_type()
221+
}
222+
223+
fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
224+
// Warning: this does not account for any ZIP reading overhead
225+
self.mergedimg_decoder.set_limits(limits)
226+
}
227+
228+
fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
229+
self.mergedimg_decoder.icc_profile()
230+
}
231+
232+
fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
233+
self.mergedimg_decoder.exif_metadata()
234+
}
235+
236+
fn orientation(&mut self) -> ImageResult<Orientation> {
237+
self.mergedimg_decoder.orientation()
238+
}
239+
240+
fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
241+
self.mergedimg_decoder.read_image(buf)
242+
}
243+
244+
fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
245+
(*self).read_image(buf)
246+
}
247+
}

tests/images/ora/layer.ora

30.9 KB
Binary file not shown.

tests/images/ora/layer.png

386 Bytes
Loading

0 commit comments

Comments
 (0)