Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ publish = false
include = ["src", "tests/reference.rs"]

[features]
default = ["pcx"]
default = ["pcx", "ora"]
pcx = ["dep:pcx"]
ora = ["image/png", "dep:zip", "dep:ouroboros"]

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

[dev-dependencies]
image = { version = "0.25.8", default-features = false, features = ["png"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Decoding support for additional image formats beyond those provided by the [`ima
| Extension | File Format Description |
| --------- | -------------------- |
| PCX | [Wikipedia](https://en.wikipedia.org/wiki/PCX#PCX_file_format) |
| ORA | [Wikipedia](https://en.wikipedia.org/wiki/OpenRaster) |

## New Formats

Expand Down
2 changes: 2 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ allow = [
"MIT",
"MIT-0",
"MPL-2.0",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
]


Expand Down
28 changes: 28 additions & 0 deletions examples/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! An example of opening an image.
extern crate image;
extern crate image_extras;

use std::env;
use std::error::Error;
use std::path::Path;

fn main() -> Result<(), Box<dyn Error>> {
image_extras::register();

let (from, into) = if env::args_os().count() == 3 {
(
env::args_os().nth(1).unwrap(),
env::args_os().nth(2).unwrap(),
)
} else {
println!("Please enter a from and into path.");
std::process::exit(1);
};

// Use the open function to load an image from a Path.
// ```open``` returns a dynamic image.
let im = image::open(Path::new(&from)).unwrap();
// Write the contents of this image using extension guessing.
im.save(Path::new(&into)).unwrap();
Ok(())
}
4 changes: 4 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ members = ["."]
[[bin]]
name = "fuzzer_script_pcx"
path = "fuzzers/fuzzer_script_pcx.rs"

[[bin]]
name = "fuzzer_script_ora"
path = "fuzzers/fuzzer_script_ora.rs"
24 changes: 24 additions & 0 deletions fuzz/fuzzers/fuzzer_script_ora.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;

use image::{ImageDecoder, Limits};
use std::io::Cursor;

fuzz_target!(|data: &[u8]| {
let reader = Cursor::new(data);
let Ok(mut decoder) =
image_extras::ora::OpenRasterDecoder::with_limits(reader, Limits::no_limits())
else {
return;
};
let mut limits = image::Limits::default();
limits.max_alloc = Some(1024 * 1024); // 1 MiB
if limits.reserve(decoder.total_bytes()).is_err() {
return;
}
if decoder.set_limits(limits).is_err() {
return;
}
let _ = std::hint::black_box(image::DynamicImage::from_decoder(decoder));
});
20 changes: 18 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@
//! // Now you can use the image crate as normal
//! let img = image::open("path/to/image.pcx").unwrap();
//! ```
use image::Limits;

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

#[cfg(feature = "ora")]
pub mod ora;

/// Register all enabled extra formats with the image crate.
pub fn register() {
let just_registered = image::hooks::register_decoding_hook(
let just_registered_pcx = image::hooks::register_decoding_hook(
"pcx".into(),
Box::new(|r| Ok(Box::new(pcx::PCXDecoder::new(r)?))),
);
if just_registered {
if just_registered_pcx {
image::hooks::register_format_detection_hook("pcx".into(), &[0x0a, 0x0], Some(b"\xFF\xF8"));
}

// OpenRaster images are ZIP files and have no simple signature to distinguish them
// from ZIP files containing other content
image::hooks::register_decoding_hook(
"ora".into(),
Box::new(|r| {
Ok(Box::new(ora::OpenRasterDecoder::with_limits(
r,
Limits::no_limits(),
)?))
}),
);
}
247 changes: 247 additions & 0 deletions src/ora.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
//! Decoding of OpenRaster Images (*.ora)
//!
//! OpenRaster is an a file format used to communicate layered images; the
//! decoder provided herein only extracts and displays the final merged raster
//! image cached by the OpenRaster file, and does not expose the details of
//! layers (which may be either raster or vector graphics) or render the merged
//! image itself.
//!
//! # Related Links
//! * <https://en.wikipedia.org/wiki/OpenRaster> - The OpenRaster format on Wikipedia
//! * <https://www.openraster.org/> - OpenRaster specification
#![forbid(unsafe_code)]
use image::codecs::png::PngDecoder;
use image::error::{DecodingError, ImageFormatHint, UnsupportedError};
use image::metadata::Orientation;
use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult, Limits};
use ouroboros::self_referencing;
use std::io::{self, BufReader, Read, Seek};
use std::marker::PhantomData;
use zip::read::{ZipArchive, ZipFile};

pub struct OpenRasterDecoder<'a, R>
where
R: Read + Seek + 'a,
{
mergedimg_decoder: PngDecoder<BufReader<SeekableArchiveFile<'a, R>>>,
}

fn openraster_format_hint() -> ImageFormatHint {
ImageFormatHint::Name("OpenRaster".into())
}

/// Adjust the format of the PngDecoder's errors to indicate OpenRaster instead
fn set_ora_image_type(err: ImageError) -> ImageError {
match err {
ImageError::Decoding(e) => {
// DecodingError does not directly expose the underlying type,
// so nest the error
ImageError::Decoding(DecodingError::new(openraster_format_hint(), e))
}
ImageError::Encoding(_) => {
// Should not be encoding any files
unreachable!();
}
ImageError::Parameter(e) => ImageError::Parameter(e),
ImageError::Limits(e) => ImageError::Limits(e),
ImageError::Unsupported(e) => ImageError::Unsupported(
UnsupportedError::from_format_and_kind(openraster_format_hint(), e.kind()),
),
ImageError::IoError(e) => ImageError::IoError(e),
}
}

#[self_referencing]
struct SeekableArchiveCore<'a, R: Read + Seek + 'a> {
archive: ZipArchive<R>,
#[covariant]
#[borrows(mut archive)]
file: ZipFile<'this, R>,
lifetime_helper: PhantomData<&'a R>,
}

/// The zip crate does not provide a seekable reader that works on compressed
/// entries, while png::Decoder requires the Seek bound (but does not currently
/// use it). This structure implements Seek by reopening and reading the zip
/// archive entry whenever it seeks backwards.
struct SeekableArchiveFile<'a, R: Read + Seek + 'a> {
core: Option<SeekableArchiveCore<'a, R>>,
file_index: usize,
position: u64,
file_size: u64,
}

impl<'a, R: Read + Seek + 'a> SeekableArchiveFile<'a, R> {
fn new(
archive: ZipArchive<R>,
file_index: usize,
) -> Result<SeekableArchiveFile<'a, R>, io::Error> {
let core = SeekableArchiveCore::try_new(archive, |x| x.by_index(file_index), PhantomData)
.map_err(|x| io::Error::other(format!("failed to open: {:?}", x)))?;
let file_size = core.with_file(|file| file.size());
Ok(SeekableArchiveFile {
core: Some(core),
file_index,
position: 0,
file_size,
})
}
}

impl<R: Read + Seek> Read for SeekableArchiveFile<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let res = self
.core
.as_mut()
.unwrap()
.with_file_mut(|file| file.read(buf));
let nread = res?;
self.position
.checked_add(nread as u64)
.ok_or_else(|| io::Error::other("seek position overflow"))?;
Ok(nread)
}
}

impl<R: Read + Seek> Seek for SeekableArchiveFile<'_, R> {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
let target_pos = match pos {
io::SeekFrom::Start(offset) => offset,
io::SeekFrom::End(offset) => self
.file_size
.checked_add_signed(offset)
.ok_or_else(|| io::Error::other("seek position over or underflow"))?,
io::SeekFrom::Current(offset) => self
.position
.checked_add_signed(offset)
.ok_or_else(|| io::Error::other("seek position over or underflow"))?,
};

if target_pos < self.position {
let core = self.core.take();
let archive = core.unwrap().into_heads().archive;

self.core = Some(
SeekableArchiveCore::try_new(archive, |x| x.by_index(self.file_index), PhantomData)
.map_err(|x| io::Error::other(format!("failed to reopen: {:?}", x)))?,
);
}
while self.position < target_pos {
const TMP_LEN: usize = 1024;
let mut tmp = [0_u8; TMP_LEN];
let cur_pos = self.position;
let nr = self
.read(&mut tmp[..std::cmp::min(TMP_LEN as u64, target_pos - cur_pos) as usize])?;
if nr == 0 {
return Err(io::Error::other("unexpected eof when seeking"));
}
self.position += nr as u64;
}

Ok(0)
}
}

impl<'a, R> OpenRasterDecoder<'a, R>
where
R: Read + Seek + 'a,
{
/// Create a new `OpenRasterDecoder` with the provided limits.
///
/// (Limits need to be specified in advance, because determining the
/// minimum information needed for the ImageDecoder trait (image size and
/// color type) may require reading through and remembering image-dependent
/// amount of data.)
///
/// Warning: While decoding limits apply to the header parsing and decoding
/// of the merged imaged component (a PNG file inside the ZIP archive that
/// forms an OpenRaster file), memory constraints on the ZIP file decoding
/// process have not yet been implemented; input ZIP files with very many
/// entries may require significant amounts of memory to read.
pub fn with_limits(r: R, limits: Limits) -> Result<OpenRasterDecoder<'a, R>, ImageError> {
let mut archive = ZipArchive::new(r)
.map_err(|e| ImageError::Decoding(DecodingError::new(openraster_format_hint(), e)))?;

/* Verify that this _is_ an OpenRaster file, and not some unrelated ZIP archive */
let mimetype_index = archive.index_for_name("mimetype").ok_or_else(|| {
ImageError::Decoding(DecodingError::new(
openraster_format_hint(),
"OpenRaster images should contain a mimetype subfile",
))
})?;

let mut mimetype_file = archive
.by_index(mimetype_index)
.map_err(|x| ImageError::Decoding(DecodingError::new(openraster_format_hint(), x)))?;

const EXPECTED_MIMETYPE: &str = "image/openraster";
let mut tmp = [0u8; EXPECTED_MIMETYPE.len()];

mimetype_file.read_exact(&mut tmp)?;

if tmp != EXPECTED_MIMETYPE.as_bytes()
|| mimetype_file.size() != EXPECTED_MIMETYPE.len() as u64
{
return Err(ImageError::Decoding(DecodingError::new(
openraster_format_hint(),
"Image did not have correct mimetype subentry to be identified as OpenRaster",
)));
}

drop(mimetype_file);

let mergedimage_index = archive.index_for_name("mergedimage.png").ok_or_else(|| {
ImageError::Decoding(DecodingError::new(
openraster_format_hint(),
"OpenRaster image missing mergedimage.png entry",
))
})?;

let file = SeekableArchiveFile::new(archive, mergedimage_index)?;
let decoder =
PngDecoder::with_limits(BufReader::new(file), limits).map_err(set_ora_image_type)?;

Ok(OpenRasterDecoder {
mergedimg_decoder: decoder,
})
}
}

impl<'a, R: Read + Seek + 'a> ImageDecoder for OpenRasterDecoder<'a, R> {
fn dimensions(&self) -> (u32, u32) {
self.mergedimg_decoder.dimensions()
}

fn color_type(&self) -> ColorType {
self.mergedimg_decoder.color_type()
}

fn original_color_type(&self) -> ExtendedColorType {
self.mergedimg_decoder.original_color_type()
}

fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
// Warning: this does not account for any ZIP reading overhead
self.mergedimg_decoder.set_limits(limits)
}

fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
self.mergedimg_decoder.icc_profile()
}

fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
self.mergedimg_decoder.exif_metadata()
}

fn orientation(&mut self) -> ImageResult<Orientation> {
self.mergedimg_decoder.orientation()
}

fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
self.mergedimg_decoder.read_image(buf)
}

fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
(*self).read_image(buf)
}
}
Binary file added tests/images/ora/layer.ora
Binary file not shown.
Binary file added tests/images/ora/layer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading