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]]
name = "pdf-writer"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644b654f2de28457bf1e25a4905a76a563d1128a33ce60cf042f721f6818feaf"
checksum = "24e9127455063c816e661caac9ecd9043ad2871f55be93014e6838a8ced2332b"
dependencies = [
"bitflags 1.3.2",
"itoa",
@ -2525,6 +2525,7 @@ dependencies = [
"comemo",
"csv",
"ecow",
"flate2",
"fontdb",
"hayagriva",
"hypher",
@ -2571,6 +2572,7 @@ dependencies = [
"unicode-math-class",
"unicode-script",
"unicode-segmentation",
"unscanny",
"usvg",
"wasmi",
]
@ -2702,6 +2704,7 @@ dependencies = [
"comemo",
"ecow",
"image",
"indexmap 2.2.5",
"miniz_oxide",
"once_cell",
"pdf-writer",
@ -2723,7 +2726,6 @@ version = "0.11.0"
dependencies = [
"bytemuck",
"comemo",
"flate2",
"image",
"pixglyph",
"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"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pdf-writer = "0.9.2"
pdf-writer = "0.9.3"
phf = { version = "0.11", features = ["macros"] }
pixglyph = "0.3"
png = "0.17"

View File

@ -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 }

View File

@ -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.

View File

@ -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.

View File

@ -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();
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 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();
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();

View File

@ -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 }

View File

@ -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,132 +241,27 @@ 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));
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 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();
}
}
/// 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.
fn render_outline_glyph(
canvas: &mut sk::Pixmap,

View File

@ -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 }

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.
pub mod color;
mod book;
mod exceptions;
mod variant;

View File

@ -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
}
}

View File

@ -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),

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