diff options
| author | Ana Gelez <ana@gelez.xyz> | 2024-04-17 17:11:20 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-04-17 15:11:20 +0000 |
| commit | 21c78abd6eecd0f6b3208405c7513be3bbd8991c (patch) | |
| tree | fe960daf42372202ed37138c6a12ad80711f757e /crates/typst-pdf | |
| parent | 4c99d6c8b38c23222fabf2465dbc030d08b6283f (diff) | |
Emojis in PDF (#3853)
Diffstat (limited to 'crates/typst-pdf')
| -rw-r--r-- | crates/typst-pdf/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-pdf/src/font.rs | 213 | ||||
| -rw-r--r-- | crates/typst-pdf/src/lib.rs | 115 | ||||
| -rw-r--r-- | crates/typst-pdf/src/page.rs | 240 |
4 files changed, 464 insertions, 105 deletions
diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index 99c52dc6..d2dcd5f5 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -22,6 +22,7 @@ bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } +indexmap = { workspace = true } miniz_oxide = { workspace = true } once_cell = { workspace = true } pdf-writer = { workspace = true } diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index 0f8b5ba0..e4b83f1d 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -3,13 +3,16 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; +use pdf_writer::writers::FontDescriptor; use pdf_writer::{Filter, Finish, Name, Rect, Str}; use ttf_parser::{name_id, GlyphId, Tag}; +use typst::layout::{Abs, Em, Ratio, Transform}; use typst::text::Font; use typst::util::SliceExt; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; -use crate::{deflate, EmExt, PdfContext}; +use crate::page::{write_frame, PageContext}; +use crate::{deflate, AbsExt, EmExt, PdfContext}; const CFF: Tag = Tag::from_bytes(b"CFF "); const CFF2: Tag = Tag::from_bytes(b"CFF2"); @@ -23,6 +26,8 @@ const SYSTEM_INFO: SystemInfo = SystemInfo { /// Embed all used fonts into the PDF. #[typst_macros::time(name = "write fonts")] pub(crate) fn write_fonts(ctx: &mut PdfContext) { + write_color_fonts(ctx); + for font in ctx.font_map.items() { let type0_ref = ctx.alloc.bump(); let cid_ref = ctx.alloc.bump(); @@ -32,7 +37,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { ctx.font_refs.push(type0_ref); let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); - let metrics = font.metrics(); let ttf = font.ttf(); // Do we have a TrueType or CFF font? @@ -103,47 +107,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { width_writer.finish(); cid.finish(); - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref); - font_descriptor - .name(Name(base_font.as_bytes())) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - if is_cff { - font_descriptor.font_file3(data_ref); - } else { - font_descriptor.font_file2(data_ref); - } - - font_descriptor.finish(); - // Write the /ToUnicode character map, which maps glyph ids back to // unicode codepoints to enable copying out of the PDF. let cmap = create_cmap(font, glyph_set); @@ -160,9 +123,173 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { } stream.finish(); + + let mut font_descriptor = + write_font_descriptor(&mut ctx.pdf, descriptor_ref, font, &base_font); + if is_cff { + font_descriptor.font_file3(data_ref); + } else { + font_descriptor.font_file2(data_ref); + } + } +} + +/// Writes color fonts as Type3 fonts +fn write_color_fonts(ctx: &mut PdfContext) { + let color_font_map = ctx.color_font_map.take_map(); + for (font, color_font) in color_font_map { + // For each Type3 font that is part of this family… + for (font_index, subfont_id) in color_font.refs.iter().enumerate() { + // Allocate some IDs. + let cmap_ref = ctx.alloc.bump(); + let descriptor_ref = ctx.alloc.bump(); + let widths_ref = ctx.alloc.bump(); + // And a map between glyph IDs and the instructions to draw this + // glyph. + let mut glyphs_to_instructions = Vec::new(); + + let start = font_index * 256; + let end = (start + 256).min(color_font.glyphs.len()); + let glyph_count = end - start; + let subset = &color_font.glyphs[start..end]; + let mut widths = Vec::new(); + + let scale_factor = font.ttf().units_per_em() as f32; + + // Write the instructions for each glyph. + for color_glyph in subset { + let instructions_stream_ref = ctx.alloc.bump(); + let width = + font.advance(color_glyph.gid).unwrap_or(Em::new(0.0)).to_font_units(); + widths.push(width); + // Create a fake page context for `write_frame`. We are only + // interested in the contents of the page. + let size = color_glyph.frame.size(); + let mut page_ctx = PageContext::new(ctx, size); + page_ctx.bottom = size.y.to_f32(); + page_ctx.content.start_color_glyph(width); + page_ctx.transform( + // Make the Y axis go upwards, while preserving aspect ratio + Transform::scale(Ratio::one(), -size.aspect_ratio()) + // Also move the origin to the top left corner + .post_concat(Transform::translate(Abs::zero(), size.y)), + ); + write_frame(&mut page_ctx, &color_glyph.frame); + + // Retrieve the stream of the page and write it. + let stream = page_ctx.content.finish(); + ctx.pdf.stream(instructions_stream_ref, &stream); + + // Use this stream as instructions to draw the glyph. + glyphs_to_instructions.push(instructions_stream_ref); + } + + // Write the Type3 font object. + let mut pdf_font = ctx.pdf.type3_font(*subfont_id); + pdf_font.pair(Name(b"Resources"), ctx.type3_font_resources_ref); + pdf_font.bbox(color_font.bbox); + pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]); + pdf_font.first_char(0); + pdf_font.last_char((glyph_count - 1) as u8); + pdf_font.pair(Name(b"Widths"), widths_ref); + pdf_font.to_unicode(cmap_ref); + pdf_font.font_descriptor(descriptor_ref); + + // Write the /CharProcs dictionary, that maps glyph names to + // drawing instructions. + let mut char_procs = pdf_font.char_procs(); + for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() { + char_procs + .pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref); + } + char_procs.finish(); + + // Write the /Encoding dictionary. + let names = (0..glyph_count) + .map(|gid| eco_format!("glyph{gid}")) + .collect::<Vec<_>>(); + pdf_font + .encoding_custom() + .differences() + .consecutive(0, names.iter().map(|name| Name(name.as_bytes()))); + pdf_font.finish(); + + // Encode a CMAP to make it possible to search or copy glyphs. + let glyph_set = ctx.glyph_sets.get_mut(&font).unwrap(); + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + for (index, glyph) in subset.iter().enumerate() { + let Some(text) = glyph_set.get(&glyph.gid) else { + continue; + }; + + if !text.is_empty() { + cmap.pair_with_multiple(index as u8, text.chars()); + } + } + ctx.pdf.cmap(cmap_ref, &cmap.finish()); + + // Write the font descriptor. + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + let base_font = eco_format!("COLOR{font_index:x}+{postscript_name}"); + write_font_descriptor(&mut ctx.pdf, descriptor_ref, &font, &base_font); + + // Write the widths array + ctx.pdf.indirect(widths_ref).array().items(widths); + } } } +/// Writes a FontDescriptor dictionary. +fn write_font_descriptor<'a>( + pdf: &'a mut pdf_writer::Pdf, + descriptor_ref: pdf_writer::Ref, + font: &'a Font, + base_font: &EcoString, +) -> FontDescriptor<'a> { + let ttf = font.ttf(); + let metrics = font.metrics(); + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let global_bbox = ttf.global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = metrics.ascender.to_font_units(); + let descender = metrics.descender.to_font_units(); + let cap_height = metrics.cap_height.to_font_units(); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); + + // Write the font descriptor (contains metrics about the font). + let mut font_descriptor = pdf.font_descriptor(descriptor_ref); + font_descriptor + .name(Name(base_font.as_bytes())) + .flags(flags) + .bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v); + + font_descriptor +} + /// Subset a font to the given glyphs. /// /// - For a font with TrueType outlines, this returns the whole OpenType font. diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index e8b1c30a..c55abcb0 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -15,13 +15,15 @@ use std::sync::Arc; use base64::Engine; use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; use pdf_writer::types::Direction; use pdf_writer::writers::Destination; -use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; +use pdf_writer::{Finish, Name, Pdf, Rect, Ref, Str, TextStr}; use typst::foundations::{Datetime, Label, NativeElement, Smart}; use typst::introspection::Location; -use typst::layout::{Abs, Dir, Em, Transform}; +use typst::layout::{Abs, Dir, Em, Frame, Transform}; use typst::model::{Document, HeadingElem}; +use typst::text::color::frame_for_glyph; use typst::text::{Font, Lang}; use typst::util::Deferred; use typst::visualize::Image; @@ -68,6 +70,7 @@ pub fn pdf( pattern::write_patterns(&mut ctx); write_named_destinations(&mut ctx); page::write_page_tree(&mut ctx); + page::write_global_resources(&mut ctx); write_catalog(&mut ctx, ident, timestamp); ctx.pdf.finish() } @@ -96,6 +99,15 @@ struct PdfContext<'a> { alloc: Ref, /// The ID of the page tree. page_tree_ref: Ref, + /// The ID of the globally shared Resources dictionary. + global_resources_ref: Ref, + /// The ID of the resource dictionary shared by Type3 fonts. + /// + /// Type3 fonts cannot use the global resources, as it would create some + /// kind of infinite recursion (they are themselves present in that + /// dictionary), which Acrobat doesn't appreciate (it fails to parse the + /// font) even if the specification seems to allow it. + type3_font_resources_ref: Ref, /// The IDs of written pages. page_refs: Vec<Ref>, /// The IDs of written fonts. @@ -123,6 +135,8 @@ struct PdfContext<'a> { pattern_map: Remapper<PdfPattern>, /// Deduplicates external graphics states used across the document. extg_map: Remapper<ExtGState>, + /// Deduplicates color glyphs. + color_font_map: ColorFontMap, /// A sorted list of all named destinations. dests: Vec<(Label, Ref)>, @@ -134,6 +148,8 @@ impl<'a> PdfContext<'a> { fn new(document: &'a Document) -> Self { let mut alloc = Ref::new(1); let page_tree_ref = alloc.bump(); + let global_resources_ref = alloc.bump(); + let type3_font_resources_ref = alloc.bump(); Self { document, pdf: Pdf::new(), @@ -142,6 +158,8 @@ impl<'a> PdfContext<'a> { languages: BTreeMap::new(), alloc, page_tree_ref, + global_resources_ref, + type3_font_resources_ref, page_refs: vec![], font_refs: vec![], image_refs: vec![], @@ -155,6 +173,7 @@ impl<'a> PdfContext<'a> { gradient_map: Remapper::new(), pattern_map: Remapper::new(), extg_map: Remapper::new(), + color_font_map: ColorFontMap::new(), dests: vec![], loc_to_dest: HashMap::new(), } @@ -455,6 +474,98 @@ where } } +/// A mapping between `Font`s and all the corresponding `ColorFont`s. +/// +/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3 +/// font, and fonts generally have more color glyphs than that. +struct ColorFontMap { + /// The mapping itself + map: IndexMap<Font, ColorFont>, + /// A list of all PDF indirect references to Type3 font objects. + all_refs: Vec<Ref>, +} + +/// A collection of Type3 font, belonging to the same TTF font. +struct ColorFont { + /// A list of references to Type3 font objects for this font family. + refs: Vec<Ref>, + /// The list of all color glyphs in this family. + /// + /// The index in this vector modulo 256 corresponds to the index in one of + /// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the + /// quotient of the index divided by 256). + glyphs: Vec<ColorGlyph>, + /// The global bounding box of the font. + bbox: Rect, + /// A mapping between glyph IDs and character indices in the `glyphs` + /// vector. + glyph_indices: HashMap<u16, usize>, +} + +/// A single color glyph. +struct ColorGlyph { + /// The ID of the glyph. + gid: u16, + /// A frame that contains the glyph. + frame: Frame, +} + +impl ColorFontMap { + /// Creates a new empty mapping + fn new() -> Self { + Self { map: IndexMap::new(), all_refs: Vec::new() } + } + + /// Takes the contents of the mapping. + /// + /// After calling this function, the mapping will be empty. + fn take_map(&mut self) -> IndexMap<Font, ColorFont> { + std::mem::take(&mut self.map) + } + + /// Obtains the reference to a Type3 font, and an index in this font + /// that can be used to draw a color glyph. + /// + /// The glyphs will be de-duplicated if needed. + fn get(&mut self, alloc: &mut Ref, font: &Font, gid: u16) -> (Ref, u8) { + let color_font = self.map.entry(font.clone()).or_insert_with(|| { + let global_bbox = font.ttf().global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + ColorFont { + bbox, + refs: Vec::new(), + glyphs: Vec::new(), + glyph_indices: HashMap::new(), + } + }); + + if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) { + // If we already know this glyph, return it. + (color_font.refs[index_of_glyph / 256], *index_of_glyph as u8) + } else { + // Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font + // if needed + let index = color_font.glyphs.len(); + if index % 256 == 0 { + let new_ref = alloc.bump(); + self.all_refs.push(new_ref); + color_font.refs.push(new_ref); + } + + let instructions = frame_for_glyph(font, gid); + color_font.glyphs.push(ColorGlyph { gid, frame: instructions }); + color_font.glyph_indices.insert(gid, index); + + (color_font.refs[index / 256], index as u8) + } + } +} + /// Additional methods for [`Abs`]. trait AbsExt { /// Convert an to a number of points. diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 42358db5..621ac91f 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -1,6 +1,10 @@ use std::collections::HashMap; use std::num::NonZeroUsize; +use crate::color::PaintEncode; +use crate::extg::ExtGState; +use crate::image::deferred_image; +use crate::{deflate_deferred, AbsExt, EmExt, PdfContext}; use ecow::{eco_format, EcoString}; use pdf_writer::types::{ ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle, @@ -13,17 +17,13 @@ use typst::layout::{ Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform, }; use typst::model::{Destination, Numbering}; -use typst::text::{Case, Font, TextItem}; -use typst::util::{Deferred, Numeric}; +use typst::text::color::is_color_glyph; +use typst::text::{Case, Font, TextItem, TextItemView}; +use typst::util::{Deferred, Numeric, SliceExt}; use typst::visualize::{ FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, }; -use crate::color::PaintEncode; -use crate::extg::ExtGState; -use crate::image::deferred_image; -use crate::{deflate_deferred, AbsExt, EmExt, PdfContext}; - /// Construct page objects. #[typst_macros::time(name = "construct pages")] pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) { @@ -44,17 +44,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod let page_ref = ctx.alloc.bump(); let size = frame.size(); - let mut ctx = PageContext { - parent: ctx, - page_ref, - uses_opacities: false, - content: Content::new(), - state: State::new(size), - saves: vec![], - bottom: 0.0, - links: vec![], - resources: HashMap::default(), - }; + let mut ctx = PageContext::new(ctx, size); // Make the coordinate system start at the top-left. ctx.bottom = size.y.to_f32(); @@ -73,7 +63,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod let page = EncodedPage { size, content: deflate_deferred(ctx.content.finish()), - id: ctx.page_ref, + id: page_ref, uses_opacities: ctx.uses_opacities, links: ctx.links, label: None, @@ -85,10 +75,8 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod /// Write the page tree. pub(crate) fn write_page_tree(ctx: &mut PdfContext) { - let resources_ref = write_global_resources(ctx); - for i in 0..ctx.pages.len() { - write_page(ctx, i, resources_ref); + write_page(ctx, i); } ctx.pdf @@ -102,30 +90,20 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) { /// We add a reference to this dictionary to each page individually instead of /// to the root node of the page tree because using the resource inheritance /// feature breaks PDF merging with Apple Preview. -fn write_global_resources(ctx: &mut PdfContext) -> Ref { - let resource_ref = ctx.alloc.bump(); - - let mut resources = ctx.pdf.indirect(resource_ref).start::<Resources>(); - ctx.colors - .write_color_spaces(resources.color_spaces(), &mut ctx.alloc); - - let mut fonts = resources.fonts(); - for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { - let name = eco_format!("F{}", f); - fonts.pair(Name(name.as_bytes()), font_ref); - } - - fonts.finish(); +pub(crate) fn write_global_resources(ctx: &mut PdfContext) { + let images_ref = ctx.alloc.bump(); + let patterns_ref = ctx.alloc.bump(); + let ext_gs_states_ref = ctx.alloc.bump(); + let color_spaces_ref = ctx.alloc.bump(); - let mut images = resources.x_objects(); + let mut images = ctx.pdf.indirect(images_ref).dict(); for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { let name = eco_format!("Im{}", im); images.pair(Name(name.as_bytes()), image_ref); } - images.finish(); - let mut patterns = resources.patterns(); + let mut patterns = ctx.pdf.indirect(patterns_ref).dict(); for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) { let name = eco_format!("Gr{}", gr); patterns.pair(Name(name.as_bytes()), gradient_ref); @@ -135,26 +113,64 @@ fn write_global_resources(ctx: &mut PdfContext) -> Ref { let name = eco_format!("P{}", p); patterns.pair(Name(name.as_bytes()), pattern_ref); } - patterns.finish(); - let mut ext_gs_states = resources.ext_g_states(); + let mut ext_gs_states = ctx.pdf.indirect(ext_gs_states_ref).dict(); for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) { let name = eco_format!("Gs{}", gs); ext_gs_states.pair(Name(name.as_bytes()), gs_ref); } ext_gs_states.finish(); + let color_spaces = ctx.pdf.indirect(color_spaces_ref).dict(); + ctx.colors.write_color_spaces(color_spaces, &mut ctx.alloc); + + let mut resources = ctx.pdf.indirect(ctx.global_resources_ref).start::<Resources>(); + resources.pair(Name(b"XObject"), images_ref); + resources.pair(Name(b"Pattern"), patterns_ref); + resources.pair(Name(b"ExtGState"), ext_gs_states_ref); + resources.pair(Name(b"ColorSpace"), color_spaces_ref); + + let mut fonts = resources.fonts(); + for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { + let name = eco_format!("F{}", f); + fonts.pair(Name(name.as_bytes()), font_ref); + } + + for font in &ctx.color_font_map.all_refs { + let name = eco_format!("Cf{}", font.get()); + fonts.pair(Name(name.as_bytes()), font); + } + fonts.finish(); + resources.finish(); + // Also write the resources for Type3 fonts, that only contains images, + // color spaces and regular fonts (COLR glyphs depend on them). + if !ctx.color_font_map.all_refs.is_empty() { + let mut resources = + ctx.pdf.indirect(ctx.type3_font_resources_ref).start::<Resources>(); + resources.pair(Name(b"XObject"), images_ref); + resources.pair(Name(b"Pattern"), patterns_ref); + resources.pair(Name(b"ExtGState"), ext_gs_states_ref); + resources.pair(Name(b"ColorSpace"), color_spaces_ref); + + let mut fonts = resources.fonts(); + for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { + let name = eco_format!("F{}", f); + fonts.pair(Name(name.as_bytes()), font_ref); + } + fonts.finish(); + + resources.finish(); + } + // Write all of the functions used by the document. ctx.colors.write_functions(&mut ctx.pdf); - - resource_ref } /// Write a page tree node. -fn write_page(ctx: &mut PdfContext, i: usize, resources_ref: Ref) { +fn write_page(ctx: &mut PdfContext, i: usize) { let page = &ctx.pages[i]; let content_id = ctx.alloc.bump(); @@ -165,7 +181,7 @@ fn write_page(ctx: &mut PdfContext, i: usize, resources_ref: Ref) { let h = page.size.y.to_f32(); page_writer.media_box(Rect::new(0.0, 0.0, w, h)); page_writer.contents(content_id); - page_writer.pair(Name(b"Resources"), resources_ref); + page_writer.pair(Name(b"Resources"), ctx.global_resources_ref); if page.uses_opacities { page_writer @@ -434,17 +450,31 @@ impl PageResource { /// An exporter for the contents of a single PDF page. pub struct PageContext<'a, 'b> { pub(crate) parent: &'a mut PdfContext<'b>, - page_ref: Ref, pub content: Content, state: State, saves: Vec<State>, - bottom: f32, + pub bottom: f32, uses_opacities: bool, links: Vec<(Destination, Rect)>, /// Keep track of the resources being used in the page. pub resources: HashMap<PageResource, usize>, } +impl<'a, 'b> PageContext<'a, 'b> { + pub fn new(parent: &'a mut PdfContext<'b>, size: Size) -> Self { + PageContext { + parent, + uses_opacities: false, + content: Content::new(), + state: State::new(size), + saves: vec![], + bottom: 0.0, + links: vec![], + resources: HashMap::default(), + } + } +} + /// A simulated graphics state used to deduplicate graphics state changes and /// keep track of the current transformation matrix for link annotations. #[derive(Debug, Clone)] @@ -555,7 +585,7 @@ impl PageContext<'_, '_> { self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); } - fn transform(&mut self, transform: Transform) { + pub fn transform(&mut self, transform: Transform) { let Transform { sx, ky, kx, sy, tx, ty } = transform; self.state.transform = self.state.transform.pre_concat(transform); if self.state.container_transform.is_identity() { @@ -670,7 +700,7 @@ impl PageContext<'_, '_> { } /// Encode a frame into the content stream. -fn write_frame(ctx: &mut PageContext, frame: &Frame) { +pub(crate) fn write_frame(ctx: &mut PageContext, frame: &Frame) { for &(pos, ref item) in frame.items() { let x = pos.x.to_f32(); let y = pos.y.to_f32(); @@ -718,21 +748,71 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { /// Encode a text run into the content stream. fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { + let ttf = text.font.ttf(); + let tables = ttf.tables(); + + // If the text run contains either only color glyphs (used for emojis for + // example) or normal text we can render it directly + let has_color_glyphs = tables.sbix.is_some() + || tables.cbdt.is_some() + || tables.svg.is_some() + || tables.colr.is_some(); + if !has_color_glyphs { + write_normal_text(ctx, pos, TextItemView::all_of(text)); + return; + } + + let color_glyph_count = + text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count(); + + if color_glyph_count == text.glyphs.len() { + write_color_glyphs(ctx, pos, TextItemView::all_of(text)); + } else if color_glyph_count == 0 { + write_normal_text(ctx, pos, TextItemView::all_of(text)); + } else { + // Otherwise we need to split it in smaller text runs + let mut offset = 0; + let mut position_in_run = Abs::zero(); + for (color, sub_run) in + text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g)) + { + let end = offset + sub_run.len(); + + // Build a sub text-run + let text_item_view = TextItemView::from_glyph_range(text, offset..end); + + // Adjust the position of the run on the line + let pos = pos + Point::new(position_in_run, Abs::zero()); + position_in_run += text_item_view.width(); + offset = end; + // Actually write the sub text-run + if color { + write_color_glyphs(ctx, pos, text_item_view); + } else { + write_normal_text(ctx, pos, text_item_view); + } + } + } +} + +// Encodes a text run (without any color glyph) into the content stream. +fn write_normal_text(ctx: &mut PageContext, pos: Point, text: TextItemView) { let x = pos.x.to_f32(); let y = pos.y.to_f32(); - *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); + *ctx.parent.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len(); - let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); - for g in &text.glyphs { - let segment = &text.text[g.range()]; + let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default(); + for g in text.glyphs() { + let t = text.text(); + let segment = &t[g.range()]; glyph_set.entry(g.id).or_insert_with(|| segment.into()); } let fill_transform = ctx.state.transforms(Size::zero(), pos); - ctx.set_fill(&text.fill, true, fill_transform); + ctx.set_fill(&text.item.fill, true, fill_transform); - let stroke = text.stroke.as_ref().and_then(|stroke| { + let stroke = text.item.stroke.as_ref().and_then(|stroke| { if stroke.thickness.to_f32() > 0.0 { Some(stroke) } else { @@ -747,8 +827,8 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { ctx.set_text_rendering_mode(TextRenderingMode::Fill); } - ctx.set_font(&text.font, text.size); - ctx.set_opacities(text.stroke.as_ref(), Some(&text.fill)); + ctx.set_font(&text.item.font, text.item.size); + ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill)); ctx.content.begin_text(); // Position the text. @@ -760,7 +840,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { let mut encoded = vec![]; // Write the glyphs with kerning adjustments. - for glyph in &text.glyphs { + for glyph in text.glyphs() { adjustment += glyph.x_offset; if !adjustment.is_zero() { @@ -773,11 +853,11 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { adjustment = Em::zero(); } - let cid = crate::font::glyph_cid(&text.font, glyph.id); + let cid = crate::font::glyph_cid(&text.item.font, glyph.id); encoded.push((cid >> 8) as u8); encoded.push((cid & 0xff) as u8); - if let Some(advance) = text.font.advance(glyph.id) { + if let Some(advance) = text.item.font.advance(glyph.id) { adjustment += glyph.x_advance - advance; } @@ -793,6 +873,46 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { ctx.content.end_text(); } +// Encodes a text run made only of color glyphs into the content stream +fn write_color_glyphs(ctx: &mut PageContext, pos: Point, text: TextItemView) { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + + let mut last_font = None; + + ctx.content.begin_text(); + ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); + // So that the next call to ctx.set_font() will change the font to one that + // displays regular glyphs and not color glyphs. + ctx.state.font = None; + + let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default(); + + for glyph in text.glyphs() { + // Retrieve the Type3 font reference and the glyph index in the font. + let (font, index) = ctx.parent.color_font_map.get( + &mut ctx.parent.alloc, + &text.item.font, + glyph.id, + ); + + if last_font != Some(font.get()) { + ctx.content.set_font( + Name(eco_format!("Cf{}", font.get()).as_bytes()), + text.item.size.to_f32(), + ); + last_font = Some(font.get()); + } + + ctx.content.show(Str(&[index])); + + glyph_set + .entry(glyph.id) + .or_insert_with(|| text.text()[glyph.range()].into()); + } + ctx.content.end_text(); +} + /// Encode a geometrical shape into the content stream. fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { let x = pos.x.to_f32(); |
