mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
M11: decode_ccitt: tests for byte-aligned Group 3 read by the codes in its data
The rule came into decode_ccitt with the decoding it moved from Export ▸ Images (#4335's "Export reads byte-aligned Group 3 by the codes in its data"), where the create test checks it through the export. Here it gets tests of its own: - 32 seeded text-like pictures, 8 to 207 samples wide, at k 0 and 1, in each layout (no codes, with end_of_line and without; codes, each starting on a byte or ending on one, with and without). Each decodes to its picture and, a byte short, to fewer rows, not to other rows read another way. - Group 4 rows on bytes keep their aligned read, end_of_line or not. - The any-input property varies end_of_line and encoded_byte_align too. With the rule this replaced (rows on bytes read unaligned under end_of_line, aligned otherwise) the test fails; cargo-mutants leaves no mutant of the rule's lines alive. The filters README says how rows on bytes are read. 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
5f3fde33d6
commit
f98a5033e2
@@ -43,7 +43,7 @@ decode_ccitt(data, ¶ms, max_rows) -> Result<Vec<u8>, FilterError> // CCITT
|
||||
| ASCIIHexDecode | white space ignored, odd digit padded with 0, `>` EOD (optional), other characters → Corrupt | upper case, newline every 64 digits, `>` |
|
||||
| ASCII85Decode | `<~` prefix, `z`, white space, `~>` (or bare `~`, or none) EOD, final partial group; `z` inside a group, a 1-char final group, overflow or bad characters → Corrupt | `z` for zero groups, newline every 75 characters, `~>` |
|
||||
| RunLengthDecode | 0–127 literal, 129–255 repeat, 128 EOD (optional); truncated run → Corrupt | runs of ≥2 repeat, literals up to 128, 128 |
|
||||
| CCITTFaxDecode | not in a chain (`Unsupported`); `decode_ccitt` decodes Group 3 and Group 4 on its own, as rows of 1-bit samples each on a new byte, black 0 unless `black_is_1`, at most `max_rows` rows (fewer with `/Rows`), the rows before any damage, up to 1,048,576 samples a row (`pdfcraft_cos::Document::decoded_image` calls it for a fax image, after the filters before it) | `encode_g4`: Group 4 (`/K -1`) with an end-of-block mark, 0 bits coded black so `/BlackIs1 false` decodes the same bits; rows packed MSB first, each on a new byte, padding bits and trailing bytes ignored; up to 32,767 samples a row (the encoder counts positions in 16 bits); no rows or columns, or too wide → Unsupported, too few bytes → Corrupt |
|
||||
| CCITTFaxDecode | not in a chain (`Unsupported`); `decode_ccitt` decodes Group 3 and Group 4 on its own, as rows of 1-bit samples each on a new byte, black 0 unless `black_is_1`, at most `max_rows` rows (fewer with `/Rows`), the rows before any damage, up to 1,048,576 samples a row; Group 3 rows on bytes are read by their end-of-line codes when the data starts with one and on bytes otherwise, whatever `end_of_line` says (`pdfcraft_cos::Document::decoded_image` calls it for a fax image, after the filters before it) | `encode_g4`: Group 4 (`/K -1`) with an end-of-block mark, 0 bits coded black so `/BlackIs1 false` decodes the same bits; rows packed MSB first, each on a new byte, padding bits and trailing bytes ignored; up to 32,767 samples a row (the encoder counts positions in 16 bits); no rows or columns, or too wide → Unsupported, too few bytes → Corrupt |
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
+138
-2
@@ -318,6 +318,135 @@ mod tests {
|
||||
assert_eq!(CcittParams::default().columns, 1728, "Table 11");
|
||||
}
|
||||
|
||||
/// Group 3 data: rows of run lengths below 64, white first, coded one-dimensionally with T.4's
|
||||
/// terminating codes (ITU-T T.4 Tables 2 and 3). 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). Fill
|
||||
/// bits (0s) go where `fill` says.
|
||||
fn group_3(rows: &[Vec<u32>], eol: bool, tag: bool, fill: Fill) -> Vec<u8> {
|
||||
// The codes for runs 0 to 63, in order.
|
||||
const WHITE: &str = concat!(
|
||||
"00110101 000111 0111 1000 1011 1100 1110 1111 10011 10100 00111 01000 001000 000011 110100 110101 101010 101011 ",
|
||||
"0100111 0001100 0001000 0010111 0000011 0000100 0101000 0101011 0010011 0100100 0011000 00000010 00000011 00011010 ",
|
||||
"00011011 00010010 00010011 00010100 00010101 00010110 00010111 00101000 00101001 00101010 00101011 00101100 00101101 ",
|
||||
"00000100 00000101 00001010 00001011 01010010 01010011 01010100 01010101 00100100 00100101 01011000 01011001 01011010 ",
|
||||
"01011011 01001010 01001011 00110010 00110011 00110100",
|
||||
);
|
||||
const BLACK: &str = concat!(
|
||||
"0000110111 010 11 10 011 0011 0010 00011 000101 000100 0000100 0000101 0000111 00000100 00000111 000011000 0000010111 ",
|
||||
"0000011000 0000001000 00001100111 00001101000 00001101100 00000110111 00000101000 00000010111 00000011000 ",
|
||||
"000011001010 000011001011 000011001100 000011001101 000001101000 000001101001 000001101010 000001101011 000011010010 ",
|
||||
"000011010011 000011010100 000011010101 000011010110 000011010111 000001101100 000001101101 000011011010 000011011011 ",
|
||||
"000001010100 000001010101 000001010110 000001010111 000001100100 000001100101 000001010010 000001010011 000000100100 ",
|
||||
"000000110111 000000111000 000000100111 000000101000 000001011000 000001011001 000000101011 000000101100 000001011010 ",
|
||||
"000001100110 000001100111",
|
||||
);
|
||||
let code = |white: bool, run: u32| (if white { WHITE } else { BLACK }).split(' ').nth(run as usize).expect("a run below 64");
|
||||
let mut bits = String::new();
|
||||
for row in rows {
|
||||
// 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 {
|
||||
bits.push_str("000000000001");
|
||||
}
|
||||
if tag {
|
||||
bits.push('1');
|
||||
}
|
||||
row.iter().enumerate().for_each(|(i, run)| bits.push_str(code(i % 2 == 0, *run)));
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
/// `rows` text-like rows of `columns` samples as runs, white first, from `seed`: long white runs
|
||||
/// and short black ones; a row may start black.
|
||||
fn text_like(seed: u64, columns: u32, rows: u32) -> Vec<Vec<u32>> {
|
||||
let mut seed = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
|
||||
let mut random = |n: u64| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
(seed % n) as u32
|
||||
};
|
||||
(0..rows)
|
||||
.map(|_| {
|
||||
let (mut runs, mut left) = (Vec::new(), columns);
|
||||
while left > 0 {
|
||||
let run = match runs.len() % 2 {
|
||||
0 if runs.is_empty() => random(31),
|
||||
0 => 1 + random(30),
|
||||
_ => 1 + random(5),
|
||||
};
|
||||
runs.push(run.min(left));
|
||||
left -= run.min(left);
|
||||
}
|
||||
runs
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Rows of runs, white first, packed as `decode_ccitt` gives them (white 1).
|
||||
fn pack(runs: &[Vec<u32>], columns: u32) -> Vec<u8> {
|
||||
let stride = (columns as usize).div_ceil(8);
|
||||
let mut out = vec![0u8; stride * runs.len()];
|
||||
for (y, row) in runs.iter().enumerate() {
|
||||
let mut x = 0;
|
||||
for (i, run) in row.iter().enumerate() {
|
||||
if i % 2 == 0 {
|
||||
(x..x + *run as usize).for_each(|x| out[y * stride + x / 8] |= 0x80 >> (x % 8));
|
||||
}
|
||||
x += *run as usize;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Group 3 rows on bytes in each layout, over many pictures, as how a misreading comes out
|
||||
/// depends on the picture: no codes (as TIFF's Modified Huffman has it), and `end_of_line`
|
||||
/// without them; codes, each starting on a byte or ending on one, `end_of_line` or not. Each
|
||||
/// decodes to its picture, and a byte short to fewer rows, not to other rows read another way.
|
||||
/// Group 4 rows on bytes keep their aligned read, `end_of_line` or not.
|
||||
#[test]
|
||||
fn group_3_rows_on_bytes_read_by_the_codes_in_the_data() {
|
||||
for seed in 0..32u64 {
|
||||
let (columns, rows) = (8 + (seed as u32 * 37) % 200, 2 + seed as u32 % 20);
|
||||
let runs = text_like(seed, columns, rows);
|
||||
let picture = pack(&runs, columns);
|
||||
for k in [0, 1] {
|
||||
for (codes, fill, end_of_line) in [
|
||||
(false, Fill::RowStart, false),
|
||||
(false, Fill::RowStart, true),
|
||||
(true, Fill::RowStart, false),
|
||||
(true, Fill::RowStart, true),
|
||||
(true, Fill::CodeEnd, false),
|
||||
(true, Fill::CodeEnd, true),
|
||||
] {
|
||||
let p = CcittParams { k, columns: i64::from(columns), end_of_line, encoded_byte_align: true, ..CcittParams::default() };
|
||||
let mut data = group_3(&runs, codes, k > 0, fill);
|
||||
let case = format!("seed {seed}, {columns} × {rows}, k {k}, codes {codes}, fill {fill:?}, end_of_line {end_of_line}");
|
||||
assert_eq!(decode_ccitt(&data, &p, rows).unwrap(), picture, "{case}");
|
||||
data.pop();
|
||||
assert!(decode_ccitt(&data, &p, rows).unwrap().len() < picture.len(), "{case}, a byte short");
|
||||
}
|
||||
}
|
||||
}
|
||||
// White rows, each the one code 1 (vertical, no change) and its fill.
|
||||
for end_of_line in [false, true] {
|
||||
let p = CcittParams { k: -1, columns: 64, end_of_line, encoded_byte_align: true, ..CcittParams::default() };
|
||||
assert_eq!(decode_ccitt(&[0x80; 16], &p, 16).unwrap(), [0xFF; 8 * 16], "Group 4, end_of_line {end_of_line}");
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
/// Any bitmap, of noise or of runs, decodes to its own bits, through an independent decoder
|
||||
/// and through `decode_ccitt`.
|
||||
@@ -337,9 +466,16 @@ mod tests {
|
||||
|
||||
/// Whatever it is given, it returns.
|
||||
#[test]
|
||||
fn any_input_returns(data in proptest::collection::vec(0u8..=255, 0..600), columns in 0u32..40_000, rows in 0u32..80, k in -2i64..3) {
|
||||
fn any_input_returns(
|
||||
data in proptest::collection::vec(0u8..=255, 0..600),
|
||||
columns in 0u32..40_000,
|
||||
rows in 0u32..80,
|
||||
k in -2i64..3,
|
||||
end_of_line in proptest::bool::ANY,
|
||||
encoded_byte_align in proptest::bool::ANY,
|
||||
) {
|
||||
let _ = encode_g4(&data, columns, rows);
|
||||
let p = CcittParams { k, columns: i64::from(columns), ..CcittParams::default() };
|
||||
let p = CcittParams { k, columns: i64::from(columns), end_of_line, encoded_byte_align, ..CcittParams::default() };
|
||||
if let Ok(out) = decode_ccitt(&data, &p, rows) {
|
||||
proptest::prop_assert!(out.len() <= (columns as usize).div_ceil(8) * rows as usize);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user