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<FontDescription>.
This commit is contained in:
@@ -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<u8> {
|
||||
// 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()
|
||||
|
||||
Reference in New Issue
Block a user