ravenrs_sheet/engine/cmd/print/
header_footer.rs1use super::layout::SheetLayout;
2use super::{Font, Page, Value, Workbook};
3
4pub(super) const HF_FIXED_SIDE_MARGIN: f32 = 0.7 * 72.0;
6
7#[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
21pub 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
29pub 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
41pub 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 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
78pub 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
90pub 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 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
113pub 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#[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
156const MAX_SECTION_BYTES: usize = 64 * 1024;
159
160fn 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
167pub 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 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 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 '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}