mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
The palette PNG encoder never wrote a pHYs chunk, so an Indexed Color document saved as PNG reopened at 72 ppi while the RGB path kept its resolution. encode_png_indexed now takes an optional dpi and the flat exporter passes the document resolution under the same embed_metadata rule as the ordinary PNG encoder. Save for Web PNG-8 is unchanged. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
872d3df2c5
commit
c450e755ae
5 files changed
+40
-9
No files matched your search
@@ -521,8 +521,16 @@ fn adler32_combine(a1: u32, a2: u32, len2: usize) -> u32 {
|
||||
}
|
||||
|
||||
/// Writes an 8-bit palette PNG (PNG-8): `indices` row-major, `palette` RGB entries (≤ 256),
|
||||
/// optional fully transparent entry (`tRNS`). Used for Indexed Color documents.
|
||||
pub fn encode_indexed(width: u32, height: u32, indices: &[u8], palette: &[[u8; 3]], transparent: Option<u8>) -> Result<Vec<u8>, CodecError> {
|
||||
/// optional fully transparent entry (`tRNS`) and optional resolution in dpi (`pHYs`, written when
|
||||
/// both axes are positive and finite). Used for Indexed Color documents.
|
||||
pub fn encode_indexed(
|
||||
width: u32,
|
||||
height: u32,
|
||||
indices: &[u8],
|
||||
palette: &[[u8; 3]],
|
||||
transparent: Option<u8>,
|
||||
dpi: Option<(f32, f32)>,
|
||||
) -> Result<Vec<u8>, CodecError> {
|
||||
if width == 0 || height == 0 || indices.len() != width as usize * height as usize {
|
||||
return Err(CodecError::InvalidImage("index data does not match the size".into()));
|
||||
}
|
||||
@@ -538,6 +546,15 @@ pub fn encode_indexed(width: u32, height: u32, indices: &[u8], palette: &[[u8; 3
|
||||
trns[usize::from(t)] = 0;
|
||||
info.trns = Some(trns.into());
|
||||
}
|
||||
if let Some((x, y)) = dpi
|
||||
&& x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& x > 0.0
|
||||
&& y > 0.0
|
||||
{
|
||||
info.pixel_dims =
|
||||
Some(png::PixelDimensions { xppu: (x / METERS_PER_INCH).round() as u32, yppu: (y / METERS_PER_INCH).round() as u32, unit: png::Unit::Meter });
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
{
|
||||
let encoder = png::Encoder::with_info(&mut out, info).map_err(|e| CodecError::encode(F, e))?;
|
||||
@@ -556,14 +573,14 @@ mod indexed_tests {
|
||||
fn png8_roundtrips_through_the_decoder() {
|
||||
let pal = [[255, 0, 0], [0, 0, 255], [0, 0, 0]];
|
||||
let idx = [0u8, 1, 2, 1, 0, 2];
|
||||
let bytes = encode_indexed(3, 2, &idx, &pal, Some(2)).unwrap();
|
||||
let bytes = encode_indexed(3, 2, &idx, &pal, Some(2), None).unwrap();
|
||||
let img = decode(&bytes, &Limits::default()).unwrap();
|
||||
assert_eq!(img.dimensions(), (3, 2));
|
||||
let px = img.convert(ChannelLayout::Rgba, SampleType::U8);
|
||||
assert_eq!(&px.data()[..8], &[255, 0, 0, 255, 0, 0, 255, 255]);
|
||||
assert_eq!(px.data()[11], 0, "transparent entry");
|
||||
assert!(encode_indexed(3, 2, &[9; 6], &pal, None).is_err());
|
||||
assert!(encode_indexed(3, 3, &idx, &pal, None).is_err());
|
||||
assert!(encode_indexed(3, 2, &[9; 6], &pal, None, None).is_err());
|
||||
assert!(encode_indexed(3, 3, &idx, &pal, None, None).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -490,7 +490,7 @@ pub fn optimize(px: &[[f32; 4]], bw: usize, rect: Rect, st: &WebSettings, icc: O
|
||||
let bytes = if st.format == WebFormat::Gif {
|
||||
photocraft_codecs::web::encode_gif_indexed(ww, hh, &idx, &pal, tt, st.interlaced).map_err(e)?
|
||||
} else {
|
||||
photocraft_codecs::encode_png_indexed(ww, hh, &idx, &pal, tt).map_err(e)?
|
||||
photocraft_codecs::encode_png_indexed(ww, hh, &idx, &pal, tt, None).map_err(e)?
|
||||
};
|
||||
(bytes, Some(pal.len()))
|
||||
}
|
||||
|
||||
@@ -664,7 +664,9 @@ fn export_mode_specific(doc: &Document, format: Format, opts: &ExportOptions) ->
|
||||
}));
|
||||
Ok(())
|
||||
});
|
||||
let bytes = codecs::encode_png_indexed(doc.size.width, doc.size.height, &idx, &table.colors, table.transparent)?;
|
||||
// Same rule as the ordinary PNG encoder: the resolution goes into `pHYs` with the metadata.
|
||||
let dpi = opts.encode.embed_metadata.then_some((doc.resolution_dpi, doc.resolution_dpi));
|
||||
let bytes = codecs::encode_png_indexed(doc.size.width, doc.size.height, &idx, &table.colors, table.transparent, dpi)?;
|
||||
let mut warnings = vec![format!("written as an 8-bit palette PNG ({} colours)", table.colors.len())];
|
||||
if opts.encode.embed_metadata && opts.xmp == XmpEmbed::All && !doc.metadata.text.is_empty() {
|
||||
warnings.push("text metadata is not supported by the palette PNG exporter; it will be dropped".into());
|
||||
|
||||
@@ -313,8 +313,8 @@ fn transparent_flat_images_open_as_layer_0() {
|
||||
("ga.png", enc(ChannelLayout::GrayA, CS::U8, Format::Png), true),
|
||||
("a.tif", enc(ChannelLayout::Rgba, CS::U8, Format::Tiff), true),
|
||||
("ga.tif", enc(ChannelLayout::GrayA, CS::U16, Format::Tiff), true),
|
||||
("pal.png", photocraft_codecs::encode_png_indexed(w, h, &[0, 1].repeat(n / 2), &[[255, 0, 0], [0, 0, 255]], Some(0)).unwrap(), true),
|
||||
("pal_opaque.png", photocraft_codecs::encode_png_indexed(w, h, &[0, 1].repeat(n / 2), &[[255, 0, 0], [0, 0, 255]], None).unwrap(), false),
|
||||
("pal.png", photocraft_codecs::encode_png_indexed(w, h, &[0, 1].repeat(n / 2), &[[255, 0, 0], [0, 0, 255]], Some(0), None).unwrap(), true),
|
||||
("pal_opaque.png", photocraft_codecs::encode_png_indexed(w, h, &[0, 1].repeat(n / 2), &[[255, 0, 0], [0, 0, 255]], None, None).unwrap(), false),
|
||||
("rgb.png", enc(ChannelLayout::Rgb, CS::U8, Format::Png), false),
|
||||
("g.png", enc(ChannelLayout::Gray, CS::U16, Format::Png), false),
|
||||
("rgb.tif", enc(ChannelLayout::Rgb, CS::U8, Format::Tiff), false),
|
||||
|
||||
@@ -37,3 +37,15 @@ fn duotone_exports_the_inks_as_rgb() {
|
||||
let px = back.layers[0].surface().unwrap().rgba(0, 0);
|
||||
assert!(px[0] > px[2] + 0.05, "warm: {px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_png_keeps_the_document_resolution() {
|
||||
// #3047: the palette PNG lost its pHYs chunk and reopened at 72 ppi.
|
||||
let mut d = doc(ColorMode::Indexed);
|
||||
d.resolution_dpi = 300.0;
|
||||
d.color_table = Some(ColorTable { colors: vec![[0, 0, 0], [255, 0, 0]], transparent: None });
|
||||
let r = photocraft_io::export(&d, "x.png", &Default::default()).unwrap();
|
||||
assert_eq!(r.bytes[25], 3, "still a palette PNG");
|
||||
let back = photocraft_io::import("x.png", &r.bytes).unwrap().document;
|
||||
assert!((back.resolution_dpi - 300.0).abs() < 0.01, "{}", back.resolution_dpi);
|
||||
}
|
||||
Reference in new issue
Block a user