Skip to content

Commit

Permalink
Merge pull request #2364 from lupine/nt-pcx-support
Browse files Browse the repository at this point in the history
Add PCX decoder support
  • Loading branch information
HeroicKatora authored Oct 30, 2024
2 parents 277be64 + 25cede1 commit cf9c532
Show file tree
Hide file tree
Showing 23 changed files with 194 additions and 5 deletions.
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 }
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();

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");
}

0 comments on commit cf9c532

Please sign in to comment.