summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-11-27 20:01:54 +0100
committerLaurenz <laurmaedje@gmail.com>2020-11-27 20:01:54 +0100
commitbc997b7c3380d5f516f0aa58efc3dd513d75fafb (patch)
tree7c16e93800f6e27bb90d87a1d2caefdfedc4e1de
parentb4f809f1ea8a469d0bdee225f47d7f09bc22aa61 (diff)
Export images in PDF 🖼
-rw-r--r--bench/src/bench.rs2
-rw-r--r--src/export/pdf.rs417
-rw-r--r--src/layout/mod.rs4
-rw-r--r--src/library/insert.rs6
-rw-r--r--tests/README.md1
-rw-r--r--tests/typeset.rs4
6 files changed, 248 insertions, 186 deletions
diff --git a/bench/src/bench.rs b/bench/src/bench.rs
index e004c5ce..bc13ed01 100644
--- a/bench/src/bench.rs
+++ b/bench/src/bench.rs
@@ -11,7 +11,7 @@ use typst::layout::layout;
use typst::parse::parse;
use typst::typeset;
-const FONT_DIR: &str = "fonts";
+const FONT_DIR: &str = "../fonts";
const COMA: &str = include_str!("../../tests/typ/coma.typ");
fn benchmarks(c: &mut Criterion) {
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index c5d8d871..af3ca0a6 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -3,8 +3,10 @@
use std::collections::HashMap;
use fontdock::FaceId;
+use image::RgbImage;
use pdf_writer::{
- CIDFontType, FontFlags, Name, PdfWriter, Rect, Ref, Str, SystemInfo, TextStream,
+ CidFontType, ColorSpace, Content, FontFlags, Name, PdfWriter, Rect, Ref, Str,
+ SystemInfo, UnicodeCmap,
};
use ttf_parser::{name_id, GlyphId};
@@ -31,136 +33,167 @@ struct PdfExporter<'a> {
/// which objects up-front to correctly declare the document catalogue, page
/// tree and so on. These offsets are computed in the beginning and stored
/// here.
- offsets: Offsets,
- // Font remapping, for more information see `remap_fonts`.
- to_pdf: HashMap<FaceId, usize>,
- to_layout: Vec<FaceId>,
+ refs: Refs,
+ /// We assign a new PDF-internal index to each used face.
+ /// There are two mappings:
+ /// Forwards from the old face ids to the new pdf indices.
+ fonts_to_pdf: HashMap<FaceId, usize>,
+ /// Backwards from the pdf indices to the old face ids.
+ fonts_to_layout: Vec<FaceId>,
+ /// The already visited images.
+ images: Vec<&'a RgbImage>,
+ /// The total number of images.
+ image_count: usize,
}
-struct Offsets {
- catalog: Ref,
- page_tree: Ref,
- pages: (i32, i32),
- contents: (i32, i32),
- fonts: (i32, i32),
-}
-
-const NUM_OBJECTS_PER_FONT: i32 = 5;
-
impl<'a> PdfExporter<'a> {
fn new(layouts: &'a [BoxLayout], loader: &'a FontLoader) -> Self {
- let (to_pdf, to_fontdock) = remap_fonts(layouts);
- let offsets = calculate_offsets(layouts.len(), to_pdf.len());
let mut writer = PdfWriter::new(1, 7);
writer.set_indent(2);
+ let mut fonts_to_pdf = HashMap::new();
+ let mut fonts_to_layout = vec![];
+ let mut image_count = 0;
+
+ for layout in layouts {
+ for (_, element) in &layout.elements {
+ match element {
+ LayoutElement::Text(shaped) => {
+ fonts_to_pdf.entry(shaped.face).or_insert_with(|| {
+ let next_id = fonts_to_layout.len();
+ fonts_to_layout.push(shaped.face);
+ next_id
+ });
+ }
+ LayoutElement::Image(_) => image_count += 1,
+ }
+ }
+ }
+
+ let refs = Refs::new(layouts.len(), fonts_to_pdf.len(), image_count);
+
Self {
writer,
layouts,
- offsets,
- to_pdf,
- to_layout: to_fontdock,
loader,
+ refs,
+ fonts_to_pdf,
+ fonts_to_layout,
+ images: vec![],
+ image_count,
}
}
fn write(mut self) -> Vec<u8> {
- self.write_preface();
+ self.write_structure();
self.write_pages();
self.write_fonts();
- self.writer.end(self.offsets.catalog)
+ self.write_images();
+ self.writer.finish(self.refs.catalog)
}
- fn write_preface(&mut self) {
+ fn write_structure(&mut self) {
// The document catalog.
- self.writer
- .catalog(self.offsets.catalog)
- .pages(self.offsets.page_tree);
+ self.writer.catalog(self.refs.catalog).pages(self.refs.page_tree);
// The root page tree.
- {
- let mut pages = self.writer.pages(self.offsets.page_tree);
- pages.kids(ids(self.offsets.pages));
-
- let mut resources = pages.resources();
- let mut fonts = resources.fonts();
- for i in 0 .. self.to_pdf.len() {
- let mut buf = itoa::Buffer::new();
- fonts.pair(
- Name(buf.format(1 + i as i32).as_bytes()),
- Ref::new(self.offsets.fonts.0 + NUM_OBJECTS_PER_FONT * i as i32),
- );
- }
+ let mut pages = self.writer.pages(self.refs.page_tree);
+ pages.kids(self.refs.pages());
+
+ let mut resources = pages.resources();
+ let mut fonts = resources.fonts();
+ for (refs, f) in self.refs.fonts().zip(0 .. self.fonts_to_pdf.len()) {
+ let name = format!("F{}", f);
+ fonts.pair(Name(name.as_bytes()), refs.type0_font);
}
+ drop(fonts);
+
+ let mut images = resources.x_objects();
+ for (id, im) in self.refs.images().zip(0 .. self.image_count) {
+ let name = format!("Im{}", im);
+ images.pair(Name(name.as_bytes()), id);
+ }
+
+ drop(images);
+ drop(resources);
+ drop(pages);
+
// The page objects (non-root nodes in the page tree).
- for ((page_id, content_id), page) in ids(self.offsets.pages)
- .zip(ids(self.offsets.contents))
- .zip(self.layouts)
+ for ((page_id, content_id), page) in
+ self.refs.pages().zip(self.refs.contents()).zip(self.layouts)
{
- let rect = Rect::new(
- 0.0,
- 0.0,
- page.size.width.to_pt() as f32,
- page.size.height.to_pt() as f32,
- );
-
self.writer
.page(page_id)
- .parent(self.offsets.page_tree)
- .media_box(rect)
+ .parent(self.refs.page_tree)
+ .media_box(Rect::new(
+ 0.0,
+ 0.0,
+ page.size.width.to_pt() as f32,
+ page.size.height.to_pt() as f32,
+ ))
.contents(content_id);
}
}
fn write_pages(&mut self) {
- for (id, page) in ids(self.offsets.contents).zip(self.layouts) {
+ for (id, page) in self.refs.contents().zip(self.layouts) {
self.write_page(id, &page);
}
}
- fn write_page(&mut self, id: Ref, page: &BoxLayout) {
- let mut text = TextStream::new();
+ fn write_page(&mut self, id: Ref, page: &'a BoxLayout) {
+ let mut content = Content::new();
- // Font switching actions are only written when the face used for
- // shaped text changes. Hence, we need to remember the active face.
+ // We only write font switching actions when the used face changes. To
+ // do that, we need to remember the active face.
let mut face = FaceId::MAX;
let mut size = Length::ZERO;
+ let mut text = content.text();
for (pos, element) in &page.elements {
- match element {
- LayoutElement::Text(shaped) => {
- // Check if we need to issue a font switching action.
- if shaped.face != face || shaped.font_size != size {
- face = shaped.face;
- size = shaped.font_size;
-
- let mut buf = itoa::Buffer::new();
- text = text.tf(
- Name(buf.format(1 + self.to_pdf[&shaped.face]).as_bytes()),
- size.to_pt() as f32,
- );
- }
+ if let LayoutElement::Text(shaped) = element {
+ // Check if we need to issue a font switching action.
+ if shaped.face != face || shaped.font_size != size {
+ face = shaped.face;
+ size = shaped.font_size;
- let x = pos.x.to_pt();
- let y = (page.size.height - pos.y - size).to_pt();
- text = text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32);
- text = text.tj(&shaped.encode_glyphs_be());
+ let name = format!("F{}", self.fonts_to_pdf[&shaped.face]);
+ text.font(Name(name.as_bytes()), size.to_pt() as f32);
}
- LayoutElement::Image(_image) => {
- // TODO: Write image.
- }
+ let x = pos.x.to_pt() as f32;
+ let y = (page.size.height - pos.y - size).to_pt() as f32;
+ text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
+ text.show(&shaped.encode_glyphs_be());
}
}
- self.writer.stream(id, &text.end());
+ drop(text);
+
+ for (pos, element) in &page.elements {
+ if let LayoutElement::Image(image) = element {
+ let name = format!("Im{}", self.images.len());
+ let size = image.size;
+ let x = pos.x.to_pt() as f32;
+ let y = (page.size.height - pos.y - size.height).to_pt() as f32;
+ let w = size.width.to_pt() as f32;
+ let h = size.height.to_pt() as f32;
+
+ content.save_state();
+ content.matrix(w, 0.0, 0.0, h, x, y);
+ content.x_object(Name(name.as_bytes()));
+ content.restore_state();
+
+ self.images.push(&image.buf);
+ }
+ }
+
+ self.writer.stream(id, &content.finish());
}
fn write_fonts(&mut self) {
- let mut id = self.offsets.fonts.0;
-
- for &face_id in &self.to_layout {
+ for (refs, &face_id) in self.refs.fonts().zip(&self.fonts_to_layout) {
let owned_face = self.loader.get_loaded(face_id);
let face = owned_face.get();
@@ -169,151 +202,179 @@ impl<'a> PdfExporter<'a> {
.find(|entry| {
entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode()
})
- .map(|entry| entry.to_string())
- .flatten()
+ .and_then(|entry| entry.to_string())
.unwrap_or_else(|| "unknown".to_string());
let base_font = format!("ABCDEF+{}", name);
let base_font = Name(base_font.as_bytes());
+ let cmap_name = Name(b"Custom");
let system_info = SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"Identity"),
supplement: 0,
};
- let units_per_em = face.units_per_em().unwrap_or(1000) as f32;
- let ratio = 1.0 / units_per_em;
- let to_glyph_unit = |font_unit: f32| (1000.0 * ratio * font_unit).round();
-
- let global_bbox = face.global_bounding_box();
- let bbox = Rect::new(
- to_glyph_unit(global_bbox.x_min as f32),
- to_glyph_unit(global_bbox.y_min as f32),
- to_glyph_unit(global_bbox.x_max as f32),
- to_glyph_unit(global_bbox.y_max as f32),
- );
-
- let monospace = face.is_monospaced();
- let italic = face.is_italic();
- let italic_angle = face.italic_angle().unwrap_or(0.0);
- let ascender = face.typographic_ascender().unwrap_or(0);
- let descender = face.typographic_descender().unwrap_or(0);
- let cap_height = face.capital_height().unwrap_or(ascender);
- let stem_v = 10.0 + 0.244 * (face.weight().to_number() as f32 - 50.0);
-
let mut flags = FontFlags::empty();
flags.set(FontFlags::SERIF, name.contains("Serif"));
- flags.set(FontFlags::FIXED_PITCH, monospace);
- flags.set(FontFlags::ITALIC, italic);
+ flags.set(FontFlags::FIXED_PITCH, face.is_monospaced());
+ flags.set(FontFlags::ITALIC, face.is_italic());
flags.insert(FontFlags::SYMBOLIC);
flags.insert(FontFlags::SMALL_CAP);
- let num_glyphs = face.number_of_glyphs();
- let widths = (0 .. num_glyphs).map(|g| {
- to_glyph_unit(face.glyph_hor_advance(GlyphId(g)).unwrap_or(0) as f32)
- });
-
- let mut mapping = vec![];
- for subtable in face.character_mapping_subtables() {
- subtable.codepoints(|n| {
- if let Some(c) = std::char::from_u32(n) {
- if let Some(g) = face.glyph_index(c) {
- mapping.push((g.0, c));
- }
- }
- })
- }
+ // Convert from OpenType font units to PDF glyph units.
+ let em_per_unit = 1.0 / face.units_per_em().unwrap_or(1000) as f32;
+ let convert = |font_unit: f32| (1000.0 * em_per_unit * font_unit).round();
+ let convert_i16 = |font_unit: i16| convert(font_unit as f32);
+ let convert_u16 = |font_unit: u16| convert(font_unit as f32);
- let type0_font_id = Ref::new(id);
- let cid_font_id = Ref::new(id + 1);
- let font_descriptor_id = Ref::new(id + 2);
- let cmap_id = Ref::new(id + 3);
- let data_id = Ref::new(id + 4);
+ let global_bbox = face.global_bounding_box();
+ let bbox = Rect::new(
+ convert_i16(global_bbox.x_min),
+ convert_i16(global_bbox.y_min),
+ convert_i16(global_bbox.x_max),
+ convert_i16(global_bbox.y_max),
+ );
+
+ let italic_angle = face.italic_angle().unwrap_or(0.0);
+ let ascender = convert_i16(face.typographic_ascender().unwrap_or(0));
+ let descender = convert_i16(face.typographic_descender().unwrap_or(0));
+ let cap_height = face.capital_height().map(convert_i16);
+ let stem_v = 10.0 + 0.244 * (f32::from(face.weight().to_number()) - 50.0);
// Write the base font object referencing the CID font.
self.writer
- .type0_font(type0_font_id)
+ .type0_font(refs.type0_font)
.base_font(base_font)
.encoding_predefined(Name(b"Identity-H"))
- .descendant_font(cid_font_id)
- .to_unicode(cmap_id);
+ .descendant_font(refs.cid_font)
+ .to_unicode(refs.cmap);
// Write the CID font referencing the font descriptor.
self.writer
- .cid_font(cid_font_id, CIDFontType::Type2)
+ .cid_font(refs.cid_font, CidFontType::Type2)
.base_font(base_font)
.system_info(system_info)
- .font_descriptor(font_descriptor_id)
+ .font_descriptor(refs.font_descriptor)
.widths()
- .individual(0, widths);
+ .individual(0, {
+ let num_glyphs = face.number_of_glyphs();
+ (0 .. num_glyphs).map(|g| {
+ let advance = face.glyph_hor_advance(GlyphId(g));
+ convert_u16(advance.unwrap_or(0))
+ })
+ });
// Write the font descriptor (contains metrics about the font).
self.writer
- .font_descriptor(font_descriptor_id)
+ .font_descriptor(refs.font_descriptor)
.font_name(base_font)
.font_flags(flags)
.font_bbox(bbox)
.italic_angle(italic_angle)
- .ascent(to_glyph_unit(ascender as f32))
- .descent(to_glyph_unit(descender as f32))
- .cap_height(to_glyph_unit(cap_height as f32))
+ .ascent(ascender)
+ .descent(descender)
+ .cap_height(cap_height.unwrap_or(ascender))
.stem_v(stem_v)
- .font_file2(data_id);
+ .font_file2(refs.data);
- // Write the CMap, which maps glyph ids back to unicode codepoints
- // to enable copying out of the PDF.
- self.writer.cmap(cmap_id, Name(b"Custom"), system_info, mapping);
+ // Write the to-unicode character map, which maps glyph ids back to
+ // unicode codepoints to enable copying out of the PDF.
+ self.writer
+ .cmap_stream(refs.cmap, &{
+ let mut cmap = UnicodeCmap::new(cmap_name, system_info);
+ for subtable in face.character_mapping_subtables() {
+ subtable.codepoints(|n| {
+ if let Some(c) = std::char::from_u32(n) {
+ if let Some(g) = face.glyph_index(c) {
+ cmap.pair(g.0, c);
+ }
+ }
+ })
+ }
+ cmap.finish()
+ })
+ .name(cmap_name)
+ .system_info(system_info);
// Write the face's bytes.
- self.writer.stream(data_id, owned_face.data());
+ self.writer.stream(refs.data, owned_face.data());
+ }
+ }
- id += NUM_OBJECTS_PER_FONT;
+ fn write_images(&mut self) {
+ for (id, image) in self.refs.images().zip(&self.images) {
+ self.writer
+ .image_stream(id, &image.as_raw())
+ .width(image.width() as i32)
+ .height(image.height() as i32)
+ .color_space(ColorSpace::DeviceRGB)
+ .bits_per_component(8);
}
}
}
-/// Assigns a new PDF-internal index to each used face and returns two mappings:
-/// - Forwards from the old face ids to the new pdf indices (hash map)
-/// - Backwards from the pdf indices to the old face ids (vec)
-fn remap_fonts(layouts: &[BoxLayout]) -> (HashMap<FaceId, usize>, Vec<FaceId>) {
- let mut to_pdf = HashMap::new();
- let mut to_layout = vec![];
-
- // We want to find out which font faces are used at all. To do that, look at
- // each text element to find out which face is uses.
- for layout in layouts {
- for (_, element) in &layout.elements {
- if let LayoutElement::Text(shaped) = element {
- to_pdf.entry(shaped.face).or_insert_with(|| {
- let next_id = to_layout.len();
- to_layout.push(shaped.face);
- next_id
- });
- }
+struct Refs {
+ catalog: Ref,
+ page_tree: Ref,
+ pages_start: i32,
+ contents_start: i32,
+ fonts_start: i32,
+ images_start: i32,
+ end: i32,
+}
+
+struct FontRefs {
+ type0_font: Ref,
+ cid_font: Ref,
+ font_descriptor: Ref,
+ cmap: Ref,
+ data: Ref,
+}
+
+impl Refs {
+ const OBJECTS_PER_FONT: usize = 5;
+
+ fn new(layouts: usize, fonts: usize, images: usize) -> Self {
+ let catalog = 1;
+ let page_tree = catalog + 1;
+ let pages_start = page_tree + 1;
+ let contents_start = pages_start + layouts as i32;
+ let fonts_start = contents_start + layouts as i32;
+ let images_start = fonts_start + (Self::OBJECTS_PER_FONT * fonts) as i32;
+ let end = images_start + images as i32;
+
+ Self {
+ catalog: Ref::new(catalog),
+ page_tree: Ref::new(page_tree),
+ pages_start,
+ contents_start,
+ fonts_start,
+ images_start,
+ end,
}
}
- (to_pdf, to_layout)
-}
+ fn pages(&self) -> impl Iterator<Item = Ref> {
+ (self.pages_start .. self.contents_start).map(Ref::new)
+ }
-/// We need to know in advance which ids to use for which objects to
-/// cross-reference them. Therefore, we calculate the indices in the beginning.
-fn calculate_offsets(layout_count: usize, font_count: usize) -> Offsets {
- let catalog = 1;
- let page_tree = catalog + 1;
- let pages = (page_tree + 1, page_tree + layout_count as i32);
- let contents = (pages.1 + 1, pages.1 + layout_count as i32);
- let font_offsets = (contents.1 + 1, contents.1 + 5 * font_count as i32);
-
- Offsets {
- catalog: Ref::new(catalog),
- page_tree: Ref::new(page_tree),
- pages,
- contents,
- fonts: font_offsets,
+ fn contents(&self) -> impl Iterator<Item = Ref> {
+ (self.contents_start .. self.images_start).map(Ref::new)
+ }
+
+ fn fonts(&self) -> impl Iterator<Item = FontRefs> {
+ (self.fonts_start .. self.images_start)
+ .step_by(Self::OBJECTS_PER_FONT)
+ .map(|id| FontRefs {
+ type0_font: Ref::new(id),
+ cid_font: Ref::new(id + 1),
+ font_descriptor: Ref::new(id + 2),
+ cmap: Ref::new(id + 3),
+ data: Ref::new(id + 4),
+ })
}
-}
-fn ids((start, end): (i32, i32)) -> impl Iterator<Item = Ref> {
- (start ..= end).map(Ref::new)
+ fn images(&self) -> impl Iterator<Item = Ref> {
+ (self.images_start .. self.end).map(Ref::new)
+ }
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 57add044..28b27899 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -8,7 +8,7 @@ mod spacing;
mod stack;
mod text;
-use image::RgbaImage;
+use image::RgbImage;
use crate::font::SharedFontLoader;
use crate::geom::*;
@@ -185,7 +185,7 @@ pub enum LayoutElement {
#[derive(Debug, Clone, PartialEq)]
pub struct ImageElement {
/// The image.
- pub buf: RgbaImage,
+ pub buf: RgbImage,
/// The document size of the image.
pub size: Size,
}
diff --git a/src/library/insert.rs b/src/library/insert.rs
index 9fe719c7..2904c958 100644
--- a/src/library/insert.rs
+++ b/src/library/insert.rs
@@ -3,7 +3,7 @@ use std::fs::File;
use std::io::BufReader;
use image::io::Reader;
-use image::RgbaImage;
+use image::RgbImage;
use crate::layout::*;
use crate::prelude::*;
@@ -25,7 +25,7 @@ pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value {
.with_guessed_format()
.map_err(|err| err.into())
.and_then(|reader| reader.decode())
- .map(|img| img.into_rgba8())
+ .map(|img| img.into_rgb8())
{
Ok(buf) => {
ctx.push(Image {
@@ -49,7 +49,7 @@ pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value {
#[derive(Clone, PartialEq)]
struct Image {
/// The image.
- buf: RgbaImage,
+ buf: RgbImage,
/// The fixed width, if any.
width: Option<Linear>,
/// The fixed height, if any.
diff --git a/tests/README.md b/tests/README.md
index 3ae908af..6ebba237 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -5,3 +5,4 @@
- `png`: PNG files produced by tests
- `ref`: Reference images which the PNGs are compared to byte-wise to determine
whether the test passed or failed
+- `res`: Resource files used by tests
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 38651766..d9a1056a 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -247,8 +247,8 @@ fn draw_text(canvas: &mut Canvas, loader: &FontLoader, shaped: &Shaped, pos: Poi
fn draw_image(canvas: &mut Canvas, image: &ImageElement, pos: Point) {
let mut pixmap = Pixmap::new(image.buf.width(), image.buf.height()).unwrap();
for (src, dest) in image.buf.pixels().zip(pixmap.pixels_mut()) {
- let [r, g, b, a] = src.0;
- *dest = ColorU8::from_rgba(r, g, b, a).premultiply();
+ let [r, g, b] = src.0;
+ *dest = ColorU8::from_rgba(r, g, b, 255).premultiply();
}
let view_width = image.size.width.to_pt() as f32;