From e8f3c6bc3efa61d878b83069a2dd0ffab1407391 Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Mon, 26 Oct 2026 17:54:03 +0100 Subject: [PATCH] =?UTF-8?q?M11:=20Export=20=E2=96=B8=20Images=20reads=20by?= =?UTF-8?q?te-aligned=20Group=203=20whichever=20way=20its=20fill=20falls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With /EncodedByteAlign and end-of-line codes, the fill may start each code on a byte or end it on one (T.4's fill, as TIFF writes it); the second was misread, and the image refused as short. With end-of-line codes the decoder now reads unaligned, as the codes mark the rows either way (probed black-box for /K 0, 1 and 2); codes laid out so under /EndOfLine false are still misread. The Group 3 test covers both layouts, /EncodedByteAlign given by reference, and a /BlackIs1 that inverts the picture given by reference (with_direct_parms resolves both before the decoder reads them). From reviews with the pdfcraft-review skill. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG --- crates/create/src/extract.rs | 48 ++++++++++++++++++++++-------------- crates/create/src/tests.rs | 48 +++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index d9895ffdd2..95019467b0 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -196,25 +196,37 @@ fn ccitt(doc: &Document, s: &Stream, data: &[u8], p: Dict) -> Result, St // As many rows as the image has, whatever /Rows says: one below /Height would cut the picture // short, and rows past /Height aren't drawn. let rows = s.dict.int(b"Height").and_then(|h| u32::try_from(h).ok()).unwrap_or(0); - let settings = hayro_ccitt::DecodeSettings { - columns, - rows, - end_of_block: flag(b"EndOfBlock", true), - end_of_line: flag(b"EndOfLine", false), - rows_are_byte_aligned: flag(b"EncodedByteAlign", false), - encoding: match k { - ..0 => hayro_ccitt::EncodingMode::Group4, - 0 => hayro_ccitt::EncodingMode::Group3_1D, - k => hayro_ccitt::EncodingMode::Group3_2D { k: u32::try_from(k).unwrap_or(u32::MAX) }, - }, - invert_black: false, - }; let stride = (columns as usize).div_ceil(8); - let mut out = - Rows { out: Vec::new(), row: vec![0; stride], x: 0, columns: columns as usize, rows: rows as usize, black_is_1: flag(b"BlackIs1", false) }; - // Rows decoded before damaged data are kept; too few rows are refused by the caller. - let _ = hayro_ccitt::decode(data, &mut out, &mut hayro_ccitt::DecoderContext::new(settings)); - Ok(out.out) + let decode = |aligned: bool| { + let settings = hayro_ccitt::DecodeSettings { + columns, + rows, + end_of_block: flag(b"EndOfBlock", true), + end_of_line: flag(b"EndOfLine", false), + rows_are_byte_aligned: aligned, + encoding: match k { + ..0 => hayro_ccitt::EncodingMode::Group4, + 0 => hayro_ccitt::EncodingMode::Group3_1D, + k => hayro_ccitt::EncodingMode::Group3_2D { k: u32::try_from(k).unwrap_or(u32::MAX) }, + }, + invert_black: false, + }; + let mut out = Rows { + out: Vec::new(), + row: vec![0; stride], + x: 0, + columns: columns as usize, + rows: rows as usize, + black_is_1: flag(b"BlackIs1", false), + }; + // Rows decoded before damaged data are kept; too few rows are refused by the caller. + let _ = hayro_ccitt::decode(data, &mut out, &mut hayro_ccitt::DecoderContext::new(settings)); + out.out + }; + // Byte-aligned rows with end-of-line codes may have the fill end each code on a byte (T.4's + // fill, as TIFF writes it) rather than start it on one; read unaligned, the codes mark the rows + // either way. Codes laid out so under `/EndOfLine false` are misread. + Ok(decode(flag(b"EncodedByteAlign", false) && !flag(b"EndOfLine", false))) } /// The file for one image: its JPEG data as is, or a PNG. An image with transparency (a soft diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 212fa3ca71..19a7e76e37 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1666,9 +1666,9 @@ fn ccitt_fax_images_export_as_their_picture() { /// Group 3 data: rows of run lengths, white first, coded one-dimensionally (ITU-T T.4), from the /// few codes these runs need. Each row starts with an end-of-line code with `eol`, then with -/// `tag` a 1 bit (the next row one-dimensional, as `/K` above 0 has it). With `align` each row -/// starts on a byte, its end-of-line code too. -fn group_3(rows: &[&[u32]], eol: bool, tag: bool, align: bool) -> Vec { +/// `tag` a 1 bit (the next row one-dimensional, as `/K` above 0 has it). Fill bits (0s) go where +/// `fill` says. +fn group_3(rows: &[&[u32]], eol: bool, tag: bool, fill: Fill) -> Vec { let code = |white: bool, run: u32| match (white, run) { (true, 16) => "101010", (true, 32) => "00011011", @@ -1678,7 +1678,9 @@ fn group_3(rows: &[&[u32]], eol: bool, tag: bool, align: bool) -> Vec { }; let mut bits = String::new(); for row in rows { - while align && !bits.len().is_multiple_of(8) { + // The end-of-line code is 12 bits. + let ends_code = if fill == Fill::CodeEnd && eol { 12 } else { 0 }; + while fill != Fill::None && !(bits.len() + ends_code).is_multiple_of(8) { bits.push('0'); } if eol { @@ -1692,6 +1694,15 @@ fn group_3(rows: &[&[u32]], eol: bool, tag: bool, align: bool) -> Vec { bits.as_bytes().chunks(8).map(|c| c.iter().enumerate().fold(0u8, |b, (i, d)| b | (u8::from(*d == b'1') << (7 - i)))).collect() } +/// Where [`group_3`] puts fill bits: none; so each row, its end-of-line code first, starts on a +/// byte; or so each end-of-line code ends on one (T.4's fill, as TIFF writes it). +#[derive(Clone, Copy, Debug, PartialEq)] +enum Fill { + None, + RowStart, + CodeEnd, +} + /// What Export ▸ Images claims for CCITT and for parameters given by reference, each checked /// against the same picture stored another way: Group 3 one-dimensional data (`/K 0`) exports as /// its picture; a CCITT soft mask and a CCITT stencil `/Mask` export as the same masks in Flate; @@ -1742,16 +1753,35 @@ fn group_3_ccitt_masks_and_parameters_by_reference_export_as_their_picture() { // One-dimensional (/K 0) plain, with end-of-line codes, with each row on a byte, and with // both; mixed (/K 1, each row tagged one-dimensional) with end-of-line codes and without. - for (k, eol, align) in [(0, false, false), (0, true, false), (0, false, true), (0, true, true), (1, true, false), (1, false, false)] { + // Byte-aligned with end-of-line codes either way: each code starting on a byte, or ending on + // one (T.4's fill, as TIFF writes it). /EncodedByteAlign is given by reference once, and so is + // a /BlackIs1 that inverts the picture. + let inverted: Vec = picture.iter().map(|white| !white).collect(); + for (k, eol, fill, align_by_reference, black_is_1) in [ + (0, false, Fill::None, false, false), + (0, true, Fill::None, false, false), + (0, false, Fill::RowStart, true, false), + (0, true, Fill::RowStart, false, false), + (0, true, Fill::CodeEnd, false, false), + (1, true, Fill::None, false, false), + (1, false, Fill::None, false, false), + (1, true, Fill::CodeEnd, false, false), + (0, false, Fill::None, false, true), + ] { let mut d = image(&[&grey(1)[..], &fax(k)].concat()); + let align = Object::Bool(fill != Fill::None); + let align = if align_by_reference { Object::Ref(doc.add(align)) } else { align }; + let inverts = Object::Ref(doc.add(Object::Bool(black_is_1))); if let Some(Object::Dict(parms)) = d.get_mut(b"DecodeParms") { parms.set(b"EndOfLine".to_vec(), Object::Bool(eol)); - parms.set(b"EncodedByteAlign".to_vec(), Object::Bool(align)); + parms.set(b"EncodedByteAlign".to_vec(), align); + parms.set(b"BlackIs1".to_vec(), inverts); } - doc.set(r, Object::Stream(Stream::from_raw(d, group_3(&rows, eol, k > 0, align)))); + doc.set(r, Object::Stream(Stream::from_raw(d, group_3(&rows, eol, k > 0, fill)))); + let case = format!("/K {k}, EndOfLine {eol}, fill {fill:?}, BlackIs1 {black_is_1}"); let out = extract_images(&doc, &[0], 0); - assert!(out.skipped.is_empty(), "/K {k}, EndOfLine {eol}, EncodedByteAlign {align}: {:?}", out.skipped); - assert_eq!(exported_white(&out.images[0].data), picture, "/K {k}, EndOfLine {eol}, EncodedByteAlign {align}"); + assert!(out.skipped.is_empty(), "{case}: {:?}", out.skipped); + assert_eq!(exported_white(&out.images[0].data), if black_is_1 { inverted.clone() } else { picture.clone() }, "{case}"); } // A grey picture under the same bits as a soft mask, then as a stencil mask: CCITT against Flate.