PSD import: ResolutionInfo stays pixels per inch when the display unit is cm (#5280)

hRes and vRes are stored as pixels per inch. The unit that follows only
controls how that number is shown, so a 300 ppi file with unit cm opened
at 762 ppi. Export already writes the document ppi and does not divide
by 2.54.

Fixes #5201

Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
Zer0C00L01andCursor Agent authored and GitHub committed 2026-10-14 01:58:27 -04:00
1 parent 2388d82165
commit a954194277
5 files changed
+41 -16

No files matched your search

+1
View File
@@ -1636,6 +1636,7 @@ fn document_to_psd_nested(doc: &Document, opts: &PsdExportOptions, depth: u32, c
// Resources.
let mut resources = vec![
// hRes/vRes are always pixels per inch. The display unit written here is inches.
ImageResource::new(ids::RESOLUTION_INFO, ResolutionInfo::from_dpi(f64::from(doc.resolution_dpi)).to_bytes()),
ImageResource::new(ids::GLOBAL_ANGLE, (doc.global_light.angle.round() as i32).to_be_bytes().to_vec()),
ImageResource::new(ids::GLOBAL_ALTITUDE, (doc.global_light.altitude.round() as i32).to_be_bytes().to_vec()),
+3 -2
View File
@@ -673,8 +673,9 @@ pub fn psd_to_document_with(file: &PsdFile, ctl: &photocraft_raster::Interrupt)
match r.id {
ids::RESOLUTION_INFO => {
if let Ok(ri) = photocraft_psd::ResolutionInfo::from_bytes(&r.data) {
let ppi = |res: f64, unit: u16| if unit == 2 { res * 2.54 } else { res };
let (x, y) = (ppi(ri.h_res(), ri.h_res_unit), ppi(ri.v_res(), ri.v_res_unit));
// Fixed 16.16 hRes/vRes are pixels per inch. The unit shorts only
// say how to show that number (1 = px/in, 2 = px/cm).
let (x, y) = (ri.h_res(), ri.v_res());
doc.resolution_dpi = x as f32;
warnings.extend(crate::unequal_resolution_warning(x, y));
}
+8 -3
View File
@@ -460,13 +460,18 @@ fn guides_and_alpha_names_roundtrip() {
}
#[test]
fn resolution_in_cm_converted() {
fn resolution_info_cm_display_unit_stays_pixels_per_inch() {
let mut f = testgen::small(Version::Psd, Compression::Raw);
let mut ri = photocraft_psd::ResolutionInfo::from_dpi(100.0);
let mut ri = photocraft_psd::ResolutionInfo::from_dpi(300.0);
ri.h_res_unit = 2;
ri.v_res_unit = 2;
f.resources[0] = photocraft_psd::ImageResource::new(1005, ri.to_bytes());
let (d, _) = psd_to_document(&f);
assert!((d.resolution_dpi - 254.0).abs() < 1e-3);
assert!((d.resolution_dpi - 300.0).abs() < 1e-3, "{}", d.resolution_dpi);
let out = document_to_psd(&d);
let written = out.resolution().unwrap();
assert!((written.h_res() - 300.0).abs() < 1e-3, "{}", written.h_res());
assert!((written.v_res() - 300.0).abs() < 1e-3, "{}", written.v_res());
}
#[test]
+17 -3
View File
@@ -59,13 +59,27 @@ fn psd_with_equal_axes_opens_silently() {
let r = import("x.psd", &psd(ResolutionInfo::from_dpi(300.0))).unwrap();
assert!((r.document.resolution_dpi - 300.0).abs() < 1e-3, "{}", r.document.resolution_dpi);
assert!(resolution_notes(&r).is_empty(), "{:?}", r.warnings);
// Each axis is read in its own unit: 100 pixels per centimetre on both is 254 ppi on both.
let mut cm = ResolutionInfo::from_dpi(100.0);
}
/// Resource 1005 stores 300 ppi even when the display unit is centimetres. Scaling
/// that fixed-point value by 2.54 opened the file at 762 ppi (#5201).
#[test]
fn resolution_info_cm_display_unit_is_pixels_per_inch() {
let mut cm = ResolutionInfo::from_dpi(300.0);
cm.h_res_unit = 2;
cm.v_res_unit = 2;
cm.width_unit = 2;
cm.height_unit = 2;
let r = import("x.psd", &psd(cm)).unwrap();
assert!((r.document.resolution_dpi - 254.0).abs() < 1e-3, "{}", r.document.resolution_dpi);
assert!((r.document.resolution_dpi - 300.0).abs() < 1e-3, "{}", r.document.resolution_dpi);
assert!(resolution_notes(&r).is_empty(), "{:?}", r.warnings);
let out = export(&r.document, "out.psd", &ExportOptions::default()).unwrap();
let file = photocraft_psd::PsdFile::from_bytes(&out.bytes).unwrap();
let written = file.resolution().unwrap();
assert!((written.h_res() - 300.0).abs() < 1e-3, "{}", written.h_res());
assert!((written.v_res() - 300.0).abs() < 1e-3, "{}", written.v_res());
assert_eq!((written.h_res_unit, written.v_res_unit), (1, 1));
}
/// A layered TIFF opens through the PSD importer with the TIFF's own resolution tags.
+12 -8
View File
@@ -45,20 +45,24 @@ pub struct ImageResource {
}
/// Fixed-point resolution info (resource 1005).
///
/// `h_res_fixed` and `v_res_fixed` are always pixels per inch (16.16 fixed).
/// `h_res_unit` and `v_res_unit` only say how to display that number
/// (1 = pixels/inch, 2 = pixels/cm) and do not rescale the stored value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolutionInfo {
/// Horizontal resolution, 16.16 fixed point (pixels per inch/cm).
/// Horizontal resolution, 16.16 fixed point, always pixels per inch.
pub h_res_fixed: u32,
/// 1 = pixels per inch, 2 = pixels per cm.
/// Display unit for the horizontal resolution (1 = px/in, 2 = px/cm).
pub h_res_unit: u16,
/// Display unit for width (1 in, 2 cm, 3 pt, 4 pica, 5 column).
/// Ruler unit for width (1 in, 2 cm, 3 pt, 4 pica, 5 column).
pub width_unit: u16,
/// Vertical resolution, 16.16 fixed.
/// Vertical resolution, 16.16 fixed point, always pixels per inch.
pub v_res_fixed: u32,
/// Vertical resolution unit.
/// Display unit for the vertical resolution (1 = px/in, 2 = px/cm).
pub v_res_unit: u16,
/// Display unit for height.
/// Ruler unit for height (1 in, 2 cm, 3 pt, 4 pica, 5 column).
pub height_unit: u16,
}
@@ -68,11 +72,11 @@ impl ResolutionInfo {
let f = (dpi * 65536.0).round().clamp(0.0, u32::MAX as f64) as u32;
ResolutionInfo { h_res_fixed: f, h_res_unit: 1, width_unit: 1, v_res_fixed: f, v_res_unit: 1, height_unit: 1 }
}
/// Horizontal resolution as float.
/// Horizontal resolution in pixels per inch.
pub fn h_res(&self) -> f64 {
f64::from(self.h_res_fixed) / 65536.0
}
/// Vertical resolution as float.
/// Vertical resolution in pixels per inch.
pub fn v_res(&self) -> f64 {
f64::from(self.v_res_fixed) / 65536.0
}