From bc997b7c3380d5f516f0aa58efc3dd513d75fafb Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 Nov 2020 20:01:54 +0100 Subject: [PATCH] =?UTF-8?q?Export=20images=20in=20PDF=20=F0=9F=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/src/bench.rs | 2 +- src/export/pdf.rs | 413 ++++++++++++++++++++++++------------------ src/layout/mod.rs | 4 +- src/library/insert.rs | 6 +- tests/README.md | 1 + tests/typeset.rs | 4 +- 6 files changed, 246 insertions(+), 184 deletions(-) diff --git a/bench/src/bench.rs b/bench/src/bench.rs index e004c5ced..bc13ed01c 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 c5d8d8716..af3ca0a68 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, - to_layout: Vec, + 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, + /// Backwards from the pdf indices to the old face ids. + fonts_to_layout: Vec, + /// 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 { - 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 pages = self.writer.pages(self.refs.page_tree); + pages.kids(self.refs.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 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); } - // 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) - { - let rect = Rect::new( - 0.0, - 0.0, - page.size.width.to_pt() as f32, - page.size.height.to_pt() as f32, - ); + 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 + self.refs.pages().zip(self.refs.contents()).zip(self.layouts) + { 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; + 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 mut buf = itoa::Buffer::new(); - text = text.tf( - Name(buf.format(1 + self.to_pdf[&shaped.face]).as_bytes()), - size.to_pt() as f32, - ); - } - - 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) - }); + // 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 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)); - } - } - }) - } + 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 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 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, Vec) { - let mut to_pdf = HashMap::new(); - let mut to_layout = vec![]; +struct Refs { + catalog: Ref, + page_tree: Ref, + pages_start: i32, + contents_start: i32, + fonts_start: i32, + images_start: i32, + end: i32, +} - // 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 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 { + (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); + fn contents(&self) -> impl Iterator { + (self.contents_start .. self.images_start).map(Ref::new) + } - Offsets { - catalog: Ref::new(catalog), - page_tree: Ref::new(page_tree), - pages, - contents, - fonts: font_offsets, + fn fonts(&self) -> impl Iterator { + (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 images(&self) -> impl Iterator { + (self.images_start .. self.end).map(Ref::new) } } - -fn ids((start, end): (i32, i32)) -> impl Iterator { - (start ..= end).map(Ref::new) -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 57add0442..28b278992 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 9fe719c7f..2904c9584 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, /// The fixed height, if any. diff --git a/tests/README.md b/tests/README.md index 3ae908af0..6ebba237b 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 38651766c..d9a1056a0 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;