1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// =============================================================================
// #######
// ### ### F: page.rs
// ## ## ## ## P: AppCore-Runtime
// ## ##
// C: 2026/08/30 05:00:00 by dnettoRaw
// ## ## ## ## U: 2026/08/30 05:00:00 by dnettoRaw
// ########### S: 1.0.2-rc
// =============================================================================
//! Defines bounded page contracts and behavior for this crate.
use serde::{Deserialize, Serialize};
use crate::{Insets, Size};
/// Semantic page role used by paginated templates.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageRole {
/// First page.
First,
/// Middle/continuation page.
Continuation,
/// Last page.
Last,
/// Master background/header/footer.
Master,
}
/// Semantic band within a page layer.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageBand {
/// Paint behind body content.
Background,
/// Repeating or role-specific header content.
Header,
/// Repeating or role-specific footer content.
Footer,
}
/// Placement assigned to a root element owned by a page layer.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PagePlacement {
/// Master, first, continuation, or last layer.
pub role: PageRole,
/// Background, header, or footer band.
pub band: PageBand,
}
/// Resolved page template metadata.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PageTemplate {
/// Stable template name.
pub name: String,
/// Semantic role.
pub role: PageRole,
/// Trim size.
pub size: Size,
/// Content margins.
pub margin: Insets,
/// Bleed extents.
pub bleed: Insets,
/// Safe-area inset.
pub safe: Insets,
/// Whether crop marks are requested at compatible export.
pub crop_marks: bool,
}
impl PageTemplate {
/// Returns the content rectangle after applying margins to the trim box.
pub fn content_bounds(&self) -> crate::Result<crate::Rect> {
let width = self
.size
.width
.checked_sub(self.margin.left)?
.checked_sub(self.margin.right)?;
let height = self
.size
.height
.checked_sub(self.margin.top)?
.checked_sub(self.margin.bottom)?;
crate::Rect::new(self.margin.left, self.margin.top, width, height)
}
/// Returns the safe-area rectangle inside the trim box.
pub fn safe_bounds(&self) -> crate::Result<crate::Rect> {
let width = self
.size
.width
.checked_sub(self.safe.left)?
.checked_sub(self.safe.right)?;
let height = self
.size
.height
.checked_sub(self.safe.top)?
.checked_sub(self.safe.bottom)?;
crate::Rect::new(self.safe.left, self.safe.top, width, height)
}
}
/// Optional first/continuation/last/master page roles.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PageTemplateSet {
/// First-page override.
pub first: Option<PageTemplate>,
/// Continuation page.
pub continuation: Option<PageTemplate>,
/// Last-page override.
pub last: Option<PageTemplate>,
/// Master composited beneath each page.
pub master: Option<PageTemplate>,
}
impl PageTemplateSet {
/// Builds all semantic page templates from one validated geometry contract.
#[must_use]
pub fn from_base(base: &PageTemplate) -> Self {
let with_role = |role| {
let mut template = base.clone();
template.role = role;
template
};
Self {
first: Some(with_role(PageRole::First)),
continuation: Some(with_role(PageRole::Continuation)),
last: Some(with_role(PageRole::Last)),
master: Some(with_role(PageRole::Master)),
}
}
/// Selects the deterministic template for a zero-based page.
#[must_use]
pub fn select(&self, index: usize, total: usize) -> Option<&PageTemplate> {
if index == 0 {
self.first.as_ref().or(self.continuation.as_ref())
} else if index + 1 == total {
self.last.as_ref().or(self.continuation.as_ref())
} else {
self.continuation.as_ref()
}
}
/// Replaces page-number placeholders without locale-dependent formatting.
#[must_use]
pub fn number_text(source: &str, index: usize, total: usize) -> String {
source
.replace("{page}", &(index + 1).to_string())
.replace("{pages}", &total.to_string())
}
/// Returns whether a layer role is active on this physical page.
#[must_use]
pub fn role_is_active(role: PageRole, index: usize, total: usize) -> bool {
match role {
PageRole::Master => true,
PageRole::First => index == 0,
PageRole::Continuation => index > 0 && index + 1 < total,
PageRole::Last => total > 1 && index + 1 == total,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_numbers_and_roles_are_deterministic() {
assert_eq!(
PageTemplateSet::number_text("Page {page}/{pages}", 1, 3),
"Page 2/3"
);
assert!(PageTemplateSet::role_is_active(PageRole::Master, 2, 3));
assert!(PageTemplateSet::role_is_active(
PageRole::Continuation,
1,
3
));
assert!(!PageTemplateSet::role_is_active(PageRole::Last, 0, 1));
}
}