pdf_composer_base/lib.rs
1// Copyright © 2024 PDF Composer (pdf_composer). All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! The 'base' crate for PDF Composer functionality (without any features enabled)
5//!
6//! This crate provides the core functionality required to generate PDF documents.
7//! Including:
8//! * Checking source documents are yaml
9//! * Setting page size
10//! * Setting page orientation
11//! * Setting page margins
12//! * Setting page metadata (PDF fields)
13//! * Setting output directory
14
15use colored::Colorize;
16use rayon::prelude::*;
17use regex::Regex;
18use serde_yml::Value;
19use std::collections::BTreeMap;
20use std::fs;
21use std::option::Option;
22use std::path::{PathBuf, MAIN_SEPARATOR_STR};
23use std::process;
24
25use pdf_composer_definitions::{
26 consts::{CROSS_MARK, DEFAULT_MARGIN, DEFAULT_OUTPUT_DIRECTORY, MM_TO_INCH},
27 fonts::FontsStandard,
28 output_directory::OutputDirectory,
29 page_properties::{PaperOrientation, PaperSize},
30 pdf_composer::PDFComposerStruct,
31 pdf_doc_entry::PDFDocInfoEntry,
32 pdf_version::PDFVersion,
33};
34/// The `build_pdf` module contains the core functions for generating PDF files.
35mod build_pdf;
36use build_pdf::{build_pdf, PDFBuilder};
37/// 'utils' module for helper functions
38mod utils;
39use utils::{merge_markdown_yaml, read_lines, yaml_mapping_to_btreemap};
40
41/// The PDF Composer trait with all the publically exposed methods
42pub trait PDFComposer {
43 /// Create a new PDF Composer instance
44 fn new() -> Self;
45 /// Same as 'new'
46 fn default() -> Self;
47 /// Set the version of the PDF as per the PDFVersion enum
48 fn set_pdf_version(&mut self, pdf_version: PDFVersion);
49 /// Set the directory into which generated PDFs will be saved
50 fn set_output_directory<T: OutputDirectory>(&mut self, output_directory: T);
51 /// Set the paper size from the PaperSize enum
52 fn set_paper_size(&mut self, paper_size: PaperSize);
53 /// Set the paper orientation from the PaperOrientation enum
54 fn set_orientation(&mut self, orientation: PaperOrientation);
55 /// Set the font to use from the FontsStandard enum
56 fn set_font(&mut self, font: FontsStandard);
57 /// Set the margins to put around the paper
58 fn set_margins(&mut self, margins: &str);
59 /// Set where the source files are to be found
60 fn add_source_files(&mut self, paths: Vec<PathBuf>);
61 /// Set the PDF document meta-data fields (such as language, keywords etc)
62 fn set_doc_info_entry(&mut self, entry: PDFDocInfoEntry);
63 /// Generate the PDF document
64 fn generate_pdfs(&self);
65}
66
67impl PDFComposer for PDFComposerStruct {
68 /// Constructor function to create a new instance of PDFComposer with default values.
69 ///
70 /// # Examples
71 ///
72 /// ```
73 /// use pdf_composer::PDFComposer;
74 ///
75 /// // Create a new PDFComposer instance with default values
76 /// let my_pdf_doc = PDFComposer::new();
77 /// ```
78 fn new() -> Self {
79 // Create and return a new instance of PDFComposer.
80 // Setting default values, where applicable.
81 Self {
82 fmy_source_files: Vec::new(),
83 output_directory: DEFAULT_OUTPUT_DIRECTORY.into(),
84 pdf_version: PDFVersion::V1_7,
85 pdf_document_entries: None,
86 paper_size: PaperSize::A4,
87 orientation: PaperOrientation::Portrait,
88 margins: [DEFAULT_MARGIN / MM_TO_INCH; 4],
89 font: FontsStandard::Helvetica,
90 }
91 }
92
93 /// Sets the PDF version for the PDFComposer instance.
94 /// Sets the PDF version for the PDF document.
95 ///
96 /// # Examples
97 ///
98 /// ```
99 /// use pdf_composer::{PDFComposer, PDFVersion};
100 ///
101 /// // Create a new PDF document
102 /// let mut my_pdf_doc = PDFComposer::new();
103 ///
104 /// // Set the PDF version to 2.0
105 /// my_pdf_doc.set_pdf_version(PDFVersion::V1_7);
106 /// ```
107 fn set_pdf_version(&mut self, pdf_version: PDFVersion) {
108 self.pdf_version = pdf_version;
109 }
110
111 /// Sets the output directory for the generated PDF documents.
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use pdf_composer::PDFComposer;
117 ///
118 /// // Create a new PDF generator instance
119 /// let mut my_pdf_doc = PDFComposer::new();
120 ///
121 /// // Set the output directory to "output/pdf"
122 /// my_pdf_doc.set_output_directory("output/pdf");
123 /// ```
124 fn set_output_directory<T: OutputDirectory>(&mut self, output_directory: T) {
125 self.output_directory = output_directory.convert();
126 }
127
128 /// Sets the paper size for the PDF documents.
129 ///
130 /// # Examples
131 ///
132 /// ```
133 /// use pdf_composer::PDFComposer;
134 ///
135 /// // Create a new PDF generator instance
136 /// let mut my_pdf_doc = PDFComposer::new();
137 ///
138 /// // Set the paper size to A5
139 /// my_pdf_doc.set_paper_size(PaperSize::A5);
140 /// ```
141 fn set_paper_size(&mut self, paper_size: PaperSize) {
142 self.paper_size = paper_size;
143 }
144
145 /// Sets the page orientation.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use pdf_composer::PDFComposer;
151 ///
152 /// // Create a new PDF generator instance
153 /// let mut my_pdf_doc = PDFComposer::new();
154 ///
155 /// // Set the orientation to Landscape
156 /// my_pdf_doc.set_orientation(PaperOrientation::Landscape);
157 /// ```
158 fn set_orientation(&mut self, orientation: PaperOrientation) {
159 self.orientation = orientation;
160 }
161
162 /// Sets the font for the PDF.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use pdf_composer::PDFComposer;
168 ///
169 /// // Create a new PDF generator instance
170 /// let mut my_pdf_doc = PDFComposer::new();
171 ///
172 /// // Set the font to Times Roman
173 /// my_pdf_doc.set_font(FontsStandard::TimesRoman);
174 /// ```
175 fn set_font(&mut self, font: FontsStandard) {
176 self.font = font;
177 }
178
179 /// Sets the page margins.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use pdf_composer::PDFComposer;
185 ///
186 /// // Create a new PDF generator instance
187 /// let mut my_pdf_doc = PDFComposer::new();
188 ///
189 /// // Set the page margins to 20mm
190 /// my_pdf_doc.set_margins("20");
191 /// ```
192 fn set_margins(&mut self, margins: &str) {
193 // println!("{} {}", "margins:".cyan(), margins);
194 // Trim (remove) white space from both ends of the margins string
195 let mut margins_vector: Vec<&str> = margins.trim().split(' ').collect();
196 // Remove all empty elements in the margins vector
197 margins_vector.retain(|ele| !ele.is_empty());
198 // println!(
199 // "{} {:?}",
200 // "margins_vector:".cyan(),
201 // margins_vector.to_owned()
202 // );
203
204 // Check to see if there are any non-integer entries for margin values
205 // If there are, then set any_letters_found to true and set all margins to default size
206 let any_letters_found = margins_vector
207 .iter()
208 .any(|&ele| ele.parse::<u32>().is_err());
209
210 if any_letters_found {
211 self.margins = [DEFAULT_MARGIN / MM_TO_INCH; 4];
212 let troublesome_margins: String = margins_vector.join(", ");
213 let margin_error_message = "".to_owned()
214 + &CROSS_MARK.red().to_string()
215 + &"Something wrong with the margin values provided "
216 .red()
217 .to_string()
218 + &"[".yellow().to_string()
219 + &troublesome_margins.yellow().to_string()
220 + &"]".yellow().to_string()
221 + "\nUsing the default value of "
222 + &DEFAULT_MARGIN.to_string()
223 + "mm for the margins.\n";
224 eprintln!("{}", margin_error_message);
225 } else {
226 self.margins = match margins_vector.len() {
227 1 => {
228 if margins_vector[0].is_empty() {
229 [DEFAULT_MARGIN / MM_TO_INCH; 4]
230 } else {
231 [f64::from(margins_vector[0].parse::<u32>().unwrap()) / MM_TO_INCH; 4]
232 }
233 }
234 2 => {
235 let top_bottom =
236 f64::from(margins_vector[0].parse::<u32>().unwrap()) / MM_TO_INCH;
237 let left_right =
238 f64::from(margins_vector[1].parse::<u32>().unwrap()) / MM_TO_INCH;
239 [top_bottom, left_right, top_bottom, left_right]
240 }
241 3 => {
242 let top = f64::from(margins_vector[0].parse::<u32>().unwrap()) / MM_TO_INCH;
243 let left_right =
244 f64::from(margins_vector[1].parse::<u32>().unwrap()) / MM_TO_INCH;
245 let bottom = f64::from(margins_vector[2].parse::<u32>().unwrap()) / MM_TO_INCH;
246 [top, left_right, bottom, left_right]
247 }
248 4 => {
249 let top = f64::from(margins_vector[0].parse::<u32>().unwrap()) / MM_TO_INCH;
250 let right = f64::from(margins_vector[1].parse::<u32>().unwrap()) / MM_TO_INCH;
251 let bottom = f64::from(margins_vector[2].parse::<u32>().unwrap()) / MM_TO_INCH;
252 let left = f64::from(margins_vector[3].parse::<u32>().unwrap()) / MM_TO_INCH;
253 [top, right, bottom, left]
254 }
255 _ => [DEFAULT_MARGIN / MM_TO_INCH; 4],
256 }
257 };
258
259 // println!("{:#?}", self.margins);
260 }
261
262 /// Adds source files to the PDFComposer instance for processing.
263 ///
264 /// # Examples
265 ///
266 /// ```
267 /// use pdf_composer::PDFComposer;
268 /// use std::path::PathBuf;
269 ///
270 /// // Create a new PDF generator instance
271 /// let mut my_pdf_doc = PDFComposer::new();
272 ///
273 /// // Define paths to source files
274 /// let source_files = vec![
275 /// PathBuf::from("source/file1.txt"),
276 /// PathBuf::from("source/file2.txt"),
277 /// ];
278 ///
279 /// // Add the source files to the PDF generator
280 /// my_pdf_doc.add_source_files(source_files);
281 /// ```
282 fn add_source_files(&mut self, paths: Vec<PathBuf>) {
283 let regex = Regex::new(r"(?m)\\").unwrap();
284
285 // Normalize the paths to be OS compliant
286 let normalized_paths: Vec<PathBuf> = paths
287 .iter()
288 .map(|p| {
289 // Normalize the paths to be OS compliant
290 let is_windows = cfg!(target_os = "windows");
291 // Convert the path separator based on the platform
292 let os_compliant_path = if is_windows {
293 p.display().to_string().replace('/', MAIN_SEPARATOR_STR)
294 } else {
295 regex
296 .replace_all(&p.as_path().display().to_string(), MAIN_SEPARATOR_STR)
297 .to_string()
298 };
299 PathBuf::from(os_compliant_path)
300 })
301 .collect();
302
303 self.fmy_source_files.extend(normalized_paths);
304 }
305
306 /// Sets a document information entry for the PDFComposer instance.
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use pdf_composer::{PDFComposer, PDFDocInfoEntry};
312 ///
313 /// // Create a new PDFComposer instance
314 /// let mut my_pdf_doc = PDFComposer::new();
315 ///
316 /// // Define a document information entry
317 /// let doc_info_entry = PDFDocInfoEntry {
318 /// doc_info_entry: "Author",
319 /// yaml_entry: "author",
320 /// };
321 ///
322 /// // Set the document information entry in the PDFComposer
323 /// my_pdf_doc.set_doc_info_entry(doc_info_entry);
324 /// ```
325 fn set_doc_info_entry(&mut self, entry: PDFDocInfoEntry) {
326 // Reserved metadata entries in the document information dictionary
327 // These are case sensitive and must be capitalised.
328 // All others will be as entered by the user.
329 let local_doc_info_entry: String = match entry.doc_info_entry.to_lowercase().as_str() {
330 "title" => "Title".to_string(),
331 "author" => "Author".to_string(),
332 "subject" => "Subject".to_string(),
333 "keywords" => "Keywords".to_string(),
334 _ => entry.doc_info_entry.to_string(),
335 };
336 let local_yaml_entry = entry.yaml_entry;
337
338 // Match and handle the Option variant to insert the entry into the PDF document entries.
339 match &mut self.pdf_document_entries {
340 Some(map) => {
341 // Case where the Option contains Some variant
342 map.insert(local_doc_info_entry.clone(), local_yaml_entry.to_owned());
343 }
344 None => {
345 // Case where the Option contains None variant
346 let mut new_map = BTreeMap::new();
347 new_map.insert(local_doc_info_entry.clone(), local_yaml_entry.to_owned());
348 self.pdf_document_entries = Some(new_map);
349 }
350 }
351 }
352
353 /// Generates PDF documents based on the configured settings and source files.
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// use pdf_composer::PDFComposer;
359 ///
360 /// // Create a PDF generator instance
361 /// let my_pdf_doc = PDFComposer::new();
362 ///
363 /// // Generate PDFs based on the configuration and source files
364 /// my_pdf_doc.generate_pdfs();
365 /// ```
366 fn generate_pdfs(&self) {
367 // Handle case where no source files are set.
368 let error_message = "".to_owned()
369 + &CROSS_MARK.on_red().to_string()
370 + &"No source files set.".on_red().to_string()
371 + " Exiting\n";
372 if self.fmy_source_files.is_empty() {
373 eprintln!("{}", error_message);
374 process::exit(0);
375 }
376
377 println!("{} {:#?}", "Files:".cyan(), &self.fmy_source_files);
378 println!(
379 "Files to process: {}\n",
380 &self.fmy_source_files.len().to_string().cyan()
381 );
382
383 // Process each source file in parallel.
384 self.fmy_source_files.par_iter().for_each(|document| {
385 // Initialize variables for processing YAML and Markdown content.
386 let mut rayon_yaml_delimiter_count = 0;
387 let mut rayon_yaml_content: String = String::default();
388 let mut rayon_markdown_content: String = String::default();
389 let mut yaml_section_complete: bool = false;
390
391 // Extract filename from PathBuf.
392 let filename = <std::path::PathBuf as Clone>::clone(document)
393 .into_os_string()
394 .into_string()
395 .unwrap();
396
397 // Attempt to read metadata of the file.
398 match fs::metadata(filename.clone()) {
399 Ok(_) => 'file_found: {
400 // File exists, proceed with reading.
401 println!("File {} exists. {}", filename.cyan(), "Reading...".green());
402 if let Ok(lines) = read_lines(&filename) {
403 // Iterate through lines and process YAML and Markdown content.
404 for line in lines.map_while(Result::ok) {
405 // Check YAML delimiters and extract content.
406 if line.trim() == "---" && rayon_yaml_delimiter_count < 2 {
407 rayon_yaml_delimiter_count += 1;
408 }
409
410 if line.trim() != "---" && rayon_yaml_delimiter_count < 2 {
411 rayon_yaml_content.push_str(&format!("{}{}", &line, "\n"));
412 }
413
414 // Check if YAML section is complete.
415 if rayon_yaml_delimiter_count == 2 && !yaml_section_complete {
416 yaml_section_complete = true;
417 continue;
418 }
419
420 // Extract Markdown content after YAML section.
421 if rayon_yaml_delimiter_count == 2 && yaml_section_complete {
422 rayon_markdown_content.push_str(&format!("{}{}", &line, "\n"));
423 }
424 }
425 }
426
427 // Parse YAML content.
428 let yaml: Value = serde_yml::from_str(&rayon_yaml_content).unwrap();
429 // Check if YAML is valid.
430 // If file exists, but is not a suitable yaml markdown file, early exit break
431 if rayon_yaml_delimiter_count == 0 || yaml == Value::Null {
432 println!("File {} is not a valid yaml file", filename.red());
433 break 'file_found;
434 } else {
435 println!("{}. {}", filename.cyan(), "Processing...".green());
436 }
437
438 // Convert YAML Front Matter to a BTreeMap.
439 let yaml_btreemap: BTreeMap<String, Value> =
440 yaml_mapping_to_btreemap(&yaml).unwrap();
441
442 // Insert YAML Front Matter into markdown.
443 let merged_markdown_yaml =
444 merge_markdown_yaml(yaml_btreemap.clone(), &rayon_markdown_content);
445
446 // Convert Markdown content to HTML.
447 // markdown:: comes from the markdown crate
448 let html: String = markdown::to_html(&merged_markdown_yaml.to_owned());
449
450 let instance_data = PDFBuilder {
451 source_file: filename.to_string(),
452 output_directory: self.output_directory.to_path_buf(),
453 pdf_version: self.pdf_version,
454 paper_size: self.paper_size,
455 orientation: self.orientation,
456 margins: self.margins,
457 font: self.font,
458 };
459
460 let dictionary_entries = match &self.pdf_document_entries {
461 None => BTreeMap::new(),
462 _ => <Option<BTreeMap<String, String>> as Clone>::clone(
463 &self.pdf_document_entries,
464 )
465 .unwrap(),
466 };
467
468 // Build the PDF document.
469 let _ = build_pdf(html, yaml_btreemap, dictionary_entries, instance_data);
470 }
471 Err(_) => {
472 // File not found, print error message.
473 println!("File {} not found.", filename.red());
474 }
475 }
476 });
477 }
478
479 fn default() -> Self {
480 Self::new()
481 }
482}