use printpdf::{ BuiltinFont, Mm, Op, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, Point, Pt, TextItem, }; use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag, TagEnd}; use crate::Error; use crate::layout::{ BLOCK_GAP_MM, BODY_PT, H1_PT, H2_PT, H3_PT, HEADING_GAP_MM, LIST_INDENT_MM, MARGIN, MONO_PT, PAGE_H, PAGE_W, QUOTE_INDENT_MM, content_bottom_mm, content_top_mm, content_width_mm, line_height_mm, text_width_mm, }; pub fn render_article(html: &str, title: &str, url: Option<&str>) -> Result, Error> { let markdown = pter::convert(html); let markdown = markdown.trim(); if markdown.is_empty() { return Err(Error::Empty); } let mut r = Renderer::new(title); if !title.trim().is_empty() { r.draw_heading(title, H1_PT); if let Some(u) = url { r.draw_paragraph( u, Style { italic: true, ..Style::default() }, ); } r.block_gap(); } r.render_markdown(markdown); r.finish() } #[derive(Copy, Clone, Default)] struct Style { bold: bool, italic: bool, mono: bool, size_pt: f32, left_indent_mm: f32, bullet: Option, number: Option, } impl Style { fn body() -> Self { Self { size_pt: BODY_PT, ..Self::default() } } } struct Renderer { title: String, /// Pages already flushed. The page currently being filled lives in `ops`. pages: Vec, /// Content operations for the page being filled. ops: Vec, y_mm: f32, } impl Renderer { fn new(title: &str) -> Self { let doc_title = if title.trim().is_empty() { "Balanced Breakfast" } else { title }; Self { title: doc_title.to_string(), pages: Vec::new(), ops: Vec::new(), y_mm: content_top_mm(), } } fn finish(mut self) -> Result, Error> { // Flush the page still being filled. self.pages .push(PdfPage::new(PAGE_W, PAGE_H, std::mem::take(&mut self.ops))); let mut doc = PdfDocument::new(&self.title); doc.with_pages(self.pages); let mut warnings = Vec::new(); Ok(doc.save(&PdfSaveOptions::default(), &mut warnings)) } fn render_markdown(&mut self, md: &str) { let opts = Options::ENABLE_STRIKETHROUGH; let parser = Parser::new_ext(md, opts); let mut style_stack: Vec