M2: a fax image with more /Rows than its /Height is decoded to its height (hayro-syntax patch 41)

A CCITTFaxDecode image whose /DecodeParms /Rows exceeds the image's
/Height (PDFBOX-771: /Rows 1084975104 on 2,592 columns) asked the decoder
for /Rows rows; the size check refused that, and every page drew blank.
The image is /Height rows (ISO 32000-2 Table 87), so the decoder is asked
for those; /Rows still counts for a stream decoded without an image's
height. pdfcraft-cos already decodes /Height rows; a test pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
mtlcl410
2026-11-07 11:55:32 +08:00
co-authored by Claude Opus 5.5
parent f42fda7342
commit d4b4218cf1
5 changed files with 57 additions and 7 deletions
+4
View File
@@ -274,6 +274,10 @@ fn fax_images_decode_with_their_parameters() {
let mut understated = parms.clone();
understated.set(b"Rows".to_vec(), Object::Int(1));
assert_eq!(image(&doc, Some(Object::Dict(understated.clone())), 10, 3).unwrap(), bits, "/Height rows, whatever /Rows says");
// More /Rows than /Height (PDFBOX-771: 1,084,975,104), as the renderer reads it too.
let mut overstated = parms.clone();
overstated.set(b"Rows".to_vec(), Object::Int(1_000_000_000));
assert_eq!(image(&doc, Some(Object::Dict(overstated)), 10, 3).unwrap(), bits, "/Height rows, not /Rows past it");
// The parameters as the file gives them, /Rows included, and Table 11's defaults.
let mut d = Dict::new();
d.set(b"Filter".to_vec(), Object::name("CCITTFaxDecode"));
+42
View File
@@ -3454,6 +3454,48 @@ trailer << /Root 1 0 R >>
}
}
/// PDFBOX-771: a fax image whose `/Rows` says more rows than its `/Height` (1,084,975,104 on a
/// 2,592-column scan) is decoded to its height, as the image is (ISO 32000-2 Table 87): the
/// decoder was asked for `/Rows` rows, which the size check refused, so every page drew blank.
/// Also through an `/Indexed` colour space, the filter's other output path.
#[test]
fn group4_fax_with_rows_past_its_height_decodes_to_the_image_height() {
const ALL_BLACK_8X4: [u8; 6] = [0x26, 0xa2, 0xfe, 0x00, 0x20, 0x02];
for space in ["/DeviceGray", "[/Indexed /DeviceRGB 1 <000000FFFFFF>]"] {
for rows in ["/Rows 5", "/Rows 1000000000", "/Rows 4294967295"] {
let content = "q 20 0 0 20 5 5 cm /Im1 Do Q";
let mut pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Image /Width 8 /Height 4 /ColorSpace {space} /BitsPerComponent 1 /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 8 {rows} >> /Length {} >> stream
",
content.len(),
ALL_BLACK_8X4.len()
)
.into_bytes();
pdf.extend_from_slice(&ALL_BLACK_8X4);
pdf.extend_from_slice(
b"
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
);
let mut renderer = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
assert!(page.error.is_none(), "{space} {rows}: {:?}", page.error);
for y in [17, 22, 27, 32] {
assert_eq!(&page.rgba[((y * 40 + 15) * 4)..][..4], &[0, 0, 0, 255], "{space} {rows}: image row at y {y} paints black");
}
assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 255, 255, 255], "{space} {rows}: outside the image stays white");
}
}
}
#[test]
fn tiff_predictor_packed_rgb_rows_keep_their_byte_boundaries() {
use hayro_syntax::object::{FromBytes, Object};
+2 -2
View File
@@ -806,8 +806,8 @@ milestone = "M2"
status = "partial"
commands = []
tools = []
evidence = []
notes = "hayro bootstrap"
evidence = ["crates/render/src/raster/tests.rs::group4_fax_with_rows_zero_decodes_to_the_image_height", "crates/render/src/raster/tests.rs::group4_fax_with_rows_below_its_height_decodes_to_the_image_height", "crates/render/src/raster/tests.rs::group4_fax_with_rows_past_its_height_decodes_to_the_image_height"]
notes = "hayro bootstrap; a CCITT fax image is decoded to its /Height whatever its /DecodeParms /Rows says (0, fewer or more rows)"
[[feature]]
id = "view.render-annotation-appearances"
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -4
View File
@@ -20,10 +20,14 @@ pub(crate) fn decode(
// PdfCraft patch (19): fewer `/Rows` than the image's `/Height` doesn't cut the image short: a
// plotter driver wrote `/Rows` equal to `/Columns`, and the rows after it were drawn black
// (#3308). The decoder still stops where the data ends.
let rows = params
.get::<u32>(ROWS)
.filter(|&rows| rows != 0)
.map_or(image_params.height, |rows| rows.max(image_params.height));
// PdfCraft patch (41): more `/Rows` than the image's `/Height` doesn't make the decoder go
// past it either: the image is `/Height` rows (ISO 32000-2 Table 87), and a `/Rows` of
// 1,084,975,104 on 2,592 columns had the size check refuse every page of a scan (PDFBOX-771).
// `/Rows` counts only for a stream decoded without an image's height.
let rows = match image_params.height {
0 => params.get::<u32>(ROWS).unwrap_or(0),
height => height,
};
let end_of_block = params.get::<bool>(END_OF_BLOCK).unwrap_or(true);
let settings = DecodeSettings {