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}