1use 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 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, pub list_indentation: Mm,
61 pub list_point_offset: Mm,
62 pub quote_indentation: Mm,
63 pub code_indentation: Mm,
65 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, 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 #[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(§ions[..], 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 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 ®ular
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}