From fbb9cddbb8f71a52a2016bc8d26fe47817d076f3 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 17:16:43 +0200 Subject: [PATCH] Add PNG DPI support via pHYs chunk and fix font picker PNG files now embed pHYs chunk for DPI when output_dpi is set, matching the existing JPEG DPI support. Also fixed FontDialogButton signal handler to properly unwrap the Option. --- pixstrip-core/src/encoder.rs | 84 ++++++++++++++++++++++++ pixstrip-gtk/src/steps/step_watermark.rs | 7 +- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/pixstrip-core/src/encoder.rs b/pixstrip-core/src/encoder.rs index e354b00..1e98a9e 100644 --- a/pixstrip-core/src/encoder.rs +++ b/pixstrip-core/src/encoder.rs @@ -129,6 +129,11 @@ impl OutputEncoder { reason: e.to_string(), })?; + // Insert pHYs chunk for DPI if requested + if self.options.output_dpi > 0 { + buf = insert_png_phys_chunk(&buf, self.options.output_dpi); + } + let optimized = oxipng::optimize_from_memory(&buf, &oxipng::Options::default()) .map_err(|e| PixstripError::Processing { operation: "png_optimize".into(), @@ -186,6 +191,85 @@ impl OutputEncoder { } } +/// Insert a pHYs chunk into PNG data to set DPI. +/// The pHYs chunk must appear before the first IDAT chunk. +/// DPI is converted to pixels per meter (1 inch = 0.0254 meters). +fn insert_png_phys_chunk(png_data: &[u8], dpi: u32) -> Vec { + // PNG pixels per meter = DPI / 0.0254 + let ppm = (dpi as f64 / 0.0254).round() as u32; + + // Build the pHYs chunk: + // 4 bytes: data length (9) + // 4 bytes: chunk type "pHYs" + // 4 bytes: pixels per unit X + // 4 bytes: pixels per unit Y + // 1 byte: unit (1 = meter) + // 4 bytes: CRC32 of type + data + let mut chunk_data = Vec::with_capacity(9); + chunk_data.extend_from_slice(&ppm.to_be_bytes()); // X pixels per unit + chunk_data.extend_from_slice(&ppm.to_be_bytes()); // Y pixels per unit + chunk_data.push(1); // unit = meter + + let mut crc_input = Vec::with_capacity(13); + crc_input.extend_from_slice(b"pHYs"); + crc_input.extend_from_slice(&chunk_data); + + let crc = crc32_png(&crc_input); + + let mut phys_chunk = Vec::with_capacity(21); + phys_chunk.extend_from_slice(&9u32.to_be_bytes()); // length + phys_chunk.extend_from_slice(b"pHYs"); // type + phys_chunk.extend_from_slice(&chunk_data); // data + phys_chunk.extend_from_slice(&crc.to_be_bytes()); // CRC + + // Find the first IDAT chunk and insert pHYs before it. + // PNG structure: 8-byte signature, then chunks (each: 4 len + 4 type + data + 4 crc) + let mut result = Vec::with_capacity(png_data.len() + phys_chunk.len()); + let mut pos = 8; // skip PNG signature + result.extend_from_slice(&png_data[..8]); + + while pos + 8 <= png_data.len() { + let chunk_len = u32::from_be_bytes([ + png_data[pos], png_data[pos + 1], png_data[pos + 2], png_data[pos + 3], + ]) as usize; + let chunk_type = &png_data[pos + 4..pos + 8]; + let total_chunk_size = 4 + 4 + chunk_len + 4; // len + type + data + crc + + if chunk_type == b"IDAT" || chunk_type == b"pHYs" { + if chunk_type == b"IDAT" { + // Insert pHYs before first IDAT + result.extend_from_slice(&phys_chunk); + } + // If existing pHYs, skip it (we're replacing it) + if chunk_type == b"pHYs" { + pos += total_chunk_size; + continue; + } + } + + result.extend_from_slice(&png_data[pos..pos + total_chunk_size]); + pos += total_chunk_size; + } + + result +} + +/// Simple CRC32 for PNG chunks (uses the standard PNG CRC polynomial) +fn crc32_png(data: &[u8]) -> u32 { + let mut crc: u32 = 0xFFFF_FFFF; + for &byte in data { + crc ^= byte as u32; + for _ in 0..8 { + if crc & 1 != 0 { + crc = (crc >> 1) ^ 0xEDB8_8320; + } else { + crc >>= 1; + } + } + } + crc ^ 0xFFFF_FFFF +} + impl Default for OutputEncoder { fn default() -> Self { Self::new() diff --git a/pixstrip-gtk/src/steps/step_watermark.rs b/pixstrip-gtk/src/steps/step_watermark.rs index 6e2f57a..7405ab8 100644 --- a/pixstrip-gtk/src/steps/step_watermark.rs +++ b/pixstrip-gtk/src/steps/step_watermark.rs @@ -443,9 +443,10 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { { let jc = state.job_config.clone(); font_button.connect_font_desc_notify(move |btn| { - let desc = btn.font_desc(); - if let Some(family) = desc.family() { - jc.borrow_mut().watermark_font_family = family.to_string(); + if let Some(desc) = btn.font_desc() { + if let Some(family) = desc.family() { + jc.borrow_mut().watermark_font_family = family.to_string(); + } } }); }