summaryrefslogtreecommitdiff
path: root/crates
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
parent4c99d6c8b38c23222fabf2465dbc030d08b6283f (diff)
Emojis in PDF (#3853)
Diffstat (limited to 'crates')
-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
-rw-r--r--crates/typst-render/Cargo.toml1
-rw-r--r--crates/typst-render/src/lib.rs146
-rw-r--r--crates/typst/Cargo.toml2
-rw-r--r--crates/typst/src/text/font/color.rs272
-rw-r--r--crates/typst/src/text/font/mod.rs2
-rw-r--r--crates/typst/src/text/item.rs62
-rw-r--r--crates/typst/src/visualize/image/svg.rs4
11 files changed, 828 insertions, 230 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();
diff --git a/crates/typst-render/Cargo.toml b/crates/typst-render/Cargo.toml
index cc58f785..56a18e80 100644
--- a/crates/typst-render/Cargo.toml
+++ b/crates/typst-render/Cargo.toml
@@ -18,7 +18,6 @@ typst-macros = { workspace = true }
typst-timing = { workspace = true }
bytemuck = { workspace = true }
comemo = { workspace = true }
-flate2 = { workspace = true }
image = { workspace = true }
pixglyph = { workspace = true }
resvg = { workspace = true }
diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs
index 28302180..401c7026 100644
--- a/crates/typst-render/src/lib.rs
+++ b/crates/typst-render/src/lib.rs
@@ -1,12 +1,10 @@
//! Rendering of Typst documents into raster images.
-use std::io::Read;
use std::sync::Arc;
use image::imageops::FilterType;
use image::{GenericImageView, Rgba};
use pixglyph::Bitmap;
-use resvg::tiny_skia::IntRect;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use typst::introspection::Meta;
@@ -14,12 +12,12 @@ use typst::layout::{
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
};
use typst::model::Document;
+use typst::text::color::{frame_for_glyph, is_color_glyph};
use typst::text::{Font, TextItem};
use typst::visualize::{
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap,
- LineJoin, Paint, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
+ LineJoin, Paint, Path, PathItem, Pattern, RelativeTo, Shape,
};
-use usvg::TreeParsing;
/// Export a frame into a raster image.
///
@@ -115,6 +113,13 @@ impl<'a> State<'a> {
}
}
+ fn pre_scale(self, scale: Axes<Abs>) -> Self {
+ Self {
+ transform: self.transform.pre_scale(scale.x.to_f32(), scale.y.to_f32()),
+ ..self
+ }
+ }
+
/// Pre concat the current item's transform.
fn pre_concat(self, transform: sk::Transform) -> Self {
Self {
@@ -236,130 +241,25 @@ fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
for glyph in &text.glyphs {
let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_f32();
- let state = state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
-
- render_svg_glyph(canvas, state, text, id)
- .or_else(|| render_bitmap_glyph(canvas, state, text, id))
- .or_else(|| render_outline_glyph(canvas, state, text, id));
-
- x += glyph.x_advance.at(text.size).to_f32();
- }
-}
-
-/// Render an SVG glyph into the canvas.
-fn render_svg_glyph(
- canvas: &mut sk::Pixmap,
- state: State,
- text: &TextItem,
- id: GlyphId,
-) -> Option<()> {
- let ts = &state.transform;
- let mut data = text.font.ttf().glyph_svg_image(id)?.data;
-
- // Decompress SVGZ.
- let mut decoded = vec![];
- if data.starts_with(&[0x1f, 0x8b]) {
- let mut decoder = flate2::read::GzDecoder::new(data);
- decoder.read_to_end(&mut decoded).ok()?;
- data = &decoded;
- }
- // Parse XML.
- let xml = std::str::from_utf8(data).ok()?;
- let document = roxmltree::Document::parse(xml).ok()?;
- let root = document.root_element();
-
- // Parse SVG.
- let opts = usvg::Options::default();
- let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
- tree.calculate_bounding_boxes();
- let view_box = tree.view_box.rect;
-
- // If there's no viewbox defined, use the em square for our scale
- // transformation ...
- let upem = text.font.units_per_em() as f32;
- let (mut width, mut height) = (upem, upem);
-
- // ... but if there's a viewbox or width, use that.
- if root.has_attribute("viewBox") || root.has_attribute("width") {
- width = view_box.width();
- }
-
- // Same as for width.
- if root.has_attribute("viewBox") || root.has_attribute("height") {
- height = view_box.height();
- }
+ if is_color_glyph(&text.font, glyph) {
+ let upem = text.font.units_per_em();
+ let text_scale = Abs::raw(text.size.to_raw() / upem);
+ let state = state
+ .pre_translate(Point::new(Abs::raw(offset as _), -text.size))
+ .pre_scale(Axes::new(text_scale, text_scale));
- let size = text.size.to_f32();
- let ts = ts.pre_scale(size / width, size / height);
-
- // Compute the space we need to draw our glyph.
- // See https://github.com/RazrFalcon/resvg/issues/602 for why
- // using the svg size is problematic here.
- let mut bbox = usvg::BBox::default();
- if let Some(tree_bbox) = tree.root.bounding_box {
- bbox = bbox.expand(tree_bbox);
- }
+ let glyph_frame = frame_for_glyph(&text.font, glyph.id);
- // Compute the bbox after the transform is applied.
- // We add a nice 5px border along the bounding box to
- // be on the safe size. We also compute the intersection
- // with the canvas rectangle
- let bbox = bbox.transform(ts)?.to_rect()?.round_out()?;
- let bbox = IntRect::from_xywh(
- bbox.left() - 5,
- bbox.y() - 5,
- bbox.width() + 10,
- bbox.height() + 10,
- )?;
-
- let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?;
-
- // We offset our transform so that the pixmap starts at the edge of the bbox.
- let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32);
- resvg::render(&tree, ts, &mut pixmap.as_mut());
-
- canvas.draw_pixmap(
- bbox.left(),
- bbox.top(),
- pixmap.as_ref(),
- &sk::PixmapPaint::default(),
- sk::Transform::identity(),
- state.mask,
- );
-
- Some(())
-}
+ render_frame(canvas, state, &glyph_frame);
+ } else {
+ let state =
+ state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
+ render_outline_glyph(canvas, state, text, id);
+ }
-/// Render a bitmap glyph into the canvas.
-fn render_bitmap_glyph(
- canvas: &mut sk::Pixmap,
- state: State,
- text: &TextItem,
- id: GlyphId,
-) -> Option<()> {
- let ts = state.transform;
- let size = text.size.to_f32();
- let ppem = size * ts.sy;
- let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
- if raster.format != ttf_parser::RasterImageFormat::PNG {
- return None;
+ x += glyph.x_advance.at(text.size).to_f32();
}
- let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?;
-
- // FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
- // and maybe also for Noto Color Emoji. And: Is the size calculation
- // correct?
- let h = text.size;
- let w = (image.width() / image.height()) * h;
- let dx = (raster.x as f32) / (image.width() as f32) * size;
- let dy = (raster.y as f32) / (image.height() as f32) * size;
- render_image(
- canvas,
- state.pre_translate(Point::new(Abs::raw(dx as _), Abs::raw((-size - dy) as _))),
- &image,
- Size::new(w, h),
- )
}
/// Render an outline glyph into the canvas. This is the "normal" case.
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index cfc4e32e..8e5e224a 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -24,6 +24,7 @@ ciborium = { workspace = true }
comemo = { workspace = true }
csv = { workspace = true }
ecow = { workspace = true }
+flate2 = { workspace = true }
fontdb = { workspace = true }
hayagriva = { workspace = true }
hypher = { workspace = true }
@@ -64,6 +65,7 @@ unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true }
unicode-script = { workspace = true }
unicode-segmentation = { workspace = true }
+unscanny = { workspace = true }
usvg = { workspace = true }
wasmi = { workspace = true }
diff --git a/crates/typst/src/text/font/color.rs b/crates/typst/src/text/font/color.rs
new file mode 100644
index 00000000..2dfd5545
--- /dev/null
+++ b/crates/typst/src/text/font/color.rs
@@ -0,0 +1,272 @@
+//! Utilities for color font handling
+
+use std::io::Read;
+
+use ecow::EcoString;
+use ttf_parser::GlyphId;
+use usvg::{TreeParsing, TreeWriting};
+
+use crate::layout::{Abs, Axes, Em, Frame, FrameItem, Point, Size};
+use crate::syntax::Span;
+use crate::text::{Font, Glyph, Lang, TextItem};
+use crate::visualize::{Color, Image, Paint, Rgb};
+
+/// Tells if a glyph is a color glyph or not in a given font.
+pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool {
+ let ttf = font.ttf();
+ let glyph_id = GlyphId(g.id);
+ ttf.glyph_raster_image(glyph_id, 160).is_some()
+ || ttf.glyph_svg_image(glyph_id).is_some()
+ || ttf.is_color_glyph(glyph_id)
+}
+
+/// Returns a frame with the glyph drawn inside.
+///
+/// The glyphs are sized in font units, [`text.item.size`] is not taken into
+/// account.
+#[comemo::memoize]
+pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame {
+ let ttf = font.ttf();
+ let upem = Abs::pt(ttf.units_per_em() as f64);
+ let glyph_id = GlyphId(glyph_id);
+
+ let mut frame = Frame::soft(Size::splat(upem));
+
+ if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) {
+ draw_raster_glyph(&mut frame, font, upem, raster_image);
+ } else if ttf.glyph_svg_image(glyph_id).is_some() {
+ draw_svg_glyph(&mut frame, upem, font, glyph_id);
+ } else if ttf.is_color_glyph(glyph_id) {
+ draw_colr_glyph(&mut frame, font, glyph_id);
+ }
+
+ frame
+}
+
+/// Draws a raster glyph in a frame.
+fn draw_raster_glyph(
+ frame: &mut Frame,
+ font: &Font,
+ upem: Abs,
+ raster_image: ttf_parser::RasterGlyphImage,
+) {
+ let image = Image::new(
+ raster_image.data.into(),
+ typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png),
+ None,
+ )
+ .unwrap();
+
+ // Apple Color emoji doesn't provide offset information (or at least
+ // not in a way ttf-parser understands), so we artificially shift their
+ // baseline to make it look good.
+ let y_offset = if font.info().family.to_lowercase() == "apple color emoji" {
+ 20.0
+ } else {
+ -(raster_image.y as f64)
+ };
+
+ let position = Point::new(
+ upem * raster_image.x as f64 / raster_image.pixels_per_em as f64,
+ upem * y_offset / raster_image.pixels_per_em as f64,
+ );
+ let aspect_ratio = image.width() / image.height();
+ let size = Axes::new(upem, upem * aspect_ratio);
+ frame.push(position, FrameItem::Image(image, size, Span::detached()));
+}
+
+/// Draws a COLR glyph in a frame.
+fn draw_colr_glyph(frame: &mut Frame, font: &Font, glyph_id: GlyphId) {
+ let mut painter = ColrPainter { font, current_glyph: glyph_id, frame };
+ font.ttf().paint_color_glyph(glyph_id, 0, &mut painter);
+}
+
+/// Draws COLR glyphs in a frame.
+struct ColrPainter<'f, 't> {
+ /// The frame in which to draw.
+ frame: &'f mut Frame,
+ /// The font of the text.
+ font: &'t Font,
+ /// The glyph that will be drawn the next time `ColrPainter::paint` is called.
+ current_glyph: GlyphId,
+}
+
+impl<'f, 't> ColrPainter<'f, 't> {
+ fn paint(&mut self, fill: Paint) {
+ self.frame.push(
+ // With images, the position corresponds to the top-left corner, but
+ // in the case of text it matches the baseline-left point. Here, we
+ // move the glyph one unit down to compensate for that.
+ Point::new(Abs::zero(), Abs::pt(self.font.units_per_em())),
+ FrameItem::Text(TextItem {
+ font: self.font.clone(),
+ size: Abs::pt(self.font.units_per_em()),
+ fill,
+ stroke: None,
+ lang: Lang::ENGLISH,
+ text: EcoString::new(),
+ glyphs: vec![Glyph {
+ id: self.current_glyph.0,
+ // Advance is not relevant here as we will draw glyph on top
+ // of each other anyway
+ x_advance: Em::zero(),
+ x_offset: Em::zero(),
+ range: 0..0,
+ span: (Span::detached(), 0),
+ }],
+ }),
+ )
+ }
+}
+
+impl<'f, 't> ttf_parser::colr::Painter for ColrPainter<'f, 't> {
+ fn outline(&mut self, glyph_id: GlyphId) {
+ self.current_glyph = glyph_id;
+ }
+
+ fn paint_foreground(&mut self) {
+ // Default to black if no color was specified
+ self.paint(Paint::Solid(Color::BLACK))
+ }
+
+ fn paint_color(&mut self, color: ttf_parser::RgbaColor) {
+ let color = Color::Rgb(Rgb::new(
+ color.red as f32 / 255.0,
+ color.green as f32 / 255.0,
+ color.blue as f32 / 255.0,
+ color.alpha as f32 / 255.0,
+ ));
+ self.paint(Paint::Solid(color));
+ }
+}
+
+/// Draws an SVG glyph in a frame.
+fn draw_svg_glyph(
+ frame: &mut Frame,
+ upem: Abs,
+ font: &Font,
+ glyph_id: GlyphId,
+) -> Option<()> {
+ let mut data = font.ttf().glyph_svg_image(glyph_id)?.data;
+
+ // Decompress SVGZ.
+ let mut decoded = vec![];
+ if data.starts_with(&[0x1f, 0x8b]) {
+ let mut decoder = flate2::read::GzDecoder::new(data);
+ decoder.read_to_end(&mut decoded).ok()?;
+ data = &decoded;
+ }
+
+ // Parse XML.
+ let xml = std::str::from_utf8(data).ok()?;
+ let document = roxmltree::Document::parse(xml).ok()?;
+
+ // Parse SVG.
+ let opts = usvg::Options::default();
+ let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
+
+ // Compute the space we need to draw our glyph.
+ // See https://github.com/RazrFalcon/resvg/issues/602 for why
+ // using the svg size is problematic here.
+ tree.calculate_bounding_boxes();
+ let mut bbox = usvg::BBox::default();
+ if let Some(tree_bbox) = tree.root.bounding_box {
+ bbox = bbox.expand(tree_bbox);
+ }
+ let bbox = bbox.to_rect()?;
+
+ let mut data = tree.to_string(&usvg::XmlOptions::default());
+
+ let width = bbox.width() as f64;
+ let height = bbox.height() as f64;
+ let left = bbox.left() as f64;
+ let top = bbox.top() as f64;
+
+ // The SVG coordinates and the font coordinates are not the same: the Y axis
+ // is mirrored. But the origin of the axes are the same (which means that
+ // the horizontal axis in the SVG document corresponds to the baseline). See
+ // the reference for more details:
+ // https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
+ //
+ // If we used the SVG document as it is, svg2pdf would produce a cropped
+ // glyph (only what is under the baseline would be visible). So we need to
+ // embed the original SVG in another one that has the exact dimensions of
+ // the glyph, with a transform to make it fit. We also need to remove the
+ // viewBox, height and width attributes from the inner SVG, otherwise usvg
+ // takes into account these values to clip the embedded SVG.
+ make_svg_unsized(&mut data);
+ let wrapper_svg = format!(
+ r#"
+ <svg
+ width="{width}"
+ height="{height}"
+ viewBox="0 0 {width} {height}"
+ xmlns="http://www.w3.org/2000/svg">
+ <g transform="matrix(1 0 0 1 {tx} {ty})">
+ {inner}
+ </g>
+ </svg>
+ "#,
+ inner = data,
+ tx = -left,
+ ty = -top,
+ );
+
+ let image = Image::new(
+ wrapper_svg.into_bytes().into(),
+ typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
+ None,
+ )
+ .unwrap();
+ let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
+ let size = Axes::new(Abs::pt(width), Abs::pt(height));
+ frame.push(position, FrameItem::Image(image, size, Span::detached()));
+
+ Some(())
+}
+
+/// Remove all size specifications (viewBox, width and height attributes) from a
+/// SVG document.
+fn make_svg_unsized(svg: &mut String) {
+ let mut viewbox_range = None;
+ let mut width_range = None;
+ let mut height_range = None;
+
+ let mut s = unscanny::Scanner::new(svg);
+
+ s.eat_until("<svg");
+ s.eat_if("<svg");
+ while !s.eat_if('>') && !s.done() {
+ s.eat_whitespace();
+ let start = s.cursor();
+ let attr_name = s.eat_until('=').trim();
+ // Eat the equal sign and the quote.
+ s.eat();
+ s.eat();
+ let mut escaped = false;
+ while (escaped || !s.eat_if('"')) && !s.done() {
+ escaped = s.eat() == Some('\\');
+ }
+ match attr_name {
+ "viewBox" => viewbox_range = Some(start..s.cursor()),
+ "width" => width_range = Some(start..s.cursor()),
+ "height" => height_range = Some(start..s.cursor()),
+ _ => {}
+ }
+ }
+
+ // Remove the `viewBox` attribute.
+ if let Some(range) = viewbox_range {
+ svg.replace_range(range.clone(), &" ".repeat(range.len()));
+ }
+
+ // Remove the `width` attribute.
+ if let Some(range) = width_range {
+ svg.replace_range(range.clone(), &" ".repeat(range.len()));
+ }
+
+ // Remove the `height` attribute.
+ if let Some(range) = height_range {
+ svg.replace_range(range, "");
+ }
+}
diff --git a/crates/typst/src/text/font/mod.rs b/crates/typst/src/text/font/mod.rs
index 42a87b7e..70111813 100644
--- a/crates/typst/src/text/font/mod.rs
+++ b/crates/typst/src/text/font/mod.rs
@@ -1,5 +1,7 @@
//! Font handling.
+pub mod color;
+
mod book;
mod exceptions;
mod variant;
diff --git a/crates/typst/src/text/item.rs b/crates/typst/src/text/item.rs
index 44d8e63a..4bc6dd21 100644
--- a/crates/typst/src/text/item.rs
+++ b/crates/typst/src/text/item.rs
@@ -65,3 +65,65 @@ impl Glyph {
usize::from(self.range.start)..usize::from(self.range.end)
}
}
+
+/// A slice of a [`TextItem`].
+pub struct TextItemView<'a> {
+ /// The whole item this is a part of
+ pub item: &'a TextItem,
+ /// The glyphs of this slice
+ pub glyph_range: Range<usize>,
+}
+
+impl<'a> TextItemView<'a> {
+ /// Build a TextItemView for the whole contents of a TextItem.
+ pub fn all_of(text: &'a TextItem) -> Self {
+ Self::from_glyph_range(text, 0..text.glyphs.len())
+ }
+
+ /// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs.
+ pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self {
+ TextItemView { item: text, glyph_range }
+ }
+
+ /// Obtains a glyph in this slice, remapping the range that it represents in
+ /// the original text so that it is relative to the start of the slice
+ pub fn glyph_at(&self, index: usize) -> Glyph {
+ let g = &self.item.glyphs[self.glyph_range.start + index];
+ let text_range = self.text_range();
+ Glyph {
+ range: (g.range.start - text_range.start as u16)
+ ..(g.range.end - text_range.start as u16),
+ ..*g
+ }
+ }
+
+ /// Returns an iterator over the glyphs of the slice.
+ ///
+ /// The range of text that each glyph represents is remapped to be relative
+ /// to the start of the slice.
+ pub fn glyphs(&self) -> impl Iterator<Item = Glyph> + '_ {
+ (0..self.glyph_range.len()).map(|index| self.glyph_at(index))
+ }
+
+ /// The plain text that this slice represents
+ pub fn text(&self) -> &str {
+ &self.item.text[self.text_range()]
+ }
+
+ /// The total width of this text slice
+ pub fn width(&self) -> Abs {
+ self.item.glyphs[self.glyph_range.clone()]
+ .iter()
+ .map(|g| g.x_advance)
+ .sum::<Em>()
+ .at(self.item.size)
+ }
+
+ /// The range of text in the original TextItem that this slice corresponds
+ /// to.
+ fn text_range(&self) -> Range<usize> {
+ let text_start = self.item.glyphs[self.glyph_range.start].range().start;
+ let text_end = self.item.glyphs[self.glyph_range.end - 1].range().end;
+ text_start..text_end
+ }
+}
diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst/src/visualize/image/svg.rs
index 9685e454..d5cae6fe 100644
--- a/crates/typst/src/visualize/image/svg.rs
+++ b/crates/typst/src/visualize/image/svg.rs
@@ -30,7 +30,9 @@ impl SvgImage {
/// Decode an SVG image without fonts.
#[comemo::memoize]
pub fn new(data: Bytes) -> StrResult<SvgImage> {
- let tree = usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
+ let mut tree =
+ usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
+ tree.calculate_bounding_boxes();
Ok(Self(Arc::new(Repr {
data,
size: tree_size(&tree),