mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-06 02:22:00 +00:00
hayro-syntax: port PdfCraft's JP2 palette without /ColorSpace (ledger P5)
A JPX image whose dictionary has no /ColorSpace, or one that does not resolve, takes its colors from the JP2 palette instead of drawing its raw indices as gray. Resolved palette colors are 8-bit samples whatever the index precision. From PdfCraft #3184, with its three jpx unit tests.
This commit is contained in:
1 parent
d9d02c42be
commit
d2ffefcddf
3 files changed
+152
-4
No files matched your search
Vendored
+10
@@ -185,6 +185,16 @@ are seen. Each section's entries are read before its `Prev` is followed; the loc
|
||||
xref-stream bounds above apply to them unchanged. Tests:
|
||||
`xref::tests::prev_as_reference_reaches_the_earlier_section` and
|
||||
`xref::tests::prev_as_reference_to_an_object_holding_the_offset_reads_that_offset`.
|
||||
hayro-syntax (P5), PdfCraft #3184: `filter::jpx::decode` applies a JP2 palette (`pclr`) when
|
||||
the image dictionary has no `/ColorSpace`, or one that does not resolve, since the colors then
|
||||
come from the JP2 header (ISO 32000-2 §7.4.9). With a `/ColorSpace` the raw indices are kept,
|
||||
as an `/Indexed` space needs. Resolved palette colors are 8-bit samples whatever the
|
||||
precision of the indices; an image without a palette keeps its own. Tests:
|
||||
`filter::jpx::tests::jpx_palette_is_applied_without_a_color_space`,
|
||||
`filter::jpx::tests::jpx_palette_indices_stay_raw_for_an_indexed_color_space` and
|
||||
`filter::jpx::tests::palette_colours_are_eight_bit_and_other_samples_keep_their_precision`.
|
||||
PdfCraft's raster test `a_jp2_palette_keeps_its_colours_whatever_the_index_precision` is not
|
||||
ported: the unit tests show the same behavior.
|
||||
|
||||
This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion,
|
||||
floating component precision and codec versions, rather than claiming full 0.8
|
||||
|
||||
Vendored
+1
-1
@@ -164,7 +164,7 @@
|
||||
"path": "src/filter/jpx.rs",
|
||||
"source_sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82",
|
||||
"source_git_blob": "74efd8b31e2b2935ff2bbceda2c1ffd36ea9c20d",
|
||||
"sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82"
|
||||
"sha256": "b0c295f815547377409a8810e9cbef35e4d6b506a3c3507669da71fcfe9d4701"
|
||||
},
|
||||
{
|
||||
"path": "src/filter/lzw_flate.rs",
|
||||
|
||||
+141
-3
@@ -21,7 +21,13 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
|
||||
use crate::object::stream::ImageColorSpace;
|
||||
|
||||
let settings = DecodeSettings {
|
||||
resolve_palette_indices: false,
|
||||
// PdfCraft patch: (PdfCraft #3184) with no /ColorSpace in the image dictionary the colors
|
||||
// come from the JP2 header (ISO 32000-2 §7.4.9), so its palette (`pclr`) is applied here;
|
||||
// with a /ColorSpace the header color data is ignored, and an /Indexed space takes the raw
|
||||
// indices. A /ColorSpace that does not resolve also leaves `num_components` unset: the
|
||||
// header's colors are then the only ones there are, so the palette is applied too (raw
|
||||
// indices would be drawn as gray).
|
||||
resolve_palette_indices: params.num_components.is_none(),
|
||||
strict: false,
|
||||
target_resolution: params.target_dimension,
|
||||
};
|
||||
@@ -41,7 +47,15 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
|
||||
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
let bpc = params.bpc.unwrap_or(image.original_bit_depth());
|
||||
// PdfCraft patch: (PdfCraft #3184) a resolved palette gives colors, decoded to 8 bits like
|
||||
// every sample; the codestream's precision is that of the indices (1 bit, say), and rescaling
|
||||
// the colors to it kept only 0 or 255 per channel.
|
||||
let palette_resolved = settings.resolve_palette_indices && has_palette(data);
|
||||
let bpc = params.bpc.unwrap_or(if palette_resolved {
|
||||
8
|
||||
} else {
|
||||
image.original_bit_depth()
|
||||
});
|
||||
let cs = match image.color_space() {
|
||||
ColorSpace::Gray => ImageColorSpace::Gray,
|
||||
ColorSpace::RGB => ImageColorSpace::Rgb,
|
||||
@@ -98,6 +112,15 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch: (PdfCraft #3184) whether JP2 file `data` has a palette box (`pclr`) in its
|
||||
/// header box (`jp2h`, ISO 15444-1 I.5.3.4). A raw codestream has none.
|
||||
fn has_palette(data: &[u8]) -> bool {
|
||||
boxes(data)
|
||||
.find(|&(kind, _, _)| kind == *b"jp2h")
|
||||
.and_then(|(_, start, end)| data.get(start..end))
|
||||
.is_some_and(|header| boxes(header).any(|(kind, _, _)| kind == *b"pclr"))
|
||||
}
|
||||
|
||||
/// The enumerated colour spaces (`EnumCS`, ISO 15444-1 I.5.3.3 and 15444-2 M.11.7.2) the decoder
|
||||
/// handles: CMYK, CIELab, sRGB, greyscale, sYCC, e-sRGB and ROMM-RGB.
|
||||
const DECODED_ENUMERATED: [u32; 7] = [12, 14, 16, 17, 18, 20, 21];
|
||||
@@ -221,8 +244,9 @@ fn scale(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{scale, without_refused_enumerated_colour};
|
||||
use super::{decode, scale, without_refused_enumerated_colour};
|
||||
use crate::bit_reader::{BitReader, bit_mask};
|
||||
use crate::object::stream::{ImageColorSpace, ImageDecodeParams};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[test]
|
||||
@@ -358,4 +382,118 @@ mod tests {
|
||||
assert_eq!(without_refused_enumerated_colour(&data), None, "{bad:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A 4x2 JP2 written for this test: one 8-bit component whose samples (0, 1) index a
|
||||
/// two-entry sRGB palette (`pclr` + `cmap`): red, then blue. Rows: 0 1 0 1 / 1 0 1 0.
|
||||
const PALETTE_JP2: &[u8] = &[
|
||||
0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a, 0x00, 0x00, 0x00,
|
||||
0x14, 0x66, 0x74, 0x79, 0x70, 0x6a, 0x70, 0x32, 0x20, 0x00, 0x00, 0x00, 0x00, 0x6a, 0x70,
|
||||
0x32, 0x20, 0x00, 0x00, 0x00, 0x55, 0x6a, 0x70, 0x32, 0x68, 0x00, 0x00, 0x00, 0x16, 0x69,
|
||||
0x68, 0x64, 0x72, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x07, 0x07,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x63, 0x6f, 0x6c, 0x72, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x10, 0x00, 0x00, 0x00, 0x14, 0x70, 0x63, 0x6c, 0x72, 0x00, 0x02, 0x03, 0x07, 0x07,
|
||||
0x07, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x14, 0x63, 0x6d, 0x61, 0x70,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00,
|
||||
0x8c, 0x6a, 0x70, 0x32, 0x63, 0xff, 0x4f, 0xff, 0x51, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x07, 0x01, 0x01, 0xff, 0x52, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
|
||||
0x04, 0x04, 0x00, 0x01, 0xff, 0x5c, 0x00, 0x07, 0x40, 0x40, 0x48, 0x48, 0x50, 0xff, 0x64,
|
||||
0x00, 0x25, 0x00, 0x01, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20,
|
||||
0x4f, 0x70, 0x65, 0x6e, 0x4a, 0x50, 0x45, 0x47, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x20, 0x32, 0x2e, 0x35, 0x2e, 0x34, 0xff, 0x90, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x17, 0x00, 0x01, 0xff, 0x93, 0xcf, 0xb4, 0x08, 0x08, 0x67, 0x90, 0x07, 0x41, 0x08,
|
||||
0xff, 0xd9,
|
||||
];
|
||||
|
||||
/// PdfCraft #3179: with no `/ColorSpace` in the image dictionary, the JP2 palette gives the
|
||||
/// colors. Raw indices (0 and 1) would come back as a gray image, drawn as noise.
|
||||
#[test]
|
||||
fn jpx_palette_is_applied_without_a_color_space() {
|
||||
let params = ImageDecodeParams {
|
||||
width: 4,
|
||||
height: 2,
|
||||
..ImageDecodeParams::default()
|
||||
};
|
||||
let result = decode(PALETTE_JP2, ¶ms).unwrap();
|
||||
let image = result.image_data.unwrap();
|
||||
assert!(matches!(image.color_space, Some(ImageColorSpace::Rgb)));
|
||||
let red = [255, 0, 0];
|
||||
let blue = [0, 0, 255];
|
||||
let expected: Vec<u8> = [red, blue, red, blue, blue, red, blue, red].concat();
|
||||
assert_eq!(&*result.data, &expected[..]);
|
||||
}
|
||||
|
||||
/// With an `/Indexed` color space in the dictionary, the PDF supplies the palette, so the raw
|
||||
/// indices are what the image holds.
|
||||
#[test]
|
||||
fn jpx_palette_indices_stay_raw_for_an_indexed_color_space() {
|
||||
let params = ImageDecodeParams {
|
||||
is_indexed: true,
|
||||
num_components: Some(1),
|
||||
width: 4,
|
||||
height: 2,
|
||||
..ImageDecodeParams::default()
|
||||
};
|
||||
let result = decode(PALETTE_JP2, ¶ms).unwrap();
|
||||
assert_eq!(&*result.data, &[0, 1, 0, 1, 1, 0, 1, 0][..]);
|
||||
}
|
||||
|
||||
/// A 1x1, 1-bit codestream (one empty packet, so its sample is the midpoint 1) as a JP2 file
|
||||
/// whose header holds `extra` boxes after `ihdr` and `colr`.
|
||||
fn one_bit_jp2(extra: &[u8]) -> Vec<u8> {
|
||||
let jp2_box = |kind: &[u8], body: &[u8]| {
|
||||
let mut b = ((8 + body.len()) as u32).to_be_bytes().to_vec();
|
||||
b.extend_from_slice(kind);
|
||||
b.extend_from_slice(body);
|
||||
b
|
||||
};
|
||||
let mut codestream = alloc::vec![0xff, 0x4f, 0xff, 0x51, 0, 41, 0, 0];
|
||||
for value in [1_u32, 1, 0, 0, 1, 1, 0, 0] {
|
||||
codestream.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
codestream.extend_from_slice(&[0, 1, 0, 1, 1]);
|
||||
codestream.extend_from_slice(&[0xff, 0x52, 0, 12, 0, 0, 0, 1, 0, 0, 4, 4, 0, 1]);
|
||||
codestream.extend_from_slice(&[0xff, 0x5c, 0, 4, 0x40, 0x40]);
|
||||
codestream.extend_from_slice(&[0xff, 0x90, 0, 10, 0, 0, 0, 0, 0, 15, 0, 1, 0xff, 0x93, 0]);
|
||||
codestream.extend_from_slice(&[0xff, 0xd9]);
|
||||
let mut header = jp2_box(b"ihdr", &[0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 7, 0, 0]);
|
||||
header.extend(jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 16]));
|
||||
header.extend_from_slice(extra);
|
||||
let mut out = alloc::vec![0, 0, 0, 12, b'j', b'P', b' ', b' ', 0x0d, 0x0a, 0x87, 0x0a];
|
||||
out.extend(jp2_box(b"ftyp", b"jp2 \0\0\0\0jp2 "));
|
||||
out.extend(jp2_box(b"jp2h", &header));
|
||||
out.extend(jp2_box(b"jp2c", &codestream));
|
||||
out
|
||||
}
|
||||
|
||||
/// PdfCraft #3184: resolved palette colors are 8-bit samples whatever the indices' precision;
|
||||
/// an image without a palette keeps its own (1 bit here).
|
||||
#[test]
|
||||
fn palette_colours_are_eight_bit_and_other_samples_keep_their_precision() {
|
||||
let params = ImageDecodeParams {
|
||||
width: 1,
|
||||
height: 1,
|
||||
..ImageDecodeParams::default()
|
||||
};
|
||||
let bpc = |data: &[u8]| {
|
||||
decode(data, ¶ms)
|
||||
.and_then(|r| r.image_data)
|
||||
.map(|d| d.bits_per_component)
|
||||
};
|
||||
let mut palette = Vec::new();
|
||||
for (kind, body) in [
|
||||
(
|
||||
&b"pclr"[..],
|
||||
&[0, 2, 3, 7, 7, 7, 255, 0, 0, 64, 128, 192][..],
|
||||
),
|
||||
(b"cmap", &[0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 2]),
|
||||
] {
|
||||
palette.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
|
||||
palette.extend_from_slice(kind);
|
||||
palette.extend_from_slice(body);
|
||||
}
|
||||
assert_eq!(bpc(&one_bit_jp2(&palette)), Some(8));
|
||||
assert_eq!(bpc(&one_bit_jp2(&[])), Some(1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user