mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
Emptied layer sub-lists and direct layer references go too
An /Order sub-list or radio-button group whose layers were all removed stayed behind, an /Order heading with it, and a list or usage rule that named a removed layer directly instead of in an array kept that layer in the file. Such sub-lists and groups are now dropped, a top-level list keeps its place, and a direct reference is treated as an entry.
This commit is contained in:
1 parent
68e2c65f55
commit
eee967330a
2 files changed
+35
-15
No files matched your search
@@ -372,29 +372,43 @@ fn once(
|
||||
}
|
||||
|
||||
/// `o` without the entries `keep` refuses, through nested arrays (an `/Order` holds sub-lists, and
|
||||
/// `/RBGroups` holds groups), each array written by reference cleaned [`once`].
|
||||
fn filtered(doc: &mut Document, o: &Object, keep: &impl Fn(&Object) -> bool, depth: usize, cleaned: &mut Cleaned) -> Result<Object, RedactError> {
|
||||
/// `/RBGroups` holds groups), each array written by reference cleaned [`once`]. `None` when `keep`
|
||||
/// refuses `o` itself, or when `o` is a sub-list (`depth` above 0) that named a layer and names
|
||||
/// none now: it goes, with the heading that labelled it.
|
||||
fn filtered(
|
||||
doc: &mut Document,
|
||||
o: &Object,
|
||||
keep: &impl Fn(&Object) -> bool,
|
||||
depth: usize,
|
||||
cleaned: &mut Cleaned,
|
||||
) -> Result<Option<Object>, RedactError> {
|
||||
if depth > MAX_DIRECT_DEPTH {
|
||||
return Err(RedactError::TooDeep);
|
||||
}
|
||||
let out = once(doc, o, cleaned, |doc, o, cleaned| {
|
||||
if !keep(o) {
|
||||
return Ok(None);
|
||||
}
|
||||
once(doc, o, cleaned, |doc, o, cleaned| {
|
||||
let Object::Array(a) = o else { return Ok(Some(o.clone())) };
|
||||
let mut out = Vec::new();
|
||||
for x in a.iter().filter(|x| keep(x)) {
|
||||
out.push(filtered(doc, x, keep, depth + 1, cleaned)?);
|
||||
for x in a {
|
||||
out.extend(filtered(doc, x, keep, depth + 1, cleaned)?);
|
||||
}
|
||||
Ok(Some(Object::Array(out)))
|
||||
})?;
|
||||
Ok(out.unwrap_or_else(|| Object::Array(Vec::new())))
|
||||
// Anything but a text string (a heading) is a layer or a sub-list.
|
||||
let names = |l: &[Object]| l.iter().any(|x| !matches!(x, Object::String(_)));
|
||||
let emptied = depth > 0 && names(a.as_slice()) && !names(out.as_slice());
|
||||
Ok((!emptied).then_some(Object::Array(out)))
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// them: on, off, order, locked, radio-button groups and usage rules (a rule, group or sub-list 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: &mut Document, mut d: Dict, keep: &impl Fn(&Object) -> bool, cleaned: &mut Cleaned) -> 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, cleaned)?;
|
||||
let v = filtered(doc, &v, keep, 0, cleaned)?.unwrap_or_else(|| Object::Array(Vec::new()));
|
||||
d.set(k.to_vec(), v);
|
||||
}
|
||||
}
|
||||
@@ -406,7 +420,7 @@ fn config_without(doc: &mut Document, mut d: Dict, keep: &impl Fn(&Object) -> bo
|
||||
kept.extend(once(doc, rule, cleaned, |doc, rule, cleaned| {
|
||||
let Some(mut rule) = rule.as_dict().cloned() else { return Ok(None) };
|
||||
if let Some(layers) = rule.get(b"OCGs").cloned() {
|
||||
let layers = filtered(doc, &layers, keep, 0, cleaned)?;
|
||||
let Some(layers) = filtered(doc, &layers, keep, 0, cleaned)? else { return Ok(None) };
|
||||
if doc.resolve(&layers).as_array().is_some_and(Vec::is_empty) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -4235,13 +4235,14 @@ fn a_shared_layer_list_is_cleaned_once() {
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
/// the file: a sub-list of the default `/Order` (its heading too, once it names no layer), a
|
||||
/// radio-button group (a group left empty goes), a usage rule (one that names it directly too), 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 /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] [6 0 R]] /AS [<< /Event /Print /OCGs [6 0 R] /Category [/Print] >> << /Event /View /OCGs 6 0 R /Category [/View] >>] >> /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"),
|
||||
@@ -4256,4 +4257,9 @@ fn a_removed_layer_leaves_every_configuration() {
|
||||
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");
|
||||
assert!(!has(b"(Group)"), "the heading of a sub-list left with no layer stayed");
|
||||
let catalog = doc.get(doc.root().unwrap());
|
||||
let config = catalog.as_dict().and_then(|c| c.get(b"OCProperties")).and_then(Object::as_dict).and_then(|p| p.get(b"D")).and_then(Object::as_dict);
|
||||
let group = Object::Array(vec![Object::Ref(ObjRef::new(7, 0))]);
|
||||
assert_eq!(config.and_then(|d| d.get(b"RBGroups")), Some(&Object::Array(vec![group])), "a radio-button group left empty stayed");
|
||||
}
|
||||
Reference in new issue
Block a user