mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Place: a photo's EXIF orientation turns it upright (#4284)
This commit is contained in:
@@ -602,7 +602,11 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
reader.limits(limits);
|
||||
let kind = reader.format().ok_or_else(|| err("not an image VectorCraft reads (see document.formats)"))?;
|
||||
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 kept = match f.id {
|
||||
_ if turned => None,
|
||||
"png" | "jpg" | "gif" | "webp" => Some(bytes.to_vec()),
|
||||
"tiff" => vectorcraft_doc::cmyk::stored_cmyk_tiff(bytes, ppi),
|
||||
_ => None,
|
||||
@@ -621,13 +625,16 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
Some(read) => read.map_err(err)?,
|
||||
None => {
|
||||
use image::ImageDecoder as _;
|
||||
let decoder = reader.into_decoder().map_err(err)?;
|
||||
let mut 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")));
|
||||
}
|
||||
image::DynamicImage::from_decoder(decoder).map_err(err)?.to_rgba8()
|
||||
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()
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -637,6 +644,14 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
as_png(img, 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.
|
||||
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)
|
||||
}
|
||||
|
||||
/// `img` stored as a PNG that keeps the file's resolution.
|
||||
fn as_png(img: image::RgbaImage, ppi: Option<(f64, f64)>) -> Result<RasterImage> {
|
||||
let size = img.dimensions();
|
||||
|
||||
@@ -837,3 +837,33 @@ fn a_failed_save_does_not_stamp_the_documents_dates() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!s.doc().unwrap().is_dirty());
|
||||
}
|
||||
|
||||
/// #4284: a photo whose EXIF orientation says the camera was turned places upright: its pixels
|
||||
/// turned as the orientation says, its width and height swapped. One without keeps its own bytes.
|
||||
#[test]
|
||||
fn a_photo_places_the_way_its_exif_orientation_says() {
|
||||
// 16 × 8, red on the left half.
|
||||
let mut rgb = image::RgbImage::from_pixel(16, 8, image::Rgb([255, 255, 255]));
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
rgb.put_pixel(x, y, image::Rgb([220, 0, 0]));
|
||||
}
|
||||
}
|
||||
let mut jpg = Vec::new();
|
||||
image::DynamicImage::ImageRgb8(rgb).write_to(&mut Cursor::new(&mut jpg), image::ImageFormat::Jpeg).unwrap();
|
||||
// An EXIF segment with Orientation 6: turn a quarter clockwise to show.
|
||||
let exif: Vec<u8> =
|
||||
[b"Exif\0\0".as_slice(), b"II", &[0x2a, 0, 8, 0, 0, 0], &[1, 0], &[0x12, 0x01, 3, 0, 1, 0, 0, 0, 6, 0, 0, 0], &[0, 0, 0, 0]].concat();
|
||||
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 plain = raster_image(&jpg).unwrap();
|
||||
assert_eq!((plain.width, plain.height, plain.blob.bytes.as_slice()), (16, 8, jpg.as_slice()), "a photo as taken keeps its bytes");
|
||||
let img = raster_image(&turned).unwrap();
|
||||
assert_eq!((img.width, img.height), (8, 16), "turned a quarter");
|
||||
let px = image::load_from_memory(&img.blob.bytes).unwrap().to_rgb8();
|
||||
let red = |p: &image::Rgb<u8>| p[0] > 150 && p[1] < 80;
|
||||
assert!(red(px.get_pixel(4, 3)) && !red(px.get_pixel(4, 12)), "the left half is on top");
|
||||
}
|
||||
|
||||
@@ -374,7 +374,11 @@ pub(crate) fn full_linked_images<'a>(d: &'a Document, doc_path: Option<&str>) ->
|
||||
continue;
|
||||
};
|
||||
let preview = d.images.get(&key).map(|b| b.bytes.clone());
|
||||
let mime = fileio::format_for_name(g.link.name()).map(|f| f.mime.to_string()).unwrap_or_else(|| img.blob.mime.clone());
|
||||
// A file read as it is keeps its own type; one turned upright (EXIF orientation) is the PNG it became.
|
||||
let mime = match fileio::format_for_name(g.link.name()) {
|
||||
Some(f) if img.blob.bytes.as_slice() == bytes.as_slice() => f.mime.to_string(),
|
||||
_ => img.blob.mime.clone(),
|
||||
};
|
||||
out.images.insert(key, ImageBlob { mime, bytes: img.blob.bytes, proxy: preview });
|
||||
}
|
||||
let mut warnings = vec![];
|
||||
|
||||
Reference in New Issue
Block a user