Skip to content

Commit

Permalink
Render images to PDF
Browse files Browse the repository at this point in the history
- Add in logging.
- Update CHANGELOG
- Reduce amount of extra space vertical used

  I had `line_spacing` set to 1.75 because I was going to use the program for
  proof-reading. That's fine, but because of how the program internals work,
  setting `line_spacing` to anything other than 1.0 causes pictures to take up
  and inordinate amount of space. I'll make it configurable from the command
  line later.

- Added some vertical space beneath headers, so that the characters
  wouldn't collide with regular text.
- Update to latest version of `printpdf`
  • Loading branch information
leroycep committed Sep 20, 2018
1 parent a3316b1 commit 849a0ea
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 105 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rendering of block quotes
- Page breaks when an html element contains `style="page-break-after:always"`
- Stdin is used as input when `-` is passed to `mdproof`
- Rendering of images
143 changes: 46 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ printpdf = "*"
rusttype = "*"
failure = "*"
scraper = "*"
image = "*"
log = "*"
30 changes: 27 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#[macro_use]
extern crate failure;
extern crate image;
extern crate printpdf;
extern crate pulldown_cmark as cmark;
extern crate rusttype;
extern crate scraper;
#[macro_use]
extern crate log;

mod page;
mod pages;
mod resources;
mod section;
mod sectioner;
mod span;
mod util;

use cmark::*;
use failure::Error;
use printpdf::{Mm, PdfDocument, PdfDocumentReference};
use printpdf::{Image, Mm, PdfDocument, PdfDocumentReference};
use rusttype::{Font, Scale};

use pages::Pages;
use resources::Resources;
use sectioner::Sectioner;
use span::Span;
use std::path::PathBuf;

const REGULAR_FONT: &[u8] = include_bytes!("../assets/Noto_Sans/NotoSans-Regular.ttf");
const BOLD_FONT: &[u8] = include_bytes!("../assets/Noto_Sans/NotoSans-Bold.ttf");
Expand All @@ -29,6 +35,9 @@ const MONO_FONT: &[u8] = include_bytes!("../assets/Inconsolata/Inconsolata-Regul

#[derive(Debug)]
pub struct Config {
/// The path from which images will be loaded
pub resources_directory: PathBuf,

pub title: String,
pub first_layer_name: String,

Expand Down Expand Up @@ -72,6 +81,8 @@ impl Config {
impl Default for Config {
fn default() -> Self {
Config {
resources_directory: PathBuf::new(),

title: "mdproof".into(),
first_layer_name: "Layer 1".into(),

Expand All @@ -89,7 +100,7 @@ impl Default for Config {
h3_font_size: Scale::uniform(20.0),
h4_font_size: Scale::uniform(16.0),

line_spacing: 1.75, // Text height * LINE_SPACING
line_spacing: 1.0, // Text height * LINE_SPACING
list_indentation: Mm(10.0),
list_point_offset: Mm(5.0),
quote_indentation: Mm(20.0),
Expand All @@ -110,10 +121,11 @@ pub fn markdown_to_pdf(markdown: &str, cfg: &Config) -> Result<PdfDocumentRefere
let parser = Parser::new(&markdown);

let max_width = cfg.page_size.0 - cfg.margin.0 * 2.0;
let mut resources = Resources::new(cfg.resources_directory.clone());
let mut lines = Sectioner::new(max_width, cfg);

for event in parser {
lines.parse_event(event);
lines.parse_event(&mut resources, event);
}

let sections = lines.get_vec();
Expand Down Expand Up @@ -180,6 +192,18 @@ pub fn markdown_to_pdf(markdown: &str, cfg: &Config) -> Result<PdfDocumentRefere
current_layer.set_font(font, font_scale.y as i64);
current_layer.write_text(text, font);
}
Span::Image { path, .. } => {
let image = Image::try_from_image(resources.load_image(path)?)?;
image.add_to_layer(
current_layer.clone(),
Some(span.pos.0),
Some(span.pos.1),
None,
None,
None,
None,
);
}
Span::Rect { width, height } => {
use printpdf::{Line, Point};
let rect_points = vec![
Expand Down
1 change: 1 addition & 0 deletions src/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ impl<'collection> Pages<'collection> {
pub fn render_sections(&mut self, sections: &[Section<'collection>], start_x: Mm) {
let min_y = self.cfg.margin.1;
for section in sections {
trace!("rendering section: {:?}", section);
let height = section.min_step();
let delta_y = height * -self.cfg.line_spacing;
if self.current_y + delta_y < min_y {
Expand Down
37 changes: 37 additions & 0 deletions src/resources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use failure::Error;
use image::{self, DynamicImage};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

pub struct Resources {
root_path: PathBuf,
images: BTreeMap<PathBuf, DynamicImage>,
}

impl Resources {
pub fn new(root_path: PathBuf) -> Self {
Self {
root_path: root_path,
images: BTreeMap::new(),
}
}

pub fn load_image<P: AsRef<Path>>(&mut self, path: P) -> Result<&DynamicImage, Error> {
let filename = self.root_path.join(path);
if self.images.contains_key(&filename) {
let image = self
.images
.get(&filename)
.expect("BTreeMap said it contained the key");
Ok(image)
} else {
let image = image::open(&filename)?;
self.images.insert(filename.clone(), image);
let image = self
.images
.get(&filename)
.expect("I just inserted the key into the map");
Ok(image)
}
}
}
35 changes: 33 additions & 2 deletions src/sectioner.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use super::Config;
use cmark::{Event, Tag};
use image::GenericImage;
use printpdf::Mm;
use resources::Resources;
use rusttype::Scale;
use section::Section;
use span::{FontType, Span};
Expand All @@ -21,6 +23,7 @@ pub struct Sectioner<'collection> {
max_width: Mm,
subsection: Option<Box<Sectioner<'collection>>>,
pub is_code: bool,
is_alt_text: bool,
cfg: &'collection Config,
}

Expand All @@ -36,17 +39,22 @@ impl<'collection> Sectioner<'collection> {
max_width: max_width,
subsection: None,
is_code: false,
is_alt_text: false,
cfg: cfg,
}
}

pub fn parse_event(&mut self, event: Event) -> Option<SubsectionType> {
pub fn parse_event(
&mut self,
resources: &mut Resources,
event: Event,
) -> Option<SubsectionType> {
if self.subsection.is_some() {
let mut subsection = self
.subsection
.take()
.expect("Checked if the subsection was `Some`");
if let Some(sub_type) = subsection.parse_event(event) {
if let Some(sub_type) = subsection.parse_event(resources, event) {
let section = match sub_type {
SubsectionType::List => Section::list_item(subsection.get_vec()),
SubsectionType::Quote => Section::block_quote(subsection.get_vec()),
Expand Down Expand Up @@ -77,6 +85,7 @@ impl<'collection> Sectioner<'collection> {
Event::End(Tag::Header(_)) => {
self.current_scale = self.cfg.default_font_size;
self.new_line();
self.push_space();
}

Event::Start(Tag::List(_)) => self.new_line(),
Expand All @@ -99,6 +108,8 @@ impl<'collection> Sectioner<'collection> {
}
Event::End(Tag::BlockQuote) => return Some(SubsectionType::Quote),

Event::Text(ref _text) if self.is_alt_text => {}

Event::Text(ref text) if self.is_code => {
let mut start = 0;
for (pos, c) in text.char_indices() {
Expand Down Expand Up @@ -129,6 +140,26 @@ impl<'collection> Sectioner<'collection> {
}
}

Event::Start(Tag::Image(url, _title)) => {
// TODO: Use title, and ignore alt-text
// Or should alt-text always be used?
if let Ok(image) = resources.load_image(url.clone().into_owned()) {
let (w, h) = image.dimensions();
let (w, h) = (
::printpdf::Px(w as usize).into_pt(300.0).into(),
::printpdf::Px(h as usize).into_pt(300.0).into(),
);
let span = Span::image(w, h, url.into_owned().into());
self.push_span(span);
self.is_alt_text = true;
} else {
warn!("Couldn't load image: {:?}", url);
}
}
Event::End(Tag::Image(_url, _title)) => {
self.is_alt_text = false;
}

Event::Start(Tag::Code) => self.current_font_type = self.current_font_type.mono(),
Event::End(Tag::Code) => self.current_font_type = self.current_font_type.unmono(),

Expand Down
24 changes: 21 additions & 3 deletions src/span.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use printpdf::{Mm, Pt};
use printpdf::Mm;
use rusttype::{Font, Scale};
use util::width_of_text;
use std::path::PathBuf;
use util::{font_height, width_of_text};

#[derive(PartialEq, Copy, Clone, Debug)]
pub enum FontType {
Expand Down Expand Up @@ -65,6 +66,11 @@ pub enum Span<'collection> {
font_type: FontType,
font_scale: Scale,
},
Image {
width: Mm,
height: Mm,
path: PathBuf,
},
Rect {
width: Mm,
height: Mm,
Expand All @@ -86,6 +92,14 @@ impl<'collection> Span<'collection> {
}
}

pub fn image(width: Mm, height: Mm, path: PathBuf) -> Self {
Span::Image {
width,
height,
path,
}
}

pub fn rect(width: Mm, height: Mm) -> Self {
Span::Rect { width, height }
}
Expand All @@ -98,13 +112,17 @@ impl<'collection> Span<'collection> {
font_scale,
..
} => width_of_text(&text, &font, *font_scale).into(),
Span::Image { width, .. } => width.clone(),
Span::Rect { width, .. } => width.clone(),
}
}

pub fn height(&self) -> Mm {
match self {
Span::Text { font_scale, .. } => Pt(font_scale.y as f64).into(),
Span::Text {
font, font_scale, ..
} => font_height(font, *font_scale).into(),
Span::Image { height, .. } => height.clone(),
Span::Rect { height, .. } => height.clone(),
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ pub fn width_of_text(text: &str, font: &Font, scale: Scale) -> Pt {
}).sum();
Pt(glyph_space_width * scale.x as f64 / units_per_em)
}

pub fn font_height(font: &Font, scale: Scale) -> Pt {
let v_metrics = font.v_metrics(scale);
let height = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) as f64;
Pt(height)
}

0 comments on commit 849a0ea

Please sign in to comment.