mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:36:13 -06:00
M11: tests for the soft mask in "the smaller wins", the byte totals and Group 3's options
- Eight-bit black-and-white text under a noisy soft mask: the grey settings win, as they shrink the mask far more than packing saves. Counting the packed image without its mask passed every test before. - The packing test checks the report's byte totals exactly. - Export reads Group 3 with end-of-line codes, with rows on a byte (EncodedByteAlign), with both, and mixed (/K 1, each row tagged one-dimensional) with end-of-line codes and without. Decoding /K 1 as one-dimensional, or ignoring EncodedByteAlign, now fails a test. /EndOfLine is passed to the decoder, but no input here tells it apart: the decoder accepts end-of-line codes either way, as ISO 32000-2 (Table 11) requires. 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
a4058c9242
commit
d2c3bda6b5
@@ -1664,9 +1664,11 @@ fn ccitt_fax_images_export_as_their_picture() {
|
||||
assert!(out.images.is_empty() && out.skipped.len() == 1, "columns other than the width: skipped");
|
||||
}
|
||||
|
||||
/// Group 3 data: rows of run lengths, white first, coded one-dimensionally (ITU-T T.4) with no
|
||||
/// end-of-line codes, from the few codes these runs need.
|
||||
fn group_3(rows: &[&[u32]]) -> Vec<u8> {
|
||||
/// 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> {
|
||||
let code = |white: bool, run: u32| match (white, run) {
|
||||
(true, 16) => "101010",
|
||||
(true, 32) => "00011011",
|
||||
@@ -1674,7 +1676,19 @@ fn group_3(rows: &[&[u32]]) -> Vec<u8> {
|
||||
(false, 32) => "000001101010",
|
||||
_ => panic!("no code here for a run of {run}"),
|
||||
};
|
||||
let bits: String = rows.iter().flat_map(|row| row.iter().enumerate().map(|(i, run)| code(i % 2 == 0, *run))).collect();
|
||||
let mut bits = String::new();
|
||||
for row in rows {
|
||||
while align && !bits.len().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()
|
||||
}
|
||||
|
||||
@@ -1726,11 +1740,19 @@ fn group_3_ccitt_masks_and_parameters_by_reference_export_as_their_picture() {
|
||||
};
|
||||
let grey = |bits: i64| [("BitsPerComponent", Object::Int(bits)), ("ColorSpace", Object::name("DeviceGray"))];
|
||||
|
||||
let d = image(&[&grey(1)[..], &fax(0)].concat());
|
||||
doc.set(r, Object::Stream(Stream::from_raw(d, group_3(&rows))));
|
||||
let out = extract_images(&doc, &[0], 0);
|
||||
assert!(out.skipped.is_empty(), "{:?}", out.skipped);
|
||||
assert_eq!(exported_white(&out.images[0].data), picture, "Group 3");
|
||||
// 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)] {
|
||||
let mut d = image(&[&grey(1)[..], &fax(k)].concat());
|
||||
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));
|
||||
}
|
||||
doc.set(r, Object::Stream(Stream::from_raw(d, group_3(&rows, eol, k > 0, align))));
|
||||
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}");
|
||||
}
|
||||
|
||||
// A grey picture under the same bits as a soft mask, then as a stencil mask: CCITT against Flate.
|
||||
let g4 = pdfcraft_filters::encode_g4(&bits, w, h).unwrap();
|
||||
|
||||
@@ -671,6 +671,34 @@ fn eight_bit_black_and_white_takes_the_smaller_of_one_bit_and_the_grey_settings(
|
||||
}
|
||||
}
|
||||
|
||||
/// The smaller of one bit and the grey settings counts the soft mask each leaves: packed, the
|
||||
/// image keeps its mask as it is; resampled by the grey settings, the mask is resampled with it.
|
||||
/// Here clean text, which packs to well under its JPEG, under a noisy mask the grey settings
|
||||
/// shrink by far more than the image's saving: the grey settings win.
|
||||
#[test]
|
||||
fn eight_bit_black_and_white_with_a_soft_mask_counts_the_mask() {
|
||||
let (w, h) = (1200u32, 1500u32);
|
||||
let mut doc = Document::new_empty();
|
||||
let mut seed = 0x2545_F491_4F6C_DD1Du64;
|
||||
let noise: Vec<u8> = (0..w * h)
|
||||
.map(|_| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
(seed >> 24) as u8
|
||||
})
|
||||
.collect();
|
||||
let mask = mono_image(&mut doc, w, h, &noise, None, &[("BitsPerComponent", 8.into()), ("ColorSpace", Object::name("DeviceGray"))]);
|
||||
let grey = [("BitsPerComponent", 8.into()), ("ColorSpace", Object::name("DeviceGray")), ("SMask", Object::Ref(mask))];
|
||||
let img = mono_image(&mut doc, w, h, &eight_bit(&bilevel(w, h, false), w, h), None, &grey);
|
||||
// 288 × 360 pt is 300 ppi, above where the grey settings downsample.
|
||||
page(&mut doc, &[("A", img)], "q 288 0 0 360 20 400 cm /A Do Q");
|
||||
optimize(&mut doc, &Settings::default()).unwrap();
|
||||
let s = stream(&doc, img);
|
||||
assert_eq!((s.dict.int(b"BitsPerComponent"), s.dict.name(b"Filter")), (Some(8), Some(&b"DCTDecode"[..])), "{:?}", s.dict);
|
||||
assert!(stream(&doc, mask).raw.len() < (w * h) as usize / 2, "the mask resampled with it");
|
||||
}
|
||||
|
||||
/// Eight-bit grey images of only black and white (as some scanners and converters write scans)
|
||||
/// become one-bit Group 4, losslessly: the page looks the same, an inverting decode array
|
||||
/// included. Not touched by this: a grey image with any other shade, an indexed image (its samples
|
||||
@@ -724,8 +752,11 @@ fn eight_bit_black_and_white_images_become_one_bit() {
|
||||
assert_eq!(r.images_recompressed, 0, "Retain keeps every image as it is");
|
||||
|
||||
let untouched = [shade, indexed, keyed].map(|r| raw(&doc, r));
|
||||
let old: usize = [plain, inverted].map(|r| raw(&doc, r).len()).iter().sum();
|
||||
let r = optimize(&mut doc, &settings).unwrap();
|
||||
assert_eq!(r.images_recompressed, 2);
|
||||
let new: usize = [plain, inverted].map(|r| raw(&doc, r).len()).iter().sum();
|
||||
assert_eq!((r.image_bytes_before, r.image_bytes_after), (old, new), "the byte totals count both");
|
||||
for i in [plain, inverted] {
|
||||
let s = stream(&doc, i);
|
||||
assert_eq!((s.dict.int(b"BitsPerComponent"), s.dict.name(b"Filter")), (Some(1), Some(&b"CCITTFaxDecode"[..])));
|
||||
|
||||
Reference in New Issue
Block a user