mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 05:58:12 -06:00
* M6: Organize pages ▸ Page properties sets the selected pages' tab order
Page properties in the Organize pages panel was listed as planned. In
the app, a page's tab order could only be set for every page at once,
with the form.tab_order commands, which offer no Unspecified. (The
form_tab_order tool already set chosen pages, Unspecified included.)
- Page properties (page.properties) opens a Tab Order dialog for the
selected pages, with Use Row Order, Use Column Order, Use Document
Structure and Unspecified. OK writes Edit::SetTabOrder for those pages
as one undo step. The dialog opens on the pages' order, or with no
choice made when they differ, and then OK is off until one is chosen.
OK on the order the pages already have adds no undo step.
- Rows and columns run right to left in a document bound on the right,
as Use Row Order and Use Column Order are described. PdfCraft reads a
right-to-left binding from /ViewerPreferences /Direction /R2L
(ISO 32000-1, 12.2, Table 150). Before, rank_tabs ranked them left to
right.
- A page whose /Tabs is an indirect object now takes its row, column or
structure order. Before, rank_tabs read only a direct name and ranked
the page in annotation order.
- pdfcraft_forms::tab_orders and Document::tab_orders read each page's
/Tabs. form_tab_order returns them as page_tabs, and reads without
changing anything when it is given no order or field.
- set_option("dialog", "page-properties") opens the dialog.
- Parity: organize.page-tab-order is shipped, and its notes say the
Actions tab of Page Properties (organize.page-actions) is still
planned; the Organize pages grid notes list Page properties.
* Page properties: OK applies only to the document the dialog was opened on
The draft keeps the document's id. When another document is active by
OK, the dialog closes without changing either document.
* Menus: list Page properties… in the Pages menu
The menu bar now lists its commands explicitly, and every command with a menu group must appear in it.
* Page properties: apply the tab order only to an unchanged document
An edit, undo or redo made while the dialog is open can move the pages
at the stored indices. OK now closes the dialog without changing a page
when the document's edit generation differs from when it opened.
* Page properties: say when a document change cancels the tab order
OK shows a notice when the document changed or another document became
active while the dialog was open, so the user knows to open it again.
181 lines
8.0 KiB
Rust
181 lines
8.0 KiB
Rust
use super::read::{fields, page_refs};
|
|
use super::{Document, Field, FormError, ObjRef, Object, invalid};
|
|
|
|
/// A page's tab order (`/Tabs`).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum TabOrder {
|
|
/// Rows, top to bottom, left to right within a row (`/R`; right to left in a document bound
|
|
/// on the right).
|
|
Row,
|
|
/// Columns, left to right, top to bottom within a column (`/C`; columns right to left in a
|
|
/// document bound on the right).
|
|
Column,
|
|
/// The document structure (`/S`; annotation order when the file has none).
|
|
Structure,
|
|
/// Unspecified: annotation order.
|
|
Annotations,
|
|
}
|
|
|
|
impl TabOrder {
|
|
fn of(name: Option<&[u8]>) -> TabOrder {
|
|
match name {
|
|
Some(b"R") => TabOrder::Row,
|
|
Some(b"C") => TabOrder::Column,
|
|
Some(b"S") => TabOrder::Structure,
|
|
_ => TabOrder::Annotations,
|
|
}
|
|
}
|
|
|
|
/// The tab order of the page at `r`, with an indirect `/Tabs` resolved.
|
|
fn of_page(doc: &Document, r: ObjRef) -> TabOrder {
|
|
let tabs = doc.get(r).as_dict().and_then(|d| d.get(b"Tabs")).map(|o| doc.resolve(o));
|
|
TabOrder::of(tabs.as_deref().and_then(Object::as_name))
|
|
}
|
|
}
|
|
|
|
/// A widget on a page for tab ordering: (page, field, widget, rect, annotation index).
|
|
type TabItem = (usize, usize, usize, [f64; 4], usize);
|
|
|
|
/// Every widget's place in the tab order.
|
|
pub(crate) fn rank_tabs(doc: &Document, pages: &[ObjRef], annot_index: &std::collections::HashMap<ObjRef, usize>, out: &mut [Field]) {
|
|
let mut items: Vec<TabItem> = Vec::new();
|
|
for (fi, f) in out.iter().enumerate() {
|
|
for (wi, w) in f.widgets.iter().enumerate() {
|
|
if let Some(p) = w.page {
|
|
items.push((p, fi, wi, w.rect, annot_index.get(&w.obj).copied().unwrap_or(usize::MAX)));
|
|
}
|
|
}
|
|
}
|
|
// Rows and columns run right to left in a document bound on the right: rank the mirrored
|
|
// rectangles there.
|
|
if right_to_left(doc) {
|
|
for i in &mut items {
|
|
i.3 = [-i.3[2], i.3[1], -i.3[0], i.3[3]];
|
|
}
|
|
}
|
|
let mut ordered: Vec<(usize, usize)> = Vec::with_capacity(items.len());
|
|
for (pi, p) in pages.iter().enumerate() {
|
|
let mut on: Vec<_> = items.iter().filter(|i| i.0 == pi).collect();
|
|
if on.is_empty() {
|
|
continue;
|
|
}
|
|
let tabs = TabOrder::of_page(doc, *p);
|
|
match tabs {
|
|
TabOrder::Row | TabOrder::Column => {
|
|
let row = tabs == TabOrder::Row;
|
|
// Primary axis: rows by top edge (down the page), columns by left edge.
|
|
on.sort_by(|a, b| if row { b.3[3].total_cmp(&a.3[3]) } else { a.3[0].total_cmp(&b.3[0]) });
|
|
let mut groups: Vec<Vec<&TabItem>> = Vec::new();
|
|
for it in on {
|
|
let fits = groups.last().is_some_and(|g| {
|
|
let first = g[0];
|
|
if row {
|
|
(first.3[3] - it.3[3]).abs() < ((first.3[3] - first.3[1]) / 2.0).max(2.0)
|
|
} else {
|
|
(it.3[0] - first.3[0]).abs() < ((first.3[2] - first.3[0]) / 2.0).max(2.0)
|
|
}
|
|
});
|
|
if fits && let Some(g) = groups.last_mut() {
|
|
g.push(it);
|
|
} else {
|
|
groups.push(vec![it]);
|
|
}
|
|
}
|
|
for mut g in groups {
|
|
g.sort_by(|a, b| if row { a.3[0].total_cmp(&b.3[0]) } else { b.3[3].total_cmp(&a.3[3]) });
|
|
ordered.extend(g.iter().map(|i| (i.1, i.2)));
|
|
}
|
|
}
|
|
TabOrder::Structure | TabOrder::Annotations => {
|
|
on.sort_by_key(|i| i.4);
|
|
ordered.extend(on.iter().map(|i| (i.1, i.2)));
|
|
}
|
|
}
|
|
}
|
|
for f in out.iter_mut() {
|
|
for w in &mut f.widgets {
|
|
w.tab = usize::MAX;
|
|
}
|
|
}
|
|
for (rank, (fi, wi)) in ordered.into_iter().enumerate() {
|
|
out[fi].widgets[wi].tab = rank;
|
|
}
|
|
}
|
|
|
|
/// Whether the document is bound on the right (`/ViewerPreferences /Direction /R2L`).
|
|
fn right_to_left(doc: &Document) -> bool {
|
|
let Some(root) = doc.root() else { return false };
|
|
let Some(prefs) = doc.get(root).as_dict().and_then(|c| c.get(b"ViewerPreferences")).map(|o| doc.resolve(o)) else { return false };
|
|
prefs.as_dict().and_then(|p| p.get(b"Direction")).map(|o| doc.resolve(o)).is_some_and(|d| d.as_name() == Some(b"R2L"))
|
|
}
|
|
|
|
/// Set the tab order of pages (0-based).
|
|
pub fn set_tab_order(doc: &mut Document, pages: &[usize], order: TabOrder) -> Result<(), FormError> {
|
|
let refs = page_refs(doc);
|
|
for &p in pages {
|
|
let r = *refs.get(p).ok_or_else(|| FormError::Invalid(format!("page {} does not exist", p + 1)))?;
|
|
doc.update_dict(r, |d| match order {
|
|
TabOrder::Row => d.set(b"Tabs".to_vec(), Object::name("R")),
|
|
TabOrder::Column => d.set(b"Tabs".to_vec(), Object::name("C")),
|
|
TabOrder::Structure => d.set(b"Tabs".to_vec(), Object::name("S")),
|
|
TabOrder::Annotations => {
|
|
d.remove(b"Tabs");
|
|
}
|
|
})?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The tab order of each page, in page order (`/Tabs`; a page without one is unspecified).
|
|
pub fn tab_orders(doc: &Document) -> Vec<TabOrder> {
|
|
page_refs(doc).into_iter().map(|r| TabOrder::of_page(doc, r)).collect()
|
|
}
|
|
|
|
/// Order tabs manually (Acrobat: Fields ▸ Tab Order ▸ Order Tabs Manually, then drag a field):
|
|
/// move field `name` one place earlier or later in its page's tab order. The page switches to
|
|
/// annotation order (`/Tabs` removed) and its widgets are reordered in `/Annots`; other
|
|
/// annotations keep their places.
|
|
pub fn move_in_tab_order(doc: &mut Document, name: &str, earlier: bool) -> Result<(), FormError> {
|
|
let all = fields(doc);
|
|
let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?;
|
|
let page = f.widgets.iter().filter_map(|w| w.page).min().ok_or_else(|| FormError::Invalid(format!("{name} is not on a page")))?;
|
|
// The page's fields in their current tab order (a field counts once, at its first widget).
|
|
let mut order: Vec<(usize, &str)> =
|
|
all.iter().filter_map(|g| g.widgets.iter().filter(|w| w.page == Some(page)).map(|w| w.tab).min().map(|t| (t, g.name.as_str()))).collect();
|
|
order.sort();
|
|
let names: Vec<&str> = order.iter().map(|(_, n)| *n).collect();
|
|
let Some(i) = names.iter().position(|n| *n == name) else { return invalid(format!("{name} is not on a page")) };
|
|
let j = if earlier { i.checked_sub(1) } else { Some(i + 1).filter(|j| *j < names.len()) };
|
|
let Some(j) = j else { return Ok(()) };
|
|
let mut names = names.into_iter().map(str::to_string).collect::<Vec<_>>();
|
|
names.swap(i, j);
|
|
// The widgets of this page in the new order.
|
|
let mut widgets: Vec<ObjRef> = Vec::new();
|
|
for n in &names {
|
|
if let Some(g) = all.iter().find(|g| &g.name == n) {
|
|
widgets.extend(g.widgets.iter().filter(|w| w.page == Some(page)).map(|w| w.obj));
|
|
}
|
|
}
|
|
let pr = *page_refs(doc).get(page).ok_or_else(|| FormError::Invalid("no such page".into()))?;
|
|
let annots_obj =
|
|
doc.get(pr).as_dict().and_then(|d| d.get(b"Annots").cloned()).ok_or_else(|| FormError::Invalid("the page has no annotations".into()))?;
|
|
let list = doc.resolve(&annots_obj).as_array().cloned().unwrap_or_default();
|
|
let mut next = widgets.into_iter();
|
|
let set: std::collections::HashSet<ObjRef> = all.iter().flat_map(|g| g.widgets.iter().filter(|w| w.page == Some(page)).map(|w| w.obj)).collect();
|
|
let new_list: Vec<Object> = list
|
|
.iter()
|
|
.map(|o| match o.as_ref() {
|
|
Some(r) if set.contains(&r) => next.next().map(Object::Ref).unwrap_or_else(|| o.clone()),
|
|
_ => o.clone(),
|
|
})
|
|
.collect();
|
|
match annots_obj {
|
|
Object::Ref(ar) => doc.set(ar, Object::Array(new_list)),
|
|
_ => doc.update_dict(pr, |d| d.set(b"Annots".to_vec(), Object::Array(new_list)))?,
|
|
}
|
|
doc.update_dict(pr, |d| {
|
|
d.remove(b"Tabs");
|
|
})?;
|
|
Ok(())
|
|
}
|