mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 14:52:22 +00:00
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:
1 parent
ea6c329731
commit
78fc1e5568
6 files changed
+188
-28
No files matched your search
+11
-5
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user