mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
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:
co-authored by
Claude Opus 5.5
parent
13c86b40e8
commit
4ff53a8d8e
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user