1use base64::Engine as _;
2use regex::Regex;
3use serde_json::Value;
4
5use super::data::NdtData;
6use super::resolver;
7use crate::ndf::{
8 audit::{Actor, AuditEvent, EventType, NdfAudit},
9 integrity::{canonical_hash, NdfIntegrity},
10 NdfDocument, NdfEmbeddedFont, NdfMeta, NdfOrigin, NDF_VERSION,
11};
12use crate::{NormaxisPdfError, Result};
13
14#[derive(Debug, Clone)]
18pub struct CompileOptions {
19 pub document_id: Option<String>,
22
23 pub generated_by: Actor,
25
26 pub ndt_template_id: Option<String>,
28
29 pub ndt_template_hash: Option<String>,
32
33 pub validate_resolved: bool,
35}
36
37impl Default for CompileOptions {
38 fn default() -> Self {
39 Self {
40 document_id: None,
41 generated_by: Actor::System {
42 id: "normordis-pdf".into(),
43 version: Some(env!("CARGO_PKG_VERSION").into()),
44 instance_id: None,
45 },
46 ndt_template_id: None,
47 ndt_template_hash: None,
48 validate_resolved: true,
49 }
50 }
51}
52
53pub fn compile_ndt(ndt: &str, data: &NdtData, options: CompileOptions) -> Result<NdfDocument> {
68 let doc = super::parse_ndt(ndt)
69 .map_err(|e| NormaxisPdfError::NdfCompileError(e.to_string()))?;
70
71 if let Some(ref placeholders) = doc.placeholders {
72 super::validator::validate(placeholders, data)
73 .map_err(|e| NormaxisPdfError::NdfCompileError(e.to_string()))?;
74 }
75
76 let body_val = serde_json::to_value(&doc.body)
78 .map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
79 let resolved_content = resolve_value_placeholders(body_val, data);
80
81 let styles_val = serde_json::to_value(&doc.style)
82 .map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
83
84 if options.validate_resolved {
85 let content_str = serde_json::to_string(&resolved_content)
86 .map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
87 let re = Regex::new(r"\{\{[a-zA-Z0-9_.]+\}\}").expect("static regex");
88 if let Some(m) = re.find(&content_str) {
89 return Err(NormaxisPdfError::NdfCompileError(format!(
90 "unresolved placeholder '{}' in content after substitution",
91 m.as_str()
92 )));
93 }
94 }
95
96 let now = chrono::Utc::now().to_rfc3339();
97
98 let meta_title = doc
99 .meta
100 .as_ref()
101 .and_then(|m| m.title.clone())
102 .unwrap_or_default();
103 let meta_compat = doc.meta.as_ref().and_then(|m| m.compat_mode);
104 let meta = NdfMeta {
105 title: meta_title,
106 entity: String::new(),
107 entity_id: None,
108 lang: "pt-PT".into(),
109 document_ref: None,
110 document_type: None,
111 classification: "public".into(),
112 subject: None,
113 keywords: None,
114 created_at: now.clone(),
115 valid_from: None,
116 valid_until: None,
117 supersedes: None,
118 compat_mode: meta_compat,
119 numbering: None,
120 };
121 let meta_val = serde_json::to_value(&meta)
122 .map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
123
124 let integrity = NdfIntegrity::compute(&resolved_content, &styles_val, &meta_val)?;
125 let document_id = options
126 .document_id
127 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
128
129 let ndt_template_hash = options.ndt_template_hash.unwrap_or_else(|| {
130 let v = serde_json::from_str::<Value>(ndt).unwrap_or(Value::Null);
131 canonical_hash(&v)
132 });
133
134 let first_event = AuditEvent {
135 seq: 1,
136 event_type: EventType::DocumentGenerated,
137 timestamp: now.clone(),
138 actor: options.generated_by.clone(),
139 content_hash: Some(integrity.content_hash.clone()),
140 note: None,
141 extra: Default::default(),
142 };
143
144 Ok(NdfDocument {
145 ndf: NDF_VERSION.into(),
146 origin: NdfOrigin {
147 ndt_template_id: options.ndt_template_id,
148 ndt_version: None,
149 ndt_template_hash: Some(ndt_template_hash),
150 ndt_data_hash: None,
151 engine_version: env!("CARGO_PKG_VERSION").into(),
152 engine_backend: "normordis-pdf".into(),
153 generated_at: now,
154 generated_by: options.generated_by,
155 },
156 revision: None,
157 meta,
158 output: serde_json::to_value(&doc.output).ok(),
159 styles: styles_val,
160 content: resolved_content,
161 page: serde_json::to_value(&doc.page).ok(),
162 embedded_fonts: vec![],
163 integrity,
164 audit: NdfAudit {
165 document_id,
166 events: vec![first_event],
167 },
168 outputs: vec![],
169 signatures: vec![],
170 })
171}
172
173pub fn parse_ndf(json: &str) -> Result<NdfDocument> {
177 serde_json::from_str(json).map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))
178}
179
180pub fn verify_ndf(json: &str) -> Result<crate::ndf::integrity::IntegrityReport> {
182 let ndf = parse_ndf(json)?;
183 ndf.verify_integrity()
184}
185
186pub fn render_ndf(ndf_json: &str) -> Result<Vec<u8>> {
193 render_ndf_inner(ndf_json, None)
194}
195
196pub fn render_ndf_with_fonts(
202 ndf_json: &str,
203 extra: &crate::fonts::FontRegistry,
204) -> Result<Vec<u8>> {
205 render_ndf_inner(ndf_json, Some(extra))
206}
207
208fn render_ndf_inner(
209 ndf_json: &str,
210 extra_fonts: Option<&crate::fonts::FontRegistry>,
211) -> Result<Vec<u8>> {
212 let ndf = parse_ndf(ndf_json)?;
213 let (ndt_doc, fonts) = rebuild_ndt_doc_and_fonts(&ndf, extra_fonts)?;
214 let (standard, compression, accessibility) = parse_output_options(ndf.output.as_ref());
215
216 let style = crate::styles::DocumentStyle::default();
217 let empty_data = empty_ndt_data();
218 let elements = super::renderer::render_template(&ndt_doc, &empty_data, &style)
219 .map_err(|e| NormaxisPdfError::Template(e.to_string()))?;
220
221 crate::document::Document {
222 title: ndf.meta.title,
223 style,
224 fonts,
225 header: None,
226 sectioned_header: None,
227 footer: None,
228 sectioned_footer: None,
229 watermark: None,
230 elements,
231 footnotes: vec![],
232 toc_entries: None,
233 compression,
234 standard,
235 signature: None,
236 traceability: None,
237 accessibility,
238 }
239 .render_to_bytes()
240}
241
242pub fn render_ndf_prepared_for_signing(
246 ndf_json: &str,
247 opts: crate::signing::SignatureOptions,
248) -> Result<crate::signing::PreparedPdf> {
249 render_ndf_prepared_for_signing_inner(ndf_json, opts, None)
250}
251
252pub fn render_ndf_prepared_for_signing_with_fonts(
254 ndf_json: &str,
255 opts: crate::signing::SignatureOptions,
256 extra: &crate::fonts::FontRegistry,
257) -> Result<crate::signing::PreparedPdf> {
258 render_ndf_prepared_for_signing_inner(ndf_json, opts, Some(extra))
259}
260
261fn render_ndf_prepared_for_signing_inner(
262 ndf_json: &str,
263 opts: crate::signing::SignatureOptions,
264 extra_fonts: Option<&crate::fonts::FontRegistry>,
265) -> Result<crate::signing::PreparedPdf> {
266 let ndf = parse_ndf(ndf_json)?;
267 let (ndt_doc, fonts) = rebuild_ndt_doc_and_fonts(&ndf, extra_fonts)?;
268 let (standard, compression, accessibility) = parse_output_options(ndf.output.as_ref());
269
270 let style = crate::styles::DocumentStyle::default();
271 let empty_data = empty_ndt_data();
272 let elements = super::renderer::render_template(&ndt_doc, &empty_data, &style)
273 .map_err(|e| NormaxisPdfError::Template(e.to_string()))?;
274
275 crate::document::Document {
276 title: ndf.meta.title,
277 style,
278 fonts,
279 header: None,
280 sectioned_header: None,
281 footer: None,
282 sectioned_footer: None,
283 watermark: None,
284 elements,
285 footnotes: vec![],
286 toc_entries: None,
287 compression,
288 standard,
289 signature: None,
290 traceability: None,
291 accessibility,
292 }
293 .render_prepared_for_signing(opts)
294}
295
296fn rebuild_ndt_doc_and_fonts(
300 ndf: &NdfDocument,
301 extra_fonts: Option<&crate::fonts::FontRegistry>,
302) -> Result<(super::model::NdtDocument, crate::fonts::FontRegistry)> {
303 let body: Vec<super::model::BodyElement> =
304 serde_json::from_value(ndf.content.clone())
305 .map_err(|e| NormaxisPdfError::SerdeError(e.to_string()))?;
306
307 let page: Option<super::model::NdtPage> =
308 ndf.page.as_ref().and_then(|v| serde_json::from_value(v.clone()).ok());
309
310 let ndt_doc = super::model::NdtDocument {
311 ndt: "2.1.0".into(),
312 id: None,
313 meta: Some(super::model::NdtMeta {
314 title: Some(ndf.meta.title.clone()),
315 compat_mode: ndf.meta.compat_mode,
316 ..Default::default()
317 }),
318 style: serde_json::from_value(ndf.styles.clone()).ok(),
319 fonts: None,
320 page,
321 output: None,
322 signature: None,
323 placeholders: None,
324 zones: None,
325 body,
326 };
327
328 let mut fonts = crate::fonts::FontRegistry::default();
330 for ef in &ndf.embedded_fonts {
331 decode_and_register_font(ef, &mut fonts)?;
332 }
333 if let Some(extra) = extra_fonts {
334 for (_name, fam) in extra.families() {
335 fonts.register(fam.clone());
336 }
337 }
338
339 Ok((ndt_doc, fonts))
340}
341
342fn decode_and_register_font(
344 ef: &NdfEmbeddedFont,
345 fonts: &mut crate::fonts::FontRegistry,
346) -> Result<()> {
347 let dec = base64::engine::general_purpose::STANDARD;
348 let regular = dec
349 .decode(&ef.regular)
350 .map_err(|e| NormaxisPdfError::FontLoadError(e.to_string()))?;
351 let bold = ef
352 .bold
353 .as_deref()
354 .map(|s| dec.decode(s).map_err(|e| NormaxisPdfError::FontLoadError(e.to_string())))
355 .transpose()?;
356 let italic = ef
357 .italic
358 .as_deref()
359 .map(|s| dec.decode(s).map_err(|e| NormaxisPdfError::FontLoadError(e.to_string())))
360 .transpose()?;
361 let bold_italic = ef
362 .bold_italic
363 .as_deref()
364 .map(|s| dec.decode(s).map_err(|e| NormaxisPdfError::FontLoadError(e.to_string())))
365 .transpose()?;
366 fonts.register_bytes(
367 &ef.family,
368 ®ular,
369 bold.as_deref(),
370 italic.as_deref(),
371 bold_italic.as_deref(),
372 )
373}
374
375fn parse_output_options(
377 output: Option<&Value>,
378) -> (
379 crate::document::PdfStandard,
380 crate::document::CompressionLevel,
381 crate::compliance::ua::AccessibilityConfig,
382) {
383 let ndt_output: Option<super::model::NdtOutput> =
384 output.and_then(|v| serde_json::from_value(v.clone()).ok());
385
386 let standard = match ndt_output.as_ref().and_then(|o| o.standard.as_deref()) {
387 Some("pdf_a_1b") | Some("pdf_a1b") => crate::document::PdfStandard::PdfA1b,
388 Some("pdf_a_2b") | Some("pdf_a2b") => crate::document::PdfStandard::PdfA2b,
389 Some("pdf_ua2") | Some("pdf_ua_2") => crate::document::PdfStandard::PdfUa2,
390 _ => crate::document::PdfStandard::Pdf17,
391 };
392
393 let compression = match ndt_output.as_ref().and_then(|o| o.compression.as_deref()) {
394 Some("none") => crate::document::CompressionLevel::None,
395 Some("fast") => crate::document::CompressionLevel::Fast,
396 Some("best") => crate::document::CompressionLevel::Best,
397 _ => crate::document::CompressionLevel::Default,
398 };
399
400 let accessibility = ndt_output
401 .as_ref()
402 .and_then(|o| o.accessibility.clone())
403 .unwrap_or_default();
404
405 (standard, compression, accessibility)
406}
407
408fn empty_ndt_data() -> NdtData {
409 NdtData {
410 ndt_data: "1.0.0".into(),
411 template_id: None,
412 template_version: None,
413 data: Default::default(),
414 }
415}
416
417fn resolve_value_placeholders(value: Value, data: &NdtData) -> Value {
421 match value {
422 Value::String(s) => Value::String(resolver::resolve_string(&s, data)),
423 Value::Array(arr) => {
424 Value::Array(arr.into_iter().map(|v| resolve_value_placeholders(v, data)).collect())
425 }
426 Value::Object(map) => {
427 let mut new_map = serde_json::Map::new();
428 for (k, v) in map {
429 new_map.insert(k, resolve_value_placeholders(v, data));
430 }
431 Value::Object(new_map)
432 }
433 other => other,
434 }
435}