p4d_mdproof/
lib.rs

1/*
2  This Source Code Form is subject to the terms of the Mozilla Public
3  License, v. 2.0. If a copy of the MPL was not distributed with this
4  file, You can obtain one at http://mozilla.org/MPL/2.0/.
5*/
6use pulldown_cmark as cmark;
7
8mod atomizer;
9mod page;
10mod pages;
11mod resources;
12mod section;
13mod sectioner;
14mod sizer;
15mod span;
16mod style;
17mod util;
18
19use anyhow::{Context, Error};
20use cmark::*;
21use printpdf::{Image, ImageTransform, Mm, PdfDocument, PdfDocumentReference};
22use rusttype::Scale;
23
24use crate::pages::Pages;
25use crate::resources::Loader;
26use crate::sectioner::Sectioner;
27use crate::span::Span;
28use crate::style::Class;
29use std::path::PathBuf;
30
31const DEFAULT_REGULAR_FONT: &str = "mdproof-default-regular";
32const DEFAULT_BOLD_FONT: &str = "mdproof-default-bold";
33const DEFAULT_ITALIC_FONT: &str = "mdproof-default-italic";
34const DEFAULT_BOLD_ITALIC_FONT: &str = "mdproof-default-bold";
35const DEFAULT_MONO_FONT: &str = "mdproof-default-mono";
36
37#[derive(Clone, Debug)]
38pub struct Config {
39    /// The path from which images will be loaded
40    pub resources_directory: PathBuf,
41
42    pub title: String,
43    pub first_layer_name: String,
44
45    pub page_size: (Mm, Mm),
46    pub margin: (Mm, Mm),
47    pub default_font: String,
48    pub bold_font: String,
49    pub italic_font: String,
50    pub bold_italic_font: String,
51    pub mono_font: String,
52
53    pub default_font_size: Scale,
54    pub h1_font_size: Scale,
55    pub h2_font_size: Scale,
56    pub h3_font_size: Scale,
57    pub h4_font_size: Scale,
58
59    pub line_spacing: f64, // Text height * LINE_SPACING
60    pub list_indentation: Mm,
61    pub list_point_offset: Mm,
62    pub quote_indentation: Mm,
63    /// The horizontal offset of code blocks
64    pub code_indentation: Mm,
65    /// The vertical space between two sections (paragraphs, lists, etc.)
66    pub section_spacing: Mm,
67}
68
69impl Default for Config {
70    fn default() -> Self {
71        Config {
72            resources_directory: PathBuf::new(),
73
74            title: "mdproof".into(),
75            first_layer_name: "Layer 1".into(),
76
77            page_size: (Mm(210.0), Mm(297.0)),
78            margin: (Mm(20.0), Mm(20.0)),
79            default_font: DEFAULT_REGULAR_FONT.into(),
80            bold_font: DEFAULT_BOLD_FONT.into(),
81            italic_font: DEFAULT_ITALIC_FONT.into(),
82            bold_italic_font: DEFAULT_BOLD_ITALIC_FONT.into(),
83            mono_font: DEFAULT_MONO_FONT.into(),
84
85            default_font_size: Scale::uniform(12.0),
86            h1_font_size: Scale::uniform(32.0),
87            h2_font_size: Scale::uniform(28.0),
88            h3_font_size: Scale::uniform(20.0),
89            h4_font_size: Scale::uniform(16.0),
90
91            line_spacing: 1.0, // Text height * LINE_SPACING
92            list_indentation: Mm(10.0),
93            list_point_offset: Mm(5.0),
94            quote_indentation: Mm(20.0),
95            code_indentation: Mm(10.0),
96            section_spacing: Mm(5.0),
97        }
98    }
99}
100
101pub fn markdown_to_pdf(markdown: &str, cfg: &Config) -> Result<PdfDocumentReference, Error> {
102    let (doc, mut page_idx, mut layer_idx) = PdfDocument::new(
103        cfg.title.clone(),
104        cfg.page_size.0,
105        cfg.page_size.1,
106        cfg.first_layer_name.clone(),
107    );
108
109    {
110        let mut resources = resources::Resources::new(cfg.clone());
111        let atomizer = atomizer::Atomizer::new(Parser::new(markdown));
112
113        let atoms: Vec<atomizer::Event> = atomizer.collect();
114        let mut loader = resources::SimpleLoader::new(PathBuf::from(&cfg.resources_directory));
115        for event in atoms.iter() {
116            // Expect to have more branches in future
117            #[allow(clippy::single_match)]
118            match event {
119                atomizer::Event::Atom(atomizer::Atom::Image { uri }) => {
120                    loader.queue_image(uri);
121                }
122                _ => {}
123            }
124        }
125
126        let _load_errors = loader.load_resources(&mut resources);
127
128        let sized_atoms: Vec<_> = sizer::Sizer::new(atoms.into_iter(), &resources).collect();
129
130        let sections = {
131            let mut lines =
132                Sectioner::new(cfg.margin.0, cfg.page_size.0 - cfg.margin.0, &resources);
133
134            for event in sized_atoms {
135                lines.parse_event(event);
136            }
137
138            lines.get_vec()
139        };
140
141        let mut pages = Pages::new(cfg, &resources);
142        pages.render_sections(&sections[..], cfg.margin.0);
143
144        let pages = pages.into_vec();
145
146        let default_font_reader = std::io::Cursor::new(resources::REGULAR_FONT);
147        let bold_font_reader = std::io::Cursor::new(resources::BOLD_FONT);
148        let italic_font_reader = std::io::Cursor::new(resources::ITALIC_FONT);
149        let bold_italic_font_reader = std::io::Cursor::new(resources::BOLD_ITALIC_FONT);
150        let mono_font_reader = std::io::Cursor::new(resources::MONO_FONT);
151
152        let regular = doc
153            .add_external_font(default_font_reader)
154            .context("Failed to add font to PDF")?;
155        let bold = doc
156            .add_external_font(bold_font_reader)
157            .context("Failed to add font to PDF")?;
158        let italic = doc
159            .add_external_font(italic_font_reader)
160            .context("Failed to add font to PDF")?;
161        let bold_italic = doc
162            .add_external_font(bold_italic_font_reader)
163            .context("Failed to add font to PDF")?;
164        let mono = doc
165            .add_external_font(mono_font_reader)
166            .context("Failed to add font to PDF")?;
167
168        let mut is_first_iteration = true;
169
170        for page in pages {
171            if !is_first_iteration {
172                let (new_page_idx, new_layer_idx) =
173                    doc.add_page(cfg.page_size.0, cfg.page_size.1, "Layer 1");
174                page_idx = new_page_idx;
175                layer_idx = new_layer_idx;
176            }
177
178            let current_layer = doc.get_page(page_idx).get_layer(layer_idx);
179            let page = page.into_vec().into_iter().peekable();
180            for span in page {
181                current_layer.begin_text_section();
182                current_layer.set_text_cursor(span.pos.0, span.pos.1);
183
184                match span.span {
185                    Span::Text { text, style, .. } => {
186                        // TODO: Abstract this piece of code away. It violates DRY.
187                        let strong = style.contains(&Class::Strong);
188                        let emphasis = style.contains(&Class::Emphasis);
189
190                        let font = if style.contains(&Class::Code) {
191                            &mono
192                        } else if strong && emphasis {
193                            &bold_italic
194                        } else if strong {
195                            &bold
196                        } else if emphasis {
197                            &italic
198                        } else {
199                            &regular
200                        };
201
202                        let font_scale = util::scale_from_style(cfg, &style);
203
204                        current_layer.set_font(font, font_scale.y as f64);
205                        for (i, c) in text.char_indices() {
206                            current_layer.end_text_section();
207                            current_layer.begin_text_section();
208                            let prev_w: Mm =
209                                util::width_of_text(&resources, &style, &text[0..i]).into();
210                            current_layer.set_text_cursor(span.pos.0 + prev_w, span.pos.1);
211                            current_layer.write_text(c, font);
212                        }
213                    }
214                    Span::Image { path, .. } => {
215                        let image = Image::from_dynamic_image(
216                            resources
217                                .get_image(&path.to_string_lossy())
218                                .expect("image to exist"),
219                        );
220                        image.add_to_layer(
221                            current_layer.clone(),
222                            ImageTransform {
223                                translate_x: Some(span.pos.0),
224                                translate_y: Some(span.pos.1),
225                                ..Default::default()
226                            },
227                        );
228                    }
229                    Span::Rect { width, height } => {
230                        use printpdf::{Line, Point};
231                        let rect_points = vec![
232                            (Point::new(span.pos.0, span.pos.1 + height), false),
233                            (Point::new(span.pos.0 + width, span.pos.1 + height), false),
234                            (Point::new(span.pos.0 + width, span.pos.1), false),
235                            (Point::new(span.pos.0, span.pos.1), false),
236                        ];
237                        let rect = Line {
238                            points: rect_points,
239                            is_closed: true,
240                            has_fill: true,
241                            has_stroke: false,
242                            is_clipping_path: false,
243                        };
244                        current_layer.add_shape(rect);
245                    }
246                }
247                current_layer.end_text_section();
248            }
249            is_first_iteration = false;
250        }
251    }
252
253    Ok(doc)
254}