From d416d6a36e8c8b8a748851f0bafb150cd93a1420 Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Mon, 26 Oct 2026 13:55:01 +0100 Subject: [PATCH] M11: say how export and redaction treat JBIG2 images they can't read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export ▸ Images skips an image whose JBIG2 page has another width or more pixels than an export takes; a JBIG2 stencil mask refused the same way is left out and the image exported opaque, as JPX and CCITT masks are, which the export test now checks. The README and parity note said only "is skipped". And redaction doesn't decode JBIG2 or Group 4 yet, so a mark over part of an image the optimizer stored either way removes the whole image; the optimizer README and parity note now say so (#4384 adds Group 4). Found in pre-push review. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG --- crates/create/README.md | 4 +++- crates/create/src/tests.rs | 6 ++++++ crates/optimize/README.md | 8 ++++---- parity/acrobat-features.toml | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/create/README.md b/crates/create/README.md index f49e53dad4..05395656a3 100644 --- a/crates/create/README.md +++ b/crates/create/README.md @@ -26,7 +26,9 @@ Every function returns a `pdfcraft_cos::Document`; the caller writes it. Image export and Save Image As apply each lossless image's Decode pair before converting its color components. JBIG2 images and masks (JBIG2 as their only filter) are decoded with `hayro-jbig2`, their `/JBIG2Globals` included, -as `JBIG2Decode` gives them: black as 0. A JBIG2 page of another width than the image is skipped. +as `JBIG2Decode` gives them: black as 0. An image whose JBIG2 page has another width than the image, or more +pixels than an exported image may have, is skipped; such a mask is left out and the image exported opaque, as JPX +and CCITT masks are. Indexed Decode values select palette entries after rounding and clipping. Color-key masks compare the original samples. Missing, null or malformed Decode arrays keep the default mapping. Soft-mask Decode intervals map samples to opacity before the mask is resized, including reversed ranges and indirect entries. diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 9b54787fc9..98fbfd7eee 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1495,6 +1495,12 @@ fn jbig2_images_export_as_their_picture() { large[11..15].copy_from_slice(&20_000u32.to_be_bytes()); large[15..19].copy_from_slice(&10_000u32.to_be_bytes()); assert!(skipped(&page(20_000, 4, &large, None, false)), "a page over the export's size"); + // Refused the same ways as a stencil /Mask, it is left out: the image exports opaque. + for (width, height, mask) in [(w + 8, h, &jbig2), (20_000, 4, &large)] { + let rgba = exported_rgba(&page(width, height, mask, None, true)).1; + assert_eq!(rgba.len() as i64, width * height * 4, "a {width}-wide mask"); + assert!(alphas(&rgba).iter().all(|a| *a == 255), "a refused {width}-wide mask is left out"); + } // A page of exactly the budget, 20,000 × 5,000, is decoded; one row more is refused. for (rows, refused) in [(5_000u32, false), (5_001, true)] { large[15..19].copy_from_slice(&rows.to_be_bytes()); diff --git a/crates/optimize/README.md b/crates/optimize/README.md index 9c1715ad81..a1c47697b5 100644 --- a/crates/optimize/README.md +++ b/crates/optimize/README.md @@ -44,10 +44,10 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. Not yet: JBIG2 symbol coding (lossy or lossless), monochrome downsampling, inline images, soft masks and `/Mask` images, and Group 4 for rows wider than 32,767 samples (JBIG2 takes rows of up to - 1,048,576). Redaction doesn't decode Group 4 yet: a mark over part of an image stored this way - removes the whole image (nothing leaks). Nor does anything that exports an image: Export ▸ - Images and Save Image As refuse it, Copy Image fails, and Word, PowerPoint and HTML export leave - the picture out. + 1,048,576). Redaction doesn't decode JBIG2 or Group 4 yet: a mark over part of such an image + removes the whole image (nothing leaks). Export reads JBIG2, but nothing that exports an image + reads Group 4 yet: Export ▸ Images and Save Image As refuse it, Copy Image fails, and Word, + PowerPoint and HTML export leave the picture out. - **Discard objects:** thumbnails, alternate images, document tags, print settings. - **Clean up:** Flate for streams with no filter; for streams with an outer ASCII85 or ASCIIHex layer, that layer is removed when the result is smaller (the inner filters keep their encoded diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e6688347af..84f7981676 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -7165,7 +7165,7 @@ status = "shipped" commands = ["export.all_images"] tools = ["doc_export_all_images"] evidence = ["crates/create/src/tests.rs::images_export_as_jpeg_unchanged_and_others_as_png", "crates/create/src/tests.rs::indexed_and_one_bit_images_decode", "crates/create/src/tests.rs::jpeg_images_with_a_soft_mask_export_as_png_with_alpha", "crates/create/src/tests.rs::soft_masks_of_another_size_and_stencil_and_colour_key_masks_become_alpha", "crates/create/src/tests.rs::soft_mask_decode_ranges_become_exported_alpha", "crates/create/src/tests.rs::soft_mask_decode_maps_packed_and_jpeg_masks_before_resizing", "crates/automation/tests/it/automation.rs::exporting_all_images_through_tools", "crates/ui-egui/tests/it/export.rs::export_dialogs_write_images_and_text", "crates/create/src/tests.rs::lossless_decode_maps_each_component_before_colour_conversion", "crates/create/src/tests.rs::lossless_indexed_decode_rounds_in_index_units_then_clamps", "crates/create/src/tests.rs::jbig2_images_export_as_their_picture", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_jbig2"] -notes = "each image once (page resources and nested forms); JPEG unchanged unless it has transparency, others PNG (gray/RGB/CMYK/ICC/Indexed, 1–16 bpc, stencil masks); transparency as PNG alpha: /SMask (scaled to the image size, /Matte un-blended), stencil /Mask and colour-key /Mask, JPEG images included (#645); JBIG2 images and masks decode (JBIG2 as the only filter, with /JBIG2Globals; a JBIG2 page of another width is skipped); JPX/CCITT masks are left out (image exported opaque); JPX/CCITT/separations, and JBIG2 inside other filters, reported as skipped; minimum size filter. Lossless images apply per-component Decode; Indexed values round and clip before palette lookup. Missing, null or malformed Decode arrays keep the default mapping. Soft-mask Decode intervals preserve opacity before resizing, including indirect arrays and endpoints. Still limited: 16-bit samples use high bytes; general JPEG image Decode, ICC transforms, and calibrated colors remain unsupported." +notes = "each image once (page resources and nested forms); JPEG unchanged unless it has transparency, others PNG (gray/RGB/CMYK/ICC/Indexed, 1–16 bpc, stencil masks); transparency as PNG alpha: /SMask (scaled to the image size, /Matte un-blended), stencil /Mask and colour-key /Mask, JPEG images included (#645); JBIG2 images and masks decode (JBIG2 as the only filter, with /JBIG2Globals; an image whose JBIG2 page has another width, or more pixels than an export takes, is skipped, and such a mask is left out, the image exported opaque); JPX/CCITT masks are left out (image exported opaque); JPX/CCITT/separations, and JBIG2 inside other filters, reported as skipped; minimum size filter. Lossless images apply per-component Decode; Indexed values round and clip before palette lookup. Missing, null or malformed Decode arrays keep the default mapping. Soft-mask Decode intervals preserve opacity before resizing, including indirect arrays and endpoints. Still limited: 16-bit samples use high bytes; general JPEG image Decode, ICC transforms, and calibrated colors remain unsupported." [[feature]] id = "create.export-html" @@ -7385,7 +7385,7 @@ status = "partial" commands = ["optimize.advanced", "optimize.reduce"] tools = ["doc_optimize", "doc_reduce"] evidence = ["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/filters/src/mq.rs::the_annex_h_test_sequence_encodes_to_its_bytes", "crates/filters/src/jbig2.rs::bitmaps_round_trip", "crates/filters/src/jbig2.rs::a_page_of_text_like_marks_round_trips_and_shrinks", "crates/optimize/tests/optimize.rs::black_and_white_images_become_jbig2", "crates/automation/tests/it/automation.rs::doc_optimize_stores_black_and_white_images_as_jbig2", "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 (the default, so Reduce File Size does it too) or JBIG2, both lossless: PDF Optimizer ▸ Images ▸ Monochrome Images (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. Group 4 wins on scans; on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. JBIG2 is a generic region (template 0, typical prediction), about a third smaller than Group 4 on 300 dpi text; a document below PDF 1.4 gets /Version 1.4. Not yet: JBIG2 symbol coding (text regions with symbol dictionaries, where JBIG2's larger gains on text come from, and its lossy mode), JBIG2Globals, monochrome downsampling, inline images, soft masks and /Mask images, Group 4 for rows wider than 32,767 samples (JBIG2 takes rows of up to 1,048,576). Redaction doesn't decode Group 4 yet (a mark over part of such an image removes it whole; nothing leaks), nor does anything that exports an image: Export ▸ Images and Save Image As refuse it, Copy Image fails, and Word, PowerPoint and HTML export leave the picture out." +notes = "CCITT Group 4 (the default, so Reduce File Size does it too) or JBIG2, both lossless: PDF Optimizer ▸ Images ▸ Monochrome Images (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. Group 4 wins on scans; on clean computer-drawn 1-bit art Flate can be smaller, and the image then stays. JBIG2 is a generic region (template 0, typical prediction), about a third smaller than Group 4 on 300 dpi text; a document below PDF 1.4 gets /Version 1.4. Not yet: JBIG2 symbol coding (text regions with symbol dictionaries, where JBIG2's larger gains on text come from, and its lossy mode), JBIG2Globals, monochrome downsampling, inline images, soft masks and /Mask images, Group 4 for rows wider than 32,767 samples (JBIG2 takes rows of up to 1,048,576). Redaction doesn't decode JBIG2 or Group 4 yet: a mark over part of such an image removes the whole image (nothing leaks). Export reads JBIG2, but nothing that exports an image reads Group 4 yet: Export ▸ Images and Save Image As refuse it, Copy Image fails, and Word, PowerPoint and HTML export leave the picture out." [[feature]] id = "optimize.only-if-smaller"