Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PCX decoder support #2364

Merged
merged 6 commits into from
Oct 30, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
features: ['', default, rayon, avif, bmp, dds, exr, ff, gif, hdr, ico, jpeg, png, pnm, qoi, tga, tiff, webp]
features: ['', default, rayon, avif, bmp, dds, exr, ff, gif, hdr, ico, jpeg, pcx, png, pnm, qoi, tga, tiff, webp]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ exr = { version = "1.5.0", optional = true }
gif = { version = "0.13", optional = true }
image-webp = { version = "0.2.0", optional = true }
mp4parse = { version = "0.17.0", optional = true }
pcx = { version = "0.2.3", optional = true }
lupine marked this conversation as resolved.
Show resolved Hide resolved
png = { version = "0.17.6", optional = true }
qoi = { version = "0.4", optional = true }
ravif = { version = "0.11.11", default-features = false, optional = true }
Expand Down Expand Up @@ -77,6 +78,7 @@ gif = ["dep:gif", "dep:color_quant"]
hdr = []
ico = ["bmp", "png"]
jpeg = ["dep:zune-core", "dep:zune-jpeg"]
pcx = ["dep:pcx"] # Note that the PCX dependency uses the WTFPL license
png = ["dep:png"]
pnm = []
qoi = ["dep:qoi"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ image format encoders and decoders.
| ICO | Yes | Yes |
| JPEG | Yes | Yes |
| EXR | Yes | Yes |
| PCX | Yes | --- |
| PNG | Yes | Yes |
| PNM | Yes | Yes |
| QOI | Yes | Yes |
Expand Down
4 changes: 4 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ allow = [
"Unicode-DFS-2016",
]

[[licenses.exceptions]]
allow = ["WTFPL"]
name = "pcx"
version = "*"

[advisories]
yanked = "deny"
Expand Down
157 changes: 157 additions & 0 deletions src/codecs/pcx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Decoding and Encoding of PCX Images
//!
//! PCX (PiCture eXchange) Format is an obsolete image format from the 1980s.
//!
//! # Related Links
//! * <https://en.wikipedia.org/wiki/PCX> - The PCX format on Wikipedia

extern crate pcx;

use std::io::{self, BufRead, Cursor, Read, Seek};
use std::iter;
use std::marker::PhantomData;
use std::mem;

use crate::color::{ColorType, ExtendedColorType};
use crate::error::{ImageError, ImageResult};
use crate::image::ImageDecoder;

/// Decoder for PCX images.
pub struct PCXDecoder<R>
where
R: BufRead + Seek,
{
dimensions: (u32, u32),
inner: pcx::Reader<R>,
}

impl<R> PCXDecoder<R>
where
R: BufRead + Seek,
{
/// Create a new `PCXDecoder`.
pub fn new(r: R) -> Result<PCXDecoder<R>, ImageError> {
let inner = pcx::Reader::new(r).map_err(ImageError::from_pcx_decode)?;
let dimensions = (u32::from(inner.width()), u32::from(inner.height()));

Ok(PCXDecoder { dimensions, inner })
}
}

impl ImageError {
fn from_pcx_decode(err: io::Error) -> ImageError {
ImageError::IoError(err)
}
}

/// Wrapper struct around a `Cursor<Vec<u8>>`
#[allow(dead_code)]
#[deprecated]
pub struct PCXReader<R>(Cursor<Vec<u8>>, PhantomData<R>);
#[allow(deprecated)]
impl<R> Read for PCXReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}

fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
if self.0.position() == 0 && buf.is_empty() {
mem::swap(buf, self.0.get_mut());
Ok(buf.len())
} else {
self.0.read_to_end(buf)
}
}
}

impl<R: BufRead + Seek> ImageDecoder for PCXDecoder<R> {
fn dimensions(&self) -> (u32, u32) {
self.dimensions
}

fn color_type(&self) -> ColorType {
ColorType::Rgb8
}

fn original_color_type(&self) -> ExtendedColorType {
if self.inner.is_paletted() {
return ExtendedColorType::Unknown(self.inner.header.bit_depth);
}

match (
self.inner.header.number_of_color_planes,
self.inner.header.bit_depth,
) {
(1, 1) => ExtendedColorType::L1,
(1, 2) => ExtendedColorType::L2,
(1, 4) => ExtendedColorType::L4,
(1, 8) => ExtendedColorType::L8,
(3, 1) => ExtendedColorType::Rgb1,
(3, 2) => ExtendedColorType::Rgb2,
(3, 4) => ExtendedColorType::Rgb4,
(3, 8) => ExtendedColorType::Rgb8,
(4, 1) => ExtendedColorType::Rgba1,
(4, 2) => ExtendedColorType::Rgba2,
(4, 4) => ExtendedColorType::Rgba4,
(4, 8) => ExtendedColorType::Rgba8,
(_, _) => unreachable!(),
}
}

fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes()));

let height = self.inner.height() as usize;
let width = self.inner.width() as usize;

match self.inner.palette_length() {
// No palette to interpret, so we can just write directly to buf
None => {
for i in 0..height {
let offset = i * 3 * width;
self.inner
.next_row_rgb(&mut buf[offset..offset + (width * 3)])
.map_err(ImageError::from_pcx_decode)?;
}
}

// We need to convert from the palette colours to RGB values inline,
// but the pcx crate can't give us the palette first. Work around it
// by taking the paletted image into a buffer, then converting it to
// RGB8 after.
Some(palette_length) => {
let mut pal_buf: Vec<u8> = iter::repeat(0).take(height * width).collect();
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved

for i in 0..height {
let offset = i * width;
self.inner
.next_row_paletted(&mut pal_buf[offset..offset + width])
.map_err(ImageError::from_pcx_decode)?;
}

let mut palette: Vec<u8> =
iter::repeat(0).take(3 * palette_length as usize).collect();
self.inner
.read_palette(&mut palette[..])
.map_err(ImageError::from_pcx_decode)?;

for i in 0..height {
for j in 0..width {
let pixel = pal_buf[i * width + j] as usize;
let offset = i * width * 3 + j * 3;

buf[offset] = palette[pixel * 3];
buf[offset + 1] = palette[pixel * 3 + 1];
buf[offset + 2] = palette[pixel * 3 + 2];
}
}
}
}

Ok(())
}

fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
(*self).read_image(buf)
}
}
15 changes: 14 additions & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub enum ImageFormat {

/// An Image in QOI Format
Qoi,

/// An Image in PCX Format
Pcx,
}

impl ImageFormat {
Expand Down Expand Up @@ -104,6 +107,7 @@ impl ImageFormat {
"pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm,
"ff" => ImageFormat::Farbfeld,
"qoi" => ImageFormat::Qoi,
"pcx" => ImageFormat::Pcx,
_ => return None,
})
}
Expand Down Expand Up @@ -179,6 +183,7 @@ impl ImageFormat {
// Qoi's MIME type is being worked on.
// See: https://github.com/phoboslab/qoi/issues/167
"image/x-qoi" => Some(ImageFormat::Qoi),
"image/vnd.zbrush.pcx" | "image/x-pcx" => Some(ImageFormat::Pcx),
_ => None,
}
}
Expand Down Expand Up @@ -226,6 +231,7 @@ impl ImageFormat {
ImageFormat::Qoi => "image/x-qoi",
// farbfeld's MIME type taken from https://www.wikidata.org/wiki/Q28206109
ImageFormat::Farbfeld => "application/octet-stream",
ImageFormat::Pcx => "image/vnd.zbrush.pcx",
}
}

Expand All @@ -250,6 +256,7 @@ impl ImageFormat {
ImageFormat::Farbfeld => true,
ImageFormat::Avif => true,
ImageFormat::Qoi => true,
ImageFormat::Pcx => true,
}
}

Expand All @@ -274,6 +281,7 @@ impl ImageFormat {
ImageFormat::OpenExr => true,
ImageFormat::Dds => false,
ImageFormat::Qoi => true,
ImageFormat::Pcx => false,
}
}

Expand Down Expand Up @@ -305,6 +313,7 @@ impl ImageFormat {
// According to: https://aomediacodec.github.io/av1-avif/#mime-registration
ImageFormat::Avif => &["avif"],
ImageFormat::Qoi => &["qoi"],
ImageFormat::Pcx => &["pcx"],
}
}

Expand All @@ -327,6 +336,7 @@ impl ImageFormat {
ImageFormat::Farbfeld => cfg!(feature = "ff"),
ImageFormat::Avif => cfg!(feature = "avif"),
ImageFormat::Qoi => cfg!(feature = "qoi"),
ImageFormat::Pcx => cfg!(feature = "pcx"),
ImageFormat::Dds => false,
}
}
Expand All @@ -350,6 +360,7 @@ impl ImageFormat {
ImageFormat::OpenExr => cfg!(feature = "exr"),
ImageFormat::Qoi => cfg!(feature = "qoi"),
ImageFormat::Hdr => cfg!(feature = "hdr"),
ImageFormat::Pcx => false,
ImageFormat::Dds => false,
}
}
Expand All @@ -372,6 +383,7 @@ impl ImageFormat {
ImageFormat::Qoi,
ImageFormat::Dds,
ImageFormat::Hdr,
ImageFormat::Pcx,
]
.iter()
.copied()
Expand Down Expand Up @@ -1647,6 +1659,7 @@ mod tests {
assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Pnm);
assert_eq!(from_path("./a.pgm").unwrap(), ImageFormat::Pnm);
assert_eq!(from_path("./a.AViF").unwrap(), ImageFormat::Avif);
assert_eq!(from_path("./a.PCX").unwrap(), ImageFormat::Pcx);
assert!(from_path("./a.txt").is_err());
assert!(from_path("./a").is_err());
}
Expand Down Expand Up @@ -1802,7 +1815,7 @@ mod tests {
fn image_formats_are_recognized() {
use ImageFormat::*;
const ALL_FORMATS: &[ImageFormat] = &[
Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld, OpenExr,
Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Dds, Bmp, Ico, Hdr, Farbfeld, OpenExr, Pcx,
];
for &format in ALL_FORMATS {
let mut file = Path::new("file.nothing").to_owned();
Expand Down
4 changes: 3 additions & 1 deletion src/image_reader/free_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub(crate) fn write_buffer_impl<W: std::io::Write + Seek>(
}
}

static MAGIC_BYTES: [(&[u8], ImageFormat); 23] = [
static MAGIC_BYTES: [(&[u8], ImageFormat); 25] = [
(b"\x89PNG\r\n\x1a\n", ImageFormat::Png),
(&[0xff, 0xd8, 0xff], ImageFormat::Jpeg),
(b"GIF89a", ImageFormat::Gif),
Expand All @@ -148,6 +148,8 @@ static MAGIC_BYTES: [(&[u8], ImageFormat); 23] = [
(b"\0\0\0\x1cftypavif", ImageFormat::Avif),
(&[0x76, 0x2f, 0x31, 0x01], ImageFormat::OpenExr), // = &exr::meta::magic_number::BYTES
(b"qoif", ImageFormat::Qoi),
(&[0x0a, 0x02], ImageFormat::Pcx),
(&[0x0a, 0x05], ImageFormat::Pcx),
];

/// Guess image format from memory block
Expand Down
2 changes: 2 additions & 0 deletions src/image_reader/image_reader_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
ImageFormat::Farbfeld => Box::new(farbfeld::FarbfeldDecoder::new(reader)?),
#[cfg(feature = "qoi")]
ImageFormat::Qoi => Box::new(qoi::QoiDecoder::new(reader)?),
#[cfg(feature = "pcx")]
ImageFormat::Pcx => Box::new(pcx::PCXDecoder::new(reader)?),
format => {
return Err(ImageError::Unsupported(
ImageFormatHint::Exact(format).into(),
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ pub mod flat;
/// | ICO | Yes | Yes |
/// | JPEG | Yes | Yes |
/// | EXR | Yes | Yes |
/// | PCX | Yes | --- |
/// | PNG | Yes | Yes |
/// | PNM | Yes | Yes |
/// | QOI | Yes | Yes |
Expand Down Expand Up @@ -263,6 +264,8 @@ pub mod codecs {
pub mod jpeg;
#[cfg(feature = "exr")]
pub mod openexr;
#[cfg(feature = "pcx")]
pub mod pcx;
#[cfg(feature = "png")]
pub mod png;
#[cfg(feature = "pnm")]
Expand Down
Binary file added tests/images/pcx/images/cga_bw.pcx
Binary file not shown.
Binary file added tests/images/pcx/images/cga_fsd.pcx
Binary file not shown.
Binary file added tests/images/pcx/images/cga_rgbi.pcx
Binary file not shown.
Binary file added tests/images/pcx/images/cga_tst1.pcx
Binary file not shown.
Binary file added tests/images/pcx/images/gmarbles.pcx
Binary file not shown.
Binary file added tests/images/pcx/images/marbles.pcx
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/reference_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ where
{
let base: PathBuf = BASE_PATH.iter().collect();
let decoders = &[
"tga", "tiff", "png", "gif", "bmp", "ico", "hdr", "pbm", "webp",
"tga", "tiff", "png", "gif", "bmp", "ico", "hdr", "pbm", "webp", "pcx",
];
for decoder in decoders {
let mut path = base.clone();
Expand Down
7 changes: 6 additions & 1 deletion tests/truncate_images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ where
{
let base: PathBuf = BASE_PATH.iter().collect();
let decoders = &[
"tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld", "exr",
"tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld", "exr", "pcx",
];
for decoder in decoders {
let mut path = base.clone();
Expand Down Expand Up @@ -99,3 +99,8 @@ fn truncate_farbfeld() {
fn truncate_exr() {
truncate_images("exr");
}

#[test]
fn truncate_pcx() {
truncate_images("pcx");
}
Loading