mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
Removed layers leave every optional-content configuration
Hidden layers took a removed layer out of the default configuration's top-level ON, OFF, Order and Locked lists only. A sub-list of /Order, a radio-button group (/RBGroups), a usage rule (/AS) and every alternate configuration in /Configs still named it, so its dictionary, name included, stayed in the full-rewrite save. The removal now cleans all of these lists, through nested arrays, in the default configuration and in every alternate one. A usage rule left with no layer goes.
This commit is contained in:
1 parent
f42fda7342
commit
ac8eb51464
2 files changed
+79
-5
No files matched your search
@@ -342,6 +342,52 @@ fn hidden_layers(doc: &Document) -> Vec<ObjRef> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `o` without the entries `keep` refuses, through nested arrays (an `/Order` holds sub-lists, and
|
||||
/// `/RBGroups` holds groups); arrays written by reference are copied in place.
|
||||
fn filtered(doc: &Document, o: &Object, keep: &impl Fn(&Object) -> bool, depth: usize) -> Result<Object, RedactError> {
|
||||
if depth > MAX_DIRECT_DEPTH {
|
||||
return Err(RedactError::TooDeep);
|
||||
}
|
||||
match &*doc.resolve(o) {
|
||||
Object::Array(a) => {
|
||||
let mut out = Vec::with_capacity(a.len());
|
||||
for x in a.iter().filter(|x| keep(x)) {
|
||||
out.push(filtered(doc, x, keep, depth + 1)?);
|
||||
}
|
||||
Ok(Object::Array(out))
|
||||
}
|
||||
_ => Ok(o.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// An optional-content configuration without the layers `keep` refuses, in every list that names
|
||||
/// them: on, off, order, locked, radio-button groups and usage rules (a rule left with no layer
|
||||
/// goes). A removed layer still named in any of them would stay in the file, its name included.
|
||||
fn config_without(doc: &Document, mut d: Dict, keep: &impl Fn(&Object) -> bool) -> Result<Dict, RedactError> {
|
||||
for k in [&b"ON"[..], b"OFF", b"Order", b"Locked", b"RBGroups"] {
|
||||
if let Some(v) = d.get(k).cloned() {
|
||||
let v = filtered(doc, &v, keep, 0)?;
|
||||
d.set(k.to_vec(), v);
|
||||
}
|
||||
}
|
||||
if let Some(Object::Array(rules)) = d.get(b"AS").map(|a| (*doc.resolve(a)).clone()) {
|
||||
let mut kept = Vec::with_capacity(rules.len());
|
||||
for rule in rules {
|
||||
let Some(mut rule) = doc.resolve(&rule).as_dict().cloned() else { continue };
|
||||
if let Some(layers) = rule.get(b"OCGs").cloned() {
|
||||
let layers = filtered(doc, &layers, keep, 0)?;
|
||||
if layers.as_array().is_some_and(Vec::is_empty) {
|
||||
continue;
|
||||
}
|
||||
rule.set(b"OCGs".to_vec(), layers);
|
||||
}
|
||||
kept.push(Object::Dict(rule));
|
||||
}
|
||||
d.set(b"AS".to_vec(), Object::Array(kept));
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Glyphs of hidden text, or content of hidden layers, on every page (`write` = remove them).
|
||||
fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Result<(usize, std::collections::HashSet<ObjRef>), RedactError> {
|
||||
let off = if layers { hidden_layers(doc) } else { Vec::new() };
|
||||
@@ -636,13 +682,17 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result<Vec<(Hidden
|
||||
if let Some(Object::Array(a)) = oc.get(b"OCGs").map(|g| (*doc.resolve(g)).clone()) {
|
||||
oc.set(b"OCGs".to_vec(), Object::Array(a.into_iter().filter(keep).collect()));
|
||||
}
|
||||
if let Some(mut d) = sub(doc, &oc, b"D") {
|
||||
for k in [&b"ON"[..], b"OFF", b"Order", b"Locked"] {
|
||||
if let Some(Object::Array(a)) = d.get(k).map(|g| (*doc.resolve(g)).clone()) {
|
||||
d.set(k.to_vec(), Object::Array(a.into_iter().filter(keep).collect()));
|
||||
if let Some(d) = sub(doc, &oc, b"D") {
|
||||
oc.set(b"D".to_vec(), Object::Dict(config_without(doc, d, &keep)?));
|
||||
}
|
||||
if let Some(Object::Array(configs)) = oc.get(b"Configs").map(|c| (*doc.resolve(c)).clone()) {
|
||||
let mut kept = Vec::with_capacity(configs.len());
|
||||
for config in configs {
|
||||
if let Some(config) = doc.resolve(&config).as_dict().cloned() {
|
||||
kept.push(Object::Dict(config_without(doc, config, &keep)?));
|
||||
}
|
||||
}
|
||||
oc.set(b"D".to_vec(), Object::Dict(d));
|
||||
oc.set(b"Configs".to_vec(), Object::Array(kept));
|
||||
}
|
||||
cat.set(b"OCProperties".to_vec(), Object::Dict(oc));
|
||||
}
|
||||
|
||||
@@ -4207,3 +4207,27 @@ fn a_redacted_form_xobject_drops_the_source_text_of_added_content() {
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(!bytes.windows(4).any(|w| w == b"1234"), "redacted text left in the saved file");
|
||||
}
|
||||
|
||||
/// A removed off layer leaves every list that names it, or its dictionary, name included, stays in
|
||||
/// the file: a sub-list of the default `/Order`, a radio-button group, a usage rule, and the
|
||||
/// alternate configurations in `/Configs`.
|
||||
#[test]
|
||||
fn a_removed_layer_leaves_every_configuration() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
let mut doc = pdf(vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [6 0 R 7 0 R] /D << /OFF [6 0 R] /Order [7 0 R [(Group) 6 0 R]] /RBGroups [[6 0 R 7 0 R]] /AS [<< /Event /Print /OCGs [6 0 R] /Category [/Print] >>] >> /Configs [8 0 R] >> >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> /Properties << /L1 6 0 R >> >> >>".to_vec(),
|
||||
stream("", b"/OC /L1 BDC BT /F1 10 Tf 10 50 Td (Gone) Tj ET EMC BT /F1 10 Tf 10 100 Td (Kept) Tj ET"),
|
||||
FONT.replace("95 0 R", "9 0 R").into_bytes(),
|
||||
b"<< /Type /OCG /Name (SECRETLAYERNAME) >>".to_vec(),
|
||||
b"<< /Type /OCG /Name (Shown layer) >>".to_vec(),
|
||||
b"<< /Name (Other view) /OFF [7 0 R] /ON [6 0 R] /Order [6 0 R 7 0 R] >>".to_vec(),
|
||||
widths(),
|
||||
]);
|
||||
remove_hidden(&mut doc, &[Hidden::HiddenLayers]).unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
let has = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle);
|
||||
assert!(has(b"(Shown layer)") && has(b"(Other view)"), "the layer that stays and the configuration stay");
|
||||
assert!(!has(b"SECRETLAYERNAME"), "the removed layer survived the save");
|
||||
}
|
||||
Reference in new issue
Block a user