mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 02:58:32 -06:00
M4: Initial View: Document Properties ▸ Initial View (navigation panel, page layout, magnification, open-to page, window and interface options) and Advanced ▸ reading options (binding, language), honoured when a document opens; script open actions are kept; Bookmarks panel options expand all, expand top-level or collapse all; doc_initial_view tool (85 tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
05195a2af6
commit
c6d82ab62a
@@ -272,6 +272,7 @@ impl Automation {
|
||||
json!({ "path": path.to_string_lossy(), "bytes_before": before, "bytes_after": bytes.len(), "merged_objects": merged })
|
||||
}
|
||||
"doc_optimize" => self.doc_optimize(&a)?,
|
||||
"doc_initial_view" => self.doc_initial_view(&a)?,
|
||||
"doc_export_images" | "doc_export_text" => self.export(name, &a)?,
|
||||
"doc_header_footer" | "doc_watermark" | "doc_background" | "doc_remove_marks" => self.marks(name, &a)?,
|
||||
"doc_unprotect" => {
|
||||
@@ -372,6 +373,94 @@ impl Automation {
|
||||
Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": !full, "document": summary(self.doc(a)?) }))
|
||||
}
|
||||
|
||||
fn doc_initial_view(&mut self, a: &Args) -> Result<Value> {
|
||||
use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N};
|
||||
let bad = |m: String| ToolError::InvalidArgs(m);
|
||||
let mut v = self.doc(a)?.initial_view();
|
||||
let before = v.clone();
|
||||
if let Some(n) = a.opt_str("navigation")? {
|
||||
v.navigation = match n {
|
||||
"page" => N::PageOnly,
|
||||
"bookmarks" => N::Bookmarks,
|
||||
"pages" => N::Pages,
|
||||
"attachments" => N::Attachments,
|
||||
"layers" => N::Layers,
|
||||
o => return Err(bad(format!("unknown navigation {o:?}"))),
|
||||
};
|
||||
}
|
||||
if let Some(l) = a.opt_str("layout")? {
|
||||
v.layout = match l {
|
||||
"default" => L::Default,
|
||||
"single" => L::SinglePage,
|
||||
"continuous" => L::SinglePageContinuous,
|
||||
"two_up" => L::TwoUp,
|
||||
"two_up_continuous" => L::TwoUpContinuous,
|
||||
"two_up_cover" => L::TwoUpCoverPage,
|
||||
"two_up_continuous_cover" => L::TwoUpContinuousCoverPage,
|
||||
o => return Err(bad(format!("unknown layout {o:?}"))),
|
||||
};
|
||||
}
|
||||
match a.get("magnification") {
|
||||
None => {}
|
||||
Some(Value::Number(p)) => v.magnification = M::Percent(p.as_f64().unwrap_or(100.0).clamp(1.0, 6400.0)),
|
||||
Some(Value::String(m)) => {
|
||||
v.magnification = match m.as_str() {
|
||||
"default" => M::Default,
|
||||
"actual" => M::ActualSize,
|
||||
"fit_page" => M::FitPage,
|
||||
"fit_width" => M::FitWidth,
|
||||
"fit_height" => M::FitHeight,
|
||||
"fit_visible" => M::FitVisible,
|
||||
o => return Err(bad(format!("unknown magnification {o:?}"))),
|
||||
}
|
||||
}
|
||||
Some(_) => return Err(bad("magnification is a name or a percentage".into())),
|
||||
}
|
||||
if let Some(p) = a.opt_int("page")? {
|
||||
v.page = (p.max(1) - 1) as usize;
|
||||
}
|
||||
for (k, f) in [
|
||||
("fit_window", &mut v.fit_window),
|
||||
("center_window", &mut v.center_window),
|
||||
("full_screen", &mut v.full_screen),
|
||||
("display_title", &mut v.display_title),
|
||||
("hide_menubar", &mut v.hide_menubar),
|
||||
("hide_toolbar", &mut v.hide_toolbar),
|
||||
("hide_window_ui", &mut v.hide_window_ui),
|
||||
] {
|
||||
if let Some(b) = a.opt_bool(k)? {
|
||||
*f = b;
|
||||
}
|
||||
}
|
||||
if let Some(l) = a.opt_str("language")? {
|
||||
v.language = (!l.trim().is_empty()).then(|| l.trim().to_string());
|
||||
}
|
||||
if let Some(b) = a.opt_str("binding")? {
|
||||
v.right_to_left = match b {
|
||||
"left" => false,
|
||||
"right" => true,
|
||||
o => return Err(bad(format!("binding is left or right, not {o:?}"))),
|
||||
};
|
||||
}
|
||||
let mut out = if v != before { self.apply(a, Edit::SetInitialView(Box::new(v.clone())))? } else { json!({}) };
|
||||
out["initial_view"] = json!({
|
||||
"navigation": format!("{:?}", v.navigation),
|
||||
"layout": format!("{:?}", v.layout),
|
||||
"magnification": format!("{:?}", v.magnification),
|
||||
"page": v.page + 1,
|
||||
"fit_window": v.fit_window,
|
||||
"center_window": v.center_window,
|
||||
"full_screen": v.full_screen,
|
||||
"display_title": v.display_title,
|
||||
"hide_menubar": v.hide_menubar,
|
||||
"hide_toolbar": v.hide_toolbar,
|
||||
"hide_window_ui": v.hide_window_ui,
|
||||
"language": v.language,
|
||||
"binding": if v.right_to_left { "right" } else { "left" },
|
||||
});
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn doc_optimize(&mut self, a: &Args) -> Result<Value> {
|
||||
use printcraft_engine::optimize::{Compression, ImageSettings, Settings};
|
||||
let id = self.doc(a)?.id;
|
||||
|
||||
@@ -712,6 +712,25 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
t("doc_reduce", "Reduce file size", "Write a smaller copy of the document to `path` with Acrobat's Reduce File Size choices: images above 225 ppi downsampled to 150 ppi and JPEG-compressed (medium quality), thumbnails dropped, identical fonts and images merged, unused objects dropped, compressed object streams. The open document is unchanged.")
|
||||
.cmd("optimize.reduce")
|
||||
.with(schema(json!({ "doc": doc(), "path": { "type": "string" } }), &["doc", "path"])),
|
||||
t("doc_initial_view", "Initial view", "Read or change how the document opens (Document Properties ▸ Initial View) and its reading options: navigation (page, bookmarks, pages, attachments, layers), layout (default, single, continuous, two_up, two_up_continuous, two_up_cover, two_up_continuous_cover), magnification (default, actual, fit_page, fit_width, fit_height, fit_visible, or a percentage), page, window options (fit_window, center_window, full_screen, display_title), interface options (hide_menubar, hide_toolbar, hide_window_ui), language and binding (left, right). Only the given ones change; returns the result. Undoable.").with(schema(
|
||||
json!({
|
||||
"doc": doc(),
|
||||
"navigation": { "type": "string", "enum": ["page", "bookmarks", "pages", "attachments", "layers"] },
|
||||
"layout": { "type": "string", "enum": ["default", "single", "continuous", "two_up", "two_up_continuous", "two_up_cover", "two_up_continuous_cover"] },
|
||||
"magnification": {},
|
||||
"page": { "type": "integer", "minimum": 1 },
|
||||
"fit_window": { "type": "boolean" },
|
||||
"center_window": { "type": "boolean" },
|
||||
"full_screen": { "type": "boolean" },
|
||||
"display_title": { "type": "boolean" },
|
||||
"hide_menubar": { "type": "boolean" },
|
||||
"hide_toolbar": { "type": "boolean" },
|
||||
"hide_window_ui": { "type": "boolean" },
|
||||
"language": { "type": "string" },
|
||||
"binding": { "type": "string", "enum": ["left", "right"] },
|
||||
}),
|
||||
&["doc"],
|
||||
)),
|
||||
t("doc_optimize", "Optimize PDF", "Write an optimized copy to `path` (Acrobat's PDF Optimizer). color / gray: { downsample, ppi, above_ppi, compression: jpeg|zip|retain, quality 1–100 } (defaults: downsample to 150 ppi above 225, JPEG 60). Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings; flate_unencoded (true). discard: Remove Hidden Information categories (metadata, attachments, comments, form-fields, hidden-text, hidden-layers, bookmarks, links-actions-scripts, private-data). Signed documents are refused. The open document is unchanged.")
|
||||
.cmd("optimize.advanced")
|
||||
.with(schema(
|
||||
|
||||
@@ -1038,3 +1038,23 @@ fn optimizing_through_tools() {
|
||||
));
|
||||
assert!(matches!(a.call("doc_optimize", &json!({ "doc": doc, "path": "x.pdf", "discard": ["everything"] })), Err(ToolError::InvalidArgs(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_view_through_tools() {
|
||||
let dir = workdir("initial-view");
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
|
||||
let r = ok(&mut a, "doc_initial_view", json!({ "doc": doc }));
|
||||
assert_eq!((r["initial_view"]["layout"].as_str(), r["initial_view"]["page"].as_u64()), (Some("Default"), Some(1)));
|
||||
let r = ok(
|
||||
&mut a,
|
||||
"doc_initial_view",
|
||||
json!({ "doc": doc, "navigation": "pages", "layout": "two_up", "magnification": 150, "page": 2, "language": "en-GB", "binding": "right", "display_title": true }),
|
||||
);
|
||||
let v = &r["initial_view"];
|
||||
assert_eq!((v["navigation"].as_str(), v["magnification"].as_str(), v["page"].as_u64()), (Some("Pages"), Some("Percent(150.0)"), Some(2)));
|
||||
assert_eq!((v["language"].as_str(), v["binding"].as_str()), (Some("en-GB"), Some("right")));
|
||||
assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Change initial view");
|
||||
assert!(matches!(a.call("doc_initial_view", &json!({ "doc": doc, "layout": "spiral" })), Err(ToolError::InvalidArgs(_))));
|
||||
assert!(matches!(a.call("doc_initial_view", &json!({ "doc": doc, "page": 9 })), Err(ToolError::Failed(_))));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pub use printcraft_organize::{BoxSpec, PageBox, SplitBy, split_ranges};
|
||||
|
||||
/// One file produced by a split: (1-based first page, last page, PDF bytes).
|
||||
pub use printcraft_organize::LabelStyle;
|
||||
pub use printcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation};
|
||||
|
||||
pub use printcraft_cos::Algorithm;
|
||||
pub use printcraft_edit::{
|
||||
@@ -148,6 +149,11 @@ pub struct Document {
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// How the document opens (Document Properties ▸ Initial View).
|
||||
pub fn initial_view(&self) -> InitialView {
|
||||
self.editor.as_ref().map(|e| printcraft_organize::initial_view(&e.cos)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Signed: at least one signature field holds a signature.
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.signatures.iter().any(|s| s.signed)
|
||||
@@ -591,6 +597,8 @@ pub enum Edit {
|
||||
pages: Vec<usize>,
|
||||
order: TabOrder,
|
||||
},
|
||||
/// Document Properties ▸ Initial View (and the language and binding).
|
||||
SetInitialView(Box<InitialView>),
|
||||
/// Order tabs manually: move a field one place earlier or later on its page.
|
||||
MoveInTabOrder {
|
||||
name: String,
|
||||
@@ -749,6 +757,7 @@ impl Edit {
|
||||
Edit::SetFieldProps { .. } => "Change field properties".into(),
|
||||
Edit::DeleteField { .. } => "Delete field".into(),
|
||||
Edit::SetTabOrder { .. } | Edit::MoveInTabOrder { .. } => "Set tab order".into(),
|
||||
Edit::SetInitialView(_) => "Change initial view".into(),
|
||||
Edit::AddHeaderFooter { replace: false, .. } => "Add header & footer".into(),
|
||||
Edit::AddHeaderFooter { .. } => "Update header & footer".into(),
|
||||
Edit::AddWatermark { replace: false, .. } => "Add watermark".into(),
|
||||
@@ -900,6 +909,7 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(),
|
||||
| Edit::DeleteField { .. }
|
||||
| Edit::SetTabOrder { .. }
|
||||
| Edit::MoveInTabOrder { .. }
|
||||
| Edit::SetInitialView(_)
|
||||
| Edit::Flatten { .. } => {
|
||||
if p.modify() {
|
||||
Ok(())
|
||||
@@ -1041,6 +1051,7 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -
|
||||
Edit::DeleteField { name } => printcraft_forms::delete_field(doc, name)?,
|
||||
Edit::SetTabOrder { pages, order } => printcraft_forms::set_tab_order(doc, pages, *order)?,
|
||||
Edit::MoveInTabOrder { name, earlier } => printcraft_forms::move_in_tab_order(doc, name, *earlier)?,
|
||||
Edit::SetInitialView(v) => printcraft_organize::set_initial_view(doc, v)?,
|
||||
Edit::AddHeaderFooter { pages, settings, replace } => {
|
||||
let date = cx.today;
|
||||
printcraft_edit::add_header_footer(doc, pages, settings, *replace, &printcraft_edit::Context { date })?;
|
||||
|
||||
@@ -16,6 +16,7 @@ mod dedupe;
|
||||
mod import;
|
||||
mod labels;
|
||||
mod outline;
|
||||
pub mod view;
|
||||
|
||||
pub use boxes::{BoxSpec, PageBox, page_boxes, set_page_box};
|
||||
pub use dedupe::dedupe_resources;
|
||||
@@ -24,6 +25,7 @@ pub use labels::{LabelRange, LabelStyle, number_pages, page_label_ranges, page_l
|
||||
pub use outline::{
|
||||
Bookmark, OutlineError, add_bookmark, bookmarks, delete_bookmark, move_bookmark, rename_bookmark, set_bookmark_open, set_bookmark_page,
|
||||
};
|
||||
pub use view::{InitialView, initial_view, set_initial_view};
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum OrganizeError {
|
||||
|
||||
@@ -652,3 +652,35 @@ fn a_page_becomes_a_form_xobject_upright() {
|
||||
assert!(res.get(b"Font").is_some(), "inherited resources come along");
|
||||
assert!(crate::page_as_form(&mut dst, &src, 7).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_view_round_trips_and_keeps_scripts() {
|
||||
use crate::view::{Layout, Magnification, Navigation};
|
||||
let mut doc = open(fixture());
|
||||
assert_eq!(crate::initial_view(&doc), crate::InitialView::default());
|
||||
let v = crate::InitialView {
|
||||
navigation: Navigation::Bookmarks,
|
||||
layout: Layout::TwoUpContinuousCoverPage,
|
||||
magnification: Magnification::Percent(150.0),
|
||||
page: 2,
|
||||
fit_window: true,
|
||||
display_title: true,
|
||||
hide_toolbar: true,
|
||||
language: Some("fr-FR".into()),
|
||||
right_to_left: true,
|
||||
..Default::default()
|
||||
};
|
||||
crate::set_initial_view(&mut doc, &v).unwrap();
|
||||
let doc = save_and_reopen(&doc);
|
||||
assert_eq!(crate::initial_view(&doc), v);
|
||||
// A script opening action survives a change that needs no destination.
|
||||
let mut doc = doc;
|
||||
let root = doc.root().unwrap();
|
||||
let mut js = printcraft_cos::Dict::new();
|
||||
js.set(b"S".to_vec(), Object::name("JavaScript"));
|
||||
js.set(b"JS".to_vec(), printcraft_cos::PdfString::text("app.alert('hi')"));
|
||||
doc.update_dict(root, |c| c.set(b"OpenAction".to_vec(), Object::Dict(js))).unwrap();
|
||||
crate::set_initial_view(&mut doc, &crate::InitialView { layout: Layout::SinglePage, ..Default::default() }).unwrap();
|
||||
assert!(doc.get(root).as_dict().unwrap().get(b"OpenAction").and_then(|o| o.as_dict()).is_some_and(|d| d.name(b"S") == Some(b"JavaScript")));
|
||||
assert!(crate::set_initial_view(&mut doc, &crate::InitialView { page: 9, ..Default::default() }).is_err());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
//! Document Properties ▸ Initial View and reading options: how the document opens (navigation
|
||||
//! panel, page layout, magnification, page; window and interface options) and its language and
|
||||
//! binding (ISO 32000-2 §7.7.2 Table 29, §12.2 viewer preferences).
|
||||
|
||||
use printcraft_cos::{Dict, Document, Object, PdfString};
|
||||
|
||||
use crate::OrganizeError;
|
||||
|
||||
/// The navigation panel shown on opening (`/PageMode`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Navigation {
|
||||
#[default]
|
||||
PageOnly,
|
||||
Bookmarks,
|
||||
Pages,
|
||||
Attachments,
|
||||
Layers,
|
||||
}
|
||||
|
||||
/// `/PageLayout`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
#[default]
|
||||
Default,
|
||||
SinglePage,
|
||||
SinglePageContinuous,
|
||||
TwoUp,
|
||||
TwoUpContinuous,
|
||||
TwoUpCoverPage,
|
||||
TwoUpContinuousCoverPage,
|
||||
}
|
||||
|
||||
/// The magnification of the opening destination.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum Magnification {
|
||||
#[default]
|
||||
Default,
|
||||
ActualSize,
|
||||
FitPage,
|
||||
FitWidth,
|
||||
FitHeight,
|
||||
FitVisible,
|
||||
/// Percent (100 = actual size).
|
||||
Percent(f64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct InitialView {
|
||||
pub navigation: Navigation,
|
||||
pub layout: Layout,
|
||||
pub magnification: Magnification,
|
||||
/// 0-based page opened to.
|
||||
pub page: usize,
|
||||
pub fit_window: bool,
|
||||
pub center_window: bool,
|
||||
pub full_screen: bool,
|
||||
/// Show the document title (not the file name) in the window title.
|
||||
pub display_title: bool,
|
||||
pub hide_menubar: bool,
|
||||
pub hide_toolbar: bool,
|
||||
pub hide_window_ui: bool,
|
||||
/// `/Lang` (e.g. "en-US").
|
||||
pub language: Option<String>,
|
||||
/// Right-edge binding (`/Direction /R2L`).
|
||||
pub right_to_left: bool,
|
||||
}
|
||||
|
||||
fn catalog(doc: &Document) -> Result<(printcraft_cos::ObjRef, Dict), OrganizeError> {
|
||||
let r = doc.root().ok_or(OrganizeError::NoPageTree)?;
|
||||
Ok((r, doc.get(r).as_dict().cloned().ok_or(OrganizeError::NoPageTree)?))
|
||||
}
|
||||
|
||||
/// The document's initial view as stored.
|
||||
pub fn initial_view(doc: &Document) -> InitialView {
|
||||
let Ok((_, c)) = catalog(doc) else { return InitialView::default() };
|
||||
let name = |d: &Dict, k: &[u8]| d.get(k).map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(<[u8]>::to_vec));
|
||||
let mut v = InitialView {
|
||||
navigation: match name(&c, b"PageMode").as_deref() {
|
||||
Some(b"UseOutlines") => Navigation::Bookmarks,
|
||||
Some(b"UseThumbs") => Navigation::Pages,
|
||||
Some(b"UseAttachments") => Navigation::Attachments,
|
||||
Some(b"UseOC") => Navigation::Layers,
|
||||
_ => Navigation::PageOnly,
|
||||
},
|
||||
full_screen: name(&c, b"PageMode").as_deref() == Some(b"FullScreen"),
|
||||
layout: match name(&c, b"PageLayout").as_deref() {
|
||||
Some(b"SinglePage") => Layout::SinglePage,
|
||||
Some(b"OneColumn") => Layout::SinglePageContinuous,
|
||||
Some(b"TwoPageLeft") => Layout::TwoUp,
|
||||
Some(b"TwoColumnLeft") => Layout::TwoUpContinuous,
|
||||
Some(b"TwoPageRight") => Layout::TwoUpCoverPage,
|
||||
Some(b"TwoColumnRight") => Layout::TwoUpContinuousCoverPage,
|
||||
_ => Layout::Default,
|
||||
},
|
||||
language: c.get(b"Lang").map(|o| doc.resolve(o)).and_then(|o| o.as_string().map(|s| s.to_text())).filter(|s| !s.is_empty()),
|
||||
..InitialView::default()
|
||||
};
|
||||
if let Some(vp) = c.get(b"ViewerPreferences").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()) {
|
||||
let flag = |k: &[u8]| matches!(vp.get(k), Some(Object::Bool(true)));
|
||||
v.fit_window = flag(b"FitWindow");
|
||||
v.center_window = flag(b"CenterWindow");
|
||||
v.display_title = flag(b"DisplayDocTitle");
|
||||
v.hide_menubar = flag(b"HideMenubar");
|
||||
v.hide_toolbar = flag(b"HideToolbar");
|
||||
v.hide_window_ui = flag(b"HideWindowUI");
|
||||
v.right_to_left = name(&vp, b"Direction").as_deref() == Some(b"R2L");
|
||||
}
|
||||
// The opening destination: an explicit array, or a GoTo action's.
|
||||
let dest = c.get(b"OpenAction").map(|o| doc.resolve(o)).and_then(|o| match &*o {
|
||||
Object::Array(a) => Some(a.clone()),
|
||||
Object::Dict(d) if d.name(b"S") == Some(b"GoTo") => d.get(b"D").map(|x| doc.resolve(x)).and_then(|x| x.as_array().cloned()),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(d) = dest {
|
||||
let pages = crate::walk(doc).unwrap_or_default();
|
||||
v.page = d
|
||||
.first()
|
||||
.and_then(|p| match p {
|
||||
Object::Ref(r) => pages.iter().position(|(pr, _)| pr == r),
|
||||
Object::Int(i) => usize::try_from(*i).ok(),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let kind = d.get(1).and_then(|k| k.as_name().map(<[u8]>::to_vec));
|
||||
v.magnification = match kind.as_deref() {
|
||||
Some(b"Fit") => Magnification::FitPage,
|
||||
Some(b"FitH") => Magnification::FitWidth,
|
||||
Some(b"FitV") => Magnification::FitHeight,
|
||||
Some(b"FitB" | b"FitBH" | b"FitBV") => Magnification::FitVisible,
|
||||
Some(b"XYZ") => match d.get(4).and_then(|z| z.as_f64()) {
|
||||
Some(z) if z > 0.0 && (z - 1.0).abs() < 1e-6 => Magnification::ActualSize,
|
||||
Some(z) if z > 0.0 => Magnification::Percent(z * 100.0),
|
||||
_ => Magnification::Default,
|
||||
},
|
||||
_ => Magnification::Default,
|
||||
};
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Write the initial view. An opening action that isn't a plain destination (a script, say) is
|
||||
/// kept unless the page or magnification asks for a destination.
|
||||
pub fn set_initial_view(doc: &mut Document, v: &InitialView) -> Result<(), OrganizeError> {
|
||||
let (root, c) = catalog(doc)?;
|
||||
let pages = crate::walk(doc)?;
|
||||
if v.page >= pages.len() {
|
||||
return Err(OrganizeError::NoSuchPage(v.page));
|
||||
}
|
||||
let page_ref = pages[v.page].0;
|
||||
let mut vp = c.get(b"ViewerPreferences").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
for (k, on) in [
|
||||
(&b"FitWindow"[..], v.fit_window),
|
||||
(b"CenterWindow", v.center_window),
|
||||
(b"DisplayDocTitle", v.display_title),
|
||||
(b"HideMenubar", v.hide_menubar),
|
||||
(b"HideToolbar", v.hide_toolbar),
|
||||
(b"HideWindowUI", v.hide_window_ui),
|
||||
] {
|
||||
if on {
|
||||
vp.set(k.to_vec(), Object::Bool(true));
|
||||
} else {
|
||||
vp.remove(k);
|
||||
}
|
||||
}
|
||||
if v.right_to_left {
|
||||
vp.set(b"Direction".to_vec(), Object::name("R2L"));
|
||||
} else {
|
||||
vp.remove(b"Direction");
|
||||
}
|
||||
let open_action_is_dest = match c.get(b"OpenAction").map(|o| doc.resolve(o)) {
|
||||
None => true,
|
||||
Some(o) => matches!(&*o, Object::Array(_)) || o.as_dict().is_some_and(|d| d.name(b"S") == Some(b"GoTo")),
|
||||
};
|
||||
let dest = match v.magnification {
|
||||
Magnification::Default if v.page == 0 => None,
|
||||
Magnification::Default => Some(vec![Object::Ref(page_ref), Object::name("XYZ"), Object::Null, Object::Null, Object::Null]),
|
||||
Magnification::ActualSize => Some(vec![Object::Ref(page_ref), Object::name("XYZ"), Object::Null, Object::Null, Object::Real(1.0)]),
|
||||
Magnification::Percent(p) => {
|
||||
Some(vec![Object::Ref(page_ref), Object::name("XYZ"), Object::Null, Object::Null, Object::Real((p / 100.0).clamp(0.01, 64.0))])
|
||||
}
|
||||
Magnification::FitPage => Some(vec![Object::Ref(page_ref), Object::name("Fit")]),
|
||||
Magnification::FitWidth => Some(vec![Object::Ref(page_ref), Object::name("FitH"), Object::Null]),
|
||||
Magnification::FitHeight => Some(vec![Object::Ref(page_ref), Object::name("FitV"), Object::Null]),
|
||||
Magnification::FitVisible => Some(vec![Object::Ref(page_ref), Object::name("FitB")]),
|
||||
};
|
||||
doc.update_dict(root, |c| {
|
||||
let mode = if v.full_screen {
|
||||
Some("FullScreen")
|
||||
} else {
|
||||
match v.navigation {
|
||||
Navigation::PageOnly => None,
|
||||
Navigation::Bookmarks => Some("UseOutlines"),
|
||||
Navigation::Pages => Some("UseThumbs"),
|
||||
Navigation::Attachments => Some("UseAttachments"),
|
||||
Navigation::Layers => Some("UseOC"),
|
||||
}
|
||||
};
|
||||
match mode {
|
||||
Some(m) => c.set(b"PageMode".to_vec(), Object::name(m)),
|
||||
None => {
|
||||
c.remove(b"PageMode");
|
||||
}
|
||||
}
|
||||
let layout = match v.layout {
|
||||
Layout::Default => None,
|
||||
Layout::SinglePage => Some("SinglePage"),
|
||||
Layout::SinglePageContinuous => Some("OneColumn"),
|
||||
Layout::TwoUp => Some("TwoPageLeft"),
|
||||
Layout::TwoUpContinuous => Some("TwoColumnLeft"),
|
||||
Layout::TwoUpCoverPage => Some("TwoPageRight"),
|
||||
Layout::TwoUpContinuousCoverPage => Some("TwoColumnRight"),
|
||||
};
|
||||
match layout {
|
||||
Some(l) => c.set(b"PageLayout".to_vec(), Object::name(l)),
|
||||
None => {
|
||||
c.remove(b"PageLayout");
|
||||
}
|
||||
}
|
||||
if vp.is_empty() {
|
||||
c.remove(b"ViewerPreferences");
|
||||
} else {
|
||||
c.set(b"ViewerPreferences".to_vec(), Object::Dict(vp.clone()));
|
||||
}
|
||||
match &v.language {
|
||||
Some(l) if !l.trim().is_empty() => c.set(b"Lang".to_vec(), PdfString::text(l.trim())),
|
||||
_ => {
|
||||
c.remove(b"Lang");
|
||||
}
|
||||
}
|
||||
match (&dest, open_action_is_dest) {
|
||||
(Some(d), _) => c.set(b"OpenAction".to_vec(), Object::Array(d.clone())),
|
||||
(None, true) => {
|
||||
c.remove(b"OpenAction");
|
||||
}
|
||||
// A script or other action stays.
|
||||
(None, false) => {}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
save_prompt(app, ctx);
|
||||
let Some(dialog) = app.dialog else {
|
||||
app.props_draft = None;
|
||||
app.view_draft = None;
|
||||
return;
|
||||
};
|
||||
// Seed the editable Description fields from the document when the dialog opens.
|
||||
@@ -22,6 +23,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
&& let Some(doc) = app.session.get(id)
|
||||
{
|
||||
app.props_draft = Some((id, INFO_KEYS.map(|k| doc.info_value(k).unwrap_or_default())));
|
||||
app.view_draft = Some((id, doc.initial_view()));
|
||||
}
|
||||
let mut apply = false;
|
||||
let mut split_now: Option<crate::SplitPlan> = None;
|
||||
@@ -70,6 +72,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
ui.horizontal(|ui| {
|
||||
for (tb, label) in [
|
||||
(PropsTab::Description, "Description"),
|
||||
(PropsTab::InitialView, "Initial View"),
|
||||
(PropsTab::Security, "Security"),
|
||||
(PropsTab::Fonts, "Fonts"),
|
||||
(PropsTab::Advanced, "Advanced"),
|
||||
@@ -117,6 +120,114 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
row(ui, "Application", i.creator.clone().unwrap_or_default());
|
||||
row(ui, "PDF producer", i.producer.clone().unwrap_or_default());
|
||||
}
|
||||
PropsTab::InitialView => {
|
||||
use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N};
|
||||
let editable = doc.allows_modification();
|
||||
let pages = i.pages.len();
|
||||
let Some((_, v)) = app.view_draft.as_mut() else { return };
|
||||
ui.label(egui::RichText::new("Layout and Magnification").font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("Navigation tab");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
egui::ComboBox::from_id_salt("iv-nav")
|
||||
.selected_text(
|
||||
format!("{:?}", v.navigation).replace("PageOnly", "Page Only").replace("Pages", "Pages Panel and Page"),
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
for (n, l) in [
|
||||
(N::PageOnly, "Page Only"),
|
||||
(N::Bookmarks, "Bookmarks Panel and Page"),
|
||||
(N::Pages, "Pages Panel and Page"),
|
||||
(N::Attachments, "Attachments Panel and Page"),
|
||||
(N::Layers, "Layers Panel and Page"),
|
||||
] {
|
||||
ui.selectable_value(&mut v.navigation, n, l);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Page layout");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
let names = [
|
||||
(L::Default, "Default"),
|
||||
(L::SinglePage, "Single Page"),
|
||||
(L::SinglePageContinuous, "Single Page Continuous"),
|
||||
(L::TwoUp, "Two-Up (Facing)"),
|
||||
(L::TwoUpContinuous, "Two-Up Continuous (Facing)"),
|
||||
(L::TwoUpCoverPage, "Two-Up (Cover Page)"),
|
||||
(L::TwoUpContinuousCoverPage, "Two-Up Continuous (Cover Page)"),
|
||||
];
|
||||
let shown = names.iter().find(|(l, _)| *l == v.layout).map_or("Default", |(_, n)| n);
|
||||
egui::ComboBox::from_id_salt("iv-layout").selected_text(shown).show_ui(ui, |ui| {
|
||||
for (l, n) in names {
|
||||
ui.selectable_value(&mut v.layout, l, n);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Magnification");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let names = [
|
||||
(M::Default, "Default"),
|
||||
(M::ActualSize, "Actual Size"),
|
||||
(M::FitPage, "Fit Page"),
|
||||
(M::FitWidth, "Fit Width"),
|
||||
(M::FitHeight, "Fit Height"),
|
||||
(M::FitVisible, "Fit Visible"),
|
||||
];
|
||||
let shown = match v.magnification {
|
||||
M::Percent(p) => format!("{p:.0}%"),
|
||||
m => names.iter().find(|(x, _)| *x == m).map_or("Default", |(_, n)| n).to_string(),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("iv-mag").selected_text(shown).show_ui(ui, |ui| {
|
||||
for (m, n) in names {
|
||||
ui.selectable_value(&mut v.magnification, m, n);
|
||||
}
|
||||
for p in [50.0, 75.0, 125.0, 150.0, 200.0] {
|
||||
ui.selectable_value(&mut v.magnification, M::Percent(p), format!("{p:.0}%"));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Open to page");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
let mut p = v.page + 1;
|
||||
if ui.add(egui::DragValue::new(&mut p).range(1..=pages.max(1))).changed() {
|
||||
v.page = p - 1;
|
||||
}
|
||||
ui.label(format!("of {pages}"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(egui::RichText::new("Window Options").font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut v.fit_window, "Resize window to initial page");
|
||||
ui.checkbox(&mut v.center_window, "Center window on screen");
|
||||
ui.checkbox(&mut v.full_screen, "Open in Full Screen mode");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Show:");
|
||||
ui.radio_value(&mut v.display_title, false, "File Name");
|
||||
ui.radio_value(&mut v.display_title, true, "Document Title");
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(egui::RichText::new("User Interface Options").font(theme::semibold(12.5)));
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut v.hide_menubar, "Hide menu bar");
|
||||
ui.checkbox(&mut v.hide_toolbar, "Hide toolbars");
|
||||
ui.checkbox(&mut v.hide_window_ui, "Hide window controls");
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
PropsTab::Security => match doc.security_summary() {
|
||||
None => {
|
||||
row(ui, "Security method", "No security".into());
|
||||
@@ -202,6 +313,28 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
row(ui, "Layers", i.layers.len().to_string());
|
||||
row(ui, "Attachments", i.attachments.len().to_string());
|
||||
row(ui, "JavaScript", yes(i.has_javascript));
|
||||
// Reading Options: binding and language.
|
||||
if let Some((_, v)) = app.view_draft.as_mut() {
|
||||
let editable = doc.allows_modification();
|
||||
ui.label(egui::RichText::new("Binding").color(t.text_muted));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(&mut v.right_to_left, false, "Left Edge");
|
||||
ui.radio_value(&mut v.right_to_left, true, "Right Edge");
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
let l = ui.label(egui::RichText::new("Language").color(t.text_muted));
|
||||
let mut lang = v.language.clone().unwrap_or_default();
|
||||
if ui
|
||||
.add_enabled(editable, egui::TextEdit::singleline(&mut lang).hint_text("e.g. en-US").desired_width(160.0))
|
||||
.labelled_by(l.id)
|
||||
.changed()
|
||||
{
|
||||
v.language = (!lang.trim().is_empty()).then(|| lang.trim().to_string());
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
// What was repaired while reading a damaged file (fidelity: never silent).
|
||||
let repairs = doc.repair_log();
|
||||
row(ui, "Repairs", if repairs.is_empty() { "None".to_string() } else { repairs.len().to_string() });
|
||||
@@ -909,6 +1042,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
if modal.should_close() || close || replaces {
|
||||
app.dialog = None;
|
||||
app.props_draft = None;
|
||||
app.view_draft = None;
|
||||
} else {
|
||||
app.dialog = Some(next);
|
||||
}
|
||||
@@ -921,14 +1055,19 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
fn draft_changes(app: &PrintCraftApp) -> Option<Vec<Edit>> {
|
||||
let (id, draft) = app.props_draft.as_ref()?;
|
||||
let doc = app.session.get(*id)?;
|
||||
Some(
|
||||
INFO_KEYS
|
||||
.iter()
|
||||
.zip(draft.iter())
|
||||
.filter(|(k, v)| doc.info_value(k).unwrap_or_default().trim() != v.trim())
|
||||
.map(|(k, v)| Edit::SetInfo { key: (*k).to_string(), value: v.clone() })
|
||||
.collect(),
|
||||
)
|
||||
let mut edits: Vec<Edit> = INFO_KEYS
|
||||
.iter()
|
||||
.zip(draft.iter())
|
||||
.filter(|(k, v)| doc.info_value(k).unwrap_or_default().trim() != v.trim())
|
||||
.map(|(k, v)| Edit::SetInfo { key: (*k).to_string(), value: v.clone() })
|
||||
.collect();
|
||||
if let Some((vid, v)) = &app.view_draft
|
||||
&& vid == id
|
||||
&& *v != doc.initial_view()
|
||||
{
|
||||
edits.push(Edit::SetInitialView(Box::new(v.clone())));
|
||||
}
|
||||
Some(edits)
|
||||
}
|
||||
|
||||
/// "Save changes?" when closing a tab or quitting with unsaved edits.
|
||||
|
||||
@@ -167,6 +167,7 @@ pub enum Dialog {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PropsTab {
|
||||
Description,
|
||||
InitialView,
|
||||
Security,
|
||||
Fonts,
|
||||
Advanced,
|
||||
@@ -224,6 +225,8 @@ pub struct PrintCraftApp {
|
||||
pub save_override: Option<String>,
|
||||
/// Document Properties ▸ Description fields being edited: (document, Title/Author/Subject/Keywords).
|
||||
pub props_draft: Option<(DocId, [String; 4])>,
|
||||
/// Document Properties ▸ Initial View (and reading options) being edited.
|
||||
pub view_draft: Option<(DocId, printcraft_engine::InitialView)>,
|
||||
/// Files picked asynchronously for combine / insert (web).
|
||||
pub requests: files::Requests,
|
||||
/// Write exported files (split) here instead of asking (tests and automation).
|
||||
@@ -335,6 +338,7 @@ impl PrintCraftApp {
|
||||
close_request: None,
|
||||
save_override: None,
|
||||
props_draft: None,
|
||||
view_draft: None,
|
||||
requests: Default::default(),
|
||||
export_dir_override: None,
|
||||
split_draft: SplitDraft::default(),
|
||||
@@ -379,6 +383,42 @@ impl PrintCraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a document the way it asks to be opened: navigation panel, layout, magnification,
|
||||
/// page (Document Properties ▸ Initial View).
|
||||
fn apply_initial_view(&mut self, index: usize, v: &printcraft_engine::InitialView) {
|
||||
use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N};
|
||||
let pages = self.session.get(self.views[index].id).map_or(0, |d| d.info.pages.len());
|
||||
match v.navigation {
|
||||
N::PageOnly => {}
|
||||
N::Bookmarks => self.right = Some(RightPanel::Bookmarks),
|
||||
N::Pages => self.right = Some(RightPanel::Pages),
|
||||
N::Attachments => self.right = Some(RightPanel::Attachments),
|
||||
N::Layers => self.right = Some(RightPanel::Layers),
|
||||
}
|
||||
let view = &mut self.views[index];
|
||||
match v.layout {
|
||||
L::Default => {}
|
||||
L::SinglePage => view.layout = canvas::PageLayout::Single,
|
||||
L::SinglePageContinuous => view.layout = canvas::PageLayout::Continuous,
|
||||
L::TwoUp | L::TwoUpContinuous => view.layout = canvas::PageLayout::TwoUp,
|
||||
L::TwoUpCoverPage | L::TwoUpContinuousCoverPage => {
|
||||
view.layout = canvas::PageLayout::TwoUp;
|
||||
view.cover = true;
|
||||
}
|
||||
}
|
||||
match v.magnification {
|
||||
M::Default => {}
|
||||
M::ActualSize => view.set_zoom(1.0),
|
||||
M::Percent(p) => view.set_zoom((p / 100.0) as f32),
|
||||
M::FitPage | M::FitVisible => view.fit = canvas::Fit::Page,
|
||||
M::FitWidth => view.fit = canvas::Fit::Width,
|
||||
M::FitHeight => view.fit = canvas::Fit::Height,
|
||||
}
|
||||
if v.page > 0 && v.page < pages {
|
||||
view.go_to_page(v.page);
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a document and make it the active tab. Encrypted files raise the password prompt.
|
||||
pub fn open_bytes(&mut self, name: &str, path: Option<String>, bytes: Vec<u8>) -> Result<(), String> {
|
||||
// Images and text files become new, unsaved PDFs (Create a PDF).
|
||||
@@ -413,8 +453,10 @@ impl PrintCraftApp {
|
||||
None
|
||||
};
|
||||
}
|
||||
let initial = doc.initial_view();
|
||||
self.views.push(DocView::new(id, &doc.info));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
self.apply_initial_view(self.views.len() - 1, &initial);
|
||||
if let Some(p) = path {
|
||||
self.recent.retain(|r| r.path != p);
|
||||
self.recent.insert(0, RecentFile { name: name.to_string(), path: p, pages, size });
|
||||
|
||||
@@ -323,6 +323,7 @@ pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) {
|
||||
let mut toggle_layer: Option<(usize, bool)> = None;
|
||||
let mut attachment_action: Option<(usize, bool)> = None; // (index, open instead of save)
|
||||
let mut bm_action: Option<BmAction> = None;
|
||||
let mut bm_expand: Option<usize> = None;
|
||||
let mut panel_edit: Option<printcraft_engine::Edit> = None;
|
||||
let mut panel_command: Option<&'static str> = None;
|
||||
let mut sig_action: Option<crate::sign_ui::PanelAction> = None;
|
||||
@@ -379,6 +380,20 @@ pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) {
|
||||
};
|
||||
view.comments.search_focus = true;
|
||||
}
|
||||
if panel == RightPanel::Bookmarks && !info.outline.is_empty() {
|
||||
let more = icons::button(ui, "ellipsis", 26.0, false, "Bookmark options");
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
for (levels, label) in
|
||||
[(usize::MAX, "Expand all bookmarks"), (1, "Expand top-level bookmarks"), (0, "Collapse all bookmarks")]
|
||||
{
|
||||
if ui.button(label).clicked() {
|
||||
bm_expand = Some(levels);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if panel == RightPanel::Bookmarks
|
||||
&& bm_editable
|
||||
&& icons::button(ui, "bookmark-plus", 26.0, false, "New bookmark (⌘B)").clicked()
|
||||
@@ -404,6 +419,7 @@ pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) {
|
||||
rename: &mut bm_rename,
|
||||
editable: bm_editable,
|
||||
current: view.current,
|
||||
expand: bm_expand,
|
||||
};
|
||||
for (i, item) in info.outline.iter().enumerate() {
|
||||
outline_item(ui, &t, info, item, &[i], info.outline.len(), &mut ctx);
|
||||
@@ -557,12 +573,17 @@ struct OutlineCtx<'a> {
|
||||
rename: &'a mut Option<(Vec<usize>, String)>,
|
||||
editable: bool,
|
||||
current: usize,
|
||||
/// Expand all (`Some(usize::MAX)`), collapse all (`Some(0)`) or expand to a depth, this frame.
|
||||
expand: Option<usize>,
|
||||
}
|
||||
|
||||
fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineItem, path: &[usize], siblings: usize, cx: &mut OutlineCtx<'_>) {
|
||||
let depth = path.len() - 1;
|
||||
let indent = depth as f32 * 16.0;
|
||||
let id = ui.id().with(("outline", path));
|
||||
if let Some(levels) = cx.expand {
|
||||
ui.data_mut(|d| d.insert_temp(id, depth < levels));
|
||||
}
|
||||
let mut open = ui.data(|d| d.get_temp::<bool>(id)).unwrap_or(item.open || depth == 0);
|
||||
let x_text = indent + 20.0;
|
||||
if let Some((rpath, text)) = cx.rename.as_mut()
|
||||
|
||||
@@ -127,3 +127,29 @@ fn number_pages_dialog_labels_the_selected_pages() {
|
||||
assert_eq!(labels, ["i", "ii", "3"]);
|
||||
assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Number pages"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanding_and_collapsing_all_bookmarks() {
|
||||
use printcraft_engine::Edit;
|
||||
let mut h = harness();
|
||||
for (parent, title) in [(vec![], "Part"), (vec![0], "Chapter"), (vec![0, 0], "Section")] {
|
||||
h.state_mut().apply_edit(Edit::AddBookmark { parent, index: 0, title: title.into(), page: 0 });
|
||||
}
|
||||
h.run_steps(3);
|
||||
let shown = |h: &Harness<'static, PrintCraftApp>, t: &str| h.query_by_label(t).is_some();
|
||||
h.get_by_label("Bookmark options").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Expand all bookmarks").click();
|
||||
h.run_steps(3);
|
||||
assert!(shown(&h, "Chapter") && shown(&h, "Section"));
|
||||
h.get_by_label("Bookmark options").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Collapse all bookmarks").click();
|
||||
h.run_steps(3);
|
||||
assert!(shown(&h, "Part") && !shown(&h, "Chapter"));
|
||||
h.get_by_label("Bookmark options").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Expand top-level bookmarks").click();
|
||||
h.run_steps(3);
|
||||
assert!(shown(&h, "Chapter") && !shown(&h, "Section"));
|
||||
}
|
||||
|
||||
@@ -194,3 +194,34 @@ trailer << /Root 1 0 R >>
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Properties(printcraft_ui_egui::PropsTab::Advanced)));
|
||||
h.get_by_label("Repair log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_view_is_edited_and_honoured_on_open() {
|
||||
use printcraft_engine::{InitialLayout, Magnification, Navigation};
|
||||
let mut h = harness();
|
||||
h.state_mut().dialog = Some(Dialog::Properties(printcraft_ui_egui::PropsTab::InitialView));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Open to page");
|
||||
{
|
||||
let (_, v) = h.state_mut().view_draft.as_mut().expect("seeded");
|
||||
v.navigation = Navigation::Bookmarks;
|
||||
v.layout = InitialLayout::TwoUpCoverPage;
|
||||
v.magnification = Magnification::FitWidth;
|
||||
v.page = 1;
|
||||
v.language = Some("de-DE".into());
|
||||
}
|
||||
h.run_steps(2);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
let id = s.views[s.active.unwrap()].id;
|
||||
let v = s.session.get(id).unwrap().initial_view();
|
||||
assert_eq!((v.navigation, v.layout, v.page, v.language.as_deref()), (Navigation::Bookmarks, InitialLayout::TwoUpCoverPage, 1, Some("de-DE")));
|
||||
assert_eq!(s.session.get(id).unwrap().can_undo(), Some("Change document properties"));
|
||||
// Opening the saved file follows it.
|
||||
let bytes = s.session.save_bytes(id).unwrap();
|
||||
let mut app = PrintCraftApp::new();
|
||||
app.open_bytes("again.pdf", None, bytes.to_vec()).unwrap();
|
||||
let view = &app.views[0];
|
||||
assert_eq!((view.current, view.cover, app.right), (1, true, Some(printcraft_ui_egui::RightPanel::Bookmarks)));
|
||||
}
|
||||
|
||||
@@ -3211,11 +3211,11 @@ title = "Expand and collapse all bookmarks"
|
||||
area = "D"
|
||||
tier = "P1"
|
||||
milestone = "M4"
|
||||
status = "planned"
|
||||
commands = []
|
||||
status = "shipped"
|
||||
commands = ["file.properties"]
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
evidence = ["crates/ui-egui/tests/bookmarks.rs::expanding_and_collapsing_all_bookmarks"]
|
||||
notes = "Bookmarks panel options: expand all, expand top-level, collapse all"
|
||||
|
||||
[[feature]]
|
||||
id = "organize.bookmarks-from-structure"
|
||||
@@ -3355,11 +3355,11 @@ title = "Edit initial view (layout, magnification, panels, window options)"
|
||||
area = "D"
|
||||
tier = "P1"
|
||||
milestone = "M4"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["file.properties"]
|
||||
tools = ["doc_initial_view"]
|
||||
evidence = ["crates/organize/src/tests.rs::initial_view_round_trips_and_keeps_scripts", "crates/ui-egui/tests/viewer.rs::initial_view_is_edited_and_honoured_on_open", "crates/automation/tests/automation.rs::initial_view_through_tools"]
|
||||
notes = "navigation panel, layout, magnification, page, window and interface options; honoured on open; script open actions kept"
|
||||
|
||||
[[feature]]
|
||||
id = "organize.custom-properties"
|
||||
@@ -3391,11 +3391,11 @@ title = "Set document language and binding"
|
||||
area = "D"
|
||||
tier = "P1"
|
||||
milestone = "M4"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["file.properties"]
|
||||
tools = ["doc_initial_view"]
|
||||
evidence = ["crates/organize/src/tests.rs::initial_view_round_trips_and_keeps_scripts", "crates/automation/tests/automation.rs::initial_view_through_tools"]
|
||||
notes = "Advanced ▸ Reading Options: binding and language"
|
||||
|
||||
[[feature]]
|
||||
id = "organize.print-presets"
|
||||
|
||||
Reference in New Issue
Block a user