summaryrefslogtreecommitdiff
path: root/crates/typst-pdf
diff options
context:
space:
mode:
authorAna Gelez <ana@gelez.xyz>2024-04-17 17:11:20 +0200
committerGitHub <noreply@github.com>2024-04-17 15:11:20 +0000
commit21c78abd6eecd0f6b3208405c7513be3bbd8991c (patch)
treefe960daf42372202ed37138c6a12ad80711f757e /crates/typst-pdf
parent4c99d6c8b38c23222fabf2465dbc030d08b6283f (diff)
Emojis in PDF (#3853)
Diffstat (limited to 'crates/typst-pdf')
-rw-r--r--crates/typst-pdf/Cargo.toml1
-rw-r--r--crates/typst-pdf/src/font.rs213
-rw-r--r--crates/typst-pdf/src/lib.rs115
-rw-r--r--crates/typst-pdf/src/page.rs240
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();