Files
figcraft/crates/format/src/density.rs
T
Denis Gomes d94609d63f M7: #1360 review: narrower density match, decoded image when the PNG is unreadable
A density now counts only within 0.005 of a whole 2x-4x multiple of
72 dpi. That covers the rounding of pixels per metre (144 dpi is stored
as 5669 or 5670) and rejects nearby resolutions: 141, 143 and 145 dpi
were read as 2x under the old 0.05.

Paste falls back to the decoded clipboard image when the clipboard's PNG
can't be read, instead of pasting nothing.

Also the line on what the objc2 crates are for, and the ROADMAP row for
the new placing default.
2026-10-27 01:39:28 -04:00

42 lines
1.6 KiB
Rust

const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
const POINTS_PER_INCH: f64 = 72.0;
const METERS_PER_INCH: f64 = 0.0254;
const UNIT_METER: u8 = 1;
const MAX_DENSITY: f64 = 4.0;
const DENSITY_TOLERANCE: f64 = 0.005;
pub fn pixel_density(bytes: &[u8]) -> f64 {
png_pixels_per_meter(bytes).map_or(1.0, density_from_pixels_per_meter)
}
pub fn placed_size(pixel_width: u32, pixel_height: u32, density: f64, max_side: f64) -> (f64, f64) {
let (w, h) = (f64::from(pixel_width) / density, f64::from(pixel_height) / density);
let fit = (max_side / w.max(h)).min(1.0);
(w * fit, h * fit)
}
fn density_from_pixels_per_meter(pixels_per_meter: u32) -> f64 {
let ratio = f64::from(pixels_per_meter) * METERS_PER_INCH / POINTS_PER_INCH;
let whole = ratio.round();
if (2.0..=MAX_DENSITY).contains(&whole) && (ratio - whole).abs() < DENSITY_TOLERANCE { whole } else { 1.0 }
}
fn png_pixels_per_meter(bytes: &[u8]) -> Option<u32> {
let mut rest = bytes.strip_prefix(PNG_SIGNATURE)?;
while rest.len() >= 12 {
let length = usize::try_from(u32::from_be_bytes(rest[..4].try_into().ok()?)).ok()?;
let kind = &rest[4..8];
let data = rest.get(8..8usize.checked_add(length)?)?;
match kind {
b"pHYs" if data.len() == 9 => {
let x = u32::from_be_bytes(data[..4].try_into().ok()?);
let y = u32::from_be_bytes(data[4..8].try_into().ok()?);
return (data[8] == UNIT_METER && x == y).then_some(x);
}
b"IDAT" | b"IEND" => return None,
_ => rest = rest.get(12usize.checked_add(length)?..)?,
}
}
None
}