mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 06:42:20 +00:00
Merge #6460: Hidden layers includes layers a View usage rule turns off
# Conflicts: # crates/redact/src/tests.rs
This commit is contained in:
commit
34a953a72a
2 files changed
+65
-8
No files matched your search
@@ -410,22 +410,58 @@ fn is_comment(subtype: &[u8]) -> bool {
|
||||
!matches!(subtype, b"FileAttachment" | b"Redact") && pdfcraft_annot::is_comment_subtype(&String::from_utf8_lossy(subtype))
|
||||
}
|
||||
|
||||
/// The layers that are off in the default configuration.
|
||||
/// The layers that are off in the default configuration: listed off (or not listed on, under
|
||||
/// `/BaseState /OFF`), or turned off on screen by one of its usage rules (`/AS`, ISO 32000-2,
|
||||
/// 8.11.4.4): a rule for the View event in the View category hides a layer whose
|
||||
/// `/Usage /View /ViewState` is OFF. Rules that turn a layer on, or apply only when printing or
|
||||
/// exporting, leave it as the lists say.
|
||||
fn hidden_layers(doc: &Document) -> Vec<ObjRef> {
|
||||
let Some((_, cat)) = catalog(doc) else { return Vec::new() };
|
||||
let Some(oc) = sub(doc, &cat, b"OCProperties") else { return Vec::new() };
|
||||
let Some(d) = sub(doc, &oc, b"D") else { return Vec::new() };
|
||||
let all: Vec<ObjRef> =
|
||||
oc.get(b"OCGs").and_then(|g| doc.resolve(g).as_array().map(|a| a.iter().filter_map(Object::as_ref).collect())).unwrap_or_default();
|
||||
let list = |k: &[u8]| -> Vec<ObjRef> {
|
||||
d.get(k).and_then(|g| doc.resolve(g).as_array().map(|a| a.iter().filter_map(Object::as_ref).collect())).unwrap_or_default()
|
||||
let refs = |o: Option<&Object>| -> Vec<ObjRef> {
|
||||
o.and_then(|g| doc.resolve(g).as_array().map(|a| a.iter().filter_map(Object::as_ref).collect())).unwrap_or_default()
|
||||
};
|
||||
let (on, off) = (list(b"ON"), list(b"OFF"));
|
||||
if d.name(b"BaseState") == Some(b"OFF") {
|
||||
all.into_iter().filter(|g| !on.contains(g)).collect()
|
||||
let all = refs(oc.get(b"OCGs"));
|
||||
let (on, off) = (refs(d.get(b"ON")), refs(d.get(b"OFF")));
|
||||
let mut hidden: Vec<ObjRef> = if d.name(b"BaseState") == Some(b"OFF") {
|
||||
all.iter().copied().filter(|g| !on.contains(g)).collect()
|
||||
} else {
|
||||
off.into_iter().filter(|g| all.contains(g)).collect()
|
||||
};
|
||||
// Each rule, layer list and layer is read once, however often the rules name them: many rules
|
||||
// can share one long list.
|
||||
let known: std::collections::HashSet<ObjRef> = all.iter().copied().collect();
|
||||
let mut looked: std::collections::HashSet<ObjRef> = hidden.iter().copied().collect();
|
||||
let mut read = std::collections::HashSet::new();
|
||||
let rules = d.get(b"AS").and_then(|a| doc.resolve(a).as_array().cloned()).unwrap_or_default();
|
||||
for rule in &rules {
|
||||
if let Some(r) = rule.as_ref()
|
||||
&& !read.insert(r)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(rule) = doc.resolve(rule).as_dict().cloned() else { continue };
|
||||
let categories = rule.get(b"Category").and_then(|c| doc.resolve(c).as_array().cloned()).unwrap_or_default();
|
||||
if rule.name(b"Event") != Some(b"View") || !categories.iter().any(|c| c.as_name() == Some(b"View")) {
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = rule.get(b"OCGs").and_then(Object::as_ref)
|
||||
&& !read.insert(r)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for g in refs(rule.get(b"OCGs")) {
|
||||
if !known.contains(&g) || !looked.insert(g) {
|
||||
continue;
|
||||
}
|
||||
let usage = doc.get(g).as_dict().and_then(|layer| sub(doc, layer, b"Usage"));
|
||||
if usage.and_then(|u| sub(doc, &u, b"View")).is_some_and(|v| v.name(b"ViewState") == Some(b"OFF")) {
|
||||
hidden.push(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
hidden
|
||||
}
|
||||
|
||||
/// The objects a layer cleanup has reached by reference, and whether the lists naming each keep it.
|
||||
|
||||
@@ -4810,3 +4810,24 @@ fn a_removed_layer_leaves_every_configuration() {
|
||||
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");
|
||||
}
|
||||
|
||||
/// A layer the default configuration lists on but a View usage rule turns off is hidden on screen
|
||||
/// (ISO 32000-2, 8.11.4.4): Hidden layers removes it. A Print rule leaves its layer as it is.
|
||||
#[test]
|
||||
fn a_layer_a_view_rule_turns_off_is_hidden() {
|
||||
use crate::sanitize::{Hidden, remove_hidden, scan};
|
||||
let mut doc = pdf(vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [6 0 R 7 0 R] /D << /AS [<< /Event /View /OCGs [6 0 R] /Category [/View] >> << /Event /Print /OCGs [7 0 R] /Category [/Print] >>] >> >> >>".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 /L2 7 0 R >> >> >>".to_vec(),
|
||||
stream("", b"/OC /L1 BDC BT /F1 10 Tf 10 50 Td (ScreenOff) Tj ET EMC /OC /L2 BDC BT /F1 10 Tf 10 100 Td (PrintOff) Tj ET EMC"),
|
||||
FONT.replace("95 0 R", "8 0 R").into_bytes(),
|
||||
b"<< /Type /OCG /Name (Screen) /Usage << /View << /ViewState /OFF >> >> >>".to_vec(),
|
||||
b"<< /Type /OCG /Name (Print) /Usage << /Print << /PrintState /OFF >> >> >>".to_vec(),
|
||||
widths(),
|
||||
]);
|
||||
assert!(scan(&doc).into_iter().any(|(h, n)| h == Hidden::HiddenLayers && n > 0));
|
||||
remove_hidden(&mut doc, &[Hidden::HiddenLayers]).unwrap();
|
||||
let c = content(&doc, 0);
|
||||
assert!(!c.contains("ScreenOff") && c.contains("(PrintOff)"), "{c}");
|
||||
}
|
||||
Reference in new issue
Block a user