mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
M11: Export ▸ Images reads byte-aligned Group 3 whichever way its fill falls
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2c7bbbae05
commit
e8f3c6bc3e
@@ -196,25 +196,37 @@ fn ccitt(doc: &Document, s: &Stream, data: &[u8], p: Dict) -> Result<Vec<u8>, 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
|
||||
|
||||
@@ -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<u8> {
|
||||
/// `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<u8> {
|
||||
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<u8> {
|
||||
};
|
||||
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<u8> {
|
||||
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<bool> = 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.
|
||||
|
||||
Reference in New Issue
Block a user