Emojis in PDF (#3853)

This commit is contained in:
Ana Gelez 2024-04-17 17:11:20 +02:00 committed by GitHub
parent 4c99d6c8b3
commit 21c78abd6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 835 additions and 235 deletions

8
Cargo.lock generated
View File

@ -1612,9 +1612,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "pdf-writer" name = "pdf-writer"
version = "0.9.2" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644b654f2de28457bf1e25a4905a76a563d1128a33ce60cf042f721f6818feaf" checksum = "24e9127455063c816e661caac9ecd9043ad2871f55be93014e6838a8ced2332b"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"itoa", "itoa",
@ -2525,6 +2525,7 @@ dependencies = [
"comemo", "comemo",
"csv", "csv",
"ecow", "ecow",
"flate2",
"fontdb", "fontdb",
"hayagriva", "hayagriva",
"hypher", "hypher",
@ -2571,6 +2572,7 @@ dependencies = [
"unicode-math-class", "unicode-math-class",
"unicode-script", "unicode-script",
"unicode-segmentation", "unicode-segmentation",
"unscanny",
"usvg", "usvg",
"wasmi", "wasmi",
] ]
@ -2702,6 +2704,7 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"image", "image",
"indexmap 2.2.5",
"miniz_oxide", "miniz_oxide",
"once_cell", "once_cell",
"pdf-writer", "pdf-writer",
@ -2723,7 +2726,6 @@ version = "0.11.0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"comemo", "comemo",
"flate2",
"image", "image",
"pixglyph", "pixglyph",
"resvg", "resvg",

View File

@ -73,7 +73,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
parking_lot = "0.12.1" parking_lot = "0.12.1"
pathdiff = "0.2" pathdiff = "0.2"
pdf-writer = "0.9.2" pdf-writer = "0.9.3"
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
pixglyph = "0.3" pixglyph = "0.3"
png = "0.17" png = "0.17"

View File

@ -22,6 +22,7 @@ bytemuck = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
image = { workspace = true } image = { workspace = true }
indexmap = { workspace = true }
miniz_oxide = { workspace = true } miniz_oxide = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
pdf-writer = { workspace = true } pdf-writer = { workspace = true }

View File

@ -3,13 +3,16 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
use pdf_writer::writers::FontDescriptor;
use pdf_writer::{Filter, Finish, Name, Rect, Str}; use pdf_writer::{Filter, Finish, Name, Rect, Str};
use ttf_parser::{name_id, GlyphId, Tag}; use ttf_parser::{name_id, GlyphId, Tag};
use typst::layout::{Abs, Em, Ratio, Transform};
use typst::text::Font; use typst::text::Font;
use typst::util::SliceExt; use typst::util::SliceExt;
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; 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 CFF: Tag = Tag::from_bytes(b"CFF ");
const CFF2: Tag = Tag::from_bytes(b"CFF2"); const CFF2: Tag = Tag::from_bytes(b"CFF2");
@ -23,6 +26,8 @@ const SYSTEM_INFO: SystemInfo = SystemInfo {
/// Embed all used fonts into the PDF. /// Embed all used fonts into the PDF.
#[typst_macros::time(name = "write fonts")] #[typst_macros::time(name = "write fonts")]
pub(crate) fn write_fonts(ctx: &mut PdfContext) { pub(crate) fn write_fonts(ctx: &mut PdfContext) {
write_color_fonts(ctx);
for font in ctx.font_map.items() { for font in ctx.font_map.items() {
let type0_ref = ctx.alloc.bump(); let type0_ref = ctx.alloc.bump();
let cid_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); ctx.font_refs.push(type0_ref);
let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
let metrics = font.metrics();
let ttf = font.ttf(); let ttf = font.ttf();
// Do we have a TrueType or CFF font? // Do we have a TrueType or CFF font?
@ -103,47 +107,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
width_writer.finish(); width_writer.finish();
cid.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 // Write the /ToUnicode character map, which maps glyph ids back to
// unicode codepoints to enable copying out of the PDF. // unicode codepoints to enable copying out of the PDF.
let cmap = create_cmap(font, glyph_set); let cmap = create_cmap(font, glyph_set);
@ -160,9 +123,173 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
} }
stream.finish(); 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. /// Subset a font to the given glyphs.
/// ///
/// - For a font with TrueType outlines, this returns the whole OpenType font. /// - For a font with TrueType outlines, this returns the whole OpenType font.

View File

@ -15,13 +15,15 @@ use std::sync::Arc;
use base64::Engine; use base64::Engine;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use indexmap::IndexMap;
use pdf_writer::types::Direction; use pdf_writer::types::Direction;
use pdf_writer::writers::Destination; 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::foundations::{Datetime, Label, NativeElement, Smart};
use typst::introspection::Location; 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::model::{Document, HeadingElem};
use typst::text::color::frame_for_glyph;
use typst::text::{Font, Lang}; use typst::text::{Font, Lang};
use typst::util::Deferred; use typst::util::Deferred;
use typst::visualize::Image; use typst::visualize::Image;
@ -68,6 +70,7 @@ pub fn pdf(
pattern::write_patterns(&mut ctx); pattern::write_patterns(&mut ctx);
write_named_destinations(&mut ctx); write_named_destinations(&mut ctx);
page::write_page_tree(&mut ctx); page::write_page_tree(&mut ctx);
page::write_global_resources(&mut ctx);
write_catalog(&mut ctx, ident, timestamp); write_catalog(&mut ctx, ident, timestamp);
ctx.pdf.finish() ctx.pdf.finish()
} }
@ -96,6 +99,15 @@ struct PdfContext<'a> {
alloc: Ref, alloc: Ref,
/// The ID of the page tree. /// The ID of the page tree.
page_tree_ref: Ref, 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. /// The IDs of written pages.
page_refs: Vec<Ref>, page_refs: Vec<Ref>,
/// The IDs of written fonts. /// The IDs of written fonts.
@ -123,6 +135,8 @@ struct PdfContext<'a> {
pattern_map: Remapper<PdfPattern>, pattern_map: Remapper<PdfPattern>,
/// Deduplicates external graphics states used across the document. /// Deduplicates external graphics states used across the document.
extg_map: Remapper<ExtGState>, extg_map: Remapper<ExtGState>,
/// Deduplicates color glyphs.
color_font_map: ColorFontMap,
/// A sorted list of all named destinations. /// A sorted list of all named destinations.
dests: Vec<(Label, Ref)>, dests: Vec<(Label, Ref)>,
@ -134,6 +148,8 @@ impl<'a> PdfContext<'a> {
fn new(document: &'a Document) -> Self { fn new(document: &'a Document) -> Self {
let mut alloc = Ref::new(1); let mut alloc = Ref::new(1);
let page_tree_ref = alloc.bump(); let page_tree_ref = alloc.bump();
let global_resources_ref = alloc.bump();
let type3_font_resources_ref = alloc.bump();
Self { Self {
document, document,
pdf: Pdf::new(), pdf: Pdf::new(),
@ -142,6 +158,8 @@ impl<'a> PdfContext<'a> {
languages: BTreeMap::new(), languages: BTreeMap::new(),
alloc, alloc,
page_tree_ref, page_tree_ref,
global_resources_ref,
type3_font_resources_ref,
page_refs: vec![], page_refs: vec![],
font_refs: vec![], font_refs: vec![],
image_refs: vec![], image_refs: vec![],
@ -155,6 +173,7 @@ impl<'a> PdfContext<'a> {
gradient_map: Remapper::new(), gradient_map: Remapper::new(),
pattern_map: Remapper::new(), pattern_map: Remapper::new(),
extg_map: Remapper::new(), extg_map: Remapper::new(),
color_font_map: ColorFontMap::new(),
dests: vec![], dests: vec![],
loc_to_dest: HashMap::new(), 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`]. /// Additional methods for [`Abs`].
trait AbsExt { trait AbsExt {
/// Convert an to a number of points. /// Convert an to a number of points.

View File

@ -1,6 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::num::NonZeroUsize; 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 ecow::{eco_format, EcoString};
use pdf_writer::types::{ use pdf_writer::types::{
ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle, ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle,
@ -13,17 +17,13 @@ use typst::layout::{
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform, Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
}; };
use typst::model::{Destination, Numbering}; use typst::model::{Destination, Numbering};
use typst::text::{Case, Font, TextItem}; use typst::text::color::is_color_glyph;
use typst::util::{Deferred, Numeric}; use typst::text::{Case, Font, TextItem, TextItemView};
use typst::util::{Deferred, Numeric, SliceExt};
use typst::visualize::{ use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, 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. /// Construct page objects.
#[typst_macros::time(name = "construct pages")] #[typst_macros::time(name = "construct pages")]
pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) { 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 page_ref = ctx.alloc.bump();
let size = frame.size(); let size = frame.size();
let mut ctx = PageContext { let mut ctx = PageContext::new(ctx, size);
parent: ctx,
page_ref,
uses_opacities: false,
content: Content::new(),
state: State::new(size),
saves: vec![],
bottom: 0.0,
links: vec![],
resources: HashMap::default(),
};
// Make the coordinate system start at the top-left. // Make the coordinate system start at the top-left.
ctx.bottom = size.y.to_f32(); 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 { let page = EncodedPage {
size, size,
content: deflate_deferred(ctx.content.finish()), content: deflate_deferred(ctx.content.finish()),
id: ctx.page_ref, id: page_ref,
uses_opacities: ctx.uses_opacities, uses_opacities: ctx.uses_opacities,
links: ctx.links, links: ctx.links,
label: None, label: None,
@ -85,10 +75,8 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
/// Write the page tree. /// Write the page tree.
pub(crate) fn write_page_tree(ctx: &mut PdfContext) { pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
let resources_ref = write_global_resources(ctx);
for i in 0..ctx.pages.len() { for i in 0..ctx.pages.len() {
write_page(ctx, i, resources_ref); write_page(ctx, i);
} }
ctx.pdf 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 /// 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 /// to the root node of the page tree because using the resource inheritance
/// feature breaks PDF merging with Apple Preview. /// feature breaks PDF merging with Apple Preview.
fn write_global_resources(ctx: &mut PdfContext) -> Ref { pub(crate) fn write_global_resources(ctx: &mut PdfContext) {
let resource_ref = ctx.alloc.bump(); 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 resources = ctx.pdf.indirect(resource_ref).start::<Resources>(); let mut images = ctx.pdf.indirect(images_ref).dict();
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();
let mut images = resources.x_objects();
for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) {
let name = eco_format!("Im{}", im); let name = eco_format!("Im{}", im);
images.pair(Name(name.as_bytes()), image_ref); images.pair(Name(name.as_bytes()), image_ref);
} }
images.finish(); 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) { for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) {
let name = eco_format!("Gr{}", gr); let name = eco_format!("Gr{}", gr);
patterns.pair(Name(name.as_bytes()), gradient_ref); 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); let name = eco_format!("P{}", p);
patterns.pair(Name(name.as_bytes()), pattern_ref); patterns.pair(Name(name.as_bytes()), pattern_ref);
} }
patterns.finish(); 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) { for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) {
let name = eco_format!("Gs{}", gs); let name = eco_format!("Gs{}", gs);
ext_gs_states.pair(Name(name.as_bytes()), gs_ref); ext_gs_states.pair(Name(name.as_bytes()), gs_ref);
} }
ext_gs_states.finish(); 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(); 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. // Write all of the functions used by the document.
ctx.colors.write_functions(&mut ctx.pdf); ctx.colors.write_functions(&mut ctx.pdf);
resource_ref
} }
/// Write a page tree node. /// 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 page = &ctx.pages[i];
let content_id = ctx.alloc.bump(); 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(); let h = page.size.y.to_f32();
page_writer.media_box(Rect::new(0.0, 0.0, w, h)); page_writer.media_box(Rect::new(0.0, 0.0, w, h));
page_writer.contents(content_id); 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 { if page.uses_opacities {
page_writer page_writer
@ -434,17 +450,31 @@ impl PageResource {
/// An exporter for the contents of a single PDF page. /// An exporter for the contents of a single PDF page.
pub struct PageContext<'a, 'b> { pub struct PageContext<'a, 'b> {
pub(crate) parent: &'a mut PdfContext<'b>, pub(crate) parent: &'a mut PdfContext<'b>,
page_ref: Ref,
pub content: Content, pub content: Content,
state: State, state: State,
saves: Vec<State>, saves: Vec<State>,
bottom: f32, pub bottom: f32,
uses_opacities: bool, uses_opacities: bool,
links: Vec<(Destination, Rect)>, links: Vec<(Destination, Rect)>,
/// Keep track of the resources being used in the page. /// Keep track of the resources being used in the page.
pub resources: HashMap<PageResource, usize>, 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 /// A simulated graphics state used to deduplicate graphics state changes and
/// keep track of the current transformation matrix for link annotations. /// keep track of the current transformation matrix for link annotations.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -555,7 +585,7 @@ impl PageContext<'_, '_> {
self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); 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; let Transform { sx, ky, kx, sy, tx, ty } = transform;
self.state.transform = self.state.transform.pre_concat(transform); self.state.transform = self.state.transform.pre_concat(transform);
if self.state.container_transform.is_identity() { if self.state.container_transform.is_identity() {
@ -670,7 +700,7 @@ impl PageContext<'_, '_> {
} }
/// Encode a frame into the content stream. /// 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() { for &(pos, ref item) in frame.items() {
let x = pos.x.to_f32(); let x = pos.x.to_f32();
let y = pos.y.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. /// Encode a text run into the content stream.
fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { 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 x = pos.x.to_f32();
let y = pos.y.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(); let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default();
for g in &text.glyphs { for g in text.glyphs() {
let segment = &text.text[g.range()]; let t = text.text();
let segment = &t[g.range()];
glyph_set.entry(g.id).or_insert_with(|| segment.into()); glyph_set.entry(g.id).or_insert_with(|| segment.into());
} }
let fill_transform = ctx.state.transforms(Size::zero(), pos); 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 { if stroke.thickness.to_f32() > 0.0 {
Some(stroke) Some(stroke)
} else { } else {
@ -747,8 +827,8 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
ctx.set_text_rendering_mode(TextRenderingMode::Fill); ctx.set_text_rendering_mode(TextRenderingMode::Fill);
} }
ctx.set_font(&text.font, text.size); ctx.set_font(&text.item.font, text.item.size);
ctx.set_opacities(text.stroke.as_ref(), Some(&text.fill)); ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill));
ctx.content.begin_text(); ctx.content.begin_text();
// Position the text. // Position the text.
@ -760,7 +840,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
let mut encoded = vec![]; let mut encoded = vec![];
// Write the glyphs with kerning adjustments. // Write the glyphs with kerning adjustments.
for glyph in &text.glyphs { for glyph in text.glyphs() {
adjustment += glyph.x_offset; adjustment += glyph.x_offset;
if !adjustment.is_zero() { if !adjustment.is_zero() {
@ -773,11 +853,11 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
adjustment = Em::zero(); 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 >> 8) as u8);
encoded.push((cid & 0xff) 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; adjustment += glyph.x_advance - advance;
} }
@ -793,6 +873,46 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
ctx.content.end_text(); 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. /// Encode a geometrical shape into the content stream.
fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
let x = pos.x.to_f32(); let x = pos.x.to_f32();

View File

@ -18,7 +18,6 @@ typst-macros = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
flate2 = { workspace = true }
image = { workspace = true } image = { workspace = true }
pixglyph = { workspace = true } pixglyph = { workspace = true }
resvg = { workspace = true } resvg = { workspace = true }

View File

@ -1,12 +1,10 @@
//! Rendering of Typst documents into raster images. //! Rendering of Typst documents into raster images.
use std::io::Read;
use std::sync::Arc; use std::sync::Arc;
use image::imageops::FilterType; use image::imageops::FilterType;
use image::{GenericImageView, Rgba}; use image::{GenericImageView, Rgba};
use pixglyph::Bitmap; use pixglyph::Bitmap;
use resvg::tiny_skia::IntRect;
use tiny_skia as sk; use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder}; use ttf_parser::{GlyphId, OutlineBuilder};
use typst::introspection::Meta; use typst::introspection::Meta;
@ -14,12 +12,12 @@ use typst::layout::{
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
}; };
use typst::model::Document; use typst::model::Document;
use typst::text::color::{frame_for_glyph, is_color_glyph};
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::visualize::{ use typst::visualize::{
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, 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. /// 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. /// Pre concat the current item's transform.
fn pre_concat(self, transform: sk::Transform) -> Self { fn pre_concat(self, transform: sk::Transform) -> Self {
Self { Self {
@ -236,132 +241,27 @@ fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
for glyph in &text.glyphs { for glyph in &text.glyphs {
let id = GlyphId(glyph.id); let id = GlyphId(glyph.id);
let offset = x + glyph.x_offset.at(text.size).to_f32(); 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) if is_color_glyph(&text.font, glyph) {
.or_else(|| render_bitmap_glyph(canvas, state, text, id)) let upem = text.font.units_per_em();
.or_else(|| render_outline_glyph(canvas, state, text, id)); 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 glyph_frame = frame_for_glyph(&text.font, glyph.id);
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);
}
x += glyph.x_advance.at(text.size).to_f32(); 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();
}
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);
}
// 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 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;
}
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. /// Render an outline glyph into the canvas. This is the "normal" case.
fn render_outline_glyph( fn render_outline_glyph(
canvas: &mut sk::Pixmap, canvas: &mut sk::Pixmap,

View File

@ -24,6 +24,7 @@ ciborium = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
csv = { workspace = true } csv = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
flate2 = { workspace = true }
fontdb = { workspace = true } fontdb = { workspace = true }
hayagriva = { workspace = true } hayagriva = { workspace = true }
hypher = { workspace = true } hypher = { workspace = true }
@ -64,6 +65,7 @@ unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true } unicode-math-class = { workspace = true }
unicode-script = { workspace = true } unicode-script = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
unscanny = { workspace = true }
usvg = { workspace = true } usvg = { workspace = true }
wasmi = { workspace = true } wasmi = { workspace = true }

View File

@ -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, "");
}
}

View File

@ -1,5 +1,7 @@
//! Font handling. //! Font handling.
pub mod color;
mod book; mod book;
mod exceptions; mod exceptions;
mod variant; mod variant;

View File

@ -65,3 +65,65 @@ impl Glyph {
usize::from(self.range.start)..usize::from(self.range.end) 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
}
}

View File

@ -30,7 +30,9 @@ impl SvgImage {
/// Decode an SVG image without fonts. /// Decode an SVG image without fonts.
#[comemo::memoize] #[comemo::memoize]
pub fn new(data: Bytes) -> StrResult<SvgImage> { 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 { Ok(Self(Arc::new(Repr {
data, data,
size: tree_size(&tree), size: tree_size(&tree),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 B

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 B

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB