mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
M11: the optimizer stores Group 3 fax images as Group 4 when smaller
A CCITT fax image that wasn't resampled was left as it was, so Group 3 images (the 1-D and 2-D codings fax software writes) stayed Group 3. Group 4 codes each row against the one above and is usually much smaller. Now only Group 4 alone skips decoding; Group 3 is stored again as Group 4 when that is smaller as stored, and Retain still keeps it. It reads an image's CCITT parameters (its /DecodeParms entry with Table 11's defaults) through pdfcraft_cos::Document::fax_params, from #4384. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1ad951e25b
commit
e221d552f9
@@ -61,10 +61,11 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going
|
||||
column. On a 600 ppi text page, Group 4 at 300 ppi is half the size.
|
||||
- **Compression:** CCITT Group 4 (`pdfcraft_filters::encode_g4`), or 1-bit Flate when that is
|
||||
smaller, which only a resampled image or one packed from eight bits may be: a 1-bit image that
|
||||
is only stored again becomes Group 4 or stays. A CCITT image is only decoded to be resampled,
|
||||
unless another filter such as `ASCII85Decode` wraps it, which storing it again drops. With Retain, a resampled image keeps its kind of
|
||||
compression (CCITT, or Flate) and nothing else changes; eight-bit grey is then left to the grey
|
||||
settings. Every change is kept only when it is smaller as stored.
|
||||
is only stored again becomes Group 4 or stays. Group 4 alone is only decoded to be resampled;
|
||||
Group 3, or Group 4 inside another filter such as `ASCII85Decode`, is stored again as Group 4
|
||||
alone when that is smaller. With Retain, a resampled image keeps its kind of compression (CCITT,
|
||||
or Flate) and nothing else changes; eight-bit grey is then left to the grey settings. Every
|
||||
change is kept only when it is smaller as stored.
|
||||
- Apart from the downsampling it is lossless: the samples keep their meaning, so the colour
|
||||
space, decode array and `/Mask` stay. Group 4 wins on scans, whose ragged edges defeat Flate;
|
||||
on clean computer-drawn 1-bit art Flate can be smaller. Not yet: JBIG2, a lone Group 3 image to
|
||||
|
||||
@@ -705,7 +705,8 @@ pub(crate) fn downsample_bits(data: &[u8], w: u32, h: u32, nw: u32, nh: u32, dar
|
||||
/// covers more would turn its tones solid (a 50% screen black, a 25% one white).
|
||||
/// - **Stored** as CCITT Group 4 with `CcittG4`, or as 1-bit Flate when that is smaller, which only
|
||||
/// a resampled image or one packed from eight bits may be: a 1-bit image that is only stored again
|
||||
/// becomes Group 4 or stays. With `Retain`, a resampled image keeps its kind of compression
|
||||
/// becomes Group 4 or stays. Group 4 alone is only decoded to be resampled; Group 3 is stored
|
||||
/// again like any other. With `Retain`, a resampled image keeps its kind of compression
|
||||
/// (CCITT, or Flate), and nothing else changes: eight-bit grey stays a grey image like any
|
||||
/// other, which the grey settings handle.
|
||||
///
|
||||
@@ -714,13 +715,13 @@ fn mono(doc: &Document, s: &Stream, ppi: f64, settings: &MonoSettings) -> Option
|
||||
let d = &s.dict;
|
||||
let downsample = settings.downsample && ppi.is_finite() && ppi > settings.above_ppi && settings.target_ppi > 0.0 && !d.contains(b"SMask");
|
||||
let retain = settings.compression == MonoCompression::Retain;
|
||||
let chain = s.filters();
|
||||
let fax = doc.fax_params(s);
|
||||
// Its kind of compression, which Retain keeps, behind any other filters.
|
||||
let ccitt = matches!(chain.last(), Some((pdfcraft_filters::Filter::CcittFax, _)));
|
||||
// A fax image is only worth decoding to resample it; behind ASCII85Decode, say, storing it
|
||||
// again as Group 4 also drops that filter.
|
||||
let fax_alone = ccitt && chain.len() == 1;
|
||||
if (retain || fax_alone) && !downsample {
|
||||
let ccitt = fax.is_some();
|
||||
// Group 4 alone is only worth decoding to resample it. Group 3, or Group 4 behind
|
||||
// ASCII85Decode, say, can be smaller stored again as Group 4 alone.
|
||||
let g4_alone = fax.is_some_and(|p| p.k < 0) && s.filters().len() == 1;
|
||||
if (retain || g4_alone) && !downsample {
|
||||
return None;
|
||||
}
|
||||
let (data, w, h, eight_bit) = one_bit(doc, s, !retain)?;
|
||||
@@ -747,7 +748,7 @@ fn mono(doc: &Document, s: &Stream, ppi: f64, settings: &MonoSettings) -> Option
|
||||
Some(out) => (out, nw, nh),
|
||||
None => (data, w, h),
|
||||
};
|
||||
if (retain || fax_alone) && !resampled {
|
||||
if (retain || g4_alone) && !resampled {
|
||||
return None;
|
||||
}
|
||||
let mut dict = d.clone();
|
||||
|
||||
@@ -1116,6 +1116,78 @@ fn monochrome_images_keep_what_they_should() {
|
||||
assert_eq!((s.dict.int(b"Width"), s.dict.name(b"Filter")), (Some(600), Some(&b"FlateDecode"[..])), "Retain: Flate stays Flate: {r:?}");
|
||||
}
|
||||
|
||||
/// `rows` coded as CCITT Group 3, one-dimensional (`/K 0`): each row its runs in turn, white first,
|
||||
/// with ITU-T T.4's terminating codes for the few lengths used here.
|
||||
fn group_3(rows: &[&[u32]]) -> Vec<u8> {
|
||||
let code = |white: bool, run: u32| match (white, run) {
|
||||
(true, 0) => "00110101",
|
||||
(true, 8) => "10011",
|
||||
(true, 16) => "101010",
|
||||
(true, 32) => "00011011",
|
||||
(false, 8) => "000101",
|
||||
(false, 16) => "0000010111",
|
||||
(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();
|
||||
bits.as_bytes().chunks(8).map(|c| c.iter().enumerate().fold(0u8, |b, (i, d)| b | (u8::from(*d == b'1') << (7 - i)))).collect()
|
||||
}
|
||||
|
||||
/// A Group 3 fax image is stored again as Group 4 when that is smaller, the same picture (to the
|
||||
/// pixel on the page); with Retain it stays Group 3.
|
||||
#[test]
|
||||
fn group_3_fax_images_become_group_4() {
|
||||
let (w, h) = (64u32, 64u32);
|
||||
let runs: Vec<&[u32]> = [&[32, 32][..], &[16; 4], &[8; 8], &[0, 16, 16, 32]].into_iter().flat_map(|r| std::iter::repeat_n(r, 16)).collect();
|
||||
// White is 1, as CCITTFaxDecode gives it without /BlackIs1.
|
||||
let mut bits = Vec::new();
|
||||
for row in &runs {
|
||||
let mut packed = [0u8; 8];
|
||||
let mut x = 0;
|
||||
for (i, run) in row.iter().enumerate() {
|
||||
for p in x..x + *run as usize {
|
||||
if i % 2 == 0 {
|
||||
packed[p / 8] |= 0x80 >> (p % 8);
|
||||
}
|
||||
}
|
||||
x += *run as usize;
|
||||
}
|
||||
bits.extend(packed);
|
||||
}
|
||||
let g3 = group_3(&runs);
|
||||
let p = pdfcraft_filters::CcittParams { k: 0, columns: w.into(), ..Default::default() };
|
||||
assert_eq!(pdfcraft_filters::decode_ccitt(&g3, &p, h).unwrap(), bits, "the Group 3 data is the picture");
|
||||
|
||||
let mut doc = Document::new_empty();
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Image"));
|
||||
d.set(b"Width".to_vec(), Object::Int(w.into()));
|
||||
d.set(b"Height".to_vec(), Object::Int(h.into()));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(1));
|
||||
d.set(b"ColorSpace".to_vec(), Object::name("DeviceGray"));
|
||||
d.set(b"Filter".to_vec(), Object::name("CCITTFaxDecode"));
|
||||
let mut parms = Dict::new();
|
||||
parms.set(b"K".to_vec(), Object::Int(0));
|
||||
parms.set(b"Columns".to_vec(), Object::Int(w.into()));
|
||||
parms.set(b"Rows".to_vec(), Object::Int(h.into()));
|
||||
d.set(b"DecodeParms".to_vec(), Object::Dict(parms));
|
||||
let fax = doc.add(Object::Stream(Stream::from_raw(d, g3.clone())));
|
||||
page(&mut doc, &[("A", fax)], "q 64 0 0 64 100 600 cm /A Do Q");
|
||||
let before = render(&doc);
|
||||
|
||||
let mut kept = doc.clone();
|
||||
optimize(&mut kept, &Settings { mono: RETAIN, ..Settings::default() }).unwrap();
|
||||
assert_eq!(stream(&kept, fax).raw[..], g3[..], "Retain keeps Group 3");
|
||||
|
||||
let r = optimize(&mut doc, &Settings::default()).unwrap();
|
||||
assert_eq!((r.images_resampled, r.images_recompressed), (0, 1), "{r:?}");
|
||||
let s = stream(&doc, fax);
|
||||
assert_eq!(doc.fax_params(&s).map(|p| p.k), Some(-1), "Group 4");
|
||||
assert!(s.raw.len() < g3.len(), "{} bytes, from {}", s.raw.len(), g3.len());
|
||||
assert_eq!(doc.decoded_image(&s).unwrap(), bits, "the same picture");
|
||||
assert!(render(&doc) == before, "the page looks the same");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_choose_what_happens() {
|
||||
let mut doc = Document::new_empty();
|
||||
|
||||
@@ -7384,8 +7384,8 @@ milestone = "M11"
|
||||
status = "partial"
|
||||
commands = ["optimize.advanced", "optimize.reduce"]
|
||||
tools = ["doc_optimize", "doc_reduce"]
|
||||
evidence = ["crates/optimize/tests/optimize.rs::eight_bit_black_and_white_images_become_one_bit", "crates/filters/src/ccitt.rs::bitmaps_round_trip", "crates/filters/src/ccitt.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/filters/src/ccitt.rs::what_it_cannot_encode_is_an_error", "crates/optimize/tests/optimize.rs::black_and_white_images_become_ccitt_group_4", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_ccitt_group_4", "crates/ui-egui/tests/it/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy"]
|
||||
notes = "CCITT Group 4, lossless: PDF Optimizer ▸ Images ▸ Monochrome Images (on by default, so Reduce File Size does it too; doc_optimize mono) stores a 1-bit image of one colour component (grey, indexed, a separation) or an image mask, kept with no filter or general-purpose ones, as Group 4 when that is smaller as stored; the samples decode to the same bits, so colour space, decode array and masks stay. An eight-bit grey image of only black and white is packed to one bit first, then stored the same way (indexed images and colour-key masks aside), unless the grey settings make it smaller still, as they can a dithered scan downsampled to JPEG, its soft mask counted. Group 4 wins on scans; on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. CCITT fax images are decoded to be downsampled (optimize.downsample-images), and a resampled image is stored as Group 4 or 1-bit Flate, whichever is smaller. Not yet: JBIG2 (lossless or lossy, which Adobe's help says beats CCITT for monochrome), a lone Group 3 image to Group 4 without resampling (one behind another filter is stored again as Group 4; byte-aligned Group 3 with end-of-line codes but none before the first row, not as T.4 sends them, as PdfCraft draws it, which at /K 0 may be other rows than were coded), inline images, soft masks and /Mask images, Group 4 for rows wider than 32,767 samples."
|
||||
evidence = ["crates/optimize/tests/optimize.rs::eight_bit_black_and_white_images_become_one_bit", "crates/filters/src/ccitt.rs::bitmaps_round_trip", "crates/filters/src/ccitt.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/filters/src/ccitt.rs::what_it_cannot_encode_is_an_error", "crates/optimize/tests/optimize.rs::black_and_white_images_become_ccitt_group_4", "crates/optimize/tests/optimize.rs::group_3_fax_images_become_group_4", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_ccitt_group_4", "crates/ui-egui/tests/it/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy"]
|
||||
notes = "CCITT Group 4, lossless: PDF Optimizer ▸ Images ▸ Monochrome Images (on by default, so Reduce File Size does it too; doc_optimize mono) stores a 1-bit image of one colour component (grey, indexed, a separation) or an image mask, kept with no filter or general-purpose ones, as Group 4 when that is smaller as stored; the samples decode to the same bits, so colour space, decode array and masks stay. An eight-bit grey image of only black and white is packed to one bit first, then stored the same way (indexed images and colour-key masks aside), unless the grey settings make it smaller still, as they can a dithered scan downsampled to JPEG, its soft mask counted. Group 4 wins on scans; on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. Group 3 fax images are stored again as Group 4 when that is smaller. CCITT fax images are decoded to be downsampled (optimize.downsample-images), and a resampled image is stored as Group 4 or 1-bit Flate, whichever is smaller. Not yet: JBIG2 (lossless or lossy, which Adobe's help says beats CCITT for monochrome), inline images, soft masks and /Mask images, Group 4 for rows wider than 32,767 samples."
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.only-if-smaller"
|
||||
|
||||
Reference in New Issue
Block a user