From 8473403a5dce297e9ca0abdc6bde74af045e07de Mon Sep 17 00:00:00 2001 From: alehel Date: Sun, 25 Oct 2026 23:08:38 +0100 Subject: [PATCH] refactor(engine): move layer visibility to layers.rs Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/layers.rs | 60 +++++++++++++++++++++++++++++++++++++ crates/engine/src/lib.rs | 56 ++-------------------------------- crates/engine/src/tests.rs | 2 +- 3 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 crates/engine/src/layers.rs diff --git a/crates/engine/src/layers.rs b/crates/engine/src/layers.rs new file mode 100644 index 000000000..c2f4f6371 --- /dev/null +++ b/crates/engine/src/layers.rs @@ -0,0 +1,60 @@ +//! Layer (optional content) visibility: what the view shows and what printing writes. + +use std::sync::Arc; + +use pdfcraft_render::{Layer, LayerOp, RenderPool}; + +use crate::{Document, render_threads}; + +/// 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. +pub(crate) 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`. +pub(crate) 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(); + doc.config.layers = Arc::new(overrides); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); +} + +/// Apply a set-layer-visibility action to `layers`, one change at a time, so a toggle flips the +/// state the changes before it left. Returns whether any layer ended up changed. +pub(crate) fn apply_layer_state(layers: &mut [Layer], groups: &[Vec<(u32, u16)>], changes: &[(LayerOp, (u32, u16))], preserve_rb: bool) -> bool { + let before: Vec = layers.iter().map(|l| l.visible).collect(); + let index: std::collections::HashMap<(u32, u16), usize> = layers.iter().enumerate().map(|(i, l)| (l.id, i)).collect(); + for &(op, ocg) in changes { + let Some(layer) = index.get(&ocg).and_then(|&i| layers.get_mut(i)) else { continue }; + let on = match op { + LayerOp::On => true, + LayerOp::Off => false, + LayerOp::Toggle => !layer.visible, + }; + layer.visible = on; + // Turning a layer off leaves the rest of its groups alone. + if on && preserve_rb { + for other in groups.iter().filter(|g| g.contains(&ocg)).flatten().filter(|&&o| o != ocg) { + if let Some(l) = index.get(other).and_then(|&i| layers.get_mut(i)) { + l.visible = false; + } + } + } + } + layers.iter().zip(before).any(|(l, was)| l.visible != was) +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fa18e5a66..7a93fc87b 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -32,6 +32,7 @@ pub mod export; pub mod field_table; mod history; pub mod js; +mod layers; pub mod links; pub mod ocr; mod open; @@ -199,8 +200,8 @@ use document::{display_bytes, info_fields, recovered_render_password, same_pages pub use edit::{BATCH_LABELS, EDIT_LABELS, Edit}; pub use error::{EditError, SourceProblem}; pub use history::Label; +use layers::{apply_layer_state, set_default_layer_state, use_layer_choices}; pub use open::{OpenContext, PreparedDocument}; -use pdfcraft_render::{Layer, LayerOp, RenderPool}; use protection::unrestricted; pub use protection::{Changes, Printing, Protection, SecuritySummary}; pub use session::Session; @@ -223,59 +224,6 @@ fn render_threads() -> usize { std::thread::available_parallelism().map(std::num::NonZeroUsize::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(); - doc.config.layers = Arc::new(overrides); - doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); -} - -/// Apply a set-layer-visibility action to `layers`, one change at a time, so a toggle flips the -/// state the changes before it left. Returns whether any layer ended up changed. -fn apply_layer_state(layers: &mut [Layer], groups: &[Vec<(u32, u16)>], changes: &[(LayerOp, (u32, u16))], preserve_rb: bool) -> bool { - let before: Vec = layers.iter().map(|l| l.visible).collect(); - let index: std::collections::HashMap<(u32, u16), usize> = layers.iter().enumerate().map(|(i, l)| (l.id, i)).collect(); - for &(op, ocg) in changes { - let Some(layer) = index.get(&ocg).and_then(|&i| layers.get_mut(i)) else { continue }; - let on = match op { - LayerOp::On => true, - LayerOp::Off => false, - LayerOp::Toggle => !layer.visible, - }; - layer.visible = on; - // Turning a layer off leaves the rest of its groups alone. - if on && preserve_rb { - for other in groups.iter().filter(|g| g.contains(&ocg)).flatten().filter(|&&o| o != ocg) { - if let Some(l) = index.get(other).and_then(|&i| layers.get_mut(i)) { - l.visible = false; - } - } - } - } - layers.iter().zip(before).any(|(l, was)| l.visible != was) -} - /// A document's working file captured for crash recovery. #[derive(Clone, Debug)] pub struct RecoverySnapshot { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a30d81b74..c622af21b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::document::is_placeholder_title; use super::edit::check_permission; use super::*; -use pdfcraft_render::{OpenError, ReadingColors}; +use pdfcraft_render::{Layer, LayerOp, OpenError, ReadingColors}; /// A minimal `n`-page document; page `i` shows "Page i+1". pub(crate) fn fixture(n: usize) -> Vec {