Skip to main content

ravenrs_sheet/engine/cmd/print/
header_footer.rs

1use super::layout::SheetLayout;
2use super::{Font, Page, Value, Workbook};
3
4/// Side margin of headers and footers, in points, when Align with Page Margins is off.
5pub(super) const HF_FIXED_SIDE_MARGIN: f32 = 0.7 * 72.0;
6
7// ---------------------------------------------------------------- header / footer
8
9/// Values for header/footer codes.
10#[derive(Clone, Debug, Default)]
11pub struct HfContext {
12    pub page: usize,
13    pub pages: usize,
14    pub sheet: String,
15    pub file: String,
16    pub path: String,
17    pub date: String,
18    pub time: String,
19}
20
21/// Today's date and the current time as headers and footers show them (`&D`, `&T`).
22pub fn hf_date_time(wb: &Workbook) -> (String, String) {
23    let now = crate::base::date::serial_in_system(wb.date_system, crate::calc::now_serial());
24    let date = crate::engine::display::format(&Value::Number(now.floor()), "m/d/yyyy", wb).text;
25    let time = crate::engine::display::format(&Value::Number(now), "h:mm AM/PM", wb).text;
26    (date, time)
27}
28
29/// Header/footer codes and the field names the Page Layout view shows for them while editing.
30pub const HF_FIELDS: [(char, &str); 8] = [
31    ('P', "&[Page]"),
32    ('N', "&[Pages]"),
33    ('D', "&[Date]"),
34    ('T', "&[Time]"),
35    ('Z', "&[Path]"),
36    ('F', "&[File]"),
37    ('A', "&[Tab]"),
38    ('G', "&[Picture]"),
39];
40
41/// Splits a header/footer string into its raw left, centre and right sections (codes kept).
42/// Text before the first `&L`/`&C`/`&R` belongs to the centre, as when printing.
43pub fn split_header_footer(spec: &str) -> [String; 3] {
44    let mut out: [String; 3] = Default::default();
45    let mut cur = 1usize;
46    let chars: Vec<char> = spec.chars().take(10_000).collect();
47    let mut i = 0;
48    while let Some(&c) = chars.get(i) {
49        let next = chars.get(i + 1).copied();
50        let sec = cur.min(2);
51        match (c, next) {
52            ('&', Some('L' | 'l')) => cur = 0,
53            ('&', Some('C' | 'c')) => cur = 1,
54            ('&', Some('R' | 'r')) => cur = 2,
55            ('&', Some('"')) => {
56                // &"Font,Style" is kept whole.
57                let end = chars[i + 2..].iter().position(|ch| *ch == '"').map_or(chars.len(), |p| i + 3 + p);
58                out[sec].extend(&chars[i..end.min(chars.len())]);
59                i = end;
60                continue;
61            }
62            ('&', Some(n)) => {
63                out[sec].push('&');
64                out[sec].push(n);
65            }
66            ('&', None) => {}
67            (ch, _) => {
68                out[sec].push(ch);
69                i += 1;
70                continue;
71            }
72        }
73        i += 2;
74    }
75    out
76}
77
78/// Joins raw left, centre and right sections into one header/footer string.
79pub fn join_header_footer(sections: &[String; 3]) -> String {
80    let mut s = String::new();
81    for (code, text) in ["&L", "&C", "&R"].iter().zip(sections) {
82        if !text.is_empty() {
83            s.push_str(code);
84            s.push_str(text);
85        }
86    }
87    s
88}
89
90/// A raw section as the user edits it: `&P` shows as `&[Page]`, `&N` as `&[Pages]`, and so on.
91pub fn codes_to_fields(raw: &str) -> String {
92    let mut out = String::new();
93    let mut it = raw.chars().peekable();
94    while let Some(c) = it.next() {
95        if c != '&' {
96            out.push(c);
97            continue;
98        }
99        // A dangling `&` at the end is dropped (printing ignores it too).
100        if let Some(n) = it.next() {
101            match HF_FIELDS.iter().find(|(code, _)| *code == n.to_ascii_uppercase()) {
102                Some((_, field)) => out.push_str(field),
103                None => {
104                    out.push('&');
105                    out.push(n);
106                }
107            }
108        }
109    }
110    out
111}
112
113/// The inverse of [`codes_to_fields`]: field names become codes again; a dangling `&` at the end
114/// becomes a literal ampersand so it can't swallow the next section's code.
115pub fn fields_to_codes(text: &str) -> String {
116    let chars: Vec<char> = text.chars().take(10_000).collect();
117    let mut out = String::new();
118    let mut i = 0;
119    while let Some(&c) = chars.get(i) {
120        if c != '&' {
121            out.push(c);
122            i += 1;
123            continue;
124        }
125        if chars.get(i + 1) == Some(&'&') {
126            out.push_str("&&");
127            i += 2;
128            continue;
129        }
130        let rest: String = chars[i..].iter().take(12).collect();
131        if let Some((code, field)) = HF_FIELDS.iter().find(|(_, f)| rest.starts_with(f)) {
132            out.push('&');
133            out.push(*code);
134            i += field.chars().count();
135            continue;
136        }
137        if i + 1 == chars.len() {
138            out.push_str("&&");
139        } else {
140            out.push('&');
141        }
142        i += 1;
143    }
144    out
145}
146
147/// One header/footer section: its text and whether `&B` made it bold.
148#[derive(Clone, Debug, Default, PartialEq)]
149pub struct HfSection {
150    pub text: String,
151    pub bold: bool,
152    pub italic: bool,
153    pub size: Option<f32>,
154}
155
156/// Most bytes one section's expanded text may hold. A code like `&A` adds the whole sheet name
157/// (or file path) each time it appears, and a JSON workbook's names have no length limit.
158const MAX_SECTION_BYTES: usize = 64 * 1024;
159
160/// Appends `s` unless the section would pass [`MAX_SECTION_BYTES`]; what doesn't fit is left out.
161fn push_field(text: &mut String, s: &str) {
162    if text.len().saturating_add(s.len()) <= MAX_SECTION_BYTES {
163        text.push_str(s);
164    }
165}
166
167/// Expands a header/footer string into its left, centre and right sections.
168pub fn expand_header_footer(spec: &str, cx: &HfContext) -> [HfSection; 3] {
169    let mut out: [HfSection; 3] = Default::default();
170    let mut cur = 1usize;
171    let chars: Vec<char> = spec.chars().take(10_000).collect();
172    let mut i = 0;
173    while i < chars.len() {
174        let c = chars[i];
175        if c != '&' {
176            if let Some(s) = out.get_mut(cur) {
177                s.text.push(c);
178            }
179            i += 1;
180            continue;
181        }
182        let Some(&code) = chars.get(i + 1) else { break };
183        i += 2;
184        let sec = &mut out[cur.min(2)];
185        match code {
186            'L' | 'l' => cur = 0,
187            'C' | 'c' => cur = 1,
188            'R' | 'r' => cur = 2,
189            'P' | 'p' => {
190                // &P, &P+n, &P-n
191                let mut n = cx.page as i64;
192                if let Some(&sign) = chars.get(i)
193                    && (sign == '+' || sign == '-')
194                {
195                    let digits: String = chars[i + 1..].iter().take_while(|d| d.is_ascii_digit()).take(6).collect();
196                    if let Ok(d) = digits.parse::<i64>() {
197                        n = if sign == '+' { n + d } else { n - d };
198                        i += 1 + digits.len();
199                    }
200                }
201                push_field(&mut sec.text, &n.to_string());
202            }
203            'N' | 'n' => push_field(&mut sec.text, &cx.pages.to_string()),
204            'D' | 'd' => push_field(&mut sec.text, &cx.date),
205            'T' | 't' => push_field(&mut sec.text, &cx.time),
206            'A' | 'a' => push_field(&mut sec.text, &cx.sheet),
207            'F' | 'f' => push_field(&mut sec.text, &cx.file),
208            'Z' | 'z' => push_field(&mut sec.text, &cx.path),
209            '&' => sec.text.push('&'),
210            'B' | 'b' => sec.bold = !sec.bold,
211            'I' | 'i' => sec.italic = !sec.italic,
212            '"' => {
213                // &"Font,Style": skip the font name; honour Bold/Italic in the style.
214                let rest: String = chars[i..].iter().take_while(|c| **c != '"').collect();
215                let lower = rest.to_ascii_lowercase();
216                if lower.contains("bold") {
217                    sec.bold = true;
218                }
219                if lower.contains("italic") || lower.contains("oblique") {
220                    sec.italic = true;
221                }
222                i += rest.chars().count() + 1;
223            }
224            d if d.is_ascii_digit() => {
225                let digits: String = std::iter::once(d).chain(chars[i..].iter().copied().take_while(char::is_ascii_digit)).take(3).collect();
226                i += digits.len() - 1;
227                if let Ok(v) = digits.parse::<f32>() {
228                    sec.size = Some(v.clamp(1.0, 409.0));
229                }
230            }
231            // Underline, strike, super/subscript, colour, picture: not rendered.
232            'K' | 'k' => {
233                i = (i + 6).min(chars.len());
234            }
235            _ => {}
236        }
237    }
238    out
239}
240
241pub(super) fn draw_header_footer(pg: &mut Page, lay: &SheetLayout, which: crate::model::HfPage, cx: &HfContext) {
242    let [_, _, _, _, hm, fm] = lay.margins;
243    let (l, r) = lay.hf_sides();
244    let text_scale = lay.hf_text_scale();
245    let pw = lay.page_w;
246    let ph = lay.page_h;
247    for (spec, header) in [(lay.settings.hf(which, false), true), (lay.settings.hf(which, true), false)] {
248        if spec.is_empty() {
249            continue;
250        }
251        let secs = expand_header_footer(spec, cx);
252        for (i, sec) in secs.iter().enumerate() {
253            if sec.text.trim().is_empty() {
254                continue;
255            }
256            let size = sec.size.unwrap_or(10.0).clamp(1.0, 72.0) * text_scale;
257            let font = Font::styled(crate::pdf::Family::Sans, sec.bold, sec.italic);
258            let lines: Vec<&str> = sec.text.split('\n').take(20).collect();
259            let n = lines.len() as f32;
260            for (j, line) in lines.iter().enumerate() {
261                let lw = crate::pdf::text_width(font, size, line);
262                let x = match i {
263                    0 => l,
264                    1 => (pw - lw) / 2.0,
265                    _ => pw - r - lw,
266                };
267                let y = if header { hm + size * 0.8 + j as f32 * size * 1.2 } else { ph - fm - size * 0.22 - (n - 1.0 - j as f32) * size * 1.2 };
268                pg.text(x, y, size, font, [0, 0, 0], line);
269            }
270        }
271    }
272}