diff --git a/Cargo.lock b/Cargo.lock index 066f2381a..fa80b9ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index cf0050487..a89420948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index 99c52dc6a..d2dcd5f5c 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -22,6 +22,7 @@ bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } +indexmap = { workspace = true } miniz_oxide = { workspace = true } once_cell = { workspace = true } pdf-writer = { workspace = true } diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index 0f8b5ba01..e4b83f1dc 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -3,13 +3,16 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; +use pdf_writer::writers::FontDescriptor; use pdf_writer::{Filter, Finish, Name, Rect, Str}; use ttf_parser::{name_id, GlyphId, Tag}; +use typst::layout::{Abs, Em, Ratio, Transform}; use typst::text::Font; use typst::util::SliceExt; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; -use crate::{deflate, EmExt, PdfContext}; +use crate::page::{write_frame, PageContext}; +use crate::{deflate, AbsExt, EmExt, PdfContext}; const CFF: Tag = Tag::from_bytes(b"CFF "); const CFF2: Tag = Tag::from_bytes(b"CFF2"); @@ -23,6 +26,8 @@ const SYSTEM_INFO: SystemInfo = SystemInfo { /// Embed all used fonts into the PDF. #[typst_macros::time(name = "write fonts")] pub(crate) fn write_fonts(ctx: &mut PdfContext) { + write_color_fonts(ctx); + for font in ctx.font_map.items() { let type0_ref = ctx.alloc.bump(); let cid_ref = ctx.alloc.bump(); @@ -32,7 +37,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { ctx.font_refs.push(type0_ref); let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); - let metrics = font.metrics(); let ttf = font.ttf(); // Do we have a TrueType or CFF font? @@ -103,47 +107,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { width_writer.finish(); cid.finish(); - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref); - font_descriptor - .name(Name(base_font.as_bytes())) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - if is_cff { - font_descriptor.font_file3(data_ref); - } else { - font_descriptor.font_file2(data_ref); - } - - font_descriptor.finish(); - // Write the /ToUnicode character map, which maps glyph ids back to // unicode codepoints to enable copying out of the PDF. let cmap = create_cmap(font, glyph_set); @@ -160,9 +123,173 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) { } stream.finish(); + + let mut font_descriptor = + write_font_descriptor(&mut ctx.pdf, descriptor_ref, font, &base_font); + if is_cff { + font_descriptor.font_file3(data_ref); + } else { + font_descriptor.font_file2(data_ref); + } } } +/// Writes color fonts as Type3 fonts +fn write_color_fonts(ctx: &mut PdfContext) { + let color_font_map = ctx.color_font_map.take_map(); + for (font, color_font) in color_font_map { + // For each Type3 font that is part of this family… + for (font_index, subfont_id) in color_font.refs.iter().enumerate() { + // Allocate some IDs. + let cmap_ref = ctx.alloc.bump(); + let descriptor_ref = ctx.alloc.bump(); + let widths_ref = ctx.alloc.bump(); + // And a map between glyph IDs and the instructions to draw this + // glyph. + let mut glyphs_to_instructions = Vec::new(); + + let start = font_index * 256; + let end = (start + 256).min(color_font.glyphs.len()); + let glyph_count = end - start; + let subset = &color_font.glyphs[start..end]; + let mut widths = Vec::new(); + + let scale_factor = font.ttf().units_per_em() as f32; + + // Write the instructions for each glyph. + for color_glyph in subset { + let instructions_stream_ref = ctx.alloc.bump(); + let width = + font.advance(color_glyph.gid).unwrap_or(Em::new(0.0)).to_font_units(); + widths.push(width); + // Create a fake page context for `write_frame`. We are only + // interested in the contents of the page. + let size = color_glyph.frame.size(); + let mut page_ctx = PageContext::new(ctx, size); + page_ctx.bottom = size.y.to_f32(); + page_ctx.content.start_color_glyph(width); + page_ctx.transform( + // Make the Y axis go upwards, while preserving aspect ratio + Transform::scale(Ratio::one(), -size.aspect_ratio()) + // Also move the origin to the top left corner + .post_concat(Transform::translate(Abs::zero(), size.y)), + ); + write_frame(&mut page_ctx, &color_glyph.frame); + + // Retrieve the stream of the page and write it. + let stream = page_ctx.content.finish(); + ctx.pdf.stream(instructions_stream_ref, &stream); + + // Use this stream as instructions to draw the glyph. + glyphs_to_instructions.push(instructions_stream_ref); + } + + // Write the Type3 font object. + let mut pdf_font = ctx.pdf.type3_font(*subfont_id); + pdf_font.pair(Name(b"Resources"), ctx.type3_font_resources_ref); + pdf_font.bbox(color_font.bbox); + pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]); + pdf_font.first_char(0); + pdf_font.last_char((glyph_count - 1) as u8); + pdf_font.pair(Name(b"Widths"), widths_ref); + pdf_font.to_unicode(cmap_ref); + pdf_font.font_descriptor(descriptor_ref); + + // Write the /CharProcs dictionary, that maps glyph names to + // drawing instructions. + let mut char_procs = pdf_font.char_procs(); + for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() { + char_procs + .pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref); + } + char_procs.finish(); + + // Write the /Encoding dictionary. + let names = (0..glyph_count) + .map(|gid| eco_format!("glyph{gid}")) + .collect::>(); + pdf_font + .encoding_custom() + .differences() + .consecutive(0, names.iter().map(|name| Name(name.as_bytes()))); + pdf_font.finish(); + + // Encode a CMAP to make it possible to search or copy glyphs. + let glyph_set = ctx.glyph_sets.get_mut(&font).unwrap(); + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + for (index, glyph) in subset.iter().enumerate() { + let Some(text) = glyph_set.get(&glyph.gid) else { + continue; + }; + + if !text.is_empty() { + cmap.pair_with_multiple(index as u8, text.chars()); + } + } + ctx.pdf.cmap(cmap_ref, &cmap.finish()); + + // Write the font descriptor. + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + let base_font = eco_format!("COLOR{font_index:x}+{postscript_name}"); + write_font_descriptor(&mut ctx.pdf, descriptor_ref, &font, &base_font); + + // Write the widths array + ctx.pdf.indirect(widths_ref).array().items(widths); + } + } +} + +/// Writes a FontDescriptor dictionary. +fn write_font_descriptor<'a>( + pdf: &'a mut pdf_writer::Pdf, + descriptor_ref: pdf_writer::Ref, + font: &'a Font, + base_font: &EcoString, +) -> FontDescriptor<'a> { + let ttf = font.ttf(); + let metrics = font.metrics(); + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let global_bbox = ttf.global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = metrics.ascender.to_font_units(); + let descender = metrics.descender.to_font_units(); + let cap_height = metrics.cap_height.to_font_units(); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); + + // Write the font descriptor (contains metrics about the font). + let mut font_descriptor = pdf.font_descriptor(descriptor_ref); + font_descriptor + .name(Name(base_font.as_bytes())) + .flags(flags) + .bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v); + + font_descriptor +} + /// Subset a font to the given glyphs. /// /// - For a font with TrueType outlines, this returns the whole OpenType font. diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index e8b1c30a1..c55abcb06 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -15,13 +15,15 @@ use std::sync::Arc; use base64::Engine; use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; use pdf_writer::types::Direction; use pdf_writer::writers::Destination; -use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; +use pdf_writer::{Finish, Name, Pdf, Rect, Ref, Str, TextStr}; use typst::foundations::{Datetime, Label, NativeElement, Smart}; use typst::introspection::Location; -use typst::layout::{Abs, Dir, Em, Transform}; +use typst::layout::{Abs, Dir, Em, Frame, Transform}; use typst::model::{Document, HeadingElem}; +use typst::text::color::frame_for_glyph; use typst::text::{Font, Lang}; use typst::util::Deferred; use typst::visualize::Image; @@ -68,6 +70,7 @@ pub fn pdf( pattern::write_patterns(&mut ctx); write_named_destinations(&mut ctx); page::write_page_tree(&mut ctx); + page::write_global_resources(&mut ctx); write_catalog(&mut ctx, ident, timestamp); ctx.pdf.finish() } @@ -96,6 +99,15 @@ struct PdfContext<'a> { alloc: Ref, /// The ID of the page tree. page_tree_ref: Ref, + /// The ID of the globally shared Resources dictionary. + global_resources_ref: Ref, + /// The ID of the resource dictionary shared by Type3 fonts. + /// + /// Type3 fonts cannot use the global resources, as it would create some + /// kind of infinite recursion (they are themselves present in that + /// dictionary), which Acrobat doesn't appreciate (it fails to parse the + /// font) even if the specification seems to allow it. + type3_font_resources_ref: Ref, /// The IDs of written pages. page_refs: Vec, /// The IDs of written fonts. @@ -123,6 +135,8 @@ struct PdfContext<'a> { pattern_map: Remapper, /// Deduplicates external graphics states used across the document. extg_map: Remapper, + /// 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, + /// A list of all PDF indirect references to Type3 font objects. + all_refs: Vec, +} + +/// 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, + /// 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, + /// The global bounding box of the font. + bbox: Rect, + /// A mapping between glyph IDs and character indices in the `glyphs` + /// vector. + glyph_indices: HashMap, +} + +/// 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 { + std::mem::take(&mut self.map) + } + + /// Obtains the reference to a Type3 font, and an index in this font + /// that can be used to draw a color glyph. + /// + /// The glyphs will be de-duplicated if needed. + fn get(&mut self, alloc: &mut Ref, font: &Font, gid: u16) -> (Ref, u8) { + let color_font = self.map.entry(font.clone()).or_insert_with(|| { + let global_bbox = font.ttf().global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + ColorFont { + bbox, + refs: Vec::new(), + glyphs: Vec::new(), + glyph_indices: HashMap::new(), + } + }); + + if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) { + // If we already know this glyph, return it. + (color_font.refs[index_of_glyph / 256], *index_of_glyph as u8) + } else { + // Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font + // if needed + let index = color_font.glyphs.len(); + if index % 256 == 0 { + let new_ref = alloc.bump(); + self.all_refs.push(new_ref); + color_font.refs.push(new_ref); + } + + let instructions = frame_for_glyph(font, gid); + color_font.glyphs.push(ColorGlyph { gid, frame: instructions }); + color_font.glyph_indices.insert(gid, index); + + (color_font.refs[index / 256], index as u8) + } + } +} + /// Additional methods for [`Abs`]. trait AbsExt { /// Convert an to a number of points. diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 42358db51..621ac91fb 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -1,6 +1,10 @@ use std::collections::HashMap; use std::num::NonZeroUsize; +use crate::color::PaintEncode; +use crate::extg::ExtGState; +use crate::image::deferred_image; +use crate::{deflate_deferred, AbsExt, EmExt, PdfContext}; use ecow::{eco_format, EcoString}; use pdf_writer::types::{ ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle, @@ -13,17 +17,13 @@ use typst::layout::{ Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform, }; use typst::model::{Destination, Numbering}; -use typst::text::{Case, Font, TextItem}; -use typst::util::{Deferred, Numeric}; +use typst::text::color::is_color_glyph; +use typst::text::{Case, Font, TextItem, TextItemView}; +use typst::util::{Deferred, Numeric, SliceExt}; use typst::visualize::{ FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, }; -use crate::color::PaintEncode; -use crate::extg::ExtGState; -use crate::image::deferred_image; -use crate::{deflate_deferred, AbsExt, EmExt, PdfContext}; - /// Construct page objects. #[typst_macros::time(name = "construct pages")] pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) { @@ -44,17 +44,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod let page_ref = ctx.alloc.bump(); let size = frame.size(); - let mut ctx = PageContext { - parent: ctx, - page_ref, - uses_opacities: false, - content: Content::new(), - state: State::new(size), - saves: vec![], - bottom: 0.0, - links: vec![], - resources: HashMap::default(), - }; + let mut ctx = PageContext::new(ctx, size); // Make the coordinate system start at the top-left. ctx.bottom = size.y.to_f32(); @@ -73,7 +63,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod let page = EncodedPage { size, content: deflate_deferred(ctx.content.finish()), - id: ctx.page_ref, + id: page_ref, uses_opacities: ctx.uses_opacities, links: ctx.links, label: None, @@ -85,10 +75,8 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod /// Write the page tree. pub(crate) fn write_page_tree(ctx: &mut PdfContext) { - let resources_ref = write_global_resources(ctx); - for i in 0..ctx.pages.len() { - write_page(ctx, i, resources_ref); + write_page(ctx, i); } ctx.pdf @@ -102,30 +90,20 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) { /// We add a reference to this dictionary to each page individually instead of /// to the root node of the page tree because using the resource inheritance /// feature breaks PDF merging with Apple Preview. -fn write_global_resources(ctx: &mut PdfContext) -> Ref { - let resource_ref = ctx.alloc.bump(); +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::(); - 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.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.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, - 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, } +impl<'a, 'b> PageContext<'a, 'b> { + pub fn new(parent: &'a mut PdfContext<'b>, size: Size) -> Self { + PageContext { + parent, + uses_opacities: false, + content: Content::new(), + state: State::new(size), + saves: vec![], + bottom: 0.0, + links: vec![], + resources: HashMap::default(), + } + } +} + /// A simulated graphics state used to deduplicate graphics state changes and /// keep track of the current transformation matrix for link annotations. #[derive(Debug, Clone)] @@ -555,7 +585,7 @@ impl PageContext<'_, '_> { self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); } - fn transform(&mut self, transform: Transform) { + pub fn transform(&mut self, transform: Transform) { let Transform { sx, ky, kx, sy, tx, ty } = transform; self.state.transform = self.state.transform.pre_concat(transform); if self.state.container_transform.is_identity() { @@ -670,7 +700,7 @@ impl PageContext<'_, '_> { } /// Encode a frame into the content stream. -fn write_frame(ctx: &mut PageContext, frame: &Frame) { +pub(crate) fn write_frame(ctx: &mut PageContext, frame: &Frame) { for &(pos, ref item) in frame.items() { let x = pos.x.to_f32(); let y = pos.y.to_f32(); @@ -718,21 +748,71 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { /// Encode a text run into the content stream. fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { + let ttf = text.font.ttf(); + let tables = ttf.tables(); + + // If the text run contains either only color glyphs (used for emojis for + // example) or normal text we can render it directly + let has_color_glyphs = tables.sbix.is_some() + || tables.cbdt.is_some() + || tables.svg.is_some() + || tables.colr.is_some(); + if !has_color_glyphs { + write_normal_text(ctx, pos, TextItemView::all_of(text)); + return; + } + + let color_glyph_count = + text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count(); + + if color_glyph_count == text.glyphs.len() { + write_color_glyphs(ctx, pos, TextItemView::all_of(text)); + } else if color_glyph_count == 0 { + write_normal_text(ctx, pos, TextItemView::all_of(text)); + } else { + // Otherwise we need to split it in smaller text runs + let mut offset = 0; + let mut position_in_run = Abs::zero(); + for (color, sub_run) in + text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g)) + { + let end = offset + sub_run.len(); + + // Build a sub text-run + let text_item_view = TextItemView::from_glyph_range(text, offset..end); + + // Adjust the position of the run on the line + let pos = pos + Point::new(position_in_run, Abs::zero()); + position_in_run += text_item_view.width(); + offset = end; + // Actually write the sub text-run + if color { + write_color_glyphs(ctx, pos, text_item_view); + } else { + write_normal_text(ctx, pos, text_item_view); + } + } + } +} + +// Encodes a text run (without any color glyph) into the content stream. +fn write_normal_text(ctx: &mut PageContext, pos: Point, text: TextItemView) { let x = pos.x.to_f32(); let y = pos.y.to_f32(); - *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); + *ctx.parent.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len(); - let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); - for g in &text.glyphs { - let segment = &text.text[g.range()]; + let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default(); + for g in text.glyphs() { + let t = text.text(); + let segment = &t[g.range()]; glyph_set.entry(g.id).or_insert_with(|| segment.into()); } let fill_transform = ctx.state.transforms(Size::zero(), pos); - ctx.set_fill(&text.fill, true, fill_transform); + ctx.set_fill(&text.item.fill, true, fill_transform); - let stroke = text.stroke.as_ref().and_then(|stroke| { + let stroke = text.item.stroke.as_ref().and_then(|stroke| { if stroke.thickness.to_f32() > 0.0 { Some(stroke) } else { @@ -747,8 +827,8 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { ctx.set_text_rendering_mode(TextRenderingMode::Fill); } - ctx.set_font(&text.font, text.size); - ctx.set_opacities(text.stroke.as_ref(), Some(&text.fill)); + ctx.set_font(&text.item.font, text.item.size); + ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill)); ctx.content.begin_text(); // Position the text. @@ -760,7 +840,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { let mut encoded = vec![]; // Write the glyphs with kerning adjustments. - for glyph in &text.glyphs { + for glyph in text.glyphs() { adjustment += glyph.x_offset; if !adjustment.is_zero() { @@ -773,11 +853,11 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { adjustment = Em::zero(); } - let cid = crate::font::glyph_cid(&text.font, glyph.id); + let cid = crate::font::glyph_cid(&text.item.font, glyph.id); encoded.push((cid >> 8) as u8); encoded.push((cid & 0xff) as u8); - if let Some(advance) = text.font.advance(glyph.id) { + if let Some(advance) = text.item.font.advance(glyph.id) { adjustment += glyph.x_advance - advance; } @@ -793,6 +873,46 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { ctx.content.end_text(); } +// Encodes a text run made only of color glyphs into the content stream +fn write_color_glyphs(ctx: &mut PageContext, pos: Point, text: TextItemView) { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + + let mut last_font = None; + + ctx.content.begin_text(); + ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); + // So that the next call to ctx.set_font() will change the font to one that + // displays regular glyphs and not color glyphs. + ctx.state.font = None; + + let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default(); + + for glyph in text.glyphs() { + // Retrieve the Type3 font reference and the glyph index in the font. + let (font, index) = ctx.parent.color_font_map.get( + &mut ctx.parent.alloc, + &text.item.font, + glyph.id, + ); + + if last_font != Some(font.get()) { + ctx.content.set_font( + Name(eco_format!("Cf{}", font.get()).as_bytes()), + text.item.size.to_f32(), + ); + last_font = Some(font.get()); + } + + ctx.content.show(Str(&[index])); + + glyph_set + .entry(glyph.id) + .or_insert_with(|| text.text()[glyph.range()].into()); + } + ctx.content.end_text(); +} + /// Encode a geometrical shape into the content stream. fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { let x = pos.x.to_f32(); diff --git a/crates/typst-render/Cargo.toml b/crates/typst-render/Cargo.toml index cc58f785f..56a18e80c 100644 --- a/crates/typst-render/Cargo.toml +++ b/crates/typst-render/Cargo.toml @@ -18,7 +18,6 @@ typst-macros = { workspace = true } typst-timing = { workspace = true } bytemuck = { workspace = true } comemo = { workspace = true } -flate2 = { workspace = true } image = { workspace = true } pixglyph = { workspace = true } resvg = { workspace = true } diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 28302180a..401c70266 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -1,12 +1,10 @@ //! Rendering of Typst documents into raster images. -use std::io::Read; use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use pixglyph::Bitmap; -use resvg::tiny_skia::IntRect; use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use typst::introspection::Meta; @@ -14,12 +12,12 @@ use typst::layout::{ Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, }; use typst::model::Document; +use typst::text::color::{frame_for_glyph, is_color_glyph}; use typst::text::{Font, TextItem}; use typst::visualize::{ Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, - LineJoin, Paint, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape, + LineJoin, Paint, Path, PathItem, Pattern, RelativeTo, Shape, }; -use usvg::TreeParsing; /// Export a frame into a raster image. /// @@ -115,6 +113,13 @@ impl<'a> State<'a> { } } + fn pre_scale(self, scale: Axes) -> 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, diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index cfc4e32e4..8e5e224ae 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -24,6 +24,7 @@ ciborium = { workspace = true } comemo = { workspace = true } csv = { workspace = true } ecow = { workspace = true } +flate2 = { workspace = true } fontdb = { workspace = true } hayagriva = { workspace = true } hypher = { workspace = true } @@ -64,6 +65,7 @@ unicode-bidi = { workspace = true } unicode-math-class = { workspace = true } unicode-script = { workspace = true } unicode-segmentation = { workspace = true } +unscanny = { workspace = true } usvg = { workspace = true } wasmi = { workspace = true } diff --git a/crates/typst/src/text/font/color.rs b/crates/typst/src/text/font/color.rs new file mode 100644 index 000000000..2dfd5545a --- /dev/null +++ b/crates/typst/src/text/font/color.rs @@ -0,0 +1,272 @@ +//! Utilities for color font handling + +use std::io::Read; + +use ecow::EcoString; +use ttf_parser::GlyphId; +use usvg::{TreeParsing, TreeWriting}; + +use crate::layout::{Abs, Axes, Em, Frame, FrameItem, Point, Size}; +use crate::syntax::Span; +use crate::text::{Font, Glyph, Lang, TextItem}; +use crate::visualize::{Color, Image, Paint, Rgb}; + +/// Tells if a glyph is a color glyph or not in a given font. +pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool { + let ttf = font.ttf(); + let glyph_id = GlyphId(g.id); + ttf.glyph_raster_image(glyph_id, 160).is_some() + || ttf.glyph_svg_image(glyph_id).is_some() + || ttf.is_color_glyph(glyph_id) +} + +/// Returns a frame with the glyph drawn inside. +/// +/// The glyphs are sized in font units, [`text.item.size`] is not taken into +/// account. +#[comemo::memoize] +pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame { + let ttf = font.ttf(); + let upem = Abs::pt(ttf.units_per_em() as f64); + let glyph_id = GlyphId(glyph_id); + + let mut frame = Frame::soft(Size::splat(upem)); + + if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) { + draw_raster_glyph(&mut frame, font, upem, raster_image); + } else if ttf.glyph_svg_image(glyph_id).is_some() { + draw_svg_glyph(&mut frame, upem, font, glyph_id); + } else if ttf.is_color_glyph(glyph_id) { + draw_colr_glyph(&mut frame, font, glyph_id); + } + + frame +} + +/// Draws a raster glyph in a frame. +fn draw_raster_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + raster_image: ttf_parser::RasterGlyphImage, +) { + let image = Image::new( + raster_image.data.into(), + typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png), + None, + ) + .unwrap(); + + // Apple Color emoji doesn't provide offset information (or at least + // not in a way ttf-parser understands), so we artificially shift their + // baseline to make it look good. + let y_offset = if font.info().family.to_lowercase() == "apple color emoji" { + 20.0 + } else { + -(raster_image.y as f64) + }; + + let position = Point::new( + upem * raster_image.x as f64 / raster_image.pixels_per_em as f64, + upem * y_offset / raster_image.pixels_per_em as f64, + ); + let aspect_ratio = image.width() / image.height(); + let size = Axes::new(upem, upem * aspect_ratio); + frame.push(position, FrameItem::Image(image, size, Span::detached())); +} + +/// Draws a COLR glyph in a frame. +fn draw_colr_glyph(frame: &mut Frame, font: &Font, glyph_id: GlyphId) { + let mut painter = ColrPainter { font, current_glyph: glyph_id, frame }; + font.ttf().paint_color_glyph(glyph_id, 0, &mut painter); +} + +/// Draws COLR glyphs in a frame. +struct ColrPainter<'f, 't> { + /// The frame in which to draw. + frame: &'f mut Frame, + /// The font of the text. + font: &'t Font, + /// The glyph that will be drawn the next time `ColrPainter::paint` is called. + current_glyph: GlyphId, +} + +impl<'f, 't> ColrPainter<'f, 't> { + fn paint(&mut self, fill: Paint) { + self.frame.push( + // With images, the position corresponds to the top-left corner, but + // in the case of text it matches the baseline-left point. Here, we + // move the glyph one unit down to compensate for that. + Point::new(Abs::zero(), Abs::pt(self.font.units_per_em())), + FrameItem::Text(TextItem { + font: self.font.clone(), + size: Abs::pt(self.font.units_per_em()), + fill, + stroke: None, + lang: Lang::ENGLISH, + text: EcoString::new(), + glyphs: vec![Glyph { + id: self.current_glyph.0, + // Advance is not relevant here as we will draw glyph on top + // of each other anyway + x_advance: Em::zero(), + x_offset: Em::zero(), + range: 0..0, + span: (Span::detached(), 0), + }], + }), + ) + } +} + +impl<'f, 't> ttf_parser::colr::Painter for ColrPainter<'f, 't> { + fn outline(&mut self, glyph_id: GlyphId) { + self.current_glyph = glyph_id; + } + + fn paint_foreground(&mut self) { + // Default to black if no color was specified + self.paint(Paint::Solid(Color::BLACK)) + } + + fn paint_color(&mut self, color: ttf_parser::RgbaColor) { + let color = Color::Rgb(Rgb::new( + color.red as f32 / 255.0, + color.green as f32 / 255.0, + color.blue as f32 / 255.0, + color.alpha as f32 / 255.0, + )); + self.paint(Paint::Solid(color)); + } +} + +/// Draws an SVG glyph in a frame. +fn draw_svg_glyph( + frame: &mut Frame, + upem: Abs, + font: &Font, + glyph_id: GlyphId, +) -> Option<()> { + let mut data = font.ttf().glyph_svg_image(glyph_id)?.data; + + // Decompress SVGZ. + let mut decoded = vec![]; + if data.starts_with(&[0x1f, 0x8b]) { + let mut decoder = flate2::read::GzDecoder::new(data); + decoder.read_to_end(&mut decoded).ok()?; + data = &decoded; + } + + // Parse XML. + let xml = std::str::from_utf8(data).ok()?; + let document = roxmltree::Document::parse(xml).ok()?; + + // Parse SVG. + let opts = usvg::Options::default(); + let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; + + // Compute the space we need to draw our glyph. + // See https://github.com/RazrFalcon/resvg/issues/602 for why + // using the svg size is problematic here. + tree.calculate_bounding_boxes(); + let mut bbox = usvg::BBox::default(); + if let Some(tree_bbox) = tree.root.bounding_box { + bbox = bbox.expand(tree_bbox); + } + let bbox = bbox.to_rect()?; + + let mut data = tree.to_string(&usvg::XmlOptions::default()); + + let width = bbox.width() as f64; + let height = bbox.height() as f64; + let left = bbox.left() as f64; + let top = bbox.top() as f64; + + // The SVG coordinates and the font coordinates are not the same: the Y axis + // is mirrored. But the origin of the axes are the same (which means that + // the horizontal axis in the SVG document corresponds to the baseline). See + // the reference for more details: + // https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics + // + // If we used the SVG document as it is, svg2pdf would produce a cropped + // glyph (only what is under the baseline would be visible). So we need to + // embed the original SVG in another one that has the exact dimensions of + // the glyph, with a transform to make it fit. We also need to remove the + // viewBox, height and width attributes from the inner SVG, otherwise usvg + // takes into account these values to clip the embedded SVG. + make_svg_unsized(&mut data); + let wrapper_svg = format!( + r#" + + + {inner} + + + "#, + 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("') && !s.done() { + s.eat_whitespace(); + let start = s.cursor(); + let attr_name = s.eat_until('=').trim(); + // Eat the equal sign and the quote. + s.eat(); + s.eat(); + let mut escaped = false; + while (escaped || !s.eat_if('"')) && !s.done() { + escaped = s.eat() == Some('\\'); + } + match attr_name { + "viewBox" => viewbox_range = Some(start..s.cursor()), + "width" => width_range = Some(start..s.cursor()), + "height" => height_range = Some(start..s.cursor()), + _ => {} + } + } + + // Remove the `viewBox` attribute. + if let Some(range) = viewbox_range { + svg.replace_range(range.clone(), &" ".repeat(range.len())); + } + + // Remove the `width` attribute. + if let Some(range) = width_range { + svg.replace_range(range.clone(), &" ".repeat(range.len())); + } + + // Remove the `height` attribute. + if let Some(range) = height_range { + svg.replace_range(range, ""); + } +} diff --git a/crates/typst/src/text/font/mod.rs b/crates/typst/src/text/font/mod.rs index 42a87b7ec..701118136 100644 --- a/crates/typst/src/text/font/mod.rs +++ b/crates/typst/src/text/font/mod.rs @@ -1,5 +1,7 @@ //! Font handling. +pub mod color; + mod book; mod exceptions; mod variant; diff --git a/crates/typst/src/text/item.rs b/crates/typst/src/text/item.rs index 44d8e63a1..4bc6dd212 100644 --- a/crates/typst/src/text/item.rs +++ b/crates/typst/src/text/item.rs @@ -65,3 +65,65 @@ impl Glyph { usize::from(self.range.start)..usize::from(self.range.end) } } + +/// A slice of a [`TextItem`]. +pub struct TextItemView<'a> { + /// The whole item this is a part of + pub item: &'a TextItem, + /// The glyphs of this slice + pub glyph_range: Range, +} + +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) -> 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 + '_ { + (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::() + .at(self.item.size) + } + + /// The range of text in the original TextItem that this slice corresponds + /// to. + fn text_range(&self) -> Range { + let text_start = self.item.glyphs[self.glyph_range.start].range().start; + let text_end = self.item.glyphs[self.glyph_range.end - 1].range().end; + text_start..text_end + } +} diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst/src/visualize/image/svg.rs index 9685e4547..d5cae6fe0 100644 --- a/crates/typst/src/visualize/image/svg.rs +++ b/crates/typst/src/visualize/image/svg.rs @@ -30,7 +30,9 @@ impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] pub fn new(data: Bytes) -> StrResult { - 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), diff --git a/tests/ref/block-clip-svg-glyphs.png b/tests/ref/block-clip-svg-glyphs.png index d8db5b61e..182fd4189 100644 Binary files a/tests/ref/block-clip-svg-glyphs.png and b/tests/ref/block-clip-svg-glyphs.png differ diff --git a/tests/ref/escape.png b/tests/ref/escape.png index 0b49606ca..395dbb777 100644 Binary files a/tests/ref/escape.png and b/tests/ref/escape.png differ diff --git a/tests/ref/eval-in-show-rule.png b/tests/ref/eval-in-show-rule.png index 91a038683..b4a802977 100644 Binary files a/tests/ref/eval-in-show-rule.png and b/tests/ref/eval-in-show-rule.png differ diff --git a/tests/ref/heading-show-where.png b/tests/ref/heading-show-where.png index 609e6ec9a..4edbfaf9d 100644 Binary files a/tests/ref/heading-show-where.png and b/tests/ref/heading-show-where.png differ diff --git a/tests/ref/issue-80-emoji-linebreak.png b/tests/ref/issue-80-emoji-linebreak.png index d35a62b3c..45128986e 100644 Binary files a/tests/ref/issue-80-emoji-linebreak.png and b/tests/ref/issue-80-emoji-linebreak.png differ diff --git a/tests/ref/loop-break-join-in-nested-blocks.png b/tests/ref/loop-break-join-in-nested-blocks.png index 6e2af47a6..143b8c6a9 100644 Binary files a/tests/ref/loop-break-join-in-nested-blocks.png and b/tests/ref/loop-break-join-in-nested-blocks.png differ diff --git a/tests/ref/math-font-fallback.png b/tests/ref/math-font-fallback.png index 50fa85c7e..c840a0645 100644 Binary files a/tests/ref/math-font-fallback.png and b/tests/ref/math-font-fallback.png differ diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index 236b9989e..be4770740 100644 Binary files a/tests/ref/math-frac-precedence.png and b/tests/ref/math-frac-precedence.png differ diff --git a/tests/ref/math-nested-normal-layout.png b/tests/ref/math-nested-normal-layout.png index 4ec7d46eb..8e7d21083 100644 Binary files a/tests/ref/math-nested-normal-layout.png and b/tests/ref/math-nested-normal-layout.png differ diff --git a/tests/ref/repr-misc.png b/tests/ref/repr-misc.png index 9a876091a..699cb5611 100644 Binary files a/tests/ref/repr-misc.png and b/tests/ref/repr-misc.png differ diff --git a/tests/ref/shaping-emoji-bad-zwj.png b/tests/ref/shaping-emoji-bad-zwj.png index 544d64eea..b80e0d5a6 100644 Binary files a/tests/ref/shaping-emoji-bad-zwj.png and b/tests/ref/shaping-emoji-bad-zwj.png differ diff --git a/tests/ref/shaping-emoji-basic.png b/tests/ref/shaping-emoji-basic.png index 090ea6111..9874ccdec 100644 Binary files a/tests/ref/shaping-emoji-basic.png and b/tests/ref/shaping-emoji-basic.png differ diff --git a/tests/ref/shaping-font-fallback.png b/tests/ref/shaping-font-fallback.png index 813e39151..fc5e0bff3 100644 Binary files a/tests/ref/shaping-font-fallback.png and b/tests/ref/shaping-font-fallback.png differ diff --git a/tests/ref/show-in-show.png b/tests/ref/show-in-show.png index 65280ad7b..c4a4d2bb7 100644 Binary files a/tests/ref/show-in-show.png and b/tests/ref/show-in-show.png differ diff --git a/tests/ref/show-selector-realistic.png b/tests/ref/show-selector-realistic.png index ae4f4a9a8..8c0f46d7e 100644 Binary files a/tests/ref/show-selector-realistic.png and b/tests/ref/show-selector-realistic.png differ diff --git a/tests/ref/show-text-in-other-show.png b/tests/ref/show-text-in-other-show.png index f29de999b..c57a0d2a6 100644 Binary files a/tests/ref/show-text-in-other-show.png and b/tests/ref/show-text-in-other-show.png differ diff --git a/tests/ref/show-text-regex-case-insensitive.png b/tests/ref/show-text-regex-case-insensitive.png index 70d70d343..85b488bd8 100644 Binary files a/tests/ref/show-text-regex-case-insensitive.png and b/tests/ref/show-text-regex-case-insensitive.png differ diff --git a/tests/ref/show-text-regex-word-boundary.png b/tests/ref/show-text-regex-word-boundary.png index c171ac027..011d9935d 100644 Binary files a/tests/ref/show-text-regex-word-boundary.png and b/tests/ref/show-text-regex-word-boundary.png differ diff --git a/tests/ref/stack-fr.png b/tests/ref/stack-fr.png index e34dd9b11..40685731f 100644 Binary files a/tests/ref/stack-fr.png and b/tests/ref/stack-fr.png differ diff --git a/tests/ref/symbol.png b/tests/ref/symbol.png index 37339d591..d0cde870f 100644 Binary files a/tests/ref/symbol.png and b/tests/ref/symbol.png differ diff --git a/tests/ref/text-copy-paste-ligatures.png b/tests/ref/text-copy-paste-ligatures.png index f0f36a869..74f49e27e 100644 Binary files a/tests/ref/text-copy-paste-ligatures.png and b/tests/ref/text-copy-paste-ligatures.png differ diff --git a/tests/ref/text-font-properties.png b/tests/ref/text-font-properties.png index 3c65fa33c..fda921942 100644 Binary files a/tests/ref/text-font-properties.png and b/tests/ref/text-font-properties.png differ