fix(engine): printing keeps layers hidden in the Layers panel hidden (#3046) (#3050)

The print-ready PDF copied the document's /OCProperties as they are, so it
printed the layers in their default state: a layer hidden in the Layers
panel disappeared on screen and came back on paper. Print as image already
used the view's layer choices (it renders with the document's config).

Once the layers have been changed, the copy imposed for printing now has
its default configuration (/D) set to them: /BaseState /ON, /ON the
visible layers, /OFF the hidden ones, its other entries kept. With the
layers untouched, the document's own configuration prints as before.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-18 03:12:26 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 13c86b40e8
commit 4ff53a8d8e
2 changed files with 81 additions and 1 deletions
+28 -1
View File
@@ -984,6 +984,27 @@ fn render_threads() -> usize {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 8) - 1
}
/// Write `layers`' visibility into the default optional-content configuration of `cos` (a copy
/// made for printing): `/BaseState /ON`, `/ON` the visible layers, `/OFF` the hidden ones. Its
/// other entries (`/Order`, `/RBGroups`, `/AS`…) are kept. A document without `/OCProperties`
/// is left alone.
fn set_default_layer_state(cos: &mut pdfcraft_cos::Document, layers: &[Layer]) {
use pdfcraft_cos::{Dict, ObjRef, Object};
let Some(root_ref) = cos.trailer().get(b"Root").and_then(Object::as_ref) else { return };
let Some(mut root) = cos.get(root_ref).as_dict().cloned() else { return };
let Some(properties) = root.get(b"OCProperties").cloned() else { return };
let Some(mut properties) = cos.resolve(&properties).as_dict().cloned() else { return };
let mut config = properties.get(b"D").and_then(|d| cos.resolve(d).as_dict().cloned()).unwrap_or_else(Dict::new);
let refs =
|visible: bool| Object::Array(layers.iter().filter(|l| l.visible == visible).map(|l| Object::Ref(ObjRef::new(l.id.0, l.id.1))).collect());
config.set(b"BaseState".to_vec(), Object::name("ON"));
config.set(b"ON".to_vec(), refs(true));
config.set(b"OFF".to_vec(), refs(false));
properties.set(b"D".to_vec(), Object::Dict(config));
root.set(b"OCProperties".to_vec(), Object::Dict(properties));
cos.set(root_ref, Object::Dict(root));
}
/// Render `doc` with the layer visibility in its `info.layers`.
fn use_layer_choices(doc: &mut Document) {
let overrides: Vec<(i32, i32, bool)> = doc.info.layers.iter().map(|l| (l.id.0 as i32, l.id.1 as i32, l.visible)).collect();
@@ -4332,12 +4353,18 @@ impl Session {
};
return pdfcraft_print::impose(&pictures, &remapped).map_err(|e| EditError::Print(e.to_string()));
}
let cos = match doc.editor.as_ref() {
let mut cos = match doc.editor.as_ref() {
Some(e) => e.cos.clone(),
None => {
pdfcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()).map_err(|e| EditError::Write(e.to_string()))?
}
};
// #3046: the layers print as they are shown. Once the Layers panel (or a set-layer
// action) has changed one, the print copy's default configuration says which are on, so a
// hidden layer stays off on paper too; untouched, the document's own configuration prints.
if !doc.config.layers.is_empty() {
set_default_layer_state(&mut cos, &doc.info.layers);
}
pdfcraft_print::impose(&cos, settings).map_err(|e| EditError::Print(e.to_string()))
}
+53
View File
@@ -1987,6 +1987,59 @@ fn oversized_redaction_overlay_preserves_the_document_and_undo() {
assert_eq!(page_texts(&s, id), ["Page 1"]);
}
/// #3046: a layer hidden in the Layers panel stays off on paper, as it is on screen; with the
/// layers untouched, the document's own configuration prints.
#[test]
fn printing_keeps_hidden_layers_hidden() {
let content = b"/OC /A BDC 1 0 0 rg 10 10 80 80 re f EMC /OC /B BDC 0 1 0 rg 110 10 80 80 re f EMC";
let objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R 6 0 R] /D << /Order [5 0 R 6 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 200 100] /Contents 4 0 R /Resources << /Properties << /A 5 0 R /B 6 0 R >> >> >>".to_vec(),
[format!("<< /Length {} >>\nstream\n", content.len()).as_bytes(), content, b"\nendstream"].concat(),
b"<< /Type /OCG /Name (Layer A) >>".to_vec(),
b"<< /Type /OCG /Name (Layer B) >>".to_vec(),
];
let mut pdf = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(pdf.len());
pdf.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
pdf.extend_from_slice(o);
pdf.extend_from_slice(b"\nendobj\n");
}
let xref = pdf.len();
pdf.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
pdf.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
pdf.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
// Red and green pixels on the first printed sheet.
let printed = |s: &Session, id: DocId| {
let bytes = s.print_pdf(id, &print::Settings { pages: vec![0], ..Default::default() }).unwrap();
let mut out = Session::new();
let printed = out.open("print.pdf", None, Arc::new(bytes), None).unwrap();
let page = render_page(&out, printed);
let px = page.rgba.as_chunks::<4>().0;
let red = px.iter().filter(|p| p[0] > 200 && p[1] < 60 && p[2] < 60).count();
let green = px.iter().filter(|p| p[1] > 200 && p[0] < 60 && p[2] < 60).count();
(red, green)
};
let mut s = Session::new();
let id = s.open("layers.pdf", None, Arc::new(pdf), None).unwrap();
let (red, green) = printed(&s, id);
assert!(red > 100 && green > 100, "untouched layers both print: {red} red, {green} green");
let a = s.get(id).unwrap().info.layers.iter().position(|l| l.name == "Layer A").unwrap();
assert!(s.set_layer_visible(id, a, false));
let (red, green) = printed(&s, id);
assert_eq!(red, 0, "Layer A was hidden, so it doesn't print");
assert!(green > 100, "Layer B still prints: {green} green");
assert!(s.set_layer_visible(id, a, true));
let (red, _) = printed(&s, id);
assert!(red > 100, "shown again, Layer A prints again: {red} red");
}
#[test]
fn printing_lays_out_sheets_that_render() {
let (s, id) = session_with(5);