1use crate::text::{Font, TextAlign};
7use chrono::Local;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum HeaderFooterPosition {
13 Header,
15 Footer,
17}
18
19#[derive(Debug, Clone)]
21pub struct HeaderFooterOptions {
22 pub font: Font,
24 pub font_size: f64,
26 pub alignment: TextAlign,
28 pub margin: f64,
30 pub show_page_numbers: bool,
32 pub date_format: Option<String>,
34}
35
36impl Default for HeaderFooterOptions {
37 fn default() -> Self {
38 Self {
39 font: Font::Helvetica,
40 font_size: 10.0,
41 alignment: TextAlign::Center,
42 margin: 36.0, show_page_numbers: true,
44 date_format: None,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct HeaderFooter {
52 position: HeaderFooterPosition,
54 content: String,
56 options: HeaderFooterOptions,
58}
59
60impl HeaderFooter {
61 pub fn new_header(content: impl Into<String>) -> Self {
71 Self {
72 position: HeaderFooterPosition::Header,
73 content: content.into(),
74 options: HeaderFooterOptions::default(),
75 }
76 }
77
78 pub fn new_footer(content: impl Into<String>) -> Self {
88 Self {
89 position: HeaderFooterPosition::Footer,
90 content: content.into(),
91 options: HeaderFooterOptions::default(),
92 }
93 }
94
95 pub fn with_options(mut self, options: HeaderFooterOptions) -> Self {
97 self.options = options;
98 self
99 }
100
101 pub fn with_font(mut self, font: Font, size: f64) -> Self {
103 self.options.font = font;
104 self.options.font_size = size;
105 self
106 }
107
108 pub fn with_alignment(mut self, alignment: TextAlign) -> Self {
110 self.options.alignment = alignment;
111 self
112 }
113
114 pub fn with_margin(mut self, margin: f64) -> Self {
116 self.options.margin = margin;
117 self
118 }
119
120 pub fn position(&self) -> HeaderFooterPosition {
122 self.position
123 }
124
125 pub fn content(&self) -> &str {
127 &self.content
128 }
129
130 pub fn options(&self) -> &HeaderFooterOptions {
132 &self.options
133 }
134
135 pub fn render(
147 &self,
148 page_number: usize,
149 total_pages: usize,
150 custom_values: Option<&HashMap<String, String>>,
151 ) -> String {
152 let mut result = self.content.clone();
153
154 result = result.replace("{{page_number}}", &page_number.to_string());
156 result = result.replace("{{total_pages}}", &total_pages.to_string());
157
158 let now = Local::now();
160 result = result.replace("{{year}}", &now.format("%Y").to_string());
161 result = result.replace("{{month}}", &now.format("%B").to_string());
162 result = result.replace("{{day}}", &now.format("%d").to_string());
163
164 if let Some(date_format) = &self.options.date_format {
165 result = result.replace("{{date}}", &now.format(date_format).to_string());
166 } else {
167 result = result.replace("{{date}}", &now.format("%Y-%m-%d").to_string());
168 }
169
170 result = result.replace("{{time}}", &now.format("%H:%M:%S").to_string());
171 result = result.replace("{{datetime}}", &now.format("%Y-%m-%d %H:%M:%S").to_string());
172
173 if let Some(custom) = custom_values {
175 for (key, value) in custom {
176 result = result.replace(&format!("{{{{{key}}}}}"), value);
177 }
178 }
179
180 result
181 }
182
183 pub fn calculate_y_position(&self, page_height: f64) -> f64 {
185 match self.position {
186 HeaderFooterPosition::Header => page_height - self.options.margin,
187 HeaderFooterPosition::Footer => self.options.margin,
188 }
189 }
190
191 pub fn calculate_x_position(&self, page_width: f64, text_width: f64) -> f64 {
193 match self.options.alignment {
194 TextAlign::Left => self.options.margin,
195 TextAlign::Center => (page_width - text_width) / 2.0,
196 TextAlign::Right => page_width - self.options.margin - text_width,
197 TextAlign::Justified => self.options.margin, }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_header_creation() {
208 let header = HeaderFooter::new_header("Test Header");
209 assert_eq!(header.position(), HeaderFooterPosition::Header);
210 assert_eq!(header.content(), "Test Header");
211 assert_eq!(header.options().font_size, 10.0);
212 }
213
214 #[test]
215 fn test_footer_creation() {
216 let footer = HeaderFooter::new_footer("Test Footer");
217 assert_eq!(footer.position(), HeaderFooterPosition::Footer);
218 assert_eq!(footer.content(), "Test Footer");
219 }
220
221 #[test]
222 fn test_with_options() {
223 let options = HeaderFooterOptions {
224 font: Font::TimesRoman,
225 font_size: 12.0,
226 alignment: TextAlign::Right,
227 margin: 20.0,
228 show_page_numbers: false,
229 date_format: Some("%d/%m/%Y".to_string()),
230 };
231
232 let header = HeaderFooter::new_header("Test").with_options(options);
233 assert_eq!(header.options().font_size, 12.0);
234 assert_eq!(header.options().margin, 20.0);
235 }
236
237 #[test]
238 fn test_render_page_numbers() {
239 let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}");
240 let rendered = footer.render(3, 10, None);
241 assert_eq!(rendered, "Page 3 of 10");
242 }
243
244 #[test]
245 fn test_render_date_placeholders() {
246 let header = HeaderFooter::new_header("Report {{year}} - {{month}}");
247 let rendered = header.render(1, 1, None);
248
249 assert!(rendered.contains("Report 20"));
251 assert!(!rendered.contains("{{year}}"));
252 assert!(!rendered.contains("{{month}}"));
253 }
254
255 #[test]
256 fn test_render_custom_values() {
257 let mut custom = HashMap::new();
258 custom.insert("title".to_string(), "Annual Report".to_string());
259 custom.insert("company".to_string(), "ACME Corp".to_string());
260
261 let header = HeaderFooter::new_header("{{company}} - {{title}}");
262 let rendered = header.render(1, 1, Some(&custom));
263 assert_eq!(rendered, "ACME Corp - Annual Report");
264 }
265
266 #[test]
267 fn test_calculate_positions() {
268 let header = HeaderFooter::new_header("Test").with_margin(50.0);
269 let footer = HeaderFooter::new_footer("Test").with_margin(50.0);
270
271 assert_eq!(header.calculate_y_position(842.0), 792.0); assert_eq!(footer.calculate_y_position(842.0), 50.0); }
274
275 #[test]
276 fn test_alignment_positions() {
277 let page_width = 595.0; let text_width = 100.0;
279 let margin = 36.0;
280
281 let left = HeaderFooter::new_header("Test")
282 .with_alignment(TextAlign::Left)
283 .with_margin(margin);
284 assert_eq!(left.calculate_x_position(page_width, text_width), margin);
285
286 let center = HeaderFooter::new_header("Test").with_alignment(TextAlign::Center);
287 assert_eq!(
288 center.calculate_x_position(page_width, text_width),
289 (page_width - text_width) / 2.0
290 );
291
292 let right = HeaderFooter::new_header("Test")
293 .with_alignment(TextAlign::Right)
294 .with_margin(margin);
295 assert_eq!(
296 right.calculate_x_position(page_width, text_width),
297 page_width - margin - text_width
298 );
299 }
300
301 #[test]
302 fn test_no_placeholders() {
303 let header = HeaderFooter::new_header("Static Header Text");
304 let rendered = header.render(1, 10, None);
305 assert_eq!(rendered, "Static Header Text");
306 }
307
308 #[test]
309 fn test_multiple_placeholders() {
310 let footer = HeaderFooter::new_footer(
311 "{{date}} | Page {{page_number}} of {{total_pages}} | {{time}}",
312 );
313 let rendered = footer.render(5, 20, None);
314
315 assert!(rendered.contains(" | Page 5 of 20 | "));
317 assert!(!rendered.contains("{{"));
318 }
319}