diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 7bb1b80066..212fa3ca71 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -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 { +/// 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 { 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 { (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(); diff --git a/crates/optimize/tests/optimize.rs b/crates/optimize/tests/optimize.rs index 73ed54d6b1..c9a5203ab6 100644 --- a/crates/optimize/tests/optimize.rs +++ b/crates/optimize/tests/optimize.rs @@ -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 = (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"[..])));