Skip to content

Commit

Permalink
feat: add initial pdf support (#114)
Browse files Browse the repository at this point in the history
* enable pdf output format for the command line

* add working pdf export

* remove the unnecessary keeping of the temp html file

* use correct error kind

* move pdf render code to own module in the render crate

* first working import of paged js

* rebase follow up fixes

* implemented get_target function for the UmiRenderer

* fix the missing code highlighting in pdfs

* improve error message output during the pdf render stage

* fix: use recommended formatting for pdfs

---------

Co-authored-by: Tobias Kleinert <[email protected]>
Co-authored-by: Sowasvonbot <>
Co-authored-by: Manuel Hatzl <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent b133fb6 commit 498ca1f
Show file tree
Hide file tree
Showing 17 changed files with 482 additions and 16 deletions.
25 changes: 21 additions & 4 deletions cli/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
};

use logid::{log, logging::event_entry::AddonKind, pipe};

use unimarkup_core::{
commons::config::{output::OutputFormatKind, Config},
Unimarkup,
Expand Down Expand Up @@ -50,11 +51,19 @@ pub fn compile(config: Config) -> Result<(), GeneralError> {
for format in um.get_formats() {
match format {
OutputFormatKind::Html => write_file(
&um.render_html()
&um.render_html(false)
.map_err(|_| GeneralError::Render)?
.to_string(),
&out_path,
OutputFormatKind::Html.extension(),
format.extension(),
)?,
OutputFormatKind::Pdf => write_raw_file(
&um.render_pdf().map_err(|err| {
log!(err);
GeneralError::Render
})?,
&out_path,
format.extension(),
)?,
OutputFormatKind::Umi => write_file(
&um.render_umi()
Expand All @@ -70,8 +79,8 @@ pub fn compile(config: Config) -> Result<(), GeneralError> {
Ok(())
}

fn write_file(
content: &str,
fn write_raw_file(
content: &[u8],
out_path: impl AsRef<Path>,
extension: &str,
) -> Result<(), GeneralError> {
Expand All @@ -91,3 +100,11 @@ fn write_file(
)
})
}

fn write_file(
content: &str,
out_path: impl AsRef<Path>,
extension: &str,
) -> Result<(), GeneralError> {
write_raw_file(content.as_bytes(), out_path, extension)
}
5 changes: 4 additions & 1 deletion commons/src/config/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct Output {
/// Defines the output format to render to.
/// If this option is not set, the input is rendered to all supported formats.
///
/// **Supported formats:** `html`
/// **Supported formats:** `html`, `pdf`
#[arg(long, alias = "output-formats", value_parser = parse_to_hashset::<OutputFormatKind>, required = false, default_value = "html")]
pub formats: HashSet<OutputFormatKind>,
/// `true` overwrites existing output files
Expand Down Expand Up @@ -71,6 +71,7 @@ pub enum OutputFormatKind {
#[default]
Html,
Umi,
Pdf,
}

impl OutputFormatKind {
Expand All @@ -79,6 +80,7 @@ impl OutputFormatKind {
match self {
OutputFormatKind::Html => "html",
OutputFormatKind::Umi => "umi",
OutputFormatKind::Pdf => "pdf",
}
}
}
Expand All @@ -90,6 +92,7 @@ impl FromStr for OutputFormatKind {
match s.to_lowercase().as_str() {
"html" => Ok(OutputFormatKind::Html),
"umi" => Ok(OutputFormatKind::Umi),
"pdf" => Ok(OutputFormatKind::Pdf),
o => Err(format!("Bad output format: {}", o)),
}
}
Expand Down
9 changes: 7 additions & 2 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub use unimarkup_commons as commons;
pub use unimarkup_inline as inline;
pub use unimarkup_parser as parser;
pub use unimarkup_render as render;
use unimarkup_render::pdf::render::render_pdf;

use crate::commons::config::output::OutputFormatKind;
use crate::commons::config::Config;
Expand Down Expand Up @@ -53,11 +54,15 @@ impl Unimarkup {
unimarkup_render::render::render(&self.doc, format, renderer)
}

pub fn render_html(&self) -> Result<Html, RenderError> {
self.render(OutputFormatKind::Html, HtmlRenderer::default())
pub fn render_html(&self, use_paged_js: bool) -> Result<Html, RenderError> {
self.render(OutputFormatKind::Html, HtmlRenderer::new(use_paged_js))
}

pub fn render_umi(&self) -> Result<Umi, RenderError> {
self.render(OutputFormatKind::Umi, UmiRenderer::default())
}

pub fn render_pdf(&self) -> Result<Vec<u8>, RenderError> {
render_pdf(&self.render_html(true)?.to_string())
}
}
2 changes: 1 addition & 1 deletion core/tests/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn run_spec_test(case: test_runner::test_file::TestCase) {
let input = test.input.trim_end();
let um = Unimarkup::parse(input, cfg);
test_file::TestOutputs {
html: Some(um.render_html().unwrap().to_string()),
html: Some(um.render_html(false).unwrap().to_string()),
um: Some(test.input.clone()),
}
},
Expand Down
2 changes: 1 addition & 1 deletion inline/tests/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn run_spec_test(case: test_runner::test_file::TestCase) {
let input = test.input.trim_end();
let um = unimarkup_core::Unimarkup::parse(input, cfg);
test_file::TestOutputs {
html: Some(um.render_html().unwrap().to_string()),
html: Some(um.render_html(false).unwrap().to_string()),
um: Some(test.input.clone()),
}
},
Expand Down
2 changes: 2 additions & 0 deletions render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ unimarkup-parser = { path = "../parser/", version = "0" }
syntect = "5.0"
rustyscript = "0.1.1"
spreadsheet-ods = "0.17.0"
headless_chrome = "1.0.9"
tempfile = "3.8.0"
mathemascii = "0.4.0"
20 changes: 14 additions & 6 deletions render/src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub struct HtmlElements(Vec<HtmlElement>);
pub struct HtmlHead {
pub elements: HtmlElements,
pub syntax_highlighting_used: bool,
pub paged_js_used: bool,
pub styles: HtmlAttributes, //TODO: replace with CSS struct
}

Expand All @@ -43,6 +44,7 @@ impl HtmlHead {
self.elements.append(&mut other.elements);
self.styles.append(&mut other.styles);
self.syntax_highlighting_used |= other.syntax_highlighting_used;
self.paged_js_used |= other.paged_js_used;
}
}

Expand Down Expand Up @@ -99,6 +101,7 @@ impl OutputFormat for Html {
head: HtmlHead {
elements: HtmlElements(Vec::new()),
syntax_highlighting_used: false,
paged_js_used: false,
styles: HtmlAttributes(Vec::new()),
},
body: HtmlBody {
Expand Down Expand Up @@ -183,13 +186,18 @@ impl std::ops::DerefMut for HtmlElements {
impl std::fmt::Display for HtmlHead {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<head>{}", self.elements)?;

if self.syntax_highlighting_used {
write!(
let highlighting = if self.paged_js_used {
let _ = write!(
f,
"<style>{}</style>",
include_str!("../../styles/syntax_highlighting.css")
)?;
"<script>{}</script>",
include_str!("paged.polyfill.min.js")
);
include_str!("../../styles/syntax_highlighting_paged_js.css")
} else {
include_str!("../../styles/syntax_highlighting.css")
};
if self.syntax_highlighting_used {
write!(f, "<style>{}</style>", highlighting)?;
}

//TODO: write other head styles (try to use LightningCss optimizations)
Expand Down
4 changes: 4 additions & 0 deletions render/src/html/paged.polyfill.min.js

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions render/src/html/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,28 @@ use super::{

#[derive(Debug, Default)]
pub struct HtmlRenderer {
use_paged_js: bool,
citation_index: usize,
}

impl HtmlRenderer {
pub fn new(use_paged_js: bool) -> Self {
HtmlRenderer {
use_paged_js,
citation_index: 0,
}
}
}

impl Renderer<Html> for HtmlRenderer {
fn get_target(&mut self) -> Result<Html, crate::log_id::RenderError> {
let html = Html::with_head(HtmlHead {
paged_js_used: self.use_paged_js,
..HtmlHead::default()
});
Ok(html)
}

fn render_paragraph(
&mut self,
paragraph: &unimarkup_parser::elements::atomic::Paragraph,
Expand Down
2 changes: 2 additions & 0 deletions render/src/html/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum HtmlTag {
Ul,
Li,
A,
Script,
}

impl HtmlTag {
Expand Down Expand Up @@ -59,6 +60,7 @@ impl HtmlTag {
HtmlTag::Ul => "ul",
HtmlTag::Li => "li",
HtmlTag::A => "a",
HtmlTag::Script => "script",
}
}
}
Expand Down
1 change: 1 addition & 0 deletions render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
mod csl_json;
pub mod html;
pub mod log_id;
pub mod pdf;
pub mod render;
pub mod umi;
3 changes: 3 additions & 0 deletions render/src/log_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ pub enum RenderError {

#[error("Output format `append()` failed. See log: '{}: {}'", .0.event_id, .0.entry_id)]
BadAppend(FinalizedEvent<LogId>),

#[error("Unexpected error during pdf render: {}", .0)]
UnexpectedPdfError(String),
}

#[derive(Debug, Clone, WarnLogId)]
Expand Down
1 change: 1 addition & 0 deletions render/src/pdf/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod render;
68 changes: 68 additions & 0 deletions render/src/pdf/render.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::io::Write;

use headless_chrome::types::PrintToPdfOptions;
use headless_chrome::{Browser, LaunchOptions};
use tempfile::Builder;

use crate::log_id::RenderError;
use crate::log_id::RenderError::UnexpectedPdfError;

/// Returns PrintToPdfOptions following the recommended settings of:
/// https://pagedjs.org/documentation/2-getting-started-with-paged.js/#using-paged.js-as-a-polyfill-in-web-browsers
fn create_pdf_options() -> Option<PrintToPdfOptions> {
Some(PrintToPdfOptions {
margin_top: Some(0f64),
margin_bottom: Some(0f64),
margin_left: Some(0f64),
margin_right: Some(0f64),
header_template: None,
footer_template: None,
print_background: Some(false),
..PrintToPdfOptions::default()
})
}

/// Renders the given html-string to a pdf represent as bytes.
/// It first writes the html-string to a temp-directory, because chrome needs a file to load as webpage.
/// Then it prints the rendered html as pdf. The result is returned and not written to disc.
///
/// # Arguments
/// * `html` - The rendered html as string
///
/// # Returns
/// The rendered PDF as bytes.
///
/// # Errors
/// * `UnexpectedPdfError` - in case something goes wrong with the underlying headless-chrome framework.
pub fn render_pdf(html: &str) -> Result<Vec<u8>, RenderError> {
let mut temp_html_file = Builder::new()
.suffix(".html")
.tempfile()
.map_err(|err| UnexpectedPdfError(err.to_string()))?;

temp_html_file
.write_all(html.as_bytes())
.map_err(|err| UnexpectedPdfError(err.to_string()))?;
let temp_file_url = format!(
"file://{}",
temp_html_file
.as_ref()
.as_os_str()
.to_str()
.ok_or(RenderError::Unimplemented)?
);

let browser = Browser::new(LaunchOptions::default())
.map_err(|err| UnexpectedPdfError(err.to_string()))?;
let pdf_bytes = browser
.new_tab()
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.navigate_to(temp_file_url.as_str())
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.wait_until_navigated()
.map_err(|err| UnexpectedPdfError(err.to_string()))?
.print_to_pdf(create_pdf_options())
.map_err(|err| UnexpectedPdfError(err.to_string()))?;

Ok(pdf_bytes)
}
8 changes: 7 additions & 1 deletion render/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ pub trait OutputFormat: Default {
pub trait Renderer<T: OutputFormat> {
// Note: Default implementation with `Err(RenderError::Unimplemented)` prevents breaking changes when adding new functions to this trait.

/// Returns the [`OutputFormat`] for the [`Renderer`]. <br>
/// May be used to set custom modifications in the output format
fn get_target(&mut self) -> Result<T, RenderError> {
Err(RenderError::Unimplemented)
}

//--------------------------------- BLOCKS ---------------------------------

/// Render a Unimarkup [`Paragraph`] to the output format `T`.
Expand Down Expand Up @@ -385,7 +391,7 @@ pub trait Renderer<T: OutputFormat> {

/// Render Unimarkup [`Block`s](Block) to the output format `T`.
fn render_blocks(&mut self, blocks: &[Block], context: &Context) -> Result<T, RenderError> {
let mut t = T::default();
let mut t = self.get_target()?;

for block in blocks {
let rendered_block = match self.render_block(block, context) {
Expand Down
5 changes: 5 additions & 0 deletions render/src/umi/render.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use unimarkup_inline::element::InlineElement;
use unimarkup_parser::elements::blocks::Block;

use crate::log_id::RenderError;
use crate::render::{Context, OutputFormat, Renderer};
use std::collections::HashMap;

Expand Down Expand Up @@ -229,6 +230,10 @@ impl Renderer<Umi> for UmiRenderer {
))
}

fn get_target(&mut self) -> Result<Umi, RenderError> {
Ok(Umi::default())
}

fn render_bibliography(
&mut self,
context: &Context,
Expand Down
Loading

0 comments on commit 498ca1f

Please sign in to comment.