Files
photocraft/crates/io/tests/exif_resolution.rs
Gert Pellin - GPE 58289b2822 fix(codecs): read the resolution from EXIF, XMP and Photoshop APP13; write it consistently (#1691) (#1744)
A camera JPEG (e.g. Nikon D3200) records its resolution only in EXIF IFD0, so it
opened at 72 ppi instead of 300, and saving wrote JFIF 72 next to the source's
EXIF 300.

Reading: a JPEG takes the first usable of APP13 Photoshop ResolutionInfo
(0x03ED), XMP tiff:X/YResolution, EXIF IFD0 X/YResolution + ResolutionUnit
(2 inch, also when absent; 3 cm), and only then the JFIF density. That is the
order Photoshop resolves them in; JFIF comes last because cameras write EXIF
only while many tools write a default JFIF 72. Other formats (WebP, HEIF, PNG
without pHYs, ...) fall back to XMP then EXIF when they have no resolution of
their own. All parsing is bounds-checked; malformed data reads as no
resolution.

Writing: JPEG, PNG, WebP, TIFF (XMP), PSD and Save for Web JPEG rewrite the
EXIF/XMP resolution to the image's: RATIONAL values in place, ResolutionUnit 2,
and an entry that can't be rewritten safely is removed from IFD0 without moving
any other data.
2026-10-09 22:07:59 +07:00

45 lines
2.0 KiB
Rust

//! #1691: the EXIF resolution of an imported photo becomes the document's, and a PSD save keeps
//! its EXIF copy in step with the ResolutionInfo resource.
use photocraft_codecs::{ChannelLayout, EncodeOptions, Format, Image, Metadata, SampleType, exif_resolution};
use photocraft_io::*;
/// Big-endian EXIF: XResolution 300/1, YResolution 300/1, ResolutionUnit 2.
fn camera_exif() -> Vec<u8> {
let mut v = b"MM\0*".to_vec();
v.extend_from_slice(&8u32.to_be_bytes());
v.extend_from_slice(&3u16.to_be_bytes());
let data = 8 + 2 + 3 * 12 + 4;
for (tag, ty, value) in [(282u16, 5u16, data as u32), (283, 5, data as u32 + 8)] {
v.extend_from_slice(&tag.to_be_bytes());
v.extend_from_slice(&ty.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&value.to_be_bytes());
}
v.extend_from_slice(&296u16.to_be_bytes());
v.extend_from_slice(&3u16.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
v.extend_from_slice(&[0, 2, 0, 0]);
v.extend_from_slice(&0u32.to_be_bytes());
for _ in 0..2 {
v.extend_from_slice(&300u32.to_be_bytes());
v.extend_from_slice(&1u32.to_be_bytes());
}
v
}
#[test]
fn webp_with_exif_resolution_imports_at_it_and_psd_save_follows_the_document() {
let meta = Metadata { exif: Some(camera_exif()), ..Default::default() };
let img = Image::from_raw(6, 4, ChannelLayout::Rgb, SampleType::U8, vec![50; 72]).unwrap().with_meta(meta);
let webp = photocraft_codecs::encode(&img, Format::WebP, &EncodeOptions::default()).unwrap();
let mut doc = import("photo.webp", &webp).unwrap().document;
assert_eq!(doc.resolution_dpi, 300.0);
doc.resolution_dpi = 240.0;
let psd = export(&doc, "photo.psd", &ExportOptions::default()).unwrap().bytes;
let back = import("photo.psd", &psd).unwrap().document;
assert_eq!(back.resolution_dpi, 240.0);
assert_eq!(back.metadata.exif.as_deref().and_then(|e| exif_resolution(e)), Some((240.0, 240.0)));
}