M8: redaction clears the covered pixels of JBIG2 images instead of removing them

Part of #283. A scan stored as JBIG2 (as the optimizer writes them) is decoded
through Document::decoded_image_within, its /JBIG2Globals included, and its covered
pixels cleared in a Flate copy that refers to no globals, as CCITT fax images are.
Before, any JBIG2 image under a mark was removed whole, blanking the page.

Still removed whole (fail-closed): a JBIG2 page of another size than the image,
which decoded_image refuses (the renderer draws the whole page into the image, so
the rows cleared wouldn't be the ones shown); globals that can't be read; damaged
data; an image claiming more than one bit or one component, even where its rows
take as many bytes; a page far past the image's raster, refused before it is
decoded.

Globals no other image uses leave the file with the original on the full rewrite
(tested); globals another image still uses stay, and the README and parity note say
they may hold the shapes of marks from under the area, not where they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-10-30 18:03:06 +01:00
1 parent ea6c329731
commit 78fc1e5568
6 files changed
+188 -28

No files matched your search

+11 -5
View File
@@ -18,10 +18,15 @@ clear_marks(&mut doc, None)?; // remove marks without app
more than a fifth of its size (or a point) both ways; zero-size text goes when its origin is
inside;
- **images**: fully covered → removed; partly covered → a copy with the covered pixels cleared
(8/16-bit and 1/2/4-bit images, image masks; Flate/LZW/RL/A85/AHx, and CCITT fax as the last
filter, decoded through `Document::decoded_image_within`; `/DecodeParms` given by reference,
numbers inside included, count). Codecs redaction doesn't decode (DCT, JPX, JBIG2) are removed
whole (fail-closed), and so is Group 3 on bytes (`/K` 0 or more, `/EncodedByteAlign`): PdfCraft's
(8/16-bit and 1/2/4-bit images, image masks; Flate/LZW/RL/A85/AHx, and CCITT fax or JBIG2 as
the last filter, decoded through `Document::decoded_image_within`, a JBIG2 image with its
`/JBIG2Globals`; `/DecodeParms` given by reference, numbers inside included, count). The copy
is Flate, and refers to no globals: when no other image uses them, they leave the file with the
original on the full rewrite; when one does, they stay as they are, and may hold the shapes of
marks under the area (not where they were). Codecs redaction doesn't decode (DCT, JPX) are
removed whole (fail-closed), and so is a JBIG2 image whose page isn't its size (the renderer
draws the whole page into the image, so the rows cleared wouldn't be the ones shown) or of more
than one bit or component, and Group 3 on bytes (`/K` 0 or more, `/EncodedByteAlign`): PdfCraft's
renderer draws some of its layouts (the fill ending each end-of-line code on a byte) as other
rows than the decoder reads, so a mark over what is shown needn't cover what is cleared; inline images under an area are removed. Images with separate `/SMask`
or `/Mask` data, and images whose data decodes to more than three times their declared raster
@@ -104,4 +109,5 @@ Patterns are heuristic: dates, protocol identifiers and longer number runs are r
Custom code sets aren't supported yet.
Not yet: pattern locales, struct-tree/alt-text cleanup, metadata and hidden-information
sanitising (M8.4), re-encoding of DCT images (they are removed instead).
sanitising (M8.4), re-encoding of DCT images (they are removed instead), and storing a cleared black-and-white
image as CCITT Group 4 or JBIG2 again (it is Flate, larger; the optimizer makes it smaller).
+19 -14
View File
@@ -1,10 +1,10 @@
//! Clearing the pixels of an image under redaction regions. The image is decoded, every pixel
//! whose footprint intersects a region is set to zero (no ink for image masks), and the result is
//! written as a new Flate image; the original object is untouched (other pages may use it).
//! CCITT fax images decode through `Document::decoded_image_within`, but for Group 3 on bytes,
//! which PdfCraft's renderer may draw as other rows. Images whose codec PdfCraft can't decode
//! (DCT, JPX, JBIG2), and Group 3 on bytes, return `None`, and the caller removes the whole image
//! instead; that is fail-closed.
//! CCITT fax and JBIG2 images decode through `Document::decoded_image_within`, but for Group 3 on
//! bytes, which PdfCraft's renderer may draw as other rows, and a JBIG2 page of other rows than the
//! image's. Images whose codec PdfCraft can't decode (DCT, JPX), and those, return `None`, and the
//! caller removes the whole image instead; that is fail-closed.
use pdfcraft_content::{Matrix, overlaps};
use pdfcraft_cos::{Document, Object, Stream};
@@ -53,18 +53,20 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]]
b"FlateDecode" | b"Fl" | b"LZWDecode" | b"LZW" | b"ASCII85Decode" | b"A85" | b"ASCIIHexDecode" | b"AHx" | b"RunLengthDecode" | b"RL"
)
};
// A CCITT fax image (its last filter) decodes through `Document::decoded_image_within`, after the
// filters before it, so a scan stored as Group 4 is cleared like any other image.
// A CCITT fax or JBIG2 image (its last filter) decodes through `Document::decoded_image_within`,
// after the filters before it, so a scan stored as Group 4 or JBIG2 is cleared like any other
// image.
let fax = |f: &Object| f.as_name().is_some_and(|n| matches!(n, b"CCITTFaxDecode" | b"CCF"));
let jbig2 = |f: &Object| f.as_name() == Some(b"JBIG2Decode".as_slice());
let readable = |f: &Object| f.as_name().is_some_and(supported);
let (ok, is_fax) = match s.dict.get(b"Filter") {
None => (true, false),
Some(f @ Object::Name(_)) => (readable(f) || fax(f), fax(f)),
let (ok, is_fax, is_jbig2) = match s.dict.get(b"Filter") {
None => (true, false, false),
Some(f @ Object::Name(_)) => (readable(f) || fax(f) || jbig2(f), fax(f), jbig2(f)),
Some(Object::Array(a)) => match a.split_last() {
Some((last, before)) if fax(last) => (before.iter().all(readable), true),
_ => (a.iter().all(readable), false),
Some((last, before)) if fax(last) || jbig2(last) => (before.iter().all(readable), fax(last), jbig2(last)),
_ => (a.iter().all(readable), false, false),
},
_ => (false, false),
_ => (false, false, false),
};
if !ok {
return None;
@@ -83,7 +85,8 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]]
let (w, h) = (w as usize, h as usize);
let mask = matches!(s.dict.get(b"ImageMask"), Some(Object::Bool(true)));
let (ncomp, bpc) = if mask { (1, 1) } else { (components(doc, s)?, s.dict.int(b"BitsPerComponent")? as usize) };
if !matches!(bpc, 1 | 2 | 4 | 8 | 16) {
// JBIG2 codes one-bit images of one component (ISO 32000-2 §7.4.7).
if !matches!(bpc, 1 | 2 | 4 | 8 | 16) || (is_jbig2 && (ncomp, bpc) != (1, 1)) {
return None;
}
// Every size comes from the file: multiply checked, so an overflowing product refuses the image
@@ -105,7 +108,9 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]]
bytes.checked_add(h)?.checked_mul(3)?.checked_add(4096)?
};
// Through the document, which reads `/DecodeParms` given by reference (`Stream::decoded*`
// alone would skip such a predictor, and the cleared bytes would then miss the covered pixels).
// alone would skip such a predictor, and the cleared bytes would then miss the covered pixels),
// and refuses a JBIG2 page of another size than the image: the renderer draws the whole page
// into the image, so the rows cleared wouldn't be the ones a mark covers on screen.
let mut data = doc.decoded_image_within(s, allowance).ok()?;
if data.len() < bytes {
return None;
+148
View File
@@ -736,6 +736,154 @@ fn a_busy_fax_image_behind_another_filter_is_cleared() {
assert_eq!((report.images_cleared, report.images_removed), (1, 0));
}
/// `/JBIG2Globals` data a decoder skips: one extension segment (ISO/IEC 14492 7.4.14) of a type it
/// needn't read, holding `secret`; its 11-byte header gives the length of the data after it.
fn jbig2_globals(secret: &[u8]) -> Vec<u8> {
let mut g = vec![0, 0, 0, 0, 0x3E, 0, 0];
g.extend_from_slice(&u32::try_from(4 + secret.len()).unwrap().to_be_bytes());
g.extend_from_slice(&[0, 0, 0, 1]);
g.extend_from_slice(secret);
g
}
/// A JBIG2 image (a scan the optimizer stored as JBIG2) has its covered pixels cleared in a copy,
/// like a fax image, instead of being removed whole: with `/JBIG2Globals`, its `/DecodeParms` by
/// reference, behind another filter with its globals at its own place, or as a mask, its decode
/// array inverting it or not.
#[test]
fn a_jbig2_image_has_its_covered_pixels_cleared_instead_of_being_removed() {
// 16 × 2, all 1 bits (white), or all 0 bits (a mask painting everywhere); the left half of
// the top row is covered.
let white = pdfcraft_filters::encode_jbig2_generic(&[0xFF; 4], 16, 2).unwrap();
let painted = pdfcraft_filters::encode_jbig2_generic(&[0x00; 4], 16, 2).unwrap();
let hex: Vec<u8> = white.iter().map(|b| format!("{b:02x}")).collect::<String>().bytes().chain(*b">").collect();
let globals = stream("", jbig2_globals(b"shared"));
let image = "/Type /XObject /Subtype /Image /Width 16 /Height 2 /BitsPerComponent 1";
for (dict, data, extra, cleared) in [
(format!("{image} /ColorSpace /DeviceGray /Filter /JBIG2Decode"), white.clone(), vec![], [0x00, 0xFF, 0xFF, 0xFF]),
(
format!("{image} /ColorSpace /DeviceGray /Filter /JBIG2Decode /DecodeParms 8 0 R"),
white.clone(),
vec![b"<< /JBIG2Globals 9 0 R >>".to_vec(), globals.clone()],
[0x00, 0xFF, 0xFF, 0xFF],
),
(
format!("{image} /ColorSpace /DeviceGray /Filter [/ASCIIHexDecode /JBIG2Decode] /DecodeParms [null << /JBIG2Globals 8 0 R >>]"),
hex,
vec![globals],
[0x00, 0xFF, 0xFF, 0xFF],
),
// A mask's 0 bits paint: cleared bits are set to 1, which paints nothing.
(format!("{image} /ImageMask true /Filter /JBIG2Decode"), painted, vec![], [0xFF, 0x00, 0x00, 0x00]),
// With /Decode [1 0] a mask's 1 bits paint, and cleared bits are 0.
(format!("{image} /ImageMask true /Decode [1 0] /Filter /JBIG2Decode"), white, vec![], [0x00, 0xFF, 0xFF, 0xFF]),
] {
let mut objs = vec![stream(&dict, data)];
objs.extend(extra);
let mut doc = one_page(b"q 160 0 0 20 0 0 cm /Im Do Q", "/XObject << /Im 7 0 R >>", objs);
mark(&mut doc, 0, &[[0.0, 12.0, 75.0, 18.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!((report.images_cleared, report.images_removed), (1, 0), "{dict}");
let saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
assert_eq!(named_image(&saved, 0, b"PCRedacted1").as_deref(), Some(&cleared[..]), "{dict}");
}
}
/// A JBIG2 image is removed whole when its covered pixels can't be cleared for certain: a JBIG2
/// page taller than the image (the renderer draws the whole page into the image, so the rows cleared
/// wouldn't be the ones shown)
/// or shorter, or of another width; globals that can't be read; data cut short; an image of more
/// than one bit or component, which JBIG2 doesn't code, even when its rows take as many bytes; and
/// a page far past the image's raster, which isn't decoded.
#[test]
fn a_jbig2_image_that_cannot_be_cleared_for_certain_is_removed_whole() {
let white = |rows: u32| pdfcraft_filters::encode_jbig2_generic(&vec![0xFF; 2 * rows as usize], 16, rows).unwrap();
// The page information (the first segment's data, after its 11-byte header) claiming 20,000
// rows of 16.
let mut tall = white(2);
tall[15..19].copy_from_slice(&20_000u32.to_be_bytes());
let image = |size: &str, more: &str| {
format!("/Type /XObject /Subtype /Image {size} /BitsPerComponent 1 /ColorSpace /DeviceGray /Filter /JBIG2Decode {more}")
};
let size = "/Width 16 /Height 2";
for (dict, data, extra) in [
(image(size, ""), white(3), vec![]),
(image("/Width 16 /Height 3", ""), white(2), vec![]),
(image("/Width 15 /Height 2", ""), white(2), vec![]),
(image(size, "/DecodeParms << /JBIG2Globals 8 0 R >>"), white(2), vec![stream("", b"not JBIG2")]),
(image(size, ""), white(2)[..20].to_vec(), vec![]),
(image(size, "").replace("/BitsPerComponent 1", "/BitsPerComponent 8"), white(2), vec![]),
(image(size, "").replace("/DeviceGray", "/DeviceRGB"), white(2), vec![]),
// Two pixels of RGB at a bit take a byte a row, as JBIG2's rows do.
(
image("/Width 2 /Height 2", "").replace("/DeviceGray", "/DeviceRGB"),
pdfcraft_filters::encode_jbig2_generic(&[0xFF; 2], 2, 2).unwrap(),
vec![],
),
(image(size, ""), tall, vec![]),
] {
let mut objs = vec![stream(&dict, data)];
objs.extend(extra);
let mut doc = one_page(b"q 160 0 0 20 0 0 cm /Im Do Q", "/XObject << /Im 7 0 R >>", objs);
mark(&mut doc, 0, &[[0.0, 12.0, 75.0, 18.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!((report.images_cleared, report.images_removed), (0, 1), "{dict}");
}
}
/// A scan stored as JBIG2 symbols, with globals, keeps every pixel outside the area once it is
/// redacted and saved, and the pixels under it are cleared; neither its JBIG2 code nor its globals,
/// which no other image uses, stay in the file.
#[test]
fn a_redacted_jbig2_scan_keeps_neither_its_code_nor_its_globals() {
const LETTERS: [[&str; 7]; 3] = [
["#####", "#....", "####.", "#....", "#....", "#....", "#####"],
[".###.", "#...#", "#....", "#....", "#....", "#...#", ".###."],
["#...#", "#...#", "#####", "#...#", "#...#", "#...#", "#...#"],
];
let (w, h) = (400usize, 40usize);
let stride = w.div_ceil(8);
let mut bits = vec![0xFFu8; stride * h];
for (line, top) in [2usize, 12, 22, 32].into_iter().enumerate() {
for (k, x) in (3..w - 6).step_by(7).enumerate() {
for (dy, row) in LETTERS[(k + line) % 3].iter().enumerate() {
for (dx, _) in row.bytes().enumerate().filter(|(_, c)| *c == b'#') {
bits[(top + dy) * stride + (x + dx) / 8] &= !(0x80 >> ((x + dx) % 8));
}
}
}
}
let coded = pdfcraft_filters::encode_jbig2(&bits, w as u32, h as u32).unwrap();
assert!(coded.len() < pdfcraft_filters::encode_jbig2_generic(&bits, w as u32, h as u32).unwrap().len(), "symbol coded");
let dict = format!(
"/Type /XObject /Subtype /Image /Width {w} /Height {h} /BitsPerComponent 1 /ColorSpace /DeviceGray /Filter /JBIG2Decode /DecodeParms << /JBIG2Globals 8 0 R >>"
);
// Half a point a pixel, its top row at y 120.
let mut doc = one_page(
b"q 200 0 0 20 0 100 cm /Im Do Q",
"/XObject << /Im 7 0 R >>",
vec![stream(&dict, coded), stream("", jbig2_globals(b"LEAK_GLOBALS"))],
);
// Columns 0 to 99 of rows 0 to 12.
mark(&mut doc, 0, &[[0.25, 113.75, 49.75, 120.0]], "");
let report = apply(&mut doc, None).unwrap();
assert_eq!((report.images_cleared, report.images_removed), (1, 0));
let mut cleared = bits.clone();
for y in 0..=12 {
for x in 0..=99 {
cleared[y * stride + x / 8] &= !(0x80 >> (x % 8));
}
}
let saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
assert_eq!(named_image(&saved, 0, b"PCRedacted1"), Some(cleared));
assert_no_secret(&doc, b"LEAK_GLOBALS");
for (r, object) in saved.scan_objects() {
if let Object::Stream(s) = &*object {
assert_ne!(s.dict.name(b"Filter"), Some(&b"JBIG2Decode"[..]), "the JBIG2 image stays as {r:?}");
}
}
}
/// Apply Redactions gives the page its own pruned /Resources, but the dictionary it drew through
/// before (inherited from the /Pages node, or shared with another page) still named the removed
/// image and form, so a full-rewrite save kept them.
+4 -4
View File
@@ -71,10 +71,10 @@ The PDF object graph *is* the model (`crates/engine/README.md` §The editing mod
## File I/O
- PDF in: any revision chain, xref tables or streams, hybrid files, damaged files (repaired and
logged), all standard-security revisions. Image codecs (DCT, JPX, JBIG2) decode through the
bootstrap renderer; CCITT decodes through `pdfcraft-filters` (`decode_ccitt`, behind
`Document::decoded_image`) for export, redaction and the optimizer, and through the renderer
for display. Export ▸ Images decodes JPEG itself.
logged), all standard-security revisions. Image codecs (DCT, JPX) decode through the bootstrap
renderer; CCITT and JBIG2 decode through `pdfcraft-filters` (`decode_ccitt`, `decode_jbig2`,
behind `Document::decoded_image`) for export and redaction, CCITT for the optimizer too, and
through the renderer for display. Export ▸ Images decodes JPEG itself.
- PDF out: incremental or full saves, object streams, re-encryption, resource de-duplication when
combining.
- Other formats: images and text in (`create`); PNG/JPEG/TIFF, text, RTF, Word and HTML out
+3 -2
View File
@@ -2,7 +2,7 @@
> Integration update 2026-10-30: lossless JBIG2 generic and exact-copy symbol coding are included; no lossy substitution. Combined tests passed at bdf5620; the subsequent review fixes require fresh CI.
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (§7.4: redaction decodes CCITT fax images, Group 3 on bytes aside; earlier, §7.4: CCITT decoding in `pdfcraft-filters`) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (§7.4: JBIG2 decoding in `pdfcraft-filters`, so redaction clears JBIG2 scans; earlier, §7.4: redaction decodes CCITT fax images, Group 3 on bytes aside) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
How much of ISO 32000-2 (PDF 2.0) and the PDF subset standards PdfCraft implements, set against
what Acrobat Pro implements. This is PdfCraft's equivalent of a "geometry" or "codec" checklist:
@@ -23,7 +23,7 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2
|---|---|---|---|---|
| §7.2–7.5 | Syntax, objects, file structure, xref tables and streams, object streams, hybrid files, incremental updates | full | **own**, tolerant, repairs damaged files | `crates/cos`; corpus 958 files; fuzzed nightly |
| §7.4 | Stream filters: Flate, LZW, ASCII85, ASCIIHex, RunLength, predictors | full | **own** | `crates/filters` |
| §7.4 | Image filters: DCT, JPX, JBIG2, CCITTFax | full | **borrowed** (decode); CCITT Group 4 and lossless JBIG2 encoding | `core.image-filters` partial; own codecs M1.1/M1.2; CCITT decoding in `pdfcraft-filters` (`decode_ccitt`, through `hayro-ccitt`) for export, redaction and the Optimizer, which encodes Group 4 (`optimize.mono-compression`); lossless JBIG2 generic-region or exact-copy symbol encoding and image export; no lossy substitution |
| §7.4 | Image filters: DCT, JPX, JBIG2, CCITTFax | full | **borrowed** (decode); CCITT Group 4 and lossless JBIG2 encoding | `core.image-filters` partial; own codecs M1.1/M1.2; CCITT decoding in `pdfcraft-filters` (`decode_ccitt`, through `hayro-ccitt`) for export, redaction and the Optimizer, which encodes Group 4 (`optimize.mono-compression`); JBIG2 decoding there too (`decode_jbig2`, through `hayro-jbig2`) for export and redaction; lossless JBIG2 generic-region or exact-copy symbol encoding; no lossy substitution |
| §7.5.8 | Linearization | R W | R only | `core.linearization` planned |
| §7.6.4 | Standard security handler R2–R6 (RC4 40/128, AES-128, AES-256), crypt filters, `/EncryptMetadata`, SASLprep | full | **own**, read and write | `crates/crypt`; written files open in Acrobat DC since #836 (#774: Acrobat needs the catalog outside object streams) |
| §7.6.5 | Public-key security handler | full | none | `core.open-public-key`, `protect.certificate-encryption` planned |
@@ -132,6 +132,7 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | §7.4: JBIG2 decoding (`hayro-jbig2`) in `pdfcraft-filters`, which `Document::decoded_image` uses with the image's `/JBIG2Globals`: export reads JBIG2 behind other filters too, and redaction clears pixels in JBIG2 images instead of removing them whole (one whose page isn't its size still is) |
| 2026-10-26 | minor | §7.4: redaction clears pixels in CCITT fax images through the CCITT decoder, instead of removing them whole (Group 4 and Group 3 off bytes; Group 3 on bytes is still removed whole, as the bootstrap renderer may draw it as other rows than the decoder reads) |
| 2026-10-26 | minor | §7.4: CCITT decoding (`hayro-ccitt`) in `pdfcraft-filters`, for export and the Optimizer |
| 2026-10-26 | minor | §7.4: CCITT Group 4 encoding (`fax`) in the Optimizer |
+3 -3
View File
@@ -131,7 +131,7 @@ status = "partial"
commands = []
tools = []
evidence = ["crates/filters/src/ccitt.rs::decoding_gives_the_rows_back", "crates/filters/src/ccitt.rs::group_3_rows_on_bytes_read_by_the_codes_in_the_data", "crates/cos/src/document/tests.rs::fax_images_decode_with_their_parameters", "crates/filters/src/jbig2/decode.rs::both_codings_decode_to_their_samples", "crates/filters/src/jbig2/decode.rs::what_it_cannot_decode_is_an_error", "crates/cos/src/document/tests.rs::jbig2_images_decode_with_their_globals"]
notes = "decoded through the hayro bootstrap for display; own codecs are M1.1/M1.2. CCITT fax decoding is in filters (decode_ccitt, through hayro-ccitt), which Document::decoded_image uses for Export ▸ Images, the optimizer and redaction; so is JBIG2 decoding (decode_jbig2, through hayro-jbig2, the page bounded before it is decoded), which it uses with the image's /JBIG2Globals (within 32 MiB) for Export ▸ Images and Copy Image. Byte-aligned Group 3 is read by its end-of-line codes when the data starts with one and on bytes otherwise; still limited: data with codes but none before the first row (not as T.4 sends them) is read on bytes, as the renderer draws it, and at /K 0 may come out as other rows, which the optimizer then writes back. The bootstrap renderer draws byte-aligned Group 3 whose fill ends each end-of-line code on a byte as other rows (black box, at every /K), which decode_ccitt reads right; redaction removes Group 3 on bytes whole"
notes = "decoded through the hayro bootstrap for display; own codecs are M1.1/M1.2. CCITT fax decoding is in filters (decode_ccitt, through hayro-ccitt), which Document::decoded_image uses for Export ▸ Images, the optimizer and redaction; so is JBIG2 decoding (decode_jbig2, through hayro-jbig2, the page bounded before it is decoded), which it uses with the image's /JBIG2Globals (within 32 MiB) for Export ▸ Images, Copy Image and redaction. Byte-aligned Group 3 is read by its end-of-line codes when the data starts with one and on bytes otherwise; still limited: data with codes but none before the first row (not as T.4 sends them) is read on bytes, as the renderer draws it, and at /K 0 may come out as other rows, which the optimizer then writes back. The bootstrap renderer draws byte-aligned Group 3 whose fill ends each end-of-line code on a byte as other rows (black box, at every /K), which decode_ccitt reads right; redaction removes Group 3 on bytes whole"
[[feature]]
id = "core.decompression-limits"
@@ -5804,8 +5804,8 @@ milestone = "M8"
status = "shipped"
commands = ["redact.apply"]
tools = ["redact_apply"]
evidence = ["crates/redact/src/tests.rs::images_are_removed_or_have_their_pixels_cleared", "crates/redact/src/tests.rs::redaction_clears_pixels_intersected_without_covering_their_centres", "crates/redact/src/tests.rs::a_fax_image_has_its_covered_pixels_cleared_instead_of_being_removed", "crates/redact/src/tests.rs::group_3_on_bytes_is_removed_whole_and_other_fax_images_cleared", "crates/redact/src/tests.rs::an_image_whose_predictor_is_given_by_reference_has_the_covered_pixel_cleared"]
notes = "intersected pixel footprints are cleared conservatively in a copy, CCITT fax images (scans stored as Group 4) included; DCT/JPX/JBIG2 images, and Group 3 on bytes (which the bootstrap renderer may draw as other rows than the decoder reads), removed whole"
evidence = ["crates/redact/src/tests.rs::images_are_removed_or_have_their_pixels_cleared", "crates/redact/src/tests.rs::redaction_clears_pixels_intersected_without_covering_their_centres", "crates/redact/src/tests.rs::a_fax_image_has_its_covered_pixels_cleared_instead_of_being_removed", "crates/redact/src/tests.rs::group_3_on_bytes_is_removed_whole_and_other_fax_images_cleared", "crates/redact/src/tests.rs::an_image_whose_predictor_is_given_by_reference_has_the_covered_pixel_cleared", "crates/redact/src/tests.rs::a_jbig2_image_has_its_covered_pixels_cleared_instead_of_being_removed", "crates/redact/src/tests.rs::a_jbig2_image_that_cannot_be_cleared_for_certain_is_removed_whole", "crates/redact/src/tests.rs::a_redacted_jbig2_scan_keeps_neither_its_code_nor_its_globals"]
notes = "intersected pixel footprints are cleared conservatively in a copy, CCITT fax and JBIG2 images (scans stored as Group 4 or JBIG2, with their /JBIG2Globals) included; DCT/JPX images, Group 3 on bytes (which the bootstrap renderer may draw as other rows than the decoder reads), and a JBIG2 image whose page isn't its size, removed whole. The copy is Flate (larger than a Group 4 or JBIG2 scan) and refers to no globals; globals another image still uses stay, and may hold the shapes of marks under the area (not where they were)"
[[feature]]
id = "protect.redact-vectors"