1use std::collections::{HashMap, HashSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 error::NormaxisPdfError,
7 fonts::FontFallbackChain,
8 layout::TextAlign,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Watermark {
25 pub text: String,
27 pub opacity: f64,
29 pub color: RgbColor,
31 pub font_size: f64,
33 pub angle_deg: f64,
35}
36
37impl Watermark {
38 pub fn new(text: impl Into<String>) -> Self {
39 Self { text: text.into(), ..Self::default() }
40 }
41 pub fn opacity(mut self, v: f64) -> Self { self.opacity = v; self }
42 pub fn color(mut self, c: RgbColor) -> Self { self.color = c; self }
43 pub fn font_size(mut self, pt: f64) -> Self { self.font_size = pt; self }
44 pub fn angle_deg(mut self, deg: f64) -> Self { self.angle_deg = deg; self }
45}
46
47impl Default for Watermark {
48 fn default() -> Self {
49 Self {
50 text: "RASCUNHO".into(),
51 opacity: 0.10,
52 color: RgbColor { r: 0.7, g: 0.7, b: 0.7 },
53 font_size: 72.0,
54 angle_deg: 45.0,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
61#[serde(rename_all = "snake_case")]
62pub enum Orientation {
63 #[default]
64 Portrait,
65 Landscape,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DocumentStyle {
71 pub page_size: PageSize,
72 pub orientation: Orientation,
73 pub margin_top_mm: f64,
74 pub margin_bottom_mm: f64,
75 pub margin_left_mm: f64,
76 pub margin_right_mm: f64,
77 pub font_size_body: f64,
78 pub font_size_title: f64,
79 pub font_size_section: f64,
80 pub font_size_small: f64,
81 pub line_height: f64,
83 pub primary_color: RgbColor,
85 pub text_color: RgbColor,
87 #[serde(default)]
89 pub named_styles: HashMap<String, NamedStyle>,
90 #[serde(default = "default_orphan_lines")]
93 pub min_orphan_lines: u8,
94 #[serde(default = "default_widow_lines")]
97 pub min_widow_lines: u8,
98 #[serde(default = "default_fallback_chain")]
101 pub font_fallback: FontFallbackChain,
102}
103
104fn default_orphan_lines() -> u8 { 2 }
105fn default_widow_lines() -> u8 { 2 }
106fn default_fallback_chain() -> FontFallbackChain {
107 FontFallbackChain::new(vec!["LiberationSans", "LiberationSerif", "LiberationMono"])
108}
109
110impl Default for DocumentStyle {
111 fn default() -> Self {
112 Self {
113 page_size: PageSize::A4,
114 orientation: Orientation::Portrait,
115 margin_top_mm: 20.0,
116 margin_bottom_mm: 20.0,
117 margin_left_mm: 25.0,
118 margin_right_mm: 20.0,
119 font_size_body: 11.0,
120 font_size_title: 16.0,
121 font_size_section: 13.0,
122 font_size_small: 9.0,
123 line_height: 1.4,
124 primary_color: RgbColor { r: 0.0, g: 0.2, b: 0.6 },
126 text_color: RgbColor { r: 0.102, g: 0.102, b: 0.102 },
128 named_styles: HashMap::new(),
129 min_orphan_lines: 2,
130 min_widow_lines: 2,
131 font_fallback: default_fallback_chain(),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138pub enum PageSize {
139 #[default]
140 A4,
141 A3,
142 Letter,
143}
144
145impl PageSize {
146 pub fn dimensions_mm(&self) -> (f64, f64) {
148 match self {
149 PageSize::A4 => (210.0, 297.0),
150 PageSize::A3 => (297.0, 420.0),
151 PageSize::Letter => (215.9, 279.4),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct RgbColor {
159 pub r: f64,
160 pub g: f64,
161 pub b: f64,
162}
163
164impl RgbColor {
165 pub const fn new(r: f64, g: f64, b: f64) -> Self {
166 Self { r, g, b }
167 }
168
169 pub fn from_hex(hex: &str) -> Option<Self> {
171 let hex = hex.trim_start_matches('#');
172 if hex.len() != 6 {
173 return None;
174 }
175 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
176 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
177 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
178 Some(Self {
179 r: r as f64 / 255.0,
180 g: g as f64 / 255.0,
181 b: b as f64 / 255.0,
182 })
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
193pub struct NamedStyle {
194 pub extends: Option<String>,
197 pub font_size: Option<f64>,
198 pub bold: Option<bool>,
199 pub italic: Option<bool>,
200 pub alignment: Option<TextAlign>,
201 pub space_before_mm: Option<f64>,
202 pub space_after_mm: Option<f64>,
203 pub indent_left_mm: Option<f64>,
204 pub indent_right_mm: Option<f64>,
205 pub indent_first_line_mm: Option<f64>,
206 pub color: Option<RgbColor>,
208 #[serde(skip_serializing_if = "Option::is_none")]
211 pub font_family: Option<String>,
212}
213
214#[derive(Debug, Clone)]
218pub struct ResolvedStyle {
219 pub font_size: f64,
220 pub bold: bool,
221 pub italic: bool,
222 pub alignment: TextAlign,
223 pub space_before_mm: f64,
224 pub space_after_mm: f64,
225 pub indent_left_mm: f64,
226 pub indent_right_mm: f64,
227 pub indent_first_line_mm: f64,
228 pub color: Option<RgbColor>,
229 pub font_family: String,
231}
232
233pub struct StyleResolver<'a> {
235 styles: &'a HashMap<String, NamedStyle>,
236 doc: &'a DocumentStyle,
237}
238
239impl<'a> StyleResolver<'a> {
240 pub fn new(styles: &'a HashMap<String, NamedStyle>, doc: &'a DocumentStyle) -> Self {
241 Self { styles, doc }
242 }
243
244 pub fn resolve(&self, name: &str) -> crate::Result<ResolvedStyle> {
249 let mut visited = HashSet::new();
250 let builtins = default_named_styles(self.doc);
252 self.resolve_chain(name, &builtins, &mut visited)
253 }
254
255 fn resolve_chain(
256 &self,
257 name: &str,
258 builtins: &HashMap<String, NamedStyle>,
259 visited: &mut HashSet<String>,
260 ) -> crate::Result<ResolvedStyle> {
261 if !visited.insert(name.to_string()) {
262 return Err(NormaxisPdfError::StyleCycleError(name.to_string()));
263 }
264
265 let style = self.styles.get(name)
267 .or_else(|| builtins.get(name))
268 .ok_or_else(|| NormaxisPdfError::UnknownStyle(name.to_string()))?;
269
270 let base = if let Some(ref parent) = style.extends {
272 self.resolve_chain(parent, builtins, visited)?
273 } else {
274 self.doc_defaults()
275 };
276
277 Ok(ResolvedStyle {
278 font_size: style.font_size.unwrap_or(base.font_size),
279 bold: style.bold.unwrap_or(base.bold),
280 italic: style.italic.unwrap_or(base.italic),
281 alignment: style.alignment.unwrap_or(base.alignment),
282 space_before_mm: style.space_before_mm.unwrap_or(base.space_before_mm),
283 space_after_mm: style.space_after_mm.unwrap_or(base.space_after_mm),
284 indent_left_mm: style.indent_left_mm.unwrap_or(base.indent_left_mm),
285 indent_right_mm: style.indent_right_mm.unwrap_or(base.indent_right_mm),
286 indent_first_line_mm: style.indent_first_line_mm.unwrap_or(base.indent_first_line_mm),
287 color: style.color.clone().or(base.color),
288 font_family: style.font_family.clone().unwrap_or(base.font_family),
289 })
290 }
291
292 fn doc_defaults(&self) -> ResolvedStyle {
294 let space_after = self.doc.font_size_body * 0.3 * 25.4 / 72.0;
295 ResolvedStyle {
296 font_size: self.doc.font_size_body,
297 bold: false,
298 italic: false,
299 alignment: TextAlign::Justify,
300 space_before_mm: 0.0,
301 space_after_mm: space_after,
302 indent_left_mm: 0.0,
303 indent_right_mm: 0.0,
304 indent_first_line_mm: 0.0,
305 color: None,
306 font_family: "LiberationSans".to_string(),
307 }
308 }
309}
310
311pub fn default_named_styles(doc: &DocumentStyle) -> HashMap<String, NamedStyle> {
316 let pt_to_mm = |pt: f64| pt * 25.4 / 72.0;
317
318 let mut m = HashMap::new();
319
320 m.insert("normal".into(), NamedStyle {
322 font_size: Some(doc.font_size_body),
323 bold: Some(false),
324 italic: Some(false),
325 alignment: Some(TextAlign::Justify),
326 space_after_mm: Some(pt_to_mm(doc.font_size_body) * 0.3),
327 ..Default::default()
328 });
329
330 m.insert("heading_1".into(), NamedStyle {
332 font_size: Some(doc.font_size_title),
333 bold: Some(true),
334 alignment: Some(TextAlign::Left),
335 space_before_mm: Some(8.0),
336 space_after_mm: Some(4.0),
337 color: Some(doc.primary_color.clone()),
338 ..Default::default()
339 });
340
341 m.insert("heading_2".into(), NamedStyle {
343 extends: Some("heading_1".into()),
344 font_size: Some(doc.font_size_section),
345 space_before_mm: Some(6.0),
346 space_after_mm: Some(3.0),
347 color: Some(doc.text_color.clone()),
348 ..Default::default()
349 });
350
351 m.insert("heading_3".into(), NamedStyle {
353 extends: Some("heading_2".into()),
354 font_size: Some(doc.font_size_body),
355 space_before_mm: Some(4.0),
356 space_after_mm: Some(2.0),
357 ..Default::default()
358 });
359
360 m.insert("caption".into(), NamedStyle {
362 extends: Some("normal".into()),
363 font_size: Some(doc.font_size_small),
364 italic: Some(true),
365 alignment: Some(TextAlign::Center),
366 space_before_mm: Some(2.0),
367 space_after_mm: Some(4.0),
368 ..Default::default()
369 });
370
371 m.insert("table_header".into(), NamedStyle {
373 extends: Some("normal".into()),
374 bold: Some(true),
375 alignment: Some(TextAlign::Left),
376 space_before_mm: Some(0.0),
377 space_after_mm: Some(0.0),
378 ..Default::default()
379 });
380
381 m.insert("table_body".into(), NamedStyle {
383 extends: Some("normal".into()),
384 alignment: Some(TextAlign::Left),
385 space_before_mm: Some(0.0),
386 space_after_mm: Some(0.0),
387 ..Default::default()
388 });
389
390 m.insert("footnote".into(), NamedStyle {
392 extends: Some("normal".into()),
393 font_size: Some(9.0),
394 space_before_mm: Some(0.5),
395 space_after_mm: Some(0.5),
396 ..Default::default()
397 });
398
399 m.insert("toc_1".into(), NamedStyle {
401 extends: Some("normal".into()),
402 font_size: Some(11.0),
403 bold: Some(true),
404 space_after_mm: Some(1.0),
405 ..Default::default()
406 });
407
408 m.insert("toc_2".into(), NamedStyle {
409 extends: Some("toc_1".into()),
410 bold: Some(false),
411 indent_left_mm: Some(8.0),
412 ..Default::default()
413 });
414
415 m.insert("toc_3".into(), NamedStyle {
416 extends: Some("toc_2".into()),
417 indent_left_mm: Some(16.0),
418 font_size: Some(10.0),
419 ..Default::default()
420 });
421
422 m
423}
424
425#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "snake_case")]
430pub enum SecurityClassification {
431 #[default]
432 Public,
433 Internal,
434 Confidential,
435 Reserved,
436}
437
438impl SecurityClassification {
439 pub fn label_pt(self) -> &'static str {
441 match self {
442 Self::Public => "Público",
443 Self::Internal => "Interno",
444 Self::Confidential => "Confidencial",
445 Self::Reserved => "Reservado",
446 }
447 }
448
449 pub fn watermark_color(self) -> RgbColor {
451 match self {
452 Self::Internal => RgbColor { r: 0.0, g: 0.0, b: 0.5 },
453 Self::Confidential => RgbColor { r: 0.8, g: 0.0, b: 0.0 },
454 Self::Reserved => RgbColor { r: 0.5, g: 0.0, b: 0.0 },
455 Self::Public => RgbColor { r: 0.7, g: 0.7, b: 0.7 },
456 }
457 }
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct TraceabilityMetadata {
471 pub engine_version: String,
473 pub framework_version: Option<String>,
475 pub entity_id: String,
477 pub document_ref: Option<String>,
479 pub classification: SecurityClassification,
481 pub generated_at: String,
483 pub ndt_version: String,
485}
486