mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 01:11:19 +00:00
fix(redact): read each Type 3 glyph program once per cleanup
clean_type3_font decoded and parsed every surviving glyph program of a font, for every font. Many small font dictionaries sharing one compressed CharProc multiplied that work without multiplying the stored data, with a fresh allowance for each decode. The resource names each indirect glyph program uses are now cached by object across fonts (resource-name interpretation stays per font, since each font prunes its own /Resources), and all decoding spends one cleanup-wide DecodeBudget; past it the cleanup fails closed.
This commit is contained in:
1 parent
14ae839a28
commit
5cc8d4326d
2 files changed
+64
-4
No files matched your search
@@ -298,3 +298,32 @@ fn type3_font_used_in_a_form_xobject_has_its_redacted_glyph_removed() {
|
||||
assert_eq!(report.glyphs, 1);
|
||||
assert!(!full_save_holds(&doc, b"SECRET_FORM_GLYPH_PAYLOAD"), "redacted glyph program survived");
|
||||
}
|
||||
|
||||
/// Type 3 cleanup decoded a glyph program again for every font listing it: 2 000 fonts sharing
|
||||
/// one large program read it 2 000 times. It is read once now, and the cleanup still keeps the
|
||||
/// glyph every font shows.
|
||||
#[test]
|
||||
fn a_glyph_program_many_type3_fonts_share_is_read_once() {
|
||||
const FONTS: usize = 2_000;
|
||||
let names: String = (0..FONTS).map(|i| format!("/T{i} {} 0 R ", 6 + i)).collect();
|
||||
let shows: String = (0..FONTS).map(|i| format!("/T{i} 10 Tf (A) Tj ")).collect();
|
||||
let mut objs = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Font << {names}>> >> >>").into_bytes(),
|
||||
stream("", format!("BT 10 100 Td {shows}ET").as_bytes()),
|
||||
stream("", [b"SHARED_GLYPH ".as_slice(), &b"0 0 m 1 1 l S ".repeat(50_000)].concat()),
|
||||
];
|
||||
objs.extend((0..FONTS).map(|_| {
|
||||
b"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 65 /Widths [0] /Encoding << /Differences [65 /A] >> /CharProcs << /A 5 0 R >> >>".to_vec()
|
||||
}));
|
||||
let mut doc = pdf(objs);
|
||||
// The glyphs are drawn at x 10 with no width, so a mark away from them leaves them all.
|
||||
mark(&mut doc, 0, &[[200.0, 200.0, 250.0, 250.0]], "");
|
||||
let started = std::time::Instant::now();
|
||||
apply(&mut doc, None).unwrap();
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(60), "{:?}", started.elapsed());
|
||||
let font = doc.get(ObjRef::new(6, 0));
|
||||
let procs = doc.resolve(font.as_dict().unwrap().get(b"CharProcs").unwrap());
|
||||
assert!(procs.as_dict().unwrap().contains(b"A"), "the glyph every font shows was unlinked");
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use pdfcraft_content::Pieces;
|
||||
use pdfcraft_cos::{CosError, Dict, Document, ObjRef, Object};
|
||||
use pdfcraft_cos::{CosError, DecodeBudget, Dict, Document, ObjRef, Object};
|
||||
|
||||
use crate::{RedactError, page_streams};
|
||||
|
||||
@@ -130,6 +130,25 @@ struct UsedInGlyphs {
|
||||
properties: HashSet<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl UsedInGlyphs {
|
||||
fn merge(&mut self, other: &UsedInGlyphs) {
|
||||
self.xobjects.extend(other.xobjects.iter().cloned());
|
||||
self.colorspaces.extend(other.colorspaces.iter().cloned());
|
||||
self.patterns.extend(other.patterns.iter().cloned());
|
||||
self.shadings.extend(other.shadings.iter().cloned());
|
||||
self.extgstates.extend(other.extgstates.iter().cloned());
|
||||
self.fonts.extend(other.fonts.iter().cloned());
|
||||
self.properties.extend(other.properties.iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// What one Type 3 cleanup reads of glyph programs: the resource names each indirect program
|
||||
/// uses, read once however many fonts share it, and one decoding allowance for all of them.
|
||||
struct GlyphReads {
|
||||
used: HashMap<ObjRef, UsedInGlyphs>,
|
||||
budget: DecodeBudget,
|
||||
}
|
||||
|
||||
fn collect_used_resources_in_stream(data: &[u8], used: &mut UsedInGlyphs) -> Result<(), CosError> {
|
||||
let parsed = pdfcraft_content::parse(data);
|
||||
if parsed.skipped > 0 {
|
||||
@@ -488,9 +507,10 @@ pub(crate) fn unlink_type3(doc: &mut Document) -> Result<(), RedactError> {
|
||||
}
|
||||
|
||||
// For each Type 3 font: unlink unused glyphs from CharProcs and prune exclusive resources.
|
||||
let mut reads = GlyphReads { used: HashMap::new(), budget: DecodeBudget::page() };
|
||||
for (font_key, font_dict) in &type3_fonts {
|
||||
let surviving = used_glyphs.get(font_key).cloned().unwrap_or_default();
|
||||
clean_type3_font(doc, &holders, &form_holders, font_key, font_dict, &surviving)?;
|
||||
clean_type3_font(doc, &holders, &form_holders, font_key, font_dict, &surviving, &mut reads)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -565,6 +585,7 @@ fn clean_type3_font(
|
||||
font_key: &FontKey,
|
||||
font_dict: &Dict,
|
||||
surviving: &HashSet<Vec<u8>>,
|
||||
reads: &mut GlyphReads,
|
||||
) -> Result<(), RedactError> {
|
||||
let Some(char_procs_val) = font_dict.get(b"CharProcs") else {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not establish safe Type 3 font cleanup: missing CharProcs".into() }.into());
|
||||
@@ -608,6 +629,10 @@ fn clean_type3_font(
|
||||
|
||||
let mut used_in_glyphs = UsedInGlyphs::default();
|
||||
for (_, proc_val) in char_procs.iter() {
|
||||
if let Some(used) = proc_val.as_ref().and_then(|r| reads.used.get(&r)) {
|
||||
used_in_glyphs.merge(used);
|
||||
continue;
|
||||
}
|
||||
let resolved = doc.resolve(proc_val);
|
||||
let Object::Stream(s) = &*resolved else {
|
||||
return Err(CosError::Syntax {
|
||||
@@ -616,8 +641,14 @@ fn clean_type3_font(
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let data = s.decoded()?;
|
||||
collect_used_resources_in_stream(&data, &mut used_in_glyphs)?;
|
||||
// Past the allowance the cleanup can't be shown complete, and the redaction is refused.
|
||||
let data = reads.budget.decode(s)?;
|
||||
let mut used = UsedInGlyphs::default();
|
||||
collect_used_resources_in_stream(&data, &mut used)?;
|
||||
used_in_glyphs.merge(&used);
|
||||
if let Some(r) = proc_val.as_ref() {
|
||||
reads.used.insert(r, used);
|
||||
}
|
||||
}
|
||||
|
||||
if prune_type3_font_resources(doc, &mut f_res, &used_in_glyphs) {
|
||||
|
||||
Reference in new issue
Block a user