Skip to content

Commit

Permalink
pnm: Scale samples according to specified maximum
Browse files Browse the repository at this point in the history
Some formats allow to specify a maximum value for samples.
Before, samples were not scaled respective to this
white point, decoding some images incorrectly dimmed.
  • Loading branch information
sophie-h committed Nov 20, 2023
1 parent d4748e2 commit a4869fd
Showing 1 changed file with 66 additions and 14 deletions.
80 changes: 66 additions & 14 deletions src/codecs/pnm/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ enum DecoderError {
UnexpectedByteInRaster(u8),
/// Specified sample was out of bounds (e.g. >1 in B&W)
SampleOutOfBounds(u8),
/// The image's maxval is zero
MaxvalZero,
/// The image's maxval exceeds 0xFFFF
MaxvalTooBig(u32),

Expand Down Expand Up @@ -133,6 +135,7 @@ impl Display for DecoderError {
DecoderError::SampleOutOfBounds(val) => {
f.write_fmt(format_args!("Sample value {} outside of bounds", val))
}
DecoderError::MaxvalZero => f.write_str("Image MAXVAL is zero"),
DecoderError::MaxvalTooBig(maxval) => {
f.write_fmt(format_args!("Image MAXVAL exceeds {}: {}", 0xFFFF, maxval))
}
Expand Down Expand Up @@ -232,7 +235,15 @@ enum TupleType {
}

trait Sample {
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize>;
type Representation;

/// Representation size in bytes
fn sample_size() -> u32 {
std::mem::size_of::<Self::Representation>() as u32
}
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
Ok((width * height * samples * Self::sample_size()) as usize)
}
fn from_bytes(bytes: &[u8], row_size: usize, output_buf: &mut [u8]) -> ImageResult<()>;
fn from_ascii(reader: &mut dyn Read, output_buf: &mut [u8]) -> ImageResult<()>;
}
Expand Down Expand Up @@ -659,10 +670,33 @@ impl<R: Read> PnmDecoder<R> {
.checked_mul(components)
.ok_or(DecoderError::Overflow)?;

S::from_bytes(&bytes, row_size, buf)
S::from_bytes(&bytes, row_size, buf)?;
}
SampleEncoding::Ascii => {
self.read_ascii::<S>(buf)?;
}
};

// Scale samples if 8bit or 16bit is not saturated
let current_sample_max = self.header.maximal_sample();
let target_sample_max = 256_u32.pow(S::sample_size()) - 1;

if current_sample_max != target_sample_max {
let factor = target_sample_max as f32 / current_sample_max as f32;

if S::sample_size() == 1 {
buf.iter_mut().for_each(|v| {
*v = (*v as f32 * factor).round() as u8;
})
} else if S::sample_size() == 2 {
for chunk in buf.chunks_exact_mut(2) {
let v = NativeEndian::read_u16(chunk);
NativeEndian::write_u16(chunk, (v as f32 * factor).round() as u16);
}
}
SampleEncoding::Ascii => self.read_ascii::<S>(buf),
}

Ok(())
}

fn read_ascii<Basic: Sample>(&mut self, output_buf: &mut [u8]) -> ImageResult<()> {
Expand Down Expand Up @@ -701,9 +735,7 @@ where
}

impl Sample for U8 {
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
Ok((width * height * samples) as usize)
}
type Representation = u8;

fn from_bytes(bytes: &[u8], _row_size: usize, output_buf: &mut [u8]) -> ImageResult<()> {
output_buf.copy_from_slice(bytes);
Expand All @@ -719,9 +751,7 @@ impl Sample for U8 {
}

impl Sample for U16 {
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
Ok((width * height * samples * 2) as usize)
}
type Representation = u16;

fn from_bytes(bytes: &[u8], _row_size: usize, output_buf: &mut [u8]) -> ImageResult<()> {
output_buf.copy_from_slice(bytes);
Expand All @@ -745,6 +775,8 @@ impl Sample for U16 {
// be ignored. Also, contrary to rgb, black pixels are encoded as a 1 while white is 0. This will
// need to be reversed for the grayscale output.
impl Sample for PbmBit {
type Representation = u8;

fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
let count = width * samples;
let linelen = (count / 8) + ((count % 8) != 0) as u32;
Expand Down Expand Up @@ -783,9 +815,7 @@ impl Sample for PbmBit {

// Encoded just like a normal U8 but we check the values.
impl Sample for BWBit {
fn bytelen(width: u32, height: u32, samples: u32) -> ImageResult<usize> {
U8::bytelen(width, height, samples)
}
type Representation = u8;

fn from_bytes(bytes: &[u8], row_size: usize, output_buf: &mut [u8]) -> ImageResult<()> {
U8::from_bytes(bytes, row_size, output_buf)?;
Expand All @@ -809,6 +839,7 @@ impl DecodableImageHeader for BitmapHeader {
impl DecodableImageHeader for GraymapHeader {
fn tuple_type(&self) -> ImageResult<TupleType> {
match self.maxwhite {
v if v == 0 => Err(DecoderError::MaxvalZero.into()),
v if v <= 0xFF => Ok(TupleType::GrayU8),
v if v <= 0xFFFF => Ok(TupleType::GrayU16),
_ => Err(DecoderError::MaxvalTooBig(self.maxwhite).into()),
Expand All @@ -819,6 +850,7 @@ impl DecodableImageHeader for GraymapHeader {
impl DecodableImageHeader for PixmapHeader {
fn tuple_type(&self) -> ImageResult<TupleType> {
match self.maxval {
v if v == 0 => Err(DecoderError::MaxvalZero.into()),
v if v <= 0xFF => Ok(TupleType::RGBU8),
v if v <= 0xFFFF => Ok(TupleType::RGBU16),
_ => Err(DecoderError::MaxvalTooBig(self.maxval).into()),
Expand All @@ -829,6 +861,7 @@ impl DecodableImageHeader for PixmapHeader {
impl DecodableImageHeader for ArbitraryHeader {
fn tuple_type(&self) -> ImageResult<TupleType> {
match self.tupltype {
_ if self.maxval == 0 => Err(DecoderError::MaxvalZero.into()),
None if self.depth == 1 => Ok(TupleType::GrayU8),
None if self.depth == 2 => Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
Expand Down Expand Up @@ -937,8 +970,8 @@ ENDHDR
assert_eq!(
image,
vec![
0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00,
0x00, 0x01
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0xFF
]
);
match PnmDecoder::new(&pamdata[..]).unwrap().into_inner() {
Expand Down Expand Up @@ -1238,6 +1271,25 @@ ENDHDR
}
}

#[test]
fn ppm_ascii() {
let ascii = b"P3 1 1 2000\n0 1000 2000";
let decoder = PnmDecoder::new(&ascii[..]).unwrap();
let mut image = vec![0; decoder.total_bytes() as usize];
decoder.read_image(&mut image).unwrap();
assert_eq!(
image,
[
0_u16.to_ne_bytes(),
(u16::MAX / 2 + 1).to_ne_bytes(),
u16::MAX.to_ne_bytes()
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
);
}

#[test]
fn dimension_overflow() {
let pamdata = b"P7
Expand Down

0 comments on commit a4869fd

Please sign in to comment.