Skip to main content

ravenrs_sheet/model/features/
print.rs

1use super::{CellRef, RangeRef};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
5pub enum Orientation {
6    #[default]
7    Portrait,
8    Landscape,
9}
10
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct PrintSettings {
13    pub orientation: Orientation,
14    /// Paper size name (`Letter`, `A4`, `Legal`…).
15    pub paper: String,
16    /// Margins in inches: left, right, top, bottom, header, footer.
17    pub margins: [f32; 6],
18    pub print_area: Option<RangeRef>,
19    pub title_rows: Option<(u32, u32)>,
20    pub title_cols: Option<(u32, u32)>,
21    /// Percent scale, or fit to pages (wide, tall).
22    pub scale: u16,
23    pub fit_to: Option<(u16, u16)>,
24    pub gridlines: bool,
25    pub headings: bool,
26    pub center_h: bool,
27    pub center_v: bool,
28    /// The header and footer of odd pages, and of every page unless the variants below apply.
29    pub header: String,
30    pub footer: String,
31    pub row_breaks: Vec<u32>,
32    pub col_breaks: Vec<u32>,
33    /// Different First Page: the sheet's first printed page uses `first_header`/`first_footer`.
34    #[serde(default)]
35    pub different_first: bool,
36    /// Different Odd & Even Pages: even-numbered pages use `even_header`/`even_footer`.
37    #[serde(default)]
38    pub different_odd_even: bool,
39    #[serde(default)]
40    pub first_header: String,
41    #[serde(default)]
42    pub first_footer: String,
43    #[serde(default)]
44    pub even_header: String,
45    #[serde(default)]
46    pub even_footer: String,
47    /// Scale with Document / Align with Page Margins (both on unless a file turned them off).
48    #[serde(default = "yes")]
49    pub hf_scale_with_doc: bool,
50    #[serde(default = "yes")]
51    pub hf_align_with_margins: bool,
52    /// Page Setup › Sheet › Page order: the sequence pages are numbered and printed in when the
53    /// sheet is more than one page wide and tall.
54    #[serde(default)]
55    pub page_order: PageOrder,
56}
57
58fn yes() -> bool {
59    true
60}
61
62/// The sequence of a sheet's pages (XLSX `pageSetup pageOrder`, ECMA-376 `ST_PageOrder`).
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub enum PageOrder {
66    /// Down the first column of pages, then down the next one (Excel's default).
67    #[default]
68    DownThenOver,
69    /// Across the first row of pages, then across the next one.
70    OverThenDown,
71}
72
73impl PageOrder {
74    /// The id `pageLayout.pageSetup` takes and XLSX writes.
75    pub fn id(self) -> &'static str {
76        match self {
77            PageOrder::DownThenOver => "downThenOver",
78            PageOrder::OverThenDown => "overThenDown",
79        }
80    }
81    pub fn parse(id: &str) -> Option<PageOrder> {
82        match id {
83            "downThenOver" => Some(PageOrder::DownThenOver),
84            "overThenDown" => Some(PageOrder::OverThenDown),
85            _ => None,
86        }
87    }
88    /// The 0-based place in the printed sequence of the page in page row `row` and page column
89    /// `col`, on a sheet `rows` pages tall and `cols` pages wide.
90    pub fn index(self, row: usize, col: usize, rows: usize, cols: usize) -> usize {
91        match self {
92            PageOrder::DownThenOver => col.saturating_mul(rows).saturating_add(row),
93            PageOrder::OverThenDown => row.saturating_mul(cols).saturating_add(col),
94        }
95    }
96}
97
98/// Which header/footer pair a printed page uses.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum HfPage {
101    Odd,
102    Even,
103    First,
104}
105
106impl HfPage {
107    pub fn id(self) -> &'static str {
108        match self {
109            HfPage::Odd => "odd",
110            HfPage::Even => "even",
111            HfPage::First => "first",
112        }
113    }
114    pub fn parse(id: &str) -> Option<HfPage> {
115        match id {
116            "odd" | "all" => Some(HfPage::Odd),
117            "even" => Some(HfPage::Even),
118            "first" => Some(HfPage::First),
119            _ => None,
120        }
121    }
122}
123
124impl PrintSettings {
125    /// The pair used by page `number` (1-based, as `&P` prints it); `first` is the sheet's first page.
126    pub fn hf_page(&self, first: bool, number: usize) -> HfPage {
127        if self.different_first && first {
128            HfPage::First
129        } else if self.different_odd_even && number.is_multiple_of(2) {
130            HfPage::Even
131        } else {
132            HfPage::Odd
133        }
134    }
135    /// The header (`footer` false) or footer of a page kind.
136    pub fn hf(&self, page: HfPage, footer: bool) -> &String {
137        match (page, footer) {
138            (HfPage::Odd, false) => &self.header,
139            (HfPage::Odd, true) => &self.footer,
140            (HfPage::Even, false) => &self.even_header,
141            (HfPage::Even, true) => &self.even_footer,
142            (HfPage::First, false) => &self.first_header,
143            (HfPage::First, true) => &self.first_footer,
144        }
145    }
146    pub fn hf_mut(&mut self, page: HfPage, footer: bool) -> &mut String {
147        match (page, footer) {
148            (HfPage::Odd, false) => &mut self.header,
149            (HfPage::Odd, true) => &mut self.footer,
150            (HfPage::Even, false) => &mut self.even_header,
151            (HfPage::Even, true) => &mut self.even_footer,
152            (HfPage::First, false) => &mut self.first_header,
153            (HfPage::First, true) => &mut self.first_footer,
154        }
155    }
156    /// Any header or footer text at all (on any kind of page).
157    pub fn has_header_footer(&self) -> bool {
158        [&self.header, &self.footer, &self.first_header, &self.first_footer, &self.even_header, &self.even_footer].iter().any(|t| !t.is_empty())
159    }
160}
161
162impl Default for PrintSettings {
163    fn default() -> Self {
164        PrintSettings {
165            orientation: Orientation::Portrait,
166            paper: "Letter".into(),
167            margins: [0.7, 0.7, 0.75, 0.75, 0.3, 0.3],
168            print_area: None,
169            title_rows: None,
170            title_cols: None,
171            scale: 100,
172            fit_to: None,
173            gridlines: false,
174            headings: false,
175            center_h: false,
176            center_v: false,
177            header: String::new(),
178            footer: String::new(),
179            row_breaks: vec![],
180            col_breaks: vec![],
181            different_first: false,
182            different_odd_even: false,
183            first_header: String::new(),
184            first_footer: String::new(),
185            even_header: String::new(),
186            even_footer: String::new(),
187            hf_scale_with_doc: true,
188            hf_align_with_margins: true,
189            page_order: PageOrder::DownThenOver,
190        }
191    }
192}
193
194#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
195pub struct SheetProtection {
196    /// Hash of the password (Excel-compatible, written to XLSX `sheetProtection`; never the
197    /// password itself).
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub password: Option<crate::model::PasswordHash>,
200    /// The internal hash older GridCraft JSON workbooks stored ([`password_hash`]); still
201    /// verified when unprotecting, never written for new passwords.
202    #[serde(rename = "password_hash", default, skip_serializing_if = "Option::is_none")]
203    pub legacy_password_hash: Option<u64>,
204    pub select_locked: bool,
205    pub select_unlocked: bool,
206    pub format_cells: bool,
207    pub format_columns: bool,
208    pub format_rows: bool,
209    pub insert_columns: bool,
210    pub insert_rows: bool,
211    pub delete_columns: bool,
212    pub delete_rows: bool,
213    pub sort: bool,
214    pub autofilter: bool,
215}
216
217/// Review › Allow Edit Ranges: cells that stay editable on a protected sheet, with or without a
218/// password (XLSX `<protectedRange>`).
219#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
220pub struct EditRange {
221    pub title: String,
222    pub ranges: Vec<RangeRef>,
223    pub password: Option<crate::model::PasswordHash>,
224    /// Windows permissions (`securityDescriptor`), kept as read from the file.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub security_descriptor: Option<String>,
227}
228
229impl EditRange {
230    pub fn contains(&self, c: CellRef) -> bool {
231        self.ranges.iter().any(|r| r.contains(c))
232    }
233}
234
235/// The non-cryptographic password hash older GridCraft versions stored for sheet protection
236/// (only verified now, see [`SheetProtection::legacy_password_hash`]).
237pub fn password_hash(pw: &str) -> u64 {
238    let mut h: u64 = 0xcbf29ce484222325 ^ 0x5348_4545_5443;
239    for b in pw.bytes() {
240        h ^= b as u64;
241        h = h.wrapping_mul(0x100000001b3);
242    }
243    h
244}
245
246/// Outline grouping info for a row or column.
247#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
248pub struct LineInfo {
249    /// Size in pixels at 96 dpi; `None` = default (rows auto-fit to content).
250    pub size: Option<f32>,
251    pub hidden: bool,
252    pub outline: u8,
253    pub collapsed: bool,
254    pub style: Option<crate::model::style::StyleId>,
255    /// The size was set by the user (rows): automatic heights follow the content.
256    #[serde(default)]
257    pub custom: bool,
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn page_order_numbers_pages_and_old_settings_load_down_then_over() {
266        // Two pages tall, three wide: (row, col) → place in the printed sequence.
267        let place = |order: PageOrder| [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)].map(|(r, c)| order.index(r, c, 2, 3));
268        assert_eq!(place(PageOrder::DownThenOver), [0, 1, 2, 3, 4, 5]);
269        assert_eq!(place(PageOrder::OverThenDown), [0, 3, 1, 4, 2, 5]);
270        assert_eq!(PageOrder::OverThenDown.index(usize::MAX, usize::MAX, usize::MAX, usize::MAX), usize::MAX);
271        for order in [PageOrder::DownThenOver, PageOrder::OverThenDown] {
272            assert_eq!(PageOrder::parse(order.id()), Some(order));
273        }
274        assert_eq!(PageOrder::parse("OverThenDown"), None);
275        // Saved under the same id the command takes; settings saved before the field existed load
276        // with Excel's default.
277        let mut p = PrintSettings { page_order: PageOrder::OverThenDown, ..Default::default() };
278        let mut saved = serde_json::to_value(&p).unwrap();
279        assert_eq!(saved["page_order"], "overThenDown");
280        assert!(saved.as_object_mut().unwrap().remove("page_order").is_some());
281        p = serde_json::from_value(saved).unwrap();
282        assert_eq!(p, PrintSettings::default());
283    }
284}