mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M8: the forms redaction follows through resources spend the page's allowance
The walk of the forms that content draws through its resources (`resource_ops`) decoded each one outside the page's decode allowance: up to 1 GiB a form, read again by each cleanup that walks them, on every page. The interpreter skips a form outside the marks, so for such a form this walk is the only read. `resource_ops` now charges the bytes it decodes to the `DecodeBudget` it is given. The content walk hands back what it left of the page's allowance (`Scope::into_budget`), and the cleanups after it spend that: the check of what the page still draws (`prune`), the properties and the Type 3 fonts, in Apply Redactions and in Remove Hidden Information. A rewritten form's cleanups spend the walk's allowance as they go. The unlinking pass's check of the properties, which walks every page that shares a resource dictionary, gives each page's content an allowance of its own. Only bytes are charged, not form entries: each cleanup reads the same forms again, so a page drawing many small forms would run out of entries having decoded little. Tests, each failing with the charge reverted, and the last three with their own part of it reverted (the hand-back on either page, the rewritten form's cleanups): - what_content_draws_through_its_resources_is_read_within_its_allowance (a 2,000-byte form read with 2,000 bytes left, refused with 1,999); - the_cleanups_after_the_content_walk_spend_what_it_left_of_the_pages_allowance; - the_cleanups_of_a_rewritten_form_spend_the_pages_allowance; - removing_hidden_text_spends_what_the_content_walk_left_of_the_pages_allowance. In each of the last three a form without resources of its own is read three times: refused at 100 MiB, more than the page's 256 MiB, and not at 50 MiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
f227d63ab1
commit
6eabed25ca
6 files changed
+142
-35
No files matched your search
@@ -202,6 +202,12 @@ impl<'a> Scope<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What is left of the page's allowance once the content is walked, for the cleanups after it
|
||||
/// that read the forms drawing through the page's resources ([`prune`]) to go on spending.
|
||||
pub(crate) fn into_budget(self) -> DecodeBudget {
|
||||
self.budget
|
||||
}
|
||||
|
||||
/// Sanitizing visits every form XObject (not only those under a region).
|
||||
fn everywhere(&self) -> bool {
|
||||
self.hidden_text.is_some() || self.layers_enabled
|
||||
@@ -1065,18 +1071,18 @@ fn xobject(
|
||||
}
|
||||
if let Some(data) = &new_data {
|
||||
let content = std::slice::from_ref(data);
|
||||
match prune_properties(doc, &mut cleaned_res, content) {
|
||||
match prune_properties(doc, &mut scope.budget, &mut cleaned_res, content) {
|
||||
Ok(retired) => out.retired_properties.extend(retired),
|
||||
Err(error) => {
|
||||
out.error = Some(error);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if let Err(error) = prune_form_resources(doc, &mut cleaned_res, content) {
|
||||
if let Err(error) = prune_form_resources(doc, &mut scope.budget, &mut cleaned_res, content) {
|
||||
out.error = Some(error);
|
||||
return None;
|
||||
}
|
||||
if let Err(error) = crate::type3::prune_type3_from_resources(doc, &mut cleaned_res, content) {
|
||||
if let Err(error) = crate::type3::prune_type3_from_resources(doc, &mut scope.budget, &mut cleaned_res, content) {
|
||||
out.error = Some(error);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
use super::{
|
||||
CosError, Dict, Document, HashMap, HashSet, MAX_DEPTH, Matrix, Metrics, ObjRef, Object, Op, Pieces, Rc, RedactError, Scope, is_vertical, num,
|
||||
overlaps, page_streams, res_dict,
|
||||
CosError, DecodeBudget, Dict, Document, HashMap, HashSet, MAX_DEPTH, Matrix, Metrics, ObjRef, Object, Op, Pieces, Rc, RedactError, Scope,
|
||||
is_vertical, num, overlaps, page_streams, res_dict,
|
||||
};
|
||||
|
||||
/// Drop every `replaced` name of the XObject resources `xo` that the rewritten `content` no
|
||||
/// longer draws, so a full-rewrite save stops reaching the original image or form it named. A
|
||||
/// name still drawn is kept: by a surviving `Do` (the same image shown again outside a mark), or
|
||||
/// by a form without /Resources of its own, which draws through `xo` too. Fails, as the cleanup of
|
||||
/// the fonts and properties these forms draw with does, when that can't be told: `Stream::decoded`
|
||||
/// refuses such a form (an image codec or a /Filter it can't run, say, or more than 1 GiB), the
|
||||
/// walk passes 2,000,000 operators, or the parser can't read all of such a form or of `content`.
|
||||
/// by a form without /Resources of its own, which draws through `xo` too. Such forms are read
|
||||
/// within `budget`, what is left of the page's allowance ([`resource_ops`]). Fails, as the cleanup
|
||||
/// of the fonts and properties these forms draw with does, when that can't be told: such a form
|
||||
/// doesn't decode (an image codec or a /Filter it can't run, say) or runs past `budget`, the walk
|
||||
/// passes 2,000,000 operators, or the parser can't read all of such a form or of `content`.
|
||||
/// Corrupt data in such a form is read, tolerantly, up to the damage, which may be nothing.
|
||||
pub(crate) fn prune(doc: &Document, xo: &mut Dict, replaced: &[Vec<u8>], content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
pub(crate) fn prune(doc: &Document, budget: &mut DecodeBudget, xo: &mut Dict, replaced: &[Vec<u8>], content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
if replaced.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let res = Dict::from_iter([(b"XObject".to_vec(), Object::Dict(xo.clone()))]);
|
||||
let ops = resource_ops(doc, &res, content)?;
|
||||
let ops = resource_ops(doc, budget, &res, content)?;
|
||||
let drawn: HashSet<&[u8]> = ops.iter().filter(|op| op.is("Do")).filter_map(Op::last_name).collect();
|
||||
for n in replaced {
|
||||
if !drawn.contains(n.as_slice()) {
|
||||
@@ -162,7 +163,8 @@ fn prune_retired_properties(doc: &Document, res: &mut Dict, names: &[Vec<u8>], c
|
||||
}
|
||||
let mut used = HashSet::new();
|
||||
for content in contents {
|
||||
for op in resource_ops(doc, res, content)? {
|
||||
// Each is one page's content or one form's, with the allowance of a page.
|
||||
for op in resource_ops(doc, &mut DecodeBudget::page(), res, content)? {
|
||||
if matches!(op.op.as_slice(), b"BDC" | b"DP")
|
||||
&& let Some(name) = op.name(1)
|
||||
{
|
||||
@@ -275,10 +277,11 @@ pub(super) fn clip_out(rects: &[[f64; 4]], ctm: &Matrix, around: [f64; 4]) -> Op
|
||||
|
||||
/// The operators of `content`, and of every form without /Resources of its own that it draws
|
||||
/// through `res`, nested ones too: what draws with these resources. Each such form is read once,
|
||||
/// with its `/DecodeParms` given by reference. An error when that can't all be read: the parser
|
||||
/// can't read all of `content` or of such a form, the walk passes 2,000,000 operators, or such a
|
||||
/// form doesn't decode.
|
||||
pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
|
||||
/// with its `/DecodeParms` given by reference, its decoded bytes charged to `budget`, so the
|
||||
/// cleanups of one page that walk the same forms spend one allowance between them. An error
|
||||
/// when that can't all be read: the parser can't read all of `content` or of such a form, the walk
|
||||
/// passes 2,000,000 operators, or such a form doesn't decode or runs past `budget`.
|
||||
pub(crate) fn resource_ops(doc: &Document, budget: &mut DecodeBudget, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
|
||||
let parsed = Pieces::join(content).parse();
|
||||
if parsed.skipped > 0 || parsed.ops.len() > 2_000_000 {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully read the content that uses these resources".into() });
|
||||
@@ -296,7 +299,7 @@ pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> R
|
||||
&& s.dict.name(b"Subtype") == Some(b"Form")
|
||||
&& s.dict.get(b"Resources").is_none_or(|r| doc.resolve(r).as_dict().is_none())
|
||||
{
|
||||
let parsed = pdfcraft_content::parse(&doc.with_direct_parms(s).decoded()?);
|
||||
let parsed = pdfcraft_content::parse(&budget.decode(&doc.with_direct_parms(s))?);
|
||||
if seen.len() > 2_000_000 || ops.len().saturating_add(parsed.ops.len()) > 2_000_000 || parsed.skipped > 0 {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect inherited form resources".into() });
|
||||
}
|
||||
@@ -307,13 +310,13 @@ pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> R
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
pub(crate) fn prune_properties(doc: &Document, res: &mut Dict, content: &[Vec<u8>]) -> Result<Vec<Object>, CosError> {
|
||||
pub(crate) fn prune_properties(doc: &Document, budget: &mut DecodeBudget, res: &mut Dict, content: &[Vec<u8>]) -> Result<Vec<Object>, CosError> {
|
||||
let mut properties = res_dict(doc, res, b"Properties");
|
||||
if properties.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut retired = Vec::new();
|
||||
let ops = resource_ops(doc, res, content)?;
|
||||
let ops = resource_ops(doc, budget, res, content)?;
|
||||
let names: Vec<Vec<u8>> = properties.iter().map(|(n, _)| n.to_vec()).collect();
|
||||
for name in names {
|
||||
if !ops.iter().any(|op| matches!(op.op.as_slice(), b"BDC" | b"DP") && op.name(1) == Some(&name))
|
||||
@@ -330,8 +333,8 @@ pub(crate) fn prune_properties(doc: &Document, res: &mut Dict, content: &[Vec<u8
|
||||
Ok(retired)
|
||||
}
|
||||
|
||||
pub(super) fn prune_form_resources(doc: &Document, res: &mut Dict, content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
let ops = resource_ops(doc, res, content)?;
|
||||
pub(super) fn prune_form_resources(doc: &Document, budget: &mut DecodeBudget, res: &mut Dict, content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
let ops = resource_ops(doc, budget, res, content)?;
|
||||
let mut xo = res_dict(doc, res, b"XObject");
|
||||
let names: Vec<Vec<u8>> = xo.iter().map(|(n, _)| n.to_vec()).collect();
|
||||
for name in names {
|
||||
|
||||
@@ -772,9 +772,11 @@ fn redact_page_content(
|
||||
let mut budget = pdfcraft_cos::DecodeBudget::page();
|
||||
let (list, data) = page_streams_within(doc, &page.dict, pi, &mut budget)?;
|
||||
let resources = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
let out = {
|
||||
// What the walk leaves of the page's allowance, the cleanups below spend.
|
||||
let (out, mut budget) = {
|
||||
let mut scope = Scope::new(rects, Mode::Apply, report, budget, images);
|
||||
process(doc, &mut scope, &data, &resources, pdfcraft_content::Matrix::IDENTITY)
|
||||
let out = process(doc, &mut scope, &data, &resources, pdfcraft_content::Matrix::IDENTITY);
|
||||
(out, scope.into_budget())
|
||||
};
|
||||
// Fail closed: the interpreter lays vertical-writing text out horizontally, so it cannot
|
||||
// tell whether those glyphs lie under a mark. Rather than report success and possibly
|
||||
@@ -825,7 +827,7 @@ fn redact_page_content(
|
||||
_ => Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
interp::prune(doc, &mut xo, &out.replaced, &page_content)?;
|
||||
interp::prune(doc, &mut budget, &mut xo, &out.replaced, &page_content)?;
|
||||
for (n, r) in &out.xobjects {
|
||||
xo.set(n.clone(), Object::Ref(*r));
|
||||
}
|
||||
@@ -845,8 +847,8 @@ fn redact_page_content(
|
||||
// A Type 3 glyph without resources paints with patterns nobody tracked: keep them all.
|
||||
let painted = if out.keeps_all_patterns { &out.patterns_seen } else { &out.patterns_painted };
|
||||
report.patterns_removed += interp::prune_patterns(doc, &mut res, &out.patterns_seen, painted, pruned_patterns);
|
||||
retired_properties.extend(interp::prune_properties(doc, &mut res, &cleaned_data)?);
|
||||
type3::prune_type3_from_resources(doc, &mut res, &cleaned_data)?;
|
||||
retired_properties.extend(interp::prune_properties(doc, &mut budget, &mut res, &cleaned_data)?);
|
||||
type3::prune_type3_from_resources(doc, &mut budget, &mut res, &cleaned_data)?;
|
||||
doc.update_dict(page.obj, |d| {
|
||||
d.set(b"Contents".to_vec(), Object::Array(new_list));
|
||||
d.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
|
||||
@@ -384,7 +384,8 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re
|
||||
let blocks = scope.layer_blocks;
|
||||
let glyphs = scope.report.glyphs;
|
||||
let retained_for_page = std::mem::take(&mut scope.retained_off_layers);
|
||||
drop(scope);
|
||||
// What the walk leaves of the page's allowance, the cleanups below spend.
|
||||
let mut budget = scope.into_budget();
|
||||
total += out.residue + blocks + glyphs;
|
||||
retained_off_layers.extend(retained_for_page);
|
||||
if !write {
|
||||
@@ -418,14 +419,14 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re
|
||||
let mut xo = res.get(b"XObject").and_then(|x| doc.resolve(x).as_dict().cloned()).unwrap_or_default();
|
||||
// A full save keeps whatever is still referenced: unlink the originals this page no
|
||||
// longer draws so the removed form or image bytes cannot survive in the saved file.
|
||||
crate::interp::prune(doc, &mut xo, &out.replaced, &page_content)?;
|
||||
crate::interp::prune(doc, &mut budget, &mut xo, &out.replaced, &page_content)?;
|
||||
for (n, r) in &out.xobjects {
|
||||
xo.set(n.clone(), Object::Ref(*r));
|
||||
}
|
||||
res.set(b"XObject".to_vec(), Object::Dict(xo));
|
||||
}
|
||||
retired_properties.extend(crate::interp::prune_properties(doc, &mut res, &page_content)?);
|
||||
crate::type3::prune_type3_from_resources(doc, &mut res, &page_content)?;
|
||||
retired_properties.extend(crate::interp::prune_properties(doc, &mut budget, &mut res, &page_content)?);
|
||||
crate::type3::prune_type3_from_resources(doc, &mut budget, &mut res, &page_content)?;
|
||||
doc.update_dict(page.obj, |d| {
|
||||
d.set(b"Contents".to_vec(), Object::Array(new_list));
|
||||
d.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
|
||||
@@ -1746,7 +1746,7 @@ fn the_pages_check_does_not_follow_a_form_with_resources_of_its_own() {
|
||||
stream(image, b"p"),
|
||||
]);
|
||||
let mut xo = Dict::from_iter([(b"FmOwn".to_vec(), Object::Ref(ObjRef::new(3, 0))), (b"Im1".to_vec(), Object::Ref(ObjRef::new(5, 0)))]);
|
||||
crate::interp::prune(&doc, &mut xo, &[b"Im1".to_vec()], &[b"/FmOwn Do".to_vec()]).unwrap();
|
||||
crate::interp::prune(&doc, &mut pdfcraft_cos::DecodeBudget::page(), &mut xo, &[b"Im1".to_vec()], &[b"/FmOwn Do".to_vec()]).unwrap();
|
||||
assert!(!xo.contains(b"Im1") && xo.contains(b"FmOwn"), "{xo:?}");
|
||||
}
|
||||
|
||||
@@ -1757,7 +1757,11 @@ fn what_content_draws_through_its_resources_is_read_up_to_two_million_operators(
|
||||
let doc = Document::new_empty();
|
||||
for (ops, read) in [(2_000_000, true), (2_000_001, false)] {
|
||||
let content = b"n ".repeat(ops);
|
||||
assert_eq!(crate::interp::resource_ops(&doc, &Dict::new(), &[content]).is_ok(), read, "{ops} operators");
|
||||
assert_eq!(
|
||||
crate::interp::resource_ops(&doc, &mut pdfcraft_cos::DecodeBudget::page(), &Dict::new(), &[content]).is_ok(),
|
||||
read,
|
||||
"{ops} operators"
|
||||
);
|
||||
}
|
||||
// The content's own `/Fm Do` is one of them.
|
||||
for (ops, read) in [(1_999_999, true), (2_000_000, false)] {
|
||||
@@ -1767,7 +1771,11 @@ fn what_content_draws_through_its_resources_is_read_up_to_two_million_operators(
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 1 1]", b"n ".repeat(ops)),
|
||||
]);
|
||||
let res = Dict::from_iter([(b"XObject".to_vec(), Object::Dict(Dict::from_iter([(b"Fm".to_vec(), Object::Ref(ObjRef::new(3, 0)))])))]);
|
||||
assert_eq!(crate::interp::resource_ops(&doc, &res, &[b"/Fm Do".to_vec()]).is_ok(), read, "a form of {ops} operators");
|
||||
assert_eq!(
|
||||
crate::interp::resource_ops(&doc, &mut pdfcraft_cos::DecodeBudget::page(), &res, &[b"/Fm Do".to_vec()]).is_ok(),
|
||||
read,
|
||||
"a form of {ops} operators"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1841,6 +1849,88 @@ fn an_image_a_surviving_form_with_parameters_by_reference_draws_is_kept() {
|
||||
assert!(surviving_references::full_save_holds(&doc, b"IMAGESECRET"), "the image the form still draws outside the mark");
|
||||
}
|
||||
|
||||
/// The walk of what content draws through its resources charges the forms it reads to the
|
||||
/// allowance it is given: a form decoding to more than is left is refused, one within it read.
|
||||
#[test]
|
||||
fn what_content_draws_through_its_resources_is_read_within_its_allowance() {
|
||||
let doc = pdf(vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [] /Count 0 >>".to_vec(),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 1 1]", b"n ".repeat(1000)),
|
||||
]);
|
||||
let res = Dict::from_iter([(b"XObject".to_vec(), Object::Dict(Dict::from_iter([(b"Fm".to_vec(), Object::Ref(ObjRef::new(3, 0)))])))]);
|
||||
for (allowance, read) in [(2000, true), (1999, false)] {
|
||||
let mut budget = pdfcraft_cos::DecodeBudget::new(allowance, 0);
|
||||
assert_eq!(crate::interp::resource_ops(&doc, &mut budget, &res, &[b"/Fm Do".to_vec()]).is_ok(), read, "{allowance} bytes");
|
||||
}
|
||||
}
|
||||
|
||||
/// The cleanups after the content walk spend what the walk left of the page's allowance, one
|
||||
/// allowance between them. A form without resources of its own that crosses the mark is read by
|
||||
/// the walk, then again by the check of what the page still draws and by the font cleanup: at
|
||||
/// 100 MiB, three reads are more than the page's 256 MiB, and the redaction is refused; at
|
||||
/// 50 MiB it goes on.
|
||||
#[test]
|
||||
fn the_cleanups_after_the_content_walk_spend_what_it_left_of_the_pages_allowance() {
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"q 50 0 0 50 20 230 cm /Im1 Do Q /FmBare Do",
|
||||
"/XObject << /FmBare 7 0 R /Im1 8 0 R >>",
|
||||
vec![
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Filter /FlateDecode", &blank),
|
||||
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"o"),
|
||||
],
|
||||
);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB");
|
||||
}
|
||||
}
|
||||
|
||||
/// The cleanups of a form the redaction rewrites spend the page's allowance too. The rewritten
|
||||
/// form draws a form without resources of its own that crosses the mark: the walk reads it, then
|
||||
/// the check of what the rewritten form still draws and its font cleanup read it again. At
|
||||
/// 100 MiB, three reads are more than the page's 256 MiB, and the redaction is refused; at 50 MiB
|
||||
/// it goes on.
|
||||
#[test]
|
||||
fn the_cleanups_of_a_rewritten_form_spend_the_pages_allowance() {
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"/FmText Do",
|
||||
"/XObject << /FmText 7 0 R /FmBare 8 0 R >>",
|
||||
vec![
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300]", b"BT /F1 10 Tf 20 250 Td (SECRET) Tj ET /FmBare Do"),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Filter /FlateDecode", &blank),
|
||||
],
|
||||
);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB");
|
||||
}
|
||||
}
|
||||
|
||||
/// Removing hidden text spends what its content walk left of the page's allowance too. The page
|
||||
/// draws a form holding hidden text, which is rewritten, and a form without resources of its own:
|
||||
/// the walk reads the second, then the check of what the page still draws and the font cleanup
|
||||
/// read it again. At 100 MiB, three reads are more than the page's 256 MiB, and the removal is
|
||||
/// refused; at 50 MiB it goes on.
|
||||
#[test]
|
||||
fn removing_hidden_text_spends_what_the_content_walk_left_of_the_pages_allowance() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"/FmHidden Do /FmBare Do",
|
||||
"/XObject << /FmHidden 7 0 R /FmBare 8 0 R >>",
|
||||
vec![
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300]", b"BT /F1 10 Tf 3 Tr 20 250 Td (HIDDEN) Tj ET"),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Filter /FlateDecode", &blank),
|
||||
],
|
||||
);
|
||||
assert_eq!(remove_hidden(&mut doc, &[Hidden::HiddenText]).is_err(), refused, "{mib} MiB");
|
||||
}
|
||||
}
|
||||
|
||||
/// A form with resources of its own draws through those, not its caller's: one on another page
|
||||
/// that names a different image /Im1 doesn't keep the redacted /Im1 in the dictionary both pages
|
||||
/// inherit.
|
||||
|
||||
@@ -343,7 +343,12 @@ fn gs_font_object(doc: &Document, state_obj: &Object) -> Option<Object> {
|
||||
|
||||
/// Prune Type 3 fonts from a rewritten page or form's `/Resources` dictionary if the rewritten
|
||||
/// content no longer draws any text using that font name.
|
||||
pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
pub(crate) fn prune_type3_from_resources(
|
||||
doc: &Document,
|
||||
budget: &mut pdfcraft_cos::DecodeBudget,
|
||||
res: &mut Dict,
|
||||
content: &[Vec<u8>],
|
||||
) -> Result<(), CosError> {
|
||||
let mut fonts = res_dict(doc, res, b"Font");
|
||||
if fonts.is_empty() {
|
||||
return Ok(());
|
||||
@@ -351,7 +356,7 @@ pub(crate) fn prune_type3_from_resources(doc: &Document, res: &mut Dict, content
|
||||
|
||||
// The operators of forms that draw through these resources count too: dropping a font a child
|
||||
// form still shows would leave its text without one.
|
||||
let ops = crate::interp::resource_ops(doc, res, content)?;
|
||||
let ops = crate::interp::resource_ops(doc, budget, res, content)?;
|
||||
|
||||
let mut drawn_font_names = HashSet::new();
|
||||
for op in &ops {
|
||||
|
||||
Reference in new issue
Block a user