From a954194277121a1673cfd579d0182929bb21a705 Mon Sep 17 00:00:00 2001 From: Zer0C00L01 Date: Wed, 14 Oct 2026 00:58:27 -0500 Subject: [PATCH] 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 --- crates/io/src/psd_export.rs | 1 + crates/io/src/psd_import.rs | 5 +++-- crates/io/tests/psd_structure.rs | 11 ++++++++--- crates/io/tests/resolution_axes.rs | 20 +++++++++++++++++--- crates/psd/src/resources.rs | 20 ++++++++++++-------- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/crates/io/src/psd_export.rs b/crates/io/src/psd_export.rs index 9db3d4799..47861dbcc 100644 --- a/crates/io/src/psd_export.rs +++ b/crates/io/src/psd_export.rs @@ -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()), diff --git a/crates/io/src/psd_import.rs b/crates/io/src/psd_import.rs index 444c2d6e5..211bba4da 100644 --- a/crates/io/src/psd_import.rs +++ b/crates/io/src/psd_import.rs @@ -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)); } diff --git a/crates/io/tests/psd_structure.rs b/crates/io/tests/psd_structure.rs index e893adaf9..5627dca3f 100644 --- a/crates/io/tests/psd_structure.rs +++ b/crates/io/tests/psd_structure.rs @@ -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] diff --git a/crates/io/tests/resolution_axes.rs b/crates/io/tests/resolution_axes.rs index 58b67d93c..f7f8cbf00 100644 --- a/crates/io/tests/resolution_axes.rs +++ b/crates/io/tests/resolution_axes.rs @@ -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. diff --git a/crates/psd/src/resources.rs b/crates/psd/src/resources.rs index c20efbfc9..4d78a062b 100644 --- a/crates/psd/src/resources.rs +++ b/crates/psd/src/resources.rs @@ -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 }