Skip to content

Add support for saving 16-bit images to pnm formats #2431

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
60 changes: 56 additions & 4 deletions src/codecs/pnm/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ impl<W: Write> PnmEncoder<W> {
}
}

/// Encode an image whose samples are represented as `u8`.
/// Encode an image whose samples are represented as a sequence of `u8` or `u16` data.
///
/// If `image` is a slice of `u8`, the samples will be interpreted based on the chosen `color` option.
/// Color types of 16-bit precision means that the bytes are reinterpreted as 16-bit samples,
/// otherwise they are treated as 8-bit samples.
/// If `image` is a slice of `u16`, the samples will be interpreted as 16-bit samples directly.
///
/// Some `pnm` subtypes are incompatible with some color options, a chosen header most
/// certainly with any deviation from the original decoded image.
Expand All @@ -150,13 +155,58 @@ impl<W: Write> PnmEncoder<W> {
S: Into<FlatSamples<'s>>,
{
let image = image.into();

// adapt samples so that they are aligned even in 16-bit samples,
// required due to the narrowing of the image buffer to &[u8]
// on dynamic image writing
let image = match (image, color) {
(
FlatSamples::U8(samples),
ExtendedColorType::L16
| ExtendedColorType::La16
| ExtendedColorType::Rgb16
| ExtendedColorType::Rgba16,
) => {
match bytemuck::try_cast_slice(samples) {
// proceed with aligned 16-bit samples
Ok(samples) => FlatSamples::U16(samples),
Err(_e) => {
// reallocation is required
let new_samples: Vec<u16> = samples
.chunks(2)
.map(|chunk| u16::from_ne_bytes([chunk[0], chunk[1]]))
.collect();

let image = FlatSamples::U16(&new_samples);

// make a separate encoding path,
// because the image buffer lifetime has changed
return self.encode_impl(image, width, height, color);
}
}
}
// should not be necessary for any other case
_ => image,
};

self.encode_impl(image, width, height, color)
}

/// Encode an image whose samples are already interpreted correctly.
fn encode_impl<'s>(
&mut self,
samples: FlatSamples<'s>,
width: u32,
height: u32,
color: ExtendedColorType,
) -> ImageResult<()> {
match self.header {
HeaderStrategy::Dynamic => self.write_dynamic_header(image, width, height, color),
HeaderStrategy::Dynamic => self.write_dynamic_header(samples, width, height, color),
HeaderStrategy::Subtype(subtype) => {
self.write_subtyped_header(subtype, image, width, height, color)
self.write_subtyped_header(subtype, samples, width, height, color)
}
HeaderStrategy::Chosen(ref header) => {
Self::write_with_header(&mut self.writer, header, image, width, height, color)
Self::write_with_header(&mut self.writer, header, samples, width, height, color)
}
}
}
Expand Down Expand Up @@ -417,7 +467,9 @@ impl<'a> CheckedDimensions<'a> {
(&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La8) => (),

(&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb8) => (),
(&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb16) => (),
(&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba8) => (),
(&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba16) => (),

(&None, _) if depth == components => (),
(&Some(ArbitraryTuplType::Custom(_)), _) if depth == components => (),
Expand Down
Binary file added tests/images/png/16bpc/basi0g16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/png/16bpc/basn2c16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions tests/save_pnm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Test saving images to PNM.
#![cfg(all(feature = "png", feature = "pnm"))]

extern crate image;
use std::fs;

use image::{GenericImageView as _, Luma, Rgb};

#[test]
fn save_16bit_to_pbm() {
let output_dir = "tests/output/pbm/images";
fs::create_dir_all(output_dir).expect("failed to create output directory");
let output_file = "tests/output/pbm/images/basi0g16.pbm";

let img = image::open("tests/images/png/16bpc/basi0g16.png").expect("failed to load image");
img.save(output_file).expect("failed to save image");

// inspect image written
let img = image::open(output_file).expect("failed to load saved image");
assert_eq!(img.color(), image::ColorType::L16);
assert_eq!(img.dimensions(), (32, 32));

let img = img.as_luma16().unwrap();

// inspect a few pixels
assert_eq!(*img.get_pixel(0, 0), Luma([0]));
assert_eq!(*img.get_pixel(31, 0), Luma([47871]));
assert_eq!(*img.get_pixel(22, 29), Luma([65535]));
}

#[test]
fn save_16bit_to_ppm() {
let output_dir = "tests/output/ppm/images";
fs::create_dir_all(output_dir).expect("failed to create output directory");
let output_file = "tests/output/ppm/images/basn2c16.ppm";

let img = image::open("tests/images/png/16bpc/basn2c16.png").expect("failed to load image");
img.save(output_file).expect("failed to save image");

// inspect image written
let img = image::open(output_file).expect("failed to load saved image");
assert_eq!(img.color(), image::ColorType::Rgb16);
assert_eq!(img.dimensions(), (32, 32));

let img = img.as_rgb16().unwrap();

// inspect a few pixels
assert_eq!(*img.get_pixel(0, 0), Rgb([65535, 65535, 0]));
assert_eq!(*img.get_pixel(31, 0), Rgb([0, 65535, 0]));
assert_eq!(*img.get_pixel(22, 29), Rgb([19026, 4228, 42281]));
}
Loading