mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
fix(render): apply the JP2 palette of a JPX image with no /ColorSpace (#3179)
hayro-syntax decoded every JPX image with resolve_palette_indices off. That is right when the image dictionary gives a /ColorSpace (an /Indexed space needs the raw indices), but with no /ColorSpace the colours come from the JP2 header (ISO 32000-2 §7.4.9) and nothing applied its palette: the indices were drawn as a gray image. The palette is now resolved exactly when the dictionary has no /ColorSpace. Two unit tests decode a 4x2 palette JP2 written for the test: without a colour space it is red/blue RGB, with /Indexed the raw indices stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
43550c448d
commit
e1aeb00be9
Vendored
+7
@@ -41,6 +41,13 @@ Covered by `raster::tests::tiff_predictor_packed_rgb_rows_keep_their_byte_bounda
|
||||
horizontal differencing and an 8-bit control) and
|
||||
`raster::tests::tiff_predictor_packed_rgb_image_renders_every_row`.
|
||||
|
||||
JPEG 2000 palettes (#3179): `hayro-syntax::filter::jpx::decode` applies a JP2 palette (`pclr`)
|
||||
when the image dictionary has no `/ColorSpace`, since the colour then comes from the JP2 header
|
||||
(ISO 32000-2 §7.4.9). With a `/ColorSpace` the raw indices are kept, as an `/Indexed` space needs.
|
||||
Without the patch a palette image with no `/ColorSpace` was drawn as its indices, in gray.
|
||||
Covered by `filter::jpx::tests::jpx_palette_is_applied_without_a_color_space` and
|
||||
`filter::jpx::tests::jpx_palette_indices_stay_raw_for_an_indexed_color_space`.
|
||||
|
||||
Text extraction: hayro-interpret exposes `OutlineGlyph::is_vertical` from a Type0 font’s CMap writing mode. PdfCraft follows negative glyph-space y for WMode 1 so upright vertical columns can be searched (#126).
|
||||
|
||||
Lab Bradford follow-up to #1237: Lab adapts XYZ to D65 with white-point ratios
|
||||
|
||||
+62
-2
@@ -21,7 +21,10 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
|
||||
use crate::object::stream::ImageColorSpace;
|
||||
|
||||
let settings = DecodeSettings {
|
||||
resolve_palette_indices: false,
|
||||
// PdfCraft patch: (#3179) with no /ColorSpace in the image dictionary the colours come from
|
||||
// the JP2 header (ISO 32000-2 §7.4.9), so its palette (`pclr`) is applied here; with a
|
||||
// /ColorSpace the header colour data is ignored, and an /Indexed space takes the raw indices.
|
||||
resolve_palette_indices: params.num_components.is_none(),
|
||||
strict: false,
|
||||
target_resolution: params.target_dimension,
|
||||
};
|
||||
@@ -221,8 +224,10 @@ 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]
|
||||
@@ -357,5 +362,60 @@ mod tests {
|
||||
data[at..at + 4].copy_from_slice(&bad.to_be_bytes());
|
||||
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,
|
||||
];
|
||||
|
||||
/// #3179: with no `/ColorSpace` in the image dictionary, the JP2 palette gives the colours. The
|
||||
/// raw indices (0 and 1) used to 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` colour 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][..]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user