mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 20:34:22 +00:00
Images: a quarter turn by the orientation swaps the resolution, and every TIFF follows the tag
Opening or placing an image whose orientation turns it a quarter (EXIF or TIFF Orientation 5 to 8) stored the pixels turned but kept the resolution as stored, so a photo at 300 x 150 ppi came in at the wrong width and height. XResolution counts pixels in the ImageWidth direction of the stored image (TIFF 6.0, https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf; EXIF uses the same tag, CIPA DC-008-2012, https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf), so a quarter turn now swaps the axes, in the size Open and Place use and in the stored image, with jxlread::oriented_resolution as JPEG XL does. Three kinds of TIFF did not follow their Orientation tag: - grayscale TIFFs with extra samples and 1-bit CCITT Group 3 TIFFs were not turned; - a CMYK TIFF with its inks together was turned but stored as RGB, losing its inks, and one with a plane per ink did not open ("Planar TIFF with CMYK color type is not supported"). They now turn as the other images do, and a CMYK TIFF keeps its inks, 8 bits each, written again with the turned resolution.
This commit is contained in:
@@ -133,6 +133,13 @@ pub(crate) fn tiff(bytes: &[u8]) -> Option<(tiff::ColorType, tiff::decoder::Deco
|
||||
Some((d.colortype().ok()?, d))
|
||||
}
|
||||
|
||||
/// The Orientation tag of TIFF `bytes`' first image, of any unsigned type; `None` for other images
|
||||
/// and TIFFs without one. Reads only its header.
|
||||
pub fn tiff_orientation(bytes: &[u8]) -> Option<u16> {
|
||||
let (_, mut d) = tiff(bytes)?;
|
||||
d.find_tag_unsigned::<u16>(tiff::tags::Tag::Orientation).ok().flatten()
|
||||
}
|
||||
|
||||
/// Does the TIFF `d` reads store one plane per channel (PlanarConfiguration 2)?
|
||||
fn planar(d: &mut tiff::decoder::Decoder<Cursor<&[u8]>>) -> bool {
|
||||
d.find_tag_unsigned::<u16>(tiff::tags::Tag::PlanarConfiguration).ok().flatten().is_some_and(|p| p != 1)
|
||||
@@ -299,7 +306,7 @@ fn jpeg_inks(bytes: &[u8]) -> Option<Inks> {
|
||||
|
||||
/// The ink amounts of a CMYK TIFF (16-bit samples to 8 bits), its inks stored together or one plane
|
||||
/// per ink.
|
||||
fn tiff_inks(bytes: &[u8]) -> Option<Inks> {
|
||||
pub fn tiff_inks(bytes: &[u8]) -> Option<Inks> {
|
||||
let (kind, mut d) = tiff(bytes)?;
|
||||
let (w, h) = d.dimensions().ok()?;
|
||||
if !matches!(kind, tiff::ColorType::CMYK(8 | 16)) {
|
||||
|
||||
@@ -814,6 +814,9 @@ pub(crate) fn size_note(bytes: &[u8]) -> Option<String> {
|
||||
/// A TIFF with more strips or tiles than the decoder can count
|
||||
/// ([`vectorcraft_doc::cmyk::oversized_tiff`]) is refused as too large to draw. JPEG XL images are
|
||||
/// read with [`jxlread`] and stored as PNG, with the ICC profile their colors are left in.
|
||||
/// A PNG, JPEG, GIF, WebP or TIFF whose orientation ([`exif_orientation`]) turns or flips it is
|
||||
/// stored as it is shown, with its resolution's axes swapped when it turns a quarter; a CMYK TIFF
|
||||
/// keeps its inks, 8 bits each ([`turned_cmyk_tiff`]).
|
||||
pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
let ppi = super::ppi::resolution(bytes);
|
||||
if let Some((w, h)) = vectorcraft_doc::cmyk::oversized_tiff(bytes) {
|
||||
@@ -852,7 +855,15 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
let f = image_format(kind).ok_or_else(|| err(format!("{kind:?} images can't be opened (see document.formats)")))?;
|
||||
// A photo whose EXIF orientation turns or flips it (a camera held upright) is stored as it is
|
||||
// seen, so it places the way it was taken (#4284); the others keep their own bytes.
|
||||
let turned = matches!(f.id, "png" | "jpg" | "gif" | "webp" | "tiff") && exif_orientation(bytes).is_some();
|
||||
let orientation = matches!(f.id, "png" | "jpg" | "gif" | "webp" | "tiff").then(|| exif_orientation(bytes)).flatten();
|
||||
let turned = orientation.is_some();
|
||||
// The resolution describes the stored pixels; a quarter turn swaps its axes.
|
||||
let shown_ppi = jxlread::oriented_resolution(ppi, orientation.map_or(1, |o| u32::from(o.to_exif())));
|
||||
if let Some(orientation) = orientation.filter(|_| f.id == "tiff")
|
||||
&& let Some(stored) = turned_cmyk_tiff(bytes, orientation, shown_ppi)
|
||||
{
|
||||
return stored;
|
||||
}
|
||||
let kept = match f.id {
|
||||
_ if turned => None,
|
||||
"png" | "jpg" | "gif" | "webp" => Some(bytes.to_vec()),
|
||||
@@ -868,36 +879,53 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
vectorcraft_doc::cmyk::extra_samples_tiff_rgba(bytes, |c| cms.cmyk_to_srgb(c, false))
|
||||
};
|
||||
let mut img = match (f.id == "tiff").then(extra_samples).flatten() {
|
||||
Some(img) => img,
|
||||
Some(img) => image::DynamicImage::ImageRgba8(img),
|
||||
None => match (f.id == "tiff").then(|| vectorcraft_doc::cmyk::group3_tiff_rgba(bytes)).flatten() {
|
||||
Some(read) => read.map_err(err)?,
|
||||
Some(read) => image::DynamicImage::ImageRgba8(read.map_err(err)?),
|
||||
None => {
|
||||
use image::ImageDecoder as _;
|
||||
let mut decoder = reader.into_decoder().map_err(err)?;
|
||||
let decoder = reader.into_decoder().map_err(err)?;
|
||||
// The allowance counts the decoded samples and the RGBA pixels they become (#1717).
|
||||
let (w, h) = decoder.dimensions();
|
||||
if decoder.total_bytes() > MAX_RASTER_ALLOC || u64::from(w) * u64::from(h) * 4 > MAX_RASTER_ALLOC {
|
||||
return Err(err(format!("an image of {w} × {h} pixels is too large to draw")));
|
||||
}
|
||||
let orientation = decoder.orientation().unwrap_or(image::metadata::Orientation::NoTransforms);
|
||||
let mut img = image::DynamicImage::from_decoder(decoder).map_err(err)?;
|
||||
img.apply_orientation(orientation);
|
||||
img.to_rgba8()
|
||||
image::DynamicImage::from_decoder(decoder).map_err(err)?
|
||||
}
|
||||
},
|
||||
};
|
||||
img.apply_orientation(orientation.unwrap_or(image::metadata::Orientation::NoTransforms));
|
||||
let mut img = img.into_rgba8();
|
||||
if f.id == "tga" {
|
||||
super::tgaread::fix_alpha(bytes, &mut img);
|
||||
}
|
||||
as_png(img, None, ppi)
|
||||
as_png(img, None, shown_ppi)
|
||||
}
|
||||
|
||||
/// The EXIF orientation of encoded image `bytes` when it turns or flips the pixels; `None` when
|
||||
/// it leaves them as stored, or the image has none.
|
||||
/// it leaves them as stored, or the image has none. A TIFF the image decoder can't open (one with
|
||||
/// extra samples) has its Orientation tag read from its first image
|
||||
/// ([`vectorcraft_doc::cmyk::tiff_orientation`]).
|
||||
pub(crate) fn exif_orientation(bytes: &[u8]) -> Option<image::metadata::Orientation> {
|
||||
use image::ImageDecoder as _;
|
||||
let mut decoder = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().ok()?.into_decoder().ok()?;
|
||||
decoder.orientation().ok().filter(|o| *o != image::metadata::Orientation::NoTransforms)
|
||||
use image::metadata::Orientation;
|
||||
let decoded = || image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().ok()?.into_decoder().ok()?.orientation().ok();
|
||||
let tagged = || vectorcraft_doc::cmyk::tiff_orientation(bytes).and_then(|v| Orientation::from_exif(u8::try_from(v).ok()?));
|
||||
decoded().or_else(tagged).filter(|o| *o != Orientation::NoTransforms)
|
||||
}
|
||||
|
||||
/// CMYK TIFF `bytes` turned or flipped by their `orientation`, as [`raster_image`] stores them:
|
||||
/// their ink amounts ([`vectorcraft_doc::cmyk::tiff_inks`], 8 bits each) turned and written again
|
||||
/// by [`ImageBlob::cmyk_tiff_at`] with the resolution `ppi` of the turned pixels. `None` for other
|
||||
/// images and for CMYK TIFFs whose inks it doesn't read.
|
||||
fn turned_cmyk_tiff(bytes: &[u8], orientation: image::metadata::Orientation, ppi: Option<(f64, f64)>) -> Option<Result<RasterImage>> {
|
||||
let inks = vectorcraft_doc::cmyk::tiff_inks(bytes)?;
|
||||
// Four 8-bit inks to a pixel, turned as four 8-bit channels are.
|
||||
let mut px = image::DynamicImage::ImageRgba8(image::RgbaImage::from_raw(inks.width, inks.height, inks.data)?);
|
||||
px.apply_orientation(orientation);
|
||||
let inks = vectorcraft_doc::cmyk::Inks::new(px.width(), px.height(), px.into_bytes())?;
|
||||
let tiff = ImageBlob::cmyk_tiff_at(&inks, ppi)?;
|
||||
Some(stored(std::sync::Arc::unwrap_or_clone(tiff.bytes), "image/tiff", (inks.width, inks.height), ppi))
|
||||
}
|
||||
|
||||
/// `img` stored as a PNG that keeps the file's resolution and carries `icc`, the ICC profile its
|
||||
|
||||
@@ -907,6 +907,213 @@ fn a_photo_places_the_way_its_exif_orientation_says() {
|
||||
assert!(red(px.get_pixel(4, 3)) && !red(px.get_pixel(4, 12)), "the left half is on top");
|
||||
}
|
||||
|
||||
/// Where the first stored pixel shows once EXIF (or TIFF) `orientation` turns the image, as
|
||||
/// (right, bottom): orientations 1 and 5 keep it at the top left, 2 and 6 put it at the top right,
|
||||
/// 3 and 7 at the bottom right, 4 and 8 at the bottom left. Values outside 1 to 8 leave it.
|
||||
fn first_pixel_at(orientation: u16) -> (bool, bool) {
|
||||
match orientation {
|
||||
2 | 6 => (true, false),
|
||||
3 | 7 => (true, true),
|
||||
4 | 8 => (false, true),
|
||||
_ => (false, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// A little-endian EXIF block (a TIFF header and directory, as a PNG `eXIf` chunk or a WebP
|
||||
/// `EXIF` chunk holds it) with an Orientation of `orientation` and an XResolution and YResolution
|
||||
/// of `x` and `y` pixels per inch.
|
||||
fn exif_block(orientation: u16, (x, y): (u32, u32)) -> Vec<u8> {
|
||||
let entry = |tag: u16, kind: u16, value: u32| [&tag.to_le_bytes()[..], &kind.to_le_bytes(), &1u32.to_le_bytes(), &value.to_le_bytes()].concat();
|
||||
// Orientation, XResolution, YResolution, ResolutionUnit inches; the two rationals follow.
|
||||
[
|
||||
b"II".as_slice(),
|
||||
&[0x2a, 0, 8, 0, 0, 0],
|
||||
&4u16.to_le_bytes(),
|
||||
&entry(0x0112, 3, u32::from(orientation)),
|
||||
&entry(0x011a, 5, 62),
|
||||
&entry(0x011b, 5, 70),
|
||||
&entry(0x0128, 3, 2),
|
||||
&[0, 0, 0, 0],
|
||||
&[x.to_le_bytes(), 1u32.to_le_bytes(), y.to_le_bytes(), 1u32.to_le_bytes()].concat(),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// A photo turned a quarter by its EXIF orientation keeps its physical size: the resolution's
|
||||
/// axes swap with the pixels', in what Place reports and in the stored image. Every orientation
|
||||
/// puts the first stored pixel in its corner once, and the stored image, read again, is not turned
|
||||
/// a second time. Values outside 1 to 8 leave the photo as stored.
|
||||
#[test]
|
||||
fn a_quarter_turn_swaps_the_resolution_axes() {
|
||||
// 48 × 32: the top-left 16 × 16 (one whole JPEG MCU) red, the rest white.
|
||||
let rgb = image::RgbImage::from_fn(48, 32, |x, y| if x < 16 && y < 16 { image::Rgb([220, 0, 0]) } else { image::Rgb([255, 255, 255]) });
|
||||
let mut jpg = Vec::new();
|
||||
image::DynamicImage::ImageRgb8(rgb).write_to(&mut Cursor::new(&mut jpg), image::ImageFormat::Jpeg).unwrap();
|
||||
for orientation in [0u16, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
|
||||
let exif = [b"Exif\0\0".as_slice(), &exif_block(orientation, (300, 150))].concat();
|
||||
// Without a JFIF density the resolution comes from the EXIF block.
|
||||
let mut turned = jpg[..2].to_vec();
|
||||
turned.extend([0xff, 0xe1]);
|
||||
turned.extend(u16::try_from(exif.len() + 2).unwrap().to_be_bytes());
|
||||
turned.extend(&exif);
|
||||
turned.extend(&jpg[2..]);
|
||||
let what = format!("orientation {orientation}");
|
||||
assert_eq!(super::ppi::resolution(&turned), Some((300.0, 150.0)), "{what}: as stored");
|
||||
let quarter = (5..=8).contains(&orientation);
|
||||
let (size, ppi) = if quarter { ((32, 48), (150.0, 300.0)) } else { ((48, 32), (300.0, 150.0)) };
|
||||
let img = raster_image(&turned).unwrap();
|
||||
assert_eq!(((img.width, img.height), img.ppi), (size, Some(ppi)), "{what}");
|
||||
assert_eq!(super::ppi::resolution(&img.blob.bytes), Some(ppi), "{what}: the stored image");
|
||||
let px = image::load_from_memory(&img.blob.bytes).unwrap().to_rgb8();
|
||||
let red = |x: u32, y: u32| px.get_pixel(x, y)[1] < 80;
|
||||
let (w, h) = size;
|
||||
let corners = [(false, false), (true, false), (true, true), (false, true)];
|
||||
let reds: Vec<_> =
|
||||
corners.into_iter().filter(|&(right, bottom)| red(if right { w - 4 } else { 4 }, if bottom { h - 4 } else { 4 })).collect();
|
||||
assert_eq!(reds, [first_pixel_at(orientation)], "{what}: the first stored pixel's corner");
|
||||
let again = raster_image(&img.blob.bytes).unwrap();
|
||||
assert_eq!(((again.width, again.height), again.ppi), (size, Some(ppi)), "{what}: read again, it isn't turned again");
|
||||
}
|
||||
}
|
||||
|
||||
/// `tiff` (little-endian) with an Orientation tag (274) of `value`, a SHORT or, when `long`, a
|
||||
/// LONG, and, when `y_ppi` is given, a YResolution (283, which `tiff` must have) of that many
|
||||
/// pixels per inch: its first directory copied to the end of the file with the tag added in order,
|
||||
/// and the header pointing there, so that every other offset in the file still holds.
|
||||
fn tiff_oriented(tiff: &[u8], value: u16, long: bool, y_ppi: Option<u32>) -> Vec<u8> {
|
||||
let at = usize::try_from(u32::from_le_bytes(tiff[4..8].try_into().unwrap())).unwrap();
|
||||
let n = usize::from(u16::from_le_bytes([tiff[at], tiff[at + 1]]));
|
||||
let mut entries: Vec<Vec<u8>> = tiff[at + 2..at + 2 + 12 * n].chunks(12).map(<[u8]>::to_vec).collect();
|
||||
let i = entries.iter().position(|e| u16::from_le_bytes([e[0], e[1]]) > 274).unwrap_or(n);
|
||||
let (kind, v) = if long { (4u16, u32::from(value).to_le_bytes()) } else { (3, [value.to_le_bytes(), [0, 0]].concat().try_into().unwrap()) };
|
||||
entries.insert(i, [&274u16.to_le_bytes()[..], &kind.to_le_bytes(), &1u32.to_le_bytes(), &v].concat());
|
||||
let mut out = tiff.to_vec();
|
||||
if out.len() % 2 == 1 {
|
||||
out.push(0);
|
||||
}
|
||||
if let Some(y) = y_ppi {
|
||||
let rational = u32::try_from(out.len()).unwrap();
|
||||
out.extend([y.to_le_bytes(), 1u32.to_le_bytes()].concat());
|
||||
let e = entries.iter_mut().find(|e| u16::from_le_bytes([e[0], e[1]]) == 283).unwrap();
|
||||
e[8..12].copy_from_slice(&rational.to_le_bytes());
|
||||
}
|
||||
let ifd = u32::try_from(out.len()).unwrap();
|
||||
out[4..8].copy_from_slice(&ifd.to_le_bytes());
|
||||
out.extend(u16::try_from(n + 1).unwrap().to_le_bytes());
|
||||
out.extend(entries.concat());
|
||||
out.extend([0; 4]);
|
||||
out
|
||||
}
|
||||
|
||||
/// TIFFs the image decoder can't read, grayscale with an alpha channel and 1-bit ones compressed
|
||||
/// with CCITT Group 3, turn and flip as their Orientation tag says, as the others do (#4284),
|
||||
/// whether the tag is a SHORT or a LONG. A quarter turn swaps the resolution's axes.
|
||||
#[test]
|
||||
fn tiffs_read_without_the_image_decoder_follow_their_orientation() {
|
||||
// 3 × 2 at 150 × 300 ppi, the first pixel black and the rest white, opaque.
|
||||
let gray = hand_tiff(1, 8, &[2], false, &[vec![0, 255, 255, 255, 255, 255], vec![255; 6]]);
|
||||
let rows: Vec<Vec<bool>> = (0..2).map(|y| (0..3).map(|x| x == 0 && y == 0).collect()).collect();
|
||||
let g = vectorcraft_testkit::tiff::Group3 {
|
||||
rows: &rows,
|
||||
compression: 2,
|
||||
t4_options: 0,
|
||||
photometric: 0,
|
||||
fill_order: 1,
|
||||
rows_per_strip: 2,
|
||||
tile: None,
|
||||
rtc: false,
|
||||
};
|
||||
let fax = vectorcraft_testkit::tiff::group3(&g);
|
||||
// The Group 3 file declares no resolution.
|
||||
for (kind, file, y_ppi) in [("Group 3", fax, None), ("gray with alpha", gray, Some(300))] {
|
||||
assert!(image::load_from_memory(&file).is_err(), "{kind}: the image decoder reads it now");
|
||||
for (orientation, long) in (1..=8).flat_map(|o| [(o, false), (o, true)]) {
|
||||
let what = format!("{kind}, orientation {orientation}, LONG {long}");
|
||||
let img = raster_image(&tiff_oriented(&file, orientation, long, y_ppi)).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
let quarter = orientation >= 5;
|
||||
let size = if quarter { (2, 3) } else { (3, 2) };
|
||||
let ppi = y_ppi.map(|y| if quarter { (f64::from(y), 150.0) } else { (150.0, f64::from(y)) });
|
||||
assert_eq!(((img.width, img.height), img.ppi), (size, ppi), "{what}");
|
||||
assert_eq!(super::ppi::resolution(&img.blob.bytes), ppi, "{what}: the stored image");
|
||||
let px = image::load_from_memory(&img.blob.bytes).unwrap().to_luma8();
|
||||
let (right, bottom) = first_pixel_at(orientation);
|
||||
let (x, y) = (if right { size.0 - 1 } else { 0 }, if bottom { size.1 - 1 } else { 0 });
|
||||
let black: Vec<_> = px.enumerate_pixels().filter(|p| p.2[0] < 128).map(|p| (p.0, p.1)).collect();
|
||||
assert_eq!(black, [(x, y)], "{what}: the first stored pixel's corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A CMYK TIFF that its Orientation tag turns or flips keeps its inks, turned, whether it stores
|
||||
/// them together or one plane per ink, at 8 or 16 bits; a quarter turn swaps the resolution's axes.
|
||||
/// Read again, the stored image is not turned a second time.
|
||||
#[test]
|
||||
fn cmyk_tiffs_turned_by_their_orientation_keep_their_inks() {
|
||||
// 3 × 2 at 150 × 300 ppi: the first pixel full cyan, the others 30% black.
|
||||
let first: [u8; 4] = [255, 0, 0, 0];
|
||||
let other: [u8; 4] = [0, 0, 0, 77];
|
||||
let channel = |i: usize| -> Vec<u16> { (0..6).map(|p| u16::from(if p == 0 { first[i] } else { other[i] })).collect() };
|
||||
for (bits, planar) in [(8, false), (8, true), (16, false), (16, true)] {
|
||||
let channels: Vec<Vec<u16>> = (0..4).map(|i| channel(i).iter().map(|v| if bits == 16 { v * 257 } else { *v }).collect()).collect();
|
||||
let file = hand_tiff(5, bits, &[], planar, &channels);
|
||||
for orientation in 1..=8 {
|
||||
let what = format!("{bits} bits, planar {planar}, orientation {orientation}");
|
||||
let img = raster_image(&tiff_oriented(&file, orientation, false, Some(300))).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
let quarter = orientation >= 5;
|
||||
let size = if quarter { (2, 3) } else { (3, 2) };
|
||||
let ppi = if quarter { (300.0, 150.0) } else { (150.0, 300.0) };
|
||||
assert_eq!(((img.width, img.height), img.ppi, img.blob.mime.as_str()), (size, Some(ppi), "image/tiff"), "{what}");
|
||||
assert_eq!(super::ppi::resolution(&img.blob.bytes), Some(ppi), "{what}: the stored image");
|
||||
let inks = img.blob.cmyk().unwrap_or_else(|| panic!("{what}: the inks are lost"));
|
||||
assert_eq!((inks.width, inks.height), size, "{what}: the inks' size");
|
||||
let (right, bottom) = first_pixel_at(orientation);
|
||||
let at = (if right { size.0 - 1 } else { 0 }, if bottom { size.1 - 1 } else { 0 });
|
||||
for (i, px) in inks.data.chunks(4).enumerate() {
|
||||
let (x, y) = (u32::try_from(i).unwrap() % size.0, u32::try_from(i).unwrap() / size.0);
|
||||
assert_eq!(px, if (x, y) == at { first } else { other }, "{what}: the inks of pixel ({x}, {y})");
|
||||
}
|
||||
let again = raster_image(&img.blob.bytes).unwrap();
|
||||
assert_eq!(((again.width, again.height), again.blob.cmyk()), (size, Some(inks)), "{what}: read again, it isn't turned again");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PNG (its `eXIf` chunk and `pHYs` resolution), a WebP (its `EXIF` chunk) and an RGB TIFF read
|
||||
/// by the image decoder, turned a quarter by their orientation, swap their resolution's axes; a
|
||||
/// half turn keeps them.
|
||||
#[test]
|
||||
fn png_webp_and_rgb_tiff_turned_a_quarter_swap_their_resolution() {
|
||||
use image::ImageEncoder as _;
|
||||
let rgb = image::RgbImage::from_fn(3, 2, |x, y| image::Rgb(if x == 0 && y == 0 { [0, 0, 0] } else { [255, 255, 255] }));
|
||||
for orientation in [3u16, 6, 8] {
|
||||
let mut png = Vec::new();
|
||||
let mut enc = image::codecs::png::PngEncoder::new(&mut png);
|
||||
enc.set_exif_metadata(exif_block(orientation, (150, 300))).unwrap();
|
||||
enc.write_image(rgb.as_raw(), 3, 2, image::ExtendedColorType::Rgb8).unwrap();
|
||||
let png = super::ppi::with_png_resolution(&png, (150.0, 300.0));
|
||||
let mut webp = Vec::new();
|
||||
let mut enc = image::codecs::webp::WebPEncoder::new_lossless(&mut webp);
|
||||
enc.set_exif_metadata(exif_block(orientation, (150, 300))).unwrap();
|
||||
enc.write_image(rgb.as_raw(), 3, 2, image::ExtendedColorType::Rgb8).unwrap();
|
||||
let channel = |c: usize| -> Vec<u16> { rgb.pixels().map(|p| u16::from(p[c])).collect() };
|
||||
let tiff = tiff_oriented(&hand_tiff(2, 8, &[], false, &[channel(0), channel(1), channel(2)]), orientation, false, Some(300));
|
||||
for (kind, file) in [("PNG", png), ("WebP", webp), ("TIFF", tiff)] {
|
||||
let what = format!("{kind}, orientation {orientation}");
|
||||
assert_eq!(super::ppi::resolution(&file), Some((150.0, 300.0)), "{what}: as stored");
|
||||
let quarter = orientation >= 5;
|
||||
let (size, ppi) = if quarter { ((2, 3), (300.0, 150.0)) } else { ((3, 2), (150.0, 300.0)) };
|
||||
let img = raster_image(&file).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert_eq!(((img.width, img.height), img.ppi), (size, Some(ppi)), "{what}");
|
||||
assert_eq!(super::ppi::resolution(&img.blob.bytes), Some(ppi), "{what}: the stored image");
|
||||
let px = image::load_from_memory(&img.blob.bytes).unwrap().to_luma8();
|
||||
let (right, bottom) = first_pixel_at(orientation);
|
||||
let at = (if right { size.0 - 1 } else { 0 }, if bottom { size.1 - 1 } else { 0 });
|
||||
let black: Vec<_> = px.enumerate_pixels().filter(|p| p.2[0] < 128).map(|p| (p.0, p.1)).collect();
|
||||
assert_eq!(black, [at], "{what}: the first stored pixel's corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// With `importer` named, a file up to the importer's limit is read whole and a larger one is an
|
||||
/// error, whatever its content.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user