From 21c78abd6eecd0f6b3208405c7513be3bbd8991c Mon Sep 17 00:00:00 2001 From: Ana Gelez Date: Wed, 17 Apr 2024 17:11:20 +0200 Subject: [PATCH] Emojis in PDF (#3853) --- Cargo.lock | 8 +- Cargo.toml | 2 +- crates/typst-pdf/Cargo.toml | 1 + crates/typst-pdf/src/font.rs | 213 +++++++++++--- crates/typst-pdf/src/lib.rs | 115 +++++++- crates/typst-pdf/src/page.rs | 240 ++++++++++++---- crates/typst-render/Cargo.toml | 1 - crates/typst-render/src/lib.rs | 148 ++-------- crates/typst/Cargo.toml | 2 + crates/typst/src/text/font/color.rs | 272 ++++++++++++++++++ crates/typst/src/text/font/mod.rs | 2 + crates/typst/src/text/item.rs | 62 ++++ crates/typst/src/visualize/image/svg.rs | 4 +- tests/ref/block-clip-svg-glyphs.png | Bin 1980 -> 1855 bytes tests/ref/escape.png | Bin 3916 -> 3907 bytes tests/ref/eval-in-show-rule.png | Bin 1191 -> 1199 bytes tests/ref/heading-show-where.png | Bin 2349 -> 2345 bytes tests/ref/issue-80-emoji-linebreak.png | Bin 211 -> 213 bytes .../ref/loop-break-join-in-nested-blocks.png | Bin 1043 -> 1039 bytes tests/ref/math-font-fallback.png | Bin 400 -> 402 bytes tests/ref/math-frac-precedence.png | Bin 3867 -> 3877 bytes tests/ref/math-nested-normal-layout.png | Bin 1253 -> 1313 bytes tests/ref/repr-misc.png | Bin 7440 -> 7442 bytes tests/ref/shaping-emoji-bad-zwj.png | Bin 647 -> 685 bytes tests/ref/shaping-emoji-basic.png | Bin 952 -> 948 bytes tests/ref/shaping-font-fallback.png | Bin 3823 -> 3702 bytes tests/ref/show-in-show.png | Bin 638 -> 640 bytes tests/ref/show-selector-realistic.png | Bin 3867 -> 3793 bytes tests/ref/show-text-in-other-show.png | Bin 758 -> 735 bytes .../ref/show-text-regex-case-insensitive.png | Bin 3771 -> 3676 bytes tests/ref/show-text-regex-word-boundary.png | Bin 1655 -> 1583 bytes tests/ref/stack-fr.png | Bin 2205 -> 2203 bytes tests/ref/symbol.png | Bin 1542 -> 1544 bytes tests/ref/text-copy-paste-ligatures.png | Bin 1127 -> 1125 bytes tests/ref/text-font-properties.png | Bin 6926 -> 6913 bytes 35 files changed, 835 insertions(+), 235 deletions(-) create mode 100644 crates/typst/src/text/font/color.rs 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 d8db5b61efa324039dacb0d5a604d92d2c8bb766..182fd418933237c54314cbc29ee279de2683e963 100644 GIT binary patch delta 1840 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 bA4oe3{D6w5G~6A!1o)$+rU$K7v5ojIp=f6T delta 3891 zcmX|^cRbba|Hjp!V}xU`IGN|zLRK=5;$&~KS5(%q=i7{9kCRQDI7-MSk}WGbpF>GT zLdf1*-_z&!dHnAG@B4ATulu^5uj}2zHNus~2QX+MRE-0tHVT(Ljm_Bm_rUAfGl+Do zYX{;9xK1TLo*ts*gg{Wm$Z=oEAfb4c!%DABXTk`=nSpQt5}$HrLq7Uy+XippWQ|5I zF}xiLt&T2=Nic_;qjcw)kVn6!_6mIFd}c478Jvp-stf94pS#A=S0RM)l>C-xQ^wl> z9!zsXB2pq~cX5Az|3S9cH-dT`4K;_%)4>e>{;sNr&2{zl8$0iOmi^Z!?I|O9@24QB z@X=se8KucO=i%2z<%# zGAict%d;&vH@E5Y16VK~s{zw3*tP<0$fHWVw|i)n1828a@VvBm+pvQPLb=|Ow4mUi z&v&g%ftmJ@fX(T5sMvJZsfN{81~0J=_oVU_Z4XZNmS5garO8_H?6_d&T9W4Gp6`yO zLMizD9{)Y@hIoVHueLyYz*bTFK-Z^y*|8VOo|KViM_asad2(EBS8KyBPJ4hjTCj$6 z!R0rCI?RxJ3KQ%&jrW&6p!p~hw_jY$}xUnZ+PuO{6`;*uhMDQeB@8IQ{ z==w)vL#wmxAwDQne+uUl8)l*)JJ!v)eSe0*V@T5VmrHjlt=kS)@Gr$xrFkYVDs=Ls zyX#v(KN>4y1CN-};&*_ZolW&m{!xXE6!gMo z)dl&fOT*ds3ti+wkG&YPhO@;JyCv;9BZBsRv4KMl*Hs;+9A|TLbKRoA3ixlKH+@XV z%;0!r1HJp?BnQ;0J$T~%RTWR)D*kjG-)u4K&*DiS%<7k_hxNtp?#M8vB_$;htuvI8 zFDxMK1uzzW_4euwHiE(ZQwu6?OWypEqgxe8pKz=p_ve<2<+ zlSs6tpNr!S+Nr{Pj zp$W1rO*9d@!!O_un?%0UYVugFBWr7G3l;o=cNftVq`4NvEx)H4h4VccxXcRG%a>or z(U~>&sK|w#`X+IUS~f(qDPJCgu(z%Z-L%)BFVu`!9dd*@G)#Vl2?&f9$V+v{Gdpt3 zT>&iYDZ#$)pul-Log~?n!Ib&&+ola}glk5|rl!omKwdrFsq&w5DS}Z0V)|;)+4^~@6xTu@iDXqnsd7i9v;9mu*(Dr$$p!o# zKNfls<@&S)Ua%Ps8cSKeWD+ulkTLPWU;ytlqT=RfUbJf|>!^+oS|Yw9hV#63(I`2!{Gd>aK%QyK6>26LMTDQ+fx3P$XWgLf#tM(wnqvjr4Z8A~x?^ zIHJa1-ltu?z)59d$GZ%q?+-)b6qh;pGrs8X;o+y}j_iyS!Ulwvs?gUY0?xO+eBl6Q>>sl*IiYMmORWyeKd1i zCFB#M=@(-%Ga@|8RM4i_|Gt1H-t}qVhXif?)vu+|!uV$+*U6f2)7Q;3UQbA>W6UDzc+-zA-m|V`eQ?aoBN@ozvi9Klua^ zv|Am2iuj->#juc3Q5p3@YLO^IPv^b_rkLXUocOkYt=SJ@ZA@eg0@~Y{nHnGeCEIPj z0S1o;&e!ve_;(r&w-PM2c2f>aehZ}4s?8$D=|s!I2g*84m!$}xXo&Z(z)sqP78Y^m zeF&$y2PwcG)H|OTGNR`SI(cYte}A;YZt0o;pxpQToKZ2uPfxP--0316qs3)4PWQ

Y2jW&&W zz5-6&J+A#okfre~uo;XY8Jo#V0xEF`$Dcv|@<)fc!?L5cZMjE_f@bVElZuJ9ky)dM z$xu>2mjCqI5=Zj}?f8xMmbWg=h7)a<%}?$zEP%VFtL`BwHgVXVmYkMgh=|$Ru4>_j zCG7#PEgXVld%d^ez5C2w)4_=LS+U#E7SFUwJ70o$N96{3*N5d%xhSQ_#YcZpAW=)M^CGKju|4B)I9mHPa-! z=hF-qI32yL%0-VI-Hv8CBa|bxf(&@<`hP`Zm<{ume2GCXY7@=>-034bRm@#JPx$uL>Jju~dIYq3%4batZ^{|h{hGj^+n)K~!gZ3wjnR-c}V4<+% z`MAZ#LV2HG`JNw2%)WF+f>zyivYF=&Et~EH$I)=QdPXy1$7rLM{X3Vb7kgD3e2bZ} zZUX>46M}GFZdzrjuU}y-nD4dX-^3f8Fk&HT_4J4fMa8c%+Dt3;&ll!^XpdKXZB33J zQx1du{bibk(p)n*2H%HVpm9@qI+>R3!MB<0sJ$#FzgAipSzhPvm5)U%fOcncj`>ik z4V>Js`0Nu8_X|>>%0-)d(uAK2R*cxPJU)Ow+|~!1Th(wxN1h@aB7x-frfq zM_-6}TiUgZC^%2fzR10+@+xQ@1lS=)TjMl#&qXUS(>ZfDUG!m75)Ye>2R9~N?Wxpe z3H?#=6MnBM3{q1Uh5&2(N!zMy3s zG{|nP+TZRoP(cL0;>!Jzo|)Oh%5q}WMICbmmV_X{PKU)i_Ah7^tqVS9FfcI4cNQbx z_{^&KYn1C2YJY|-_-zAeQ99hwf*Dy^&zNS59N-^yA_;MG^E~Vi^B~EEMRk#B1C=H3 zApL=CP68<6_}ezK5BO!;RI8RQW()xiDb*yh2a;VthfMnOf{$V~vMALaKW*QFMwP>Wb5w(Stcm8`X{koDROMre?wsH9sjs<3*7W*jQvG&<4et zNS)t_AU7n&-?xia;Uq+TtdqCn)>6elcvK-3VMSJ6KKFZ_ya6^j0tdY~ z1rB&q9yGnG+dH7X39G|MTxTE;Wp)o=`CNkBr&5Kp;Q0N!KIPr+IgO-OtrTQnOMXUu zW52}0t1~NWhQl6TSN4*Z%uz*-?HNpl37)%Bfb{L^oE~xFGiMA+ra;e#5em>+#=NCH zb{kWT22S%k?MJh`wQAktw5@Kx8mQhtNwMm0M9unP@lBIAg`yD&W?U=KEk5 z7o54;A_<86R+}Pa&Wi7?>(>X=YQ?NurO85Xvg?ZC&3olZMl52YA4qFmmC3l_SKy+m zNQv~dvrAU@wX;#}c7Unt+Zp#fL%ZkgDh-fgi&o{kFu+<>GdbrY%8rv?`I?VrfoSi9 zG^?l5NZ*pAT#>M!6umWWRm^g{UhXx27Ff4ssypxeAX($5FK`jdxu_|uFA&1dtHn|> zx#smL@!h@4xmtpVqgwwn8S|qg;t3PyzkgN#7UF;78%xibj&S@MFayWPODJ_xK%mf6 z)t+R^UcM?umIT)V!5CnkSW+~VJ)=g@y4i=TVl{UX-Pw&E*E;kaS22Qby(BhpPuig@ z(FYQ&T$5MdblU~=JLM5;JSgb3vOUM>KN`7&%1RLqmjOSI?g+Yw3IqoWsl-(DXA4m=VP(Esd2IZv*2G}(ZklV)C>`PbV}6Rx6T@Z?>xXkOX-*G^s+|TijBO3E z?HaCjlm>P_ag|1t!;;kuhfE#~cl%OBQso@XH3Sn7z6w0~^Y+)%_{Jfb!SGhrXPhlJ w=me3zfssJg@8ppHBI5-9A9DXzEAp5>pny$<63gV&6%yjo($GaztJy{U58WVd8~^|S diff --git a/tests/ref/eval-in-show-rule.png b/tests/ref/eval-in-show-rule.png index 91a038683f5cc1df8368b33bb367b6151dbf7e3b..b4a80297715bbf89b826c957e6e2246a235ffc3e 100644 GIT binary patch delta 880 zcmV-$1CRWt39ku|Bmsz#B_Krl{`L9(`1ttq`2P3z_xASo@A&@l_x+9>0i6<4%==B7Ro zp33pSz`&Ku^uNEqzP`S_y}g#k@4URcySuxM$MTas0Vg4^udlAIuC1-DtgNi7tE+pr z>#C}%c(v=Nr>AtY>T9;}lZycxFkr9io}Qkaotk7RCwC$)ah4DaU93-x1y0mMvS?ZF?Pn9C4?-Q?Ac`t zi9|9OOHm=qor@4zCfPNbT#W3p(~y0Mf5$oVv`3vY-CO7UUjM-Rd>?#22!bFqWqW@+ z3Fs6?9R?F~&eK3@A)wkq9o`f$&_I5Yb8FvA`_OO2e}iKSuJ*{deT-M>d{^x&8Hw9MlP{b!EOi70Z|-jQbUCbL52xs$IP-fHv{ zO^yCO$)=OFftV4Dcqi_%=`M)P&jZq__#D(7*Kh$f`%Yw?+O*z-IUX zH7#B4e__A6Sk2{W#?(q)%;k$IdqHo4YUkr^+4n}AtgvhTnu``F+{WjzgO@43`wib4Q}awijubqPgAj z#Rup?mgTB;gip~UeCpw~6S5xR0JIiAGaz2fiXaG$+<)+3j6(%A{sI61002ovPDHLk G0$_rcSos+M delta 876 zcmV-y1C#u(38x8=BmshvB_I*`{`mO#^Z5Sv_xJYp_VM@qkzW-a>GJvT@bK^N@8#KXhG!otFy%JHnm*}%ZSzrVk}zP`P^y_UxByu7@-yStN2 z0ViyJz3s2Budc4Ht*xzfyz#88taiHXtE;Pfx9h5^s(7{Qr>CcMv+Aa%rlqB&aIxy6 zqoZ!H>20s+p`oFmprD_hpPrtcot>Rzs_2`Wn_;QvRjlq;tLvDUm|dplT&Cw*r{R{C zmRhCem6er~-vJdxjg5^`VMV&Z(aDsB@-!>zv=~A9$bdgYO4H5QOGz&u=F+ zI)zY=!Ni<1G?Hp+Ra&UWn*;_L$xpIw?tN(={;hC-Xms9{-f0i6~<@|A;IjD7+f zmOijR#V@t19yG+nXNYsDr&FH+-b5DO;fbB95c}OEH-QxVm%bla@rd`fus^Zb3_qZ% zwaYz!;#XIzxh%z)T)~UEd@*It??*uGe7r65{;1>Smi1-QMBgzfqhcwJf=$=WX+9^@ z??o4v&s6tM@TJGSe)={R9lUCZwGbWTdD9(XV)g_Xm8f?K39VjJ;Dhc zPM|Pv0NdGdV1tI?qWqi|3acVfhUHJXsO80f>>8}Fax_O`cE^m8yQ6S7)EyKNci~n;>`s?Xo#s>H}*#do8^)lhjl2&%Oz*&+)a;zA1+1@ zvV8e=59p~wSUz{_{83r^a2>Q36<;Gt%!(igP27Lj-iZ;0HRg8!0000OGqD3PSbNEykJeH_bi8v8zGKFzOrJLB?X=Hi^+b8&j!=eKn} zzvulvPw$5x(*B8kkO(Ab30j&2EkR4r(j;gJTH1Sq{{G8wUVmR-UtV5B5JyCwpPzL* zbz)+ouC6XKGxPlXTtw7rwLya(A0PMi^))gwa&d9#?(QbCwY8O;oJ_>n*tn#mQ!DJ?BkBBUV^FE20cX_uFmhlhu6WM^k*=kD(A&4xsX$HvAg zps}l~tLy7);iE{P7mi4IZQI@5JvusSZf=fFH@^W`;N*gfhSsCptRX%F0UAPr;qc6%&96Xyu5;#zs+NZES38ZEe@q)>!4?;lUD7 zke!^I;(z1gagvgf*pm|tOFEpLok=kYV_{)oOiWBiM~9*SV`F2~Qh-wZfMsoMO_DP7 z26RtP4;wZ$H7OfXdxg)23~4kPd>MWTbfUX^IyX7W(`9 z@9*zTOiXfeau`iXNdYL;51*C}e`RF_O$ZAMV-@;9oW>Eqp`igEb98i6ZVugN==k^; zKQ%SAt*wo#)x)l1a&nUTBQmkCuaC&~_I7%DIuTG-R#sBqHa0d$F>sifnI$GB-rU@X zgnvawDWO?eSxgQJ3PL>=7Z*uY2M33Yj0`}Vo0~Ijd3iZBG?e8WG&eU_QBff_@9piS zzecYa;B-1b(X@dQAHz{*(rjE@TxhQq zEgD8QrlcVi4CN8+AeSVhTpYnKqsrz)n13J~g8b61w6?akx3_~B(A>xL^tAle@y-Tc zq4nZ%eSMu50uhs--`P+VAvLtm!^6Wg*QB%jdiPh1l1RTeG&Dpxm7htaNzf9s1T9U1 zmY}6c&=Rx+Elq-!p#QY%r)QnlYVKdZsK3wbo=vWQ`E+o!r%~xGXw}W#|DRWt$A6Ej zTdU{aezt3_KW&w99RGuiZ%m9D;|ph;Q{!zquefE#ID^LN44aqC3vqE8vpD06xtZuT zVPjmzrG-L)Ubz(70j|*HSHXW#3~B3 zBfj{WLR%Vsq8-uBh3){fe<{6Mwlfl4E;`}`LAm|D4*a_N;F-PMHuYBn zWBs$iN(22+z2d0eEZXB&RDecL_Oz(8`9fqlv%9H>&CXgK(M-08f7s9!%jEcJe_^zU z6CgkXcRH46otb^7+sI~vfq%#Wm4W$ZulCyJ7wB+eE?sxgAq>8}NM{n66c&w)KJ-WR zakWWl)qXoP&c9Jsjm6Vku8y?>=pe7Ii8koe`=)K<{&C5zuZw_mV^JCnTo-GA_e!56N4=}@`- zAM_1p@_Esif9!G>Dop2fg$~fQL*w<+u5CfndEjC@Iz#8Q0KHK<5DnCWEEb?^^D zfm`SXZ<#|;K1DvT+(z&txP0Bsk7L&r)mAm|CW}HtXml9LCx62`46cVz{@v!7{jbLv zR06#wi)nNcSqWN(1T8^J&@v=w30j5(EkR4rG9+jTTE_nZohuY>UMFMShlRJ>vcOr# zpu)}dscC$Gv*7ae%?e;9UKGobKLH`RHV}*5U7N85o^-QFXV_T}? zxUrCIu)9Y?QGY-S^DvoP@ukun5MPxh5}ZFwFT63wLXvs}mcO5AQSVUDxYQUTqP^cc z1ZdVa-|jN7uK$zy@6QbuHv4*T?Bs}zwSU}O2x6vS!8AEMA8AaR92U$*miaLlg*Aiw zSECzc?f;3!ZeBSVSTj2m^n-N@xQGTOKx1%FAON_@Sbu_di)#|$UQ7)IBK+`?2;3=X zvU_NYXZXFz%8-rsb!dcVi0GF*L;7qlrDI(>NY&?C`#>fdbqAstq0!-avZ)($wnlHp zyV};AY<~c~b8qiv5=ly4N<&<2qd&Y>F=KzZT?0OpnjvJ-);E#sB~S07*qoM6N<$ Ef?>yWg#Z8m delta 2318 zcmV+p3Gw!+60H)DB!BctL_t(|+U%HHD5Y%_$HzHxh!n~NksOMUqU4a2gc3>)B_$E{ z<#53T7bN0ygOH2Q1?5w)FT7RWd88qn5&Q6GwmX<0J%8-bcmzVaktE;QS!^1bSv$L~vcX#(DAra!S zv9Sth?E3op=H^EDC=%#}BT8P|c6WD=j*gm}nX|w6vrR8uiG{ z%|&qe&;9-VMPZJPjz&dAm6erQT3XiE*Nc^pkB>z~MSlqi2}JDe?FDGGC@?S(>8Pox zVGSqB7H@BFp6lu9NxYz-Kx`iv7$85(?Ck8Ss;ZE+&CN}H105V3{LfFz%gfUSU0z;p zVPWy~^fWUwLmUu78DNSN9UX0DWhMHj;7;X=1waI}@`%R9M$uz!Y;0_8ZP(V;*yZ8j z!4}bwoqwF1;^X6Sl9H0h$rBArI-H%IDKQ#jVPRoROiV{dhoS&uV`KDEfYSYdWo>Ou zk#g$|=$@V)5;iq8DGBMl!Y3iOE-o(ceSCaELPBtOdpHU58xUv;kaDKh-t6k?>Ov5v zrl!);(g2F!AmTW#uC6+Ro}ZuRM6} zCnqQPsi~=LZEYN_9!?#TlaurxQHgzheMGjmx6{+piGZ@QvXcI`v9Upkfy2zqEHN?h z_J8(P6f81I3(d;PVsTJV5bCkGxJaowI5=cvWB}US+?-|0%gdpmp={?tb8~YQ6%`_R zZ*MR2HG0i0UJJEa&C!~gnv#K+DLg#9wzgJ(Kr>NjG#Y-H$*5B?w1E;I!%z8pbrHlp!4q&4~z`Z_NJA|^qiOs2&#uyZYu8+V+N|O@{s$S~m>4z27tXj%jknEt#gQ#Wj8o?&xH&V8%w;n%n={LT z&SWlgFvewE3KR;owA>2q%09~0A-V7Q<_ zTO;#tsC2pEXSzY%jQ<`$dlnX#H{y&K+zOOwCFk}*x#Zl3MW>#m9aH_pyI=QQ)i}MI z0(6Vr&HP)<-2=W5L!&RWt4UYXqCY!kWtdXZpX&yh*{?MU&<73X>1Z4}2Y>XYJ?fs< znzX&10P`f~s|{W6wCKOk4XJE1S+_yCb<(s#`GBec%5=)~&f?Qs1#64T`c1E;%jGLH z(2rH9zSNsUd-}2((CEqbI!#hZ2bU6CYuZ`%>!@`$k<8#9R<(u`DZbi=dd%Vk2++Wt z2uJIuTzlJkSk~ta7OM@+zkhJ0V=OdBhZ9qI-S+n{O^b9ssA}9>zVqXXBQykLf7J=N zpV3x+*FVgkQD~qc<%lha_P(Z%|4=nO1JDvpAMN4u^-IgjXejcjb`V&c6_$~VF()LTI#T0(5EpMKR=_k94+0;{^qJE}0^Kb3^_^k-3p%>iqZ= zXu^pkLj)RKhF7=SKNDYBU=D=KM<$UJaf$12die-+m}khTYPPxQa4W(=I23BbX<#N# zrfG*-JJ+T&@0$Fx{1*AZvVq`BaOt|8FUOY4>O3`o^%+Z+H61cSj*o`Y^2zWXgXEB5taz3zIpTesD=OVIy-##mvWnhof5CYy=$acpx{95?1-Rg?B1|16+|c^FIG zbjKGtAigS1Bsf2qUU*~Hxft~bY=1vrr`e>Saj7vxMEjtlAJFV=zT0YlU|;`7)1O}% z%q;u1&vJZljJNZ|E8CWx$ z6!fDPRd5jvOn}DVpg;g{zJ_J|FM*2KnM65x7&(sHq_#<=9Pvn%tJoNLF5C={g27WJa;T~Pz+9?d@_7c(eCt;!W4!b?zPBAa2XP` o1T8~?mY^kQ84|PvE#vn13x7hLDov-Zd;kCd07*qoM6N<$f)AONYXATM diff --git a/tests/ref/issue-80-emoji-linebreak.png b/tests/ref/issue-80-emoji-linebreak.png index d35a62b3c2335818a314bb0246d24bd5cd9632e6..45128986eb5a5e97d9d226c005ceefbdaf74bfe5 100644 GIT binary patch delta 185 zcmV;q07n1Q0o4JJB!4_mOjJex|NsBz=>O2u|G>llx{?2`ng5@qslv?Gr_`*l(}~BP z|8^w*T{QndOH+lKtxu_0VyF|3KK}p?hya4Y^rKAx002};L_t(|+U?T05r8ld#ZX~< zgE2?{y^?r-xFG@K7pVX-P18r*IyhL=3x;+N4Wc??*ad<*U^3VQyxL>91gg4XI0TY9 nVK@bfx?t$f&>^eyyzjqW8O8{402iov00000NkvXXu0mjfH``hb delta 183 zcmV;o07(DU0n-7HB!4+jOjJex|NsBr;r-f{(%+;1z{LNrwA!!O{JKKI&0znhf2yp~ zhr6i%Pyk_aur-%s|1dnBFPZ-U4u}APK6;l300010Nklc+ z{);t%7yi(|!;PkZVVb6|>>a?;d%==xm%-J+VYmc>I$$^iRxtI7;S|`^8-_<9sS}1< lAgUvV(z*h^4Svq|UoZb{2QFC+wyFRC002ovPDHLkV1k~1TF?Lh diff --git a/tests/ref/loop-break-join-in-nested-blocks.png b/tests/ref/loop-break-join-in-nested-blocks.png index 6e2af47a60c27d137cdfb417bc200d2cd9cd80c6..143b8c6a934df4971cb1cb511e2fddc7b8f0b177 100644 GIT binary patch delta 801 zcmV++1K#|T2#*MmB!3oAOjJex|Ns8}{{8*^{QUg){Qvs;`uX|!`1ttu_xJYp_VxAk z^z`)V_Wtwp^XT>d^78WV@bK^N@8k0Q;qw0B@%`-V?CR?3=;-L@=jZ0;=H=z(f%yl$4W`lai8>Y^H*ckdTj$kByCuYNB?GjEsqiiHC=W zhK7cPg@uHKgk+muf`Wp9fq{U4fMJ+Yla2wmf1duJQUCw}v`IukRCwC$)Mrx@K^%bb z=MtkBMU<#0Rw5F_hKWY7EP56sM6sbLmS7BGVpnACVi&P@IC}5!4at$f2guFvhWpL# z%)Z!}XLe@ikI7^*%{&`+l$C9BV8qd|kL}^04dCg$6|F8})>ai-u2wFk{0ec!e*sS? zf4~JPWvD{UzyRB*1^(orJ5bF>CvoqJ$o&szUBp>6fEFNCmAZ4nF^j}?1D|q8I+RVp zecn4s_pJmdcmuSil>_&s!;tn87+>!EuDujChi5m(80&O|g+Ie-E6X7+r z$#1=Ywa2#q=b7avt#xU^@WR$eIU1Kse^&PZZXrUU!y;<2;TPxsIu*&D2;V%--j@K+ z58lfBe#!2Ag{_NvCup=ob1W=MPgMAgh5LZ|jrsq8eGmDtuJRh*M*usuLbM25zM?ep zc@;_%z034ASt9DXl>SZgN!al*AD)3{v%ft)pZ2=J(|tST%dscv=zc3y<-v=gwv+?)~WK=;!C>=H}+o>;2{B<>TYy;^N}r z;o;xk-`?Kd#N_h{^eo$lr+=oVrlqB&q@<*xqM~@TvUjwxp`oFko}P8ErggBToSdASo12=NnsTk0 znVFfEmX?*3m2atzl$4Z{larE?l8}&)kB^Uyjg5?qjERYfhlhuThK7ZOg@lBJWSd`P zn_hx~f`Ng7fPjEvm{Wg$e|~;`UzA2%kvnvBbaHZXaFd$>q<EP_s4KQ01QZ)8BAOVH#IB%c7rThP>q+mOFUTJf z_yRY>8}2u|Gy7s^p4pw9#bU8ork*vMPFCz$ffb|uK!U2)MNp+; zLI?0ZllX(_)_)0XRI2o*Pl-9EPn`4pcqDx+ zeDwsoo*|wbxR&Xjm1O1<#lZcLVinbAAJR@A7t4A05nGLX zj0o#Lq5b2-QndecEkN3&Dp98E6=tsb=&&D8@8fBoADm5o@z>LFJ?Z0-hbbU_XjEmx z)4;8r?opl^N(MKa`x@%=LtL(e3-0L!W*(P@2FP?WzDgv@h72)7o7fl6bj1A8@{Q6A mCS5|tcHizr)<Dk%Y)z#J1)YQ|{)6&w?;lJSjx{>SQmX`l^B$1dN zfB!&Bd3kxQPpNTnaam%hXJ=<*WMmVMK3!d1T3T9CQc_P(PyYZ8Jv}{$0D``O^F;sv z0G3HaK~#9!?bJsSfI z3sNo*;B6j)hf>Gwshiex2H?yk2c?z>U`8Bg0DCOiD78R{>Q)ahTat-FR{vfmmB1PZ z&r*mwu3NZHZdHI<1W^NmgA|}nd$ms}jR1xQ7z6NkFTqEp3?GV>rcl(E(=_cX-iY!N UOp47BHUIzs07*qoM6N<$f&;OnMF0Q* delta 347 zcmV-h0i^zt1CRrdK7Zxqnyb&Lsj174$b7t*qN1XopP!wbotBoC zj*gCvjg5?qjQ?FUgM)*Cfq{U4fPa5~etv#^eSLX(d2w-ZXMbmBWMpI$k3L;pU0PaN z{}3KESfEl;Qcq7${{Rj>Jw1p3g3%L5y#N3Jlu1NERCwC$)JGD6KokJc1``G_3o0mP zQ8A10{^xRm{|KX1*1T1ppwTo<`+uEhhTk|NvN?cK@<=5ulsfE=-L&Ql0BrT;oTh2t@dkv~5;rTlXq5l}002ovPDHLkV1l#*v0MNE diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index 236b9989e2641515c28c138f2e4425dd0ddb1442..be47707407b7005ab6ceade9b66eed52e6093771 100644 GIT binary patch delta 3565 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 delta 3611 zcmV+$4&?Er9-AJJBmwu4B_DtG_V)Gl_4M@g|L^eg^Yi%c`111d@$vES@bK^N@9ysI z{_V)^?d|OB?Cb06>gww0>FMa`=;iIs=jZ3}-|*(<=H=z(P@8+1c3G*w@$B*4Eb5)z$yTw9S9j=hEM!)YR0| z)6>$@($Ue;(9qD&&(F@z&iu-0&CSi>zu?Tw%*)Hm%F4>g$;rsb$p5dj$H&LU)PBas z#>K_O#KgqI!^6VD!ok78+OOFBxyPXnVFfGn3!s^ikFv{mX?;4m6eo~l#`Q_l9G~FPMVAuh4DGxy?vLNhMJ^X*pZ-&dc7Y`2)kB=-b?1TXuA`w*i zV-{+N#p1&CE(C__FR%b*=i9Lr_iPIxTM_kPHkDn4#ooPEdkfLuiH+3;;2|}l2SO0d z!UDn#?PD8U3m*lT2KDv>_n!?!Z^WeR=PjDu)q4YkjgV{T-8ZhkLwL!#^_$i>ti0uxyXO~1cphYJBTWajlyoAI8o>L@aUv{^=jw3HtHBw^k+=bzhivUoSE+{3N3hZS z5Y8ojO^$1>W1I-9s>C983it}Zi(!92;0dHs2jspu6L?4!h)ZPW_-4dTPOQVMegK$; zEy3?n3pqQHh&s8xGYKbDUo0$c#x*5v8OXVy!|X@~FrlWoskwC}+#iob=hW1c0w)ST zIGG5p+nTAxMB9H7AiNRL5Q}Kw@-rEjs76u&D33(2F@i-D5F9MShBzSe{DXht@zIjx z3qV3^6icf9#yWtt_eFm{i6wPb2pb>f*znSa1+QiOb;8ZP{8hr)4W!|e0=J94(iOl{ z0da3XIn{(6-~NMMYih`0gOA>z1&+E?sz3hJ*$z8j-TP|-Hm{hP7vabPPxah4Z-X2ikMPt7VH>+;5 zqDG*a4h2AVyqiT65o1MxV3#fxh&n;(yDXI#WN>2o%!_pC!@k0iNfwVXR|^o777`o? z+k9X{X=x^*euOvIuy|DGm%ma3RK4@vcTRzxrJ@<w=diLdHy}#pdZP+iUpC|lpKQ#N-4sCzTc70d5qnvUV8;)>K$z0q`GRF#Cu63X4^ID7p>eGXfh$TBxbtrrD8iJ8|$| zUpM90tdmT{!%P*%Om3jpJ=ZB*I|66cfcHe*5Hn7M6?a(BmuHomy0az9d zD9R45s4>$-g4bXQCaF)d{R{vJ05x%luKDSH3qX(# z8$nmtcm)@f`^bN<#xb9Aww=L&YK8xAa7@0L5|_fzJB(AxSEF(0fB~m2rQ?dMUZoso z_9SX>q||?)!kHbp8e9;!H=)-LC%W~CJ8&RlFku8I8X6M&aKXgv>_wai`^M~S3l5x` zoqZg~G%wH2?)2P2dw7UO8WHpxnAhIT2zG73Jw_3YxOSc2j*mdNx~F;|+9tfEchk@^ zo9pU&Hw`X#V>l*t)5+opX*fB)M<^tz-~O%s0`L){pLhl}i|`c#Jfb&uWyZVYEH z|GsyHO*lX3C-MrL>+*F!mRGnl+|be2nqw0-82ef>ZLYgd7;{5d;c!)ZwOvQ*{?TVc z+`6jw0x*$hq<4&1~iU-fd0wB8wv$DZ+(Lgp< z%7@NH;zr6D3SS2x&IghDW%RFrb_H1YUSfaK4DPg(`E+2Tpi&rf8~OFQxkr&wzA-m<94Yn9xw*rh zyJHUz7kKM%?r0Xvlw=-b5p-U}xlwfQ#=kRetgj)_WvnXNRi|p^1C;WJl^46k+S03Wcpn1X<6m^H)7LE@AAS523XCMy~r7*lY z0D$lnrf^>K9)-KYY9l5_4c7911o*dN(xm{g=&tZUASM9=SW9~h7|~#|d^>O~2ta!r zx3~bLwBD-|_N)vYs0$Q^GFmJB5%zz|VgX|GxWzLN_dGA9z-~T&Iq@XH-IOUw0C_#0l;_IG8uFnF0SV$@HC1p3k7a44KIa8zML!U zi6TMv9MfrBgln3l3&Kfv`yv#yww^8!_DocB3TSI<5{C4RhR1S+JvS>&$0UEP3}_qc z1+oy;#AA}51HkHmlhL?KyAl9pJ5bxe*ET~AW}R{XGzJ9Ek?{lEMG*i%fC9*A;@BWo zynF%B$`Bk=`Tvf^p?)ij`SgvkI1a91K5GuJI9gXyi*u1iYFP{$etqj#n{e*hty@<; zcf=kZ4#`IlDL_GwKN9QLV?}=k(!;yp`$t6A-hVBMpC=&k&_h^+x8J}@*2E$1#!nI5 z9|YXgBGJ=>l{u{GkFy}D2kQ*O@FJqIk`4u5wR;Bd{&4xYk#EC6L`gLDauBJ)BKL>6 z>f`?k%qi#n5Ks@`XG^=4HmMWVr_rpBnuMQKy+`o zydP0+>n;_YzDTTFhedGzfA5fCk+x@IljrN`?zQbQ4kWTTGNmu=C5VR#fUul7#pZq@ zEHC1rkiN4=N|qpl53gMM(q6WBD1<57&zIAPzdaBSfS2^0J@P9AdhUj<7;E*wkpkW< z)M1hvfwdLcxXFssSdo8EtDxPB4n3keBWCJaNIisE6APjpIe1DAkOM$g--t>xoAL6hGehYL#0HZ0u zdn`y&0+%;pQR*xEod$^-B5q-_W>0=g6S9{OJy==^msz0Im*sz0=^nw>qg9yHo@(S8 z97a^YzETdeFo8$a-qJVu6iC(7dLw#69(D=JFbS`q?g`3R5Y@5N%q>{N?O)v+h(tlp z>Jd!*uh7?A=~aQrs(!k6QNw~%&&*es@>gww7?9J-w=;`U{=;-F|=8=vHf9B@q<>lr1+u!8m z za)`TXy_w@>j;@7gu!Cp2e^=#WbCdl6CkBIqgM)*D zlQ04v7s}1RIaoI%>u%o}lxgdLwSD~C**eh7=K05t&BrOpUR+Kyi}frl^l-^>cFdkLk(2s%8ZSVACTt1ND(F0^&yEW8^V>CvhMiqRWlgk z`?RT^z8+sk;;F?zXu-u0+A<+&-+^Vd!Tp{>41d%HbuShpIGtBmP%KtaUATVx=C!k& z7-#^_Kaqto8Fg=ff`8Mt{nxi`^93@iHyk{4;>fNo%a@x|-{O|72zLEUa}*!*@WTKT z)$NTqhs4=7-4|vtt%31XdQA?-tii6L+tK`<0M+efxoibv z^?#hiuH6rCf<;+urOWgJfOvS*7UQOUkpMkIRh?c;NxRG+Fc(7qhJ&aEC2xi2(K;_cvSrW0000hkjP@$vET@bL2Q?Dg*G@9*#K?(XgF?fd2E>+9?4 z>gw?9&Fbms>FMd{=;rR`-|Ob$=jZ3`<=^b&;^yY&<>lqw?0?ndg$;rpZ$I`>N z#>U3a!noAHurspMpU=9r)X7@UyROH)w!Xf;y}iA>yu8k{s=B(m$+f1rxw*!(q@l`+ zwzjsY#f-_XrIUXF7=N#?udc4H!l#w3t*ySNo}asXs;a7}sHnc5lBK1kz?_7nq@;+v zYp|wry_xQ=ePjd8Y)ad)y+l$4aQiF>e%bE%7Wkr9!RzJgYcj*hN{XRw23 zyMI@cEdnPDbeBnkgM)*Eg5yld0cNY2!vvc=5ju1cfCx+Wx1 z#X-0t9rJdso!!F_)TBc7^mXkN5>Gw=Li0|8&_vLlA!j33o4e?EA)90WR-9k_hk0hc2F3tZH;SM2}-ti!2l zj~l~)fO1QIOPU#O#~`4JL#pWfiL-iCx6lJh*K9Aqr9{Wk(UJN-h2|5%wuvhzR?x9K z5xI&XJ2yAi(q$qD=+n<)ziA9=>gwwJ`e2JS=x6bqQciClA0Kr=u*Gx?YA#_YEF%GE sZ!p6E0CpE^7xvNq`Y0F$qhLS*0N{R}^w{O;WB>pF07*qoM6N<$g3!Ny(*OVf diff --git a/tests/ref/repr-misc.png b/tests/ref/repr-misc.png index 9a876091a60bfe055fe5134e0130d80bdd8675f2..699cb56114aecd73b4ac77346d642f05d3144e0a 100644 GIT binary patch delta 7428 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 AHvj+t delta 7426 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 sh@^Dm661dj`hQJ2qzv(r^4_>?ex zuF|uhgmtsRRjlsvfh}^anuM%uaH7YQmzh?l%({v}SEIILo{M9hi&~q0q<=?Vn0={! zJZy!QrFl17mR+BDKWc-PfO~pjftXyAMwNC&k9AF4kvm_AU{QsES$vj*a8P7+f>(<+ zW_5puY(t=4M1M2%?lG~uV+#!3G4~D+=bZ8UNAupmI$KZ_MJcVaikeze zRed=~SecC#SYc}E?CNOkZUBjq#_Befw{Qyc(8PG}@TeIiY!*u&v+wR_CD7bIFgR8T z8jP(>+J9ZqY+M4Uo1SY25jJbAZ=TCkN&+)yx89vAs_?4u?)oi+7dQUg--rGwkeL56 zjN_Y=>$hk8FN;slTI{I_6fPb^ANS5Bo%5cpOYs^!iX`?xf%Cfq-KlBY{3R?z()Z(& v51`=Sx|XMNMltn>G|v8)YabO z=GD)>;Q80s+2Yp1_Rgoz*WAa_*~QP-!_L;m&C#RO`M}K7z{}L0)B2p!`Mk)`=CyZ- z$@%%LVC<`dth&Uky2FFR>*1z)dbrQu^$7gu!6jxh!DLj=HU^QQL}X{DFt%)q?Kgp71S~^`iklv*(G@( zi}_VF6MhXlJEF$6MrD(#K-=(99?wJ#8~*XZB#Pl*lxzoP0yNSnTW^U}vhMscG#Tl$@AS zkQ$R(8cr9BRh>;VfxygJwKyv!qbMaMt+aHs03QXTU=-j606U~AK$1&OQ2+n{07*qo IM6N<$g1v+*(+A=}+SUKjh0fa4&Dqn(*Wbq0-uu*C#MRyX(o_G(aR0`0^?%S=@y%=h%1!^nX8O_~ z{K-lG!DsHtU;M>Nyv)bA%Ea};O#8q^|Gq=F$;AJ%%w*R(G|Ft{o zxLWnML;tZrr@_tEr_|!PPsX&Xq`<mim>gGw8oOO!;!PZkFvy&vcdP9 zBagAekFmjyvBQtCz>cxP)17gPs_Krg!i})NjIY1gnqi8r!Ox*aimknft-!mKp@piu zgsQr`lA?pDx`L>^^pFgKsJeovxq+y;fTy|taCo$gsDG}Nhr@rsw2P_5mO=k^B%+If zq>6u~iF%NF)BjyGk#f+xPQ-zvIe2++bk4JiqM0kut zc#J`Ij9Fr+K68gYbA~;0hZB!Jd{jPqQayN3JabGrZ%R6ENj7RlG-g3DV?QuqJujGg z0z3c!0e^Q%L_t(|+U?XuQ)5vOhT$H`_~1?;XmED|1h-(p-Q696yWijr2Rp#u1gfaJ zX4NND^|R<+zPk?yA>=c&)M~?if@_-AA8_e($+}?sw+pyp69c0Cf>Wadf&iB$Q=@Tr z*StQ!<)~K2)V4&Z)y{w`KB3ZJC^h6J!~-tlQh$G_uaj@ryo4P#8UvRH!*?VB;qjF6 z5lKLJt2;lZZbOo2ObH>xRL$+Zn*&^%GQhPW16(08z;!JHTn941buI&38H?{SXj%v% z#Drz-f*4gy1wyQ|{~&^bF{M(e0Aybj3yj@xewPHeRP5F5Bj;;E!cUy9iU{95FjgWW z%zvIacK}>JRNIWPM}BWcOG*eKrt0gTUw_~H`wqB%7i8-T_4?upbHL?URMs;*(Au;g z2e{no+pNFYP1RgPVuw4M`+RJs8?Mm9?Y*;JRx?7v6W+E$!fg{S7E{&eVUNb4(Y*Ar sj!X3LCw?y6pbk%<{cg;{X5v07*qoM6N<$f?=N(#Q*>R delta 922 zcmV;L17-ZQ2e=22EPwIz_3-oY|M1!G^YQKR^XBaD?dj*`>+k2^+yB;@(cj|I-sI5U z;Q!T+(B0tB-QUmM-_G3P|J+^w*Kz;Rjm+EK-qg?k(S^#{;QrTG`_x?h(o@IO&c@Wv z#naBj)6f6MaR0`0_0U#P5Qe(|F%l?x<&W3R{ym->$qC~ zu|S=~)1AZC;<-=8w5$KDGoil9p}fzgy2qKm&-SW8_^2+Jx5byY#h16lzN(*;wZf9L z#fq@)lC;8-vwy;nv&E6K!jZDUkFvy&vcdP9BagAfkFmp!vBQtCz>cxN)17gRu)&S6 zzm2cHjIY3qufL10zu1~#i>|(kuD;KqMv1MywV#rRt-pw^y@;&6hO552l%czlqV$jq zgQ&KFr@Vrvw}Pj&fv2{#jHs@ahr@rsw2P_5mO=k{GJm9ufu)Iho`rajdef0|(f?FO zoPKVYYrc|b%93WxlyqWOhn@dGOH+fEQG%6Cf00aolZemzZA+hd&Z`^tm?)$?vcX*O&tR* wz76dwPsrh*Ri#pSqrAF)h^A2^gqX4)ixUrq=t;@4(EtDd07*qoM6N<$f?QAvumAu6 diff --git a/tests/ref/shaping-font-fallback.png b/tests/ref/shaping-font-fallback.png index 813e39151d25cdddeb1f957975fccbfc71fd3c66..fc5e0bff33715a6a246db7a619c549ab2a340887 100644 GIT binary patch delta 3701 zcmV-*4vO*b9rhfMB!4|gL_t(|+U=bObQI+phG#bvg#%)_0T15G(F0r}ASj>$QdD{g zMd@8Y5m7pqUM|u^1Vxb|y-F7@ctLtiLK=iX0->dnPI@-k-MP=<_bf|DHVc=4kncTn z=6v&ScC+)&H{Z4HZBqb7JH5Tb?Xrwmd(9}R?PA%9wk76{Qov=FU7FcLlV z;PJY5Tb+d(z=r&wi0KYkxhz-25cJ114zx{U2nl-$|WHJfG_d#?* zLc*JGzPWGTK3gs;X48(h$Ptj&$;rvDz4jV^_51I?_v+P4D83J(mn~c7>FIfgwV-iu z8nn6IF!!R0Uw;BN33!Rut5m7N`o@hL-Me=eik&6eYPHUtJD1?yci&A*OCwO^#{SV{ z*X)0)n1D?c^LqL6uf|H_`X{3 z=6be;P1SD8XtUW&5EvLJEIUQ?=+UE9aQ*uAmMvS_N`J@Lu75kVZ;|5X+2}XAg_3PKq$jHzO9v&VZpMU;2!CP;= z^(2iZ{pFWm{Qdn2N|Y#(o}RAR!DuwXwdNe2d4J{^_4Wb+r&oA*c&^`5Au1|rRXKKr|UmfHD9nS{?`v4vvV3 z(451~n>TGtg@%Tr?Bw-5VYyYSR@bgwi;s`DH6A{Ec+{v-kCSM!_`-z?1k{R>B})bb z1b^6Pic$jzfWcs34K=f7&7w0Jxj?%c@NS8I6yHdS!!*s;2G>-Ou{kNN=Jw7l}lD{b1evHzaxyD8AC-nMO9{>%IC zzi(?KW;cU^f*#vw(ym>*o3`D%piiGZr%s(BfOnf>>*nUhnm=YY^xQCM z(xeR=HW2je*%LyQoXCOc*s-JfC=!jJdGqEWAt3};uU_rkxif(Ra;5#Xw_3Go1iN%4?p}sK_EDI@F3|)K+ZO8+LWMPy?U9Mnf8i4dBADMmrlyiId-m+9P@zKKzI{oh-o1OXCmaA8OO-11^UpsMfB4~t>_fM1 z-E!QAa05X89yxM^i{ZkB3s0Ur`RlL0zW3gH{3Rbnc9W7ce(}W@2xccvoIrXXF=B-6 z?HxLFpz_)_`7a#f8Z~MJ-v(vv#eWxHT)cQOZ{*0eYuAp?T)uqyC!c)6ACs_2Nl63> zpMCZj?VO9mw4)>QuEB!`v$KzlXbRubrAs??>eQ%FBUJ6yty_}|oMJTg=+T2c*|B2> z-=`_V@qQ6dnr7;B*4jUhvZP`JBv>B4Ew&A`Z!BjJSuGjrxlWPjh^e*2AT zFkrv{h~{G%r-1Zse{Vw?D(yz0apT66H9iMgNd-QcM4m8V0&nEJA=62PF=NI=M@RF= z6eY66mbn854kS=WOibhpP{)4!@kg#1f5jgxd@aiBfddEBJ~#kbGjZZXj)Cqp$VU#R zDo_d>0B(mV8kAjhOHLa7fqyV;*f7qjC&?^Ceo;5s?R;eZ`LWVUVFMs=p#Nnb}nffI!jV*2#y z+^G2a`U=s~dnXX0g=isKAVdq%g2zjAMz|h#P>5E0GZ zGQQyDmtRI(QY~0E)qfh6FJHb)*Dxi*ks&T@m7aK751tIsES3{3%}1JBJ=zfK=G4^W z1bVCWdw?p2AW&F{M(-(Ct{hy`$B!nWB#93`_<#WA6jg$tR;^kP?Z6Pnt9EtV-F~5s z69N%Z&kc3z)F~`P<6lF3Vtwh-r59d!0f`9D;oEP&jrM_R2Y>Y_EJ&Y}4o8N#_@nC6 ztPzKF$o1=~pC8GZSRFYm&4_+TS%@{rF^mX6t465C)KyrBhEvu!+T7jUSt}_IDHf59 zF5UU_=X2IG@`{V014Epi8Ld9e9Oh~DYNtnS)8kKCLwA~wwN&klW%=r>uh6s6ZBcvM zw{Jgu_;AGBLVt8IM448SsAD)b#MzJ)xZUd0-*$hUy1MSYS};;!VZ7_$a>V6McM@bS zH3V{52-v(E+1HNgaYuBEiP(}9ZjOQRyKw8)EvBF-q|RCV-_lc2;6yWM$j8S=Xp-JL zAzC0r3(-QfK!_Hi1&@m8=rrqzaIbcdZwSRu)JV!Dk9a0W~3FGL|qXbxtkba&LqVY7)ACI7w%e-N^0p|!^%*Vsc@Y{dI z-08<-VfoY1QyRBw56hLC_l4*KTP8Y?8S_$X8Ag8b;zecn!S;eT7Ox6l<7pt8ih*MZ z%Ok=P{wLLlxdC?{j=spqNP8hM@itJWaI8S|=zrbE8jkpT@5Pgb%vdyF%)jBcN`tm6 z1YWyFI*=J-&MnT>BZavLuaP1e*B5pT3|Lt6o(7^xXB=1L5hf~C5Qz~I>Da^ngdIvH znz9$-Yg6XE+O=y#bpPdZA-dW4I;69E&rfp@?LcNcvUL(JF8v;07D72wM61$)+=Ke_ z#D5cw9Y|@i08b>oKCD6bED;jF{PIgKN@dHI#ZX4f)d?YyH0NN@f`I$q3*Jq~RflMn zKC}CFm@u%zjA^YWPi!^3Tf33%J&w$9ATve=Zm(3S5{htGSQtVgf&ixu>5Rn1=POsJ zvV@|jaY&wcqPYN)v+90>g2;X%ej_AO7Jqq(RKj+N%?~*o!y&FeG}bwD=I|DLhFeZ9 zZ9V1njvdOo|I4l2^f$V!-P?JiclTwJdwyS^Wxwb5j%7yr!MOrsq{WHFJr9OXERN(a z-{kDsv&5KC@SU$;zn(OII)AWWfYqJAHjm{H+IwQ{o(r47FGP3Rn!wUyQ7_tg)qfe8 z5x^rFd|~`$NPaPlNY4+~V=lqwR-Z$g1Ji8_n=~E`+{3HR$c*Y9hBKxx5{*fZTPx|l zC~%^=>j(&tDPN`cPKXww1wynCEfAuGXdzl4L>E4yv(4H2ef{?Pp8doB>^>jAKNoMm zKYY*b_41BSN)$H#FBP0_Xr6aPkbhw^T}w1ZWfi|1lNA`2;&(miLU`(h$Ta^OiK`Cy zheU*Hc2HW3JPq6iW4^O>G0Pt|w<%Mm7!3TmH8~+ZB{@kmSSyz+W6CzU7_Bk!ap5sh zMO=)w1H&KlMhq#eg<_|OM!&$_hAd03Kdfb@Wn^ZWiWM!|^o>fbYrmdlwtpHlqi!%1 zaVcVZ3rB^S6PXW?Iaul86p9rf$PsW=GMOd8bI&~&8X9J?n2Aa4eg2n{(la$pr^w%4 z-$D7?UHvDW{cTkHO%SPyiS#BLa2j`b{55p7AsO*H%L9G`2fQN8dW|sx|&l{ z&Q2~F4K%kxNwYyTDw3JOK<_4l&3+_n&mVv<4zKS+8z2 zdrvi=9&QO$VPbP2_h2;*iZUeq> zYVt5ZT8g^WFIT>j25xz1Q7a5-9xlcZm(;yl*fOocV45+?uw%PLPSq$RHzU0EQJc{DM(U?2bHn{Sw3)P7SvnUSYP+UDcL zIw&>zPhjY@d6ktNJdYV&iMv5Go-Im^n$eW!AKGtXCr0GuL z89K3p^Hj$?XKr*28jsVKaUf(az}AUT*?#k#cizG0Oh#h^<>8WCB;~FncQ~HW6{6*b z$-|$4M$b+&xHiyARwTB@UM$}5Yps-HplglT6-#W3VF4AfD>gKWY-|`6 zv*2PE8wwH-5s;36DAIi}`^yfQkcZ%dh^TK)ZYGC$_g&uo&6#uO&VQz!(Dp^Fo<^WS zYtY&>XboC})}}#g(AqTU?+|omkce;+53L0UPWC-b^D}_<_4O?&DG|!zJmG76B%kDy zT zIP1!lD^sRS(TJ-7`ryHXAt51(n#>rXK3$y0|0wzXF{oaYpOwIm`IFCEwQ5EA>eZ`B zlO}1zrh+alEp>Ny$LZd^dtP21jtb|%9c`uWyP-u{P>0&_d9!BCjvhVQt5>hfmoIC? zrh>kB@#3mgt9I|+J$CHa6DLk6&=>!*c(@Vg_%Oo{Cnu-k;$ltN z2=u3uwJg=}F2TQ#zpDj@9DXnwQ714K)Nnt~3k$(ZqL2?j9RAca^((jrtwC$kpfzZ1 z8ngzjL2J{X%>}flr>D^y_JeT650*%fR8XAf-bm-9Z^mL<7+qZ8oOtS$4 z24rPriCQz}3lkF)al*pFrcRw&5p+sQ%E^-_pFVwR6f0i^XtWhM8I9T6+Q!AjRR)>~ z=s|-9tzW-hRAM7Wj7UmKk_o4ZNz|yQD0!MGmYSNXNX5j&Fjb55;>C+`${A5byhDc$l%OGp5Ky_fxki^l zrjc03j2T1Em_2)TpFVxsv}xn-?=Mf-wQHA!g@v=Tv-;QI5S18$q;#%dzmCR6C*ucz zo0`JH!sO&+97-A$r*r4d>WgIUb?erN3N~cOkjTi$IsRv1ua{)42J|o-@>u7vVwkjMIYG+L4!3B z>YDtXjT<-C8E9e()=r0m|HE-U0+O%mmZrpHiaNtg804f_aXs~zhUVKt8 z_b_qdL^Tdkp+uhz4i4sFxO3;u+qZ9@K7G1p&z_vgr3MZhNC&5K{`~pms9U#g5z$MQ zEa8hPU&&0O5r@ z(gJ-S5leM#WeXN8*sx*4gb5R-PoK_UVCmAOaG_zgY}rCmx^Uq_LPEmKnKJ>+Wm%_y z^j>~!Ll{c!Mq1y#eZ9TCxeizf1+GjWuUWH(FVb&_bV6apiWTyqjj~LXsK}i)YnHlB zc6K&DV9uO5BreqCuknS2pM^jk7#Jw;p$a2v)~;Pk8wit)c%(UHf|5o9FdQanCKQQ8;ib(9XN_A4a#nAZqbLPw$vNP#UgRU*(y_CMVdGlrlDvuvO)}Zz5oi+_x zgVvz6Y0w(9wmJuG$Q3N=O*uu0U-g+B8gz{;-Jg}EYLgZ|3we#d$E)eM#BPN7=nPgfJX0W(xeHjnd3(jk&;CJ{{3-|9Xp09A*D|l(ZsUvR-2uRfAg6A3*~`z zJ!NzYwM{&7r4eY_fDR!S1ynSqp3MTZ+`oqSL>V0&-LPRpruh-GyLRo0_JL{#dK4C< zPja-}OS^fFC0pU1KMkg2!xr0VWsWWrRYR5u;J)w_(Qe|g9X@S9%dw>y(GMw$=tf3B z%w`AJ>eZ{+)MYN9VM@s|2^$+5N-5mS$+M=2kM z1iQLgL4YH0%lzw`N*_*Oi;-OEvT)%-^lWrn)ZS5}MzP8QG1rV9hA7ig5;e(ST`@bC zv)sS!dHR)s4t6PtbO*GN7K~JAj-w79M?CJTH_ju3;Y*2M-U@R{v%*xqHYA2re0zNR z_N|A9hneo2W&gLHsi<|(EE;xXYVv5^Ktr7x(ENsVfAm@il$D4Q7!bi;!nf)WRar-+>j zL5?6459jc34jK_AFp5evJe)p<%N=fk1~`2oT-WZM-M@3Ilh3*M-M{nu z-rwuWTrFs03Lng=jyRYQyf^B(ZM&X->DA+>(*-NLsiMoUc|9*PE*F?E@MCd;#R%zV zjey3}znH8+-VlutQ0riM z?2{RD55I!UD1{1}Y6@|2c@_m`A(S%#EmH?_59-eo4;mY#XtLr+#n*=u7@s9VVq|0_ zH>J&+H)AN{oVyc3B5BTt!8-`J1iB!UNiOml=9K#{5KQva8$;4m5XIOqIKg1O9*}fM!LQlK~-`$pu z-9NeZ?L+babt5)oxQNmR#0avu5Zezsqg9U8^1fD zKp!47g>j_D;K=xxqpKso^=iJO`&5e z5CJA8G{;v4np%I<@sq95!J(Ugp3CLNVq5#@_(XQCGxZ#xYlGraD(66ptcAeH2_g&n zv?5*+Xe1R8H7MrT{(A?Y)6YB7D($9n+uyf_&0f&+MY2&*mG4SH<8Bks zsMY#Eh)om#ei>OK+QAPYP>*)SL*O7wa=qz)LIXBjv%j<#c zoVk;{o&l~$V80>Qy<0F;(8~md z3F+%4G1ZlRuVw$@O2zIg@f$X5purPOW$<2X+_=#=+_Y&^K|ul97=|MHbmG`h&0?KaJu&=Hs8Ko5Ihjs%h^=%iG~WGyyWpTP9%M1OU>_%o0c{)_Gg|6#VjWb__d^Vw z;#GdY4*t&#FWf&q_w*)uu0%*i1DXdDq^u}22K^6>L+r#nRpaO9$B>{4B9=`B?G197 z!mmC^is@_j5e= zz8qW(mIq-&$dd#TT0#pFT0%=`K|)JtK|)Jt2`y}f&~I!wbAJNHa@16#x0_~S99J*V z94SZZP0Yu!K9Y-=8O9H{I!I`cuE*HD6}PUZ%-0E;(tXH=`B}W&kN!$^kkBAK&zQCC zPRdM&Xbu_HpLnna)-CCyjPeqCjnLfsD;IIR3g!kxqco!UWq@q}>DU{!xGP6_ z3C*l%tk;;(sm|?L>1Ejk$rBr@muh4q-2A zW*pwh#n5A)aqcur87tr#)_xNn-KW{J1O0tSEaJTr`a-rouos8R5SW6Zx)dcvXm8cN zUT4z0t9@ybcJqQYUyV8}OMHBis~bJ($A}wV4?lfhw4L?1XuNK?KOm8yj6~pcEI*Q1 zOc`E|Mqs()(;)0-$5uM5)#QIW!O-MIctSQOBUKc zX_4pzLn;HqQleh9McksOB*Gernc-qmZn{Qh%4~B+;#T{U^N9zpR}`J#K!lx#!?^e1 z?9GqEU^$R%NpgfhpaohY&;l*c5`h+Ii9idqKufj*^!q-XIe!6D1x{AuUTthN}O(z*a_>({{<Epf`r*)nB=YnkqC~5sR_;ItJTimQ{N6y#mk}I9S%32@T-%F%;)R%bbs}-wlHPMk0>s0L&&XhePUez)90}MxfV+UR>lkmltDI+%816Fn`SGQgFpG~w^K9y z%=xks`ujJWtEH650M8foH{tGWHv9LW_Z3nL`0R$RfYyf&;7B<_L9SJop|lWP9qQNX z&FqsAc)GOtr{pE>?MS^gE|-3LwqEpf0ApVGM)^50uHNjZhmFq*?+8*UmeDBOF71_x z$Js>Y5nhY&#%IIani?N&w`${Vsc|BnWW z2y~{Ic;VP^f*;-pv_zl|$iAz+{xXeI`lfo7nY7-$BX>3;>SUvTKm4y&CwIhJo_F0(@WCN z%4Dy`jnR5CTAaI!rO$oOR~hZ}Sapa@)r0d-A?TxdHA2bkcz1dOQ52X0^q>ew`kzRRFZcv}XC2@^tT=7h~Iqt20j_=+LwZ@LdfWh7hfx zPfDa9f;Qq>iJ5J4^}_~Qlu%{O?$A`-pEui_uApyEnJ@8lFuuY{E)uD&60Hq6=r9cQ zqd}*)s^7mjb!*c6;xPemV`fHge>qkF`k@u_wh5CfXqnFb`YGYH`}3>M0XAfy9|yX9 z!qg=;<8FK_dHmx-31;p}7l16dOeWRY43_1JMmc|&Kix3;u<#k^Cj*TmA6=_Jvwkyi ze0$y$H~Dk77DbI2JaTZLu>=sGkKyA4xE<)Ar1HAIZo`VM|DIP_ms@eOu&Uu&V+RZ& zT0^^w0ebmn^U9q3{E8gTyJtlH4?q|7XvbC7LAm9HWBkM>6=uGDSs_q7*nSJ zaa1wxKs-YDWKHJZ(!P@+x+h)e7MtHrn*c;SO`}J{@yrl435*1}T%KQ5%^>8FG+y4P zXK#jtxG<3(L|Dc>N(4;cmpd*6ctg%&kGE zw5T`C?#UM17qrc>aprKcKPoXX&CbYD=S-CS{SVV5OjNcdtzcDoapFi zlgUK?K%=9hjg5`8p0cvC+z~W>?VOw(IG}8|+u>-n+QGp=E;=_iH#Ied8KF>!A%5!h z`lZm=*ciDeGcz+TE>0$s!8aHTBO@aYheNGa3j_irOQljQ$5xv+Z^lBK&E{U9fxy?- zci+B!eSLix_V)J1#KhdWb7$ACT{@la?AfzBckZmHsPOmqPfbmQvu)cpq_wrR<>uz1 z;l#$q(xT$ySWgNiFFD+>w=>gnmhFfT6; zC;>V=JRC!mGY1;wj}8M^{r&y4QUJ~MUz`svnG?DV&?h7$a6@lzZ+8TJ`0(NU{Cpmd z2L}zbtE(#{Bm@m4A|e8T6x^-)9Z7q5QT61V4=zAB3WWkUDw>Q+rNR;%0xZWS z2+8GgMB#|VV)nwq#6UC93^Wr1%|J6T&$~l=r4MT|G``18QthRwM@rXzwk||+*qZ7PgA#Z35h7dhcPA9D;{xJR7L{Iu} zs(C%#>m}Ht-2RK`Y89m=Q1w-ciXfqYR_pq&wB4Hw^Ry(hiOYPPkbfkcl73*I7gdx; zkzoYg<v+iE)&2ih*8K%O3=esXvm*NYKq2bSR9*$LLBa z9SS9-;vwjq^YmpfH8qfAir^rpwu(@;n7@!l7cUUxu+zCzD$b|+nq|;)YC8Tc;p4O{ zxHo7BXg=_zkUgZFC2#@OfqnvOI8w<&&;|ot{)ImBBhXBUrE@9tH~Z$&(nb`9;s26f zk(RcQVkw0ZNOpy0kwq6NWHE(+B@0)DEOKeEiCV==A^}ar3k0$dV!VrDz=((ouc!k7 zMY9myd_IR6HTI5TtR5Z?3UJ7?g12>OhTh&Z?e#`5;@H9QQ)6EJ{zO z5U-tKF;DFk@9Rey=#k3{Tj+y5)i^o6FCEh*X?r6#*V550(LX|5UCGF>jOOTISM9-G z`TSlc$84d0d?)?M`j;tm|MbSU=#+K5GrBpNmppo4`R}kx^RJ((GO+)1x5-3-Hu6!p z**VZq6VR2U!q~608*+Z8me5ep&i$o%H4{7A=&sgP;5R(iGXDnk?&6&Eb&*c#&I0~8!G5P=p@6VP|%~D>}VvZB*E;UItrmk7z+>d zWH}Zjv`(S|S1^pQn7Nr4p;WY}z%*={Kcca0&i%4AX}_Llb7imi#?8ar+&md<`+m8R z>%Q;nx*q*p-|Ktd>hs>m0_@naR7?v$twtz77l$FI)bjK70EF|d)$u?}*5FJcqluBhb zrw~$WfCrGpa(0f;Jv}{YNZiGCL_*kn8Y@!h{rmR^MImP;7o-Qb;WqD^wiRzvK1JK7&LGFQZ6ST-bKU0t0DZk^O0l^wC7xWl~EB6=M-aA5D=y_h&d zjvqg69_Ude->FboX0<|<*t2I3mPuP3*BJ=2d-ra08m>%tc6K6^)Vh3+&iCky6gq{@ zNTE~c6gne?PN6eW=oC7I&PbtC=!_IPg-)R}mQ!dy$ZKM%G+8Q_re_@ElcllAsW{x4 z)0vCaTi@Pdk#P9jU+fZCQ@r!tEytb3;{1*B6YuqL)#6%_oZ;e}VfDM$X?QViYyvTKMc8tOM=DYlEwGzg||I?ZssY z!0aIwOdqWh1TS5Djvk3`l(FSie;(DMk*HHgYIda$AU#bZOR@Wj&L z!a%vLYwU;KQl-#Nm7s+jB)|N1@_~l#tHaYN^gKedyBKCwdsqIshNK~02$O8;vL+p z#Z#>}YCq^Ns!GgYsfy$Cs}pPbZ-$qKrkCv)otj;{LLa+0wrt0>(V6_Wj*JvKg-)R} zR)WyJWbP~bsq%dY%~0rmCBsQnS6BPmeX6`vp^-V;wrvYqZ7G472$K+yL|Eo5F<=uN z9Ub@X1Mh&ntWKeEIH(X>$7y?eJF4a6$&(EY4QL!!dV70mgiPVdqUWYfoA8t_5*pG1 zZ)s_1iLqE1OiEW*7tRIwf@DDu6$%31fP$bl|9yi=0n6aJux~!dv_6#o7W5IwWfFZ> zmCz_Fcp(MV0Tfgu_{NPJKxF|~!TnNd=gysh z0>a{W1l88oMrXM;M`!{C#0lOCW#u?nD*U0CLuuSCG=@;}>WQ&+io0SC#jQwtbqZ~J z5-Zxc$*QU<4Y|C1`*saRkRFm!b@S%UL4VfN)P&IJzOb7NlvSo<(@?~AfYWlh9FXXl zGiT-qtwzTT2+^Xah2f;7^oP*)heqJdt3+t!l^l_tM1cY0uqC)1tpq~{A=hY2RU0cP zTY-hah)aKk*p~Oh5oDqr07^t4yP_;eTM52v*Df2PfO7>LTnFSRB-SP{p^Uaveh-$M z_gwI>8n|7vkv`v}Gg9ajI)%Pz*kTi-t*I8Y(3FVOwxPj_iWH0NxDi09 zECLFGu_#a|En5>-setS~llF1RGrSkttw7WoZtY-G*ll*7C=Y4ur z@>#L;m6%alO?tCqSApc&qdbM)MvqnNgrQvq!GSH6$ z-7;=ypETlbd^2(EqdYNY?nz8Q7FaBmsI9m#ouE<9ALfW^N1TQ4PN2CSJs=Q{OiJ5( zN1A(vxE}rFpmF3QtK?|bZ^Vyn%@%T#mpfD_YRuq~g9D8vfcRV#4=2FwK!1Gt`h~K( z)N8-ST)uVmTpkQgTz+LzQ8hxef_4=E^y1CtnLhRTbs3y@&P@0{0G;2d8dF#XW#;FP z@#5`FsF%sE7e9;IvD>eo*T zPQS@@oQ&gWx)Yuo6QDCrBV$J z4sy}CxjCUwh#7%EfFXWrG#Y1Ubaa$ll%Ad*6B8qqO5y8ty8HL<+wFFxQpxA@kt~r& zupC=$+_(`7tyZghfd&E}AD?~u_Vx7iVA$Q=9TgRI_wL=DJ9nzp>a%ChZr{GWw6xUE z&o4PS8P1k1Taeb=+!YOtXs-X_d~nH}&~1P| zE-sE6dU<)dBk04259j3Mbai#XK?7}XZx0FzLIVj43q$M3&CT`o^_`!e4-5=MXPB9p zS-*aLM@L6tVPQZ(Ky`I>Lqh{C2sOKB&z`2HCY+DWn>SZgRiRg)H3tU=8;!=^?iH}gP*JDpN0*zkD+&E!V9ZwAG7rT5`&0;4&X4VxSk)_&b4P@{eTD6ZFe%Iut@}735G&K$6jAUHt$2|@?&gq#hZG{L5UAwPQz3^YF|Z@9-F~P6e_m_0#f*wEgg}#1`^e5|2r_eL?&B^&EUb}Q7PYXUWbindI z?U&}8lcF;0?I}8;M-9&@5W1!s$4wo-4P06FeCV8ZdTnl z;@`ltowk-2L>80&50E(FhZYT zp)oJ#^LecNSkQGcf}eE6Y2zd`01GZ=N%+r4qmhm^j_y<^GYW;mI0=n!JRA-O_I+&8 z#bObt0{gzCQmF(j0YZWApPhi;f@<-|BIYpUaTs1e3RSCBkfBs61*HPs0zx2ZI2`73 zIhYsMbtPpo85T?=5@1O12DMs^QC!sl+7$|gaH?ah#%*1xRI=GDu5}oWbUF=##Nz#a zKbcGd$h2CmajMa{_9Kx z$&W^(5s$|`w8C+mU~()L8w>`LzyOfX-f!>zUc0K}IFA1f6?7qVli^h;=+$m^HIh^k zWp+_rg;6Msg@oQL$AW}bP*mg!h7lHJjfa zeEI6!yfOCO=gmPr=bX>y^PR8H`}6)Dh=A{{0ck8=4=g~Bqc6{^Jc?c1?T+UmH@K$xvtx0=&% zWxBb!8KI=srRV7M9GxNworF%2gib;yp;IKGlh7%W&`Ibdbc!T&5;{c^ItiVGPVv78 zJv)2L!5_;pS064`@0_~EB4OXzKiMU)rg-n$YmRv*KQ}P-#E1P{wYXL+QK2vO4mJPK z(a}2yVEJWWq~$^vdgXLm=a;9>xs}(s`2F8#eQ9|5u|5Aey>#p!wC^Vs8c&mOcSjers&L_JkG9Q*qweS0k+5Ll0e=;~cKATxwymWBL6^0?XLbG4t z;`Ge^!Sgx36kWg1ynJ{hJQ7(CZEF}#Lgxz|u0d3$(SsFTFCH9ng(sE{?SoTwZDVKp zCUb>$sszpDAbI@f8xK@;bqr4@q3<9x+Z&cPhcAw^b>o#4mNwcCS8{n9&n+F&_Mun1 zcn#fg+MkA|SsF4lD@@MtyY|l3i(N-gww^fC{zdZ{wBhlVwx-kP7A`b%!lHNk2*fMP zx0x%PIC%|e6W8}=CNwL)M_%iG@}q&<=cGQrCq_=bT90+!C^VtBeb7YEI5xfVwj(=r zYpzkA)u^U4$LZs5_H5dB*`pf~r6h^>ocN>h!uag1znP{g*5RjRNbl%P#YBTQT<(Of z+W`vt(M#9MJ|9-x3ry&IgEGgw-B?4)Xy&u5_1J2_2eUZ~K> zoOSEg1+BJ_z)XZm2uLC2z+MVdXdDhIgw}D|(9nQtIdbGkMMVV~ z$CbXmJ{loYc(UlZYSk({rHh1yw7^?xYHDIE76y~j*4Bn|LB1ea5JZ`Rz&D^EsEtJn zCIu{m>%zYIAk+F#{#(#TAeTw>DJY>)R`5a!sskvfNbr>_SISxF6`m5|vv%!Tpd9X# zcTj*~9 z3ymR^yn146o#L*TLvbt8E=-|qPhv$IH(69vq#>8ruV1gh2-26NR9(G#bQLf{?G`#xj=+gUda*Z zNfa0`4qJlT(Mm9M5OR&SRJE~!vK3eujJWhyh;4bl96=`90iZ+#vMb7hw3XmnwrsH> zGB{Vj!F526LSk(K6Ut~yXgbB!6X4OjJex|Ns8}{{8*^{QUg;`}_L(`tbPv_4W1i^z`iZ{`2$m z@$vED^8W1X?Cb06>gwv+?)}#6{paWB<>lql>ip#7merGBo1nwpag z0U;uGi;Ihjii(Jch=+%VhK7b@n_hx~f`Ng7VVF{Ue0*P&Mw3zjQGYoaK<@wm0X0cP zK~#9!?bORRLQxb3@UNoM!!vXVDMH!QMXom?qNMVOoVX;BxA^);**18}7-s z_P6%h5ClQcgC*Pnv;^JIS@GE(s?Eb+^#FC&H$PyyTY}DK|Uu? zLi2{lVzE&NxZ7cjB@?m)zS=rvrr9PlxUg4Np>g@>jIj%u$WsL_rhh)5k%%anoFgI$ zTo~6qjbPeXNhl+0(2+V1`5=jY|+<>ch#;o;%m-`~XK{N3H%!{q$j z+}yt3@YdGW)z#Is-TTnc(9X`zrq=q)%gdhA`o+b?#KgqI!#Bgh!NI`5z>m!NzP`Se z%i_Geyp753h{^f8ySuu&y1BWzlE&7zx3`7G?SjJLv9YnRu&{u>)sf2^7kIR?ceJsg zp`m%Rrl6ppb+DzHnwoO0k@_JecWR<`i;Ihjii(Jch=+%VX`Nq&hK6LDUV?&xfq{Wx zm{NRve3Lx^QGaI3IpF{R0XIoRK~#9!?bKN}LQxpT@uxzi6PYQJ22tT9Wk_Tm%9yB3 zVMip9S$uaVYjx|wT4!JF-xv6=^;|p<1VK=O&fg5wddyH*_Q~igPr_NU0>AXHPC#=z z3x%1=K;Dn`6;$Qy01`7$TB#5DJhlv`RBQ@oiThqTynnXyrhIteqG|I}okX!4FP)>$ zI#td-c!HGjVZ+C+J{R|)tie^lb6J@>y}@H@JZ3QP1r)|mXboHgcm1IU)y+N>=WYW+ zfFT)k<0xzH$i`Kv%v`!oJc}UwTYb-Xbf~Kayfqv%#=^@kEjV-RG1+<00X~eyc~nRg zk-O_eBvpZn;qP};Sct+bLNW`uP%m1mVVaIcmV$Dat45MdFm^yK;LAls!BZA>fiuZV qMB9fW5_r?vytHc5iYFBW;ZHwmcQ*7y``f<&0000aaQKLqEmRN&g>;es)#g5^|NWb9z8N-b7(H-n>C&a1HD}Homn8S&k3WtbJ9g^SsmqryfBWsX7cX8M zJ$(4#hwjpesAkwhh79@s`|nN1;#;|LrHpPj1JHZzwU-Ipa?34&;PcNvf7@-h$y|N) z)y4#rYk#l3*8HD&=9vWx794lnaY|C#v(G;JyYIe}ncSu$lsngL4cwNUMs%yyFo0%5 zqUPzRpFVTu%*g-p%P)KGxhEjNO|=05g1EJ4(W3qL-~WXdUU=n|SB^jacv8Rd#v2U- zn%7---Rjk=>(#4A05{xlL-Xd%KmPdRLk>A)z<+=NBSwt4=%R}>zxCEzCr_UI`RAY4 zuU}vPty{Og_~MIMMNLjV`Q*`~N2}AWT|0u@3_$O@@4jDr@x_rx9vL=z%$PBTBGa*B z$M@fVpK6?V;)xGF_@E3sW4PJ2haGm9$wz*{m`QEAjBMMyH^gBL4GAtCYC=~VV-Jlhe+cxVB<>uWc zvDc~Qsi&T56{pLijylS}w${oy=bZDw2Op$cb?Ve1R8ptBvBG-y?hWWGuDBxAVetcK zZLNgl3 zcb;hnz^(O|Lq7P*`mvo zmrr_IyLRoL-XfA^%a;AW1}rS^gb5Q0V_OuabSlglTv)$a;fy}dWf6JqIe(X%IoZsa z>^uao^29f#>^aam*O8|j=q&_jUNe7)Pd#ngw2kZGjW^zC+O%o96g%8}^UYbh^;=^rR51hhmB#AT0A}b_0?X=S> z?m^JVS(d3d-FM%870`hkYJUMi5Gj9EheP6S)dDT{L&QY*FL)C>gvZ*XNfT}vZ$G$D z;Xt+(N(kX+4)W@VLC9m`#EB7@Nnd{XB~B*`G*$%A`Ui1nVgedB6ckEyqGyzaN9b0B zEm~1RZ2ZXN$YZ{cCtPFQVhBXGF10ckT$oH+3RtT1&p$urg^JQJk$;i+2A4*HSHWIs zTU9`tNvBSo@Bo+{`itce1p0@dRWEEa2nNJFX^mJeH59Mom_)jU3nt8z%GPEOQk^?@ z7NMA78K5yLC^xOlFah1BO&b&hRtTmD0FkEv2Q(_6q9_3@M2Vt4V{6f(MfA^I!&hMb z2q1v|`s=TE*<}|a7k_*kp0${kg(&xjAATTs@hzUjzj86XDWDdJ1hrsb#9UQC(*m>( zl*1^S5?HUl{`!c>O-8tZM`cgDckiAB+I)r&9}Y4yNZf@50WV5jyLRp9u)Icx?Z&Gm zpiR#tFTeb9sBv{YQQS=R+!CX^mHLMuc14CMfG-4t<_@4`#DCxBA|amAb5U0=OWoF%8K3TSdh$~0)ufEF7xKF1@W_I&&EQ8AgL{~LR+#7pi}>BNH9%yHfGIe z$a)f86IL-sOH@P{K*OO5fqv+rhdk`AyY8}+zXun8M;vj4 zh9uOXLkCg#cH3SnYqDGaGM7?wQt{^+q2C!+t?)7A~p&@I=B20OtDX(#i>QGAE%shio!B;)Th;U z_uY3B3FpV9&KovtXg{z7gM9aYAQsHXsdnW9ZErDxvGNfOs zF>sQt40d)o8Mi`T%-p4X5|3M96T%KeE3z~I|4J+%&ovB5eR;{F)ns8jQHB+Dr>#{8nXK)CAJC(D)HugL+ zthB^;WTQD`+06s=_UAkV&qMG$sIO07S{9U_*H!BD#e!zOP@lt5f-K-;^*ycx|2#} zgdAQc=ZXAzsXTdJXNo(2h$qXYBG}vwN1Mzz-7z=8h(e(OH#RcEGc@&PuB@omrpGdgA^#ajJQ$cJ%9fExEmeH7?0@2*&1Urc&@iG z8(VZPV?10#H9gmZvRY`s;#I0p`9O<$F-_j*8(xoT@MwaV)_& zQ0sF6J)|4s8v>2qUOaP)@b6sninMi?5*aaCF{VfRE{@QO2Q7>?mRtv z)F{2d0c&ETk{A@KuOvFrVKQqB3uLoVJCQRf>d+^utv>O%RA_(ejrSmv40~qm6EEleCn>;j!gVxE$y|PrPO>%0KKubi;VUIuF5ft|JFJPt^tJ4W)%8 zYf9%XEh$>Ger0iK4s<0X9%uz0diwlg_>`H|1!(>(&%Cl;K0-_ePJ|O&MoW=_LcyG)r&$*ByXej^WS`bD7JRKYs&{Z zowlORUAuOTw;tKE#&p*Al#zF$4#_~N9c2neQ3;l-JkWZh=>@4|UB%xY;#jcB)sw*u zDSxCzQ!4)c5LZVe@nU651{#-$2&atjb$k|NsCOnfeb>iE)7E&YIBwiHpg28`gGXM| zWv__583?k}N!jen*7zE`Fndr1-yg;TLWFuhf&YDxoboaooXAtXP6&5eteacI=8B z3nCUov4DbIQL$1K1f-}G5etYQAVr!W9RU$kq<2A@fCvcB{*sf--n<|t_yW6dCr_Tt zojWsk=6}wdIp=@p?pDatc1gSC5adATK<6n3ItMxjI!`&!Ie*Z3%7M;-E|M0l-q3c! z>ic>ve{I;x9Oy!)PMuzV{qI9=LJi#!;h2opsh(Tdurr-MS~9cp_V}a^=c8 zb?T(cg@Z?o9e*&sMvtY}bzbWC@2d4H)ay>Qzx?t`vdlK@*stf)jfLjSh#SZk{^Hk@#Bv_o;h>o=+UE(KmPb$y?Uj#Z@&3v z(#@gOYPbGJsmkqUfw)?O;r^*}v(c;KZdQtm7ccJIxqov2J!Hs`Pe1*Xz~a^~zx>jz zTQ`?oyLMf&WXTUd{Ls61Z+hU?^5x4tYsQQjE=g|AoH>IB5B}zxZ+`vt*UvutZ0XXa z(ZhfN1Kgz(QO&S>_Ut)*`gGH=_*Si2C8OIN0QBL9A8rB`Z=FU6s zG$x?jbAQi0=HI4Gn*|FNoN>k(N>bZPFTHfilqoXf>NRS+`#F0@x~ijMNQ5*=bV892dY!IZe4=h0YH~5S#rXJ38$QLO4#T@g9aIj z%yZ8@_r(`qP>r+CKKrGYUXo#F3^&{Mq?1lE`N*%FIj2^`!3|HS)S^V$(CdLS*QT0q z8GklxSh_szwA1`U^bbsyIwe+`Q#fmOFbm#yn^0@ito8{}*uRnkO{MM~o zzw^#J?p}WR7soOzte#&OTBTLV@~!tO6?UU> z)t9q1&p-cst2kYrdg`hEwY63*yX><5{rjg|<;#~RR8ptBvBH`*Z3^frRjQ;qEPep3 zt@V*4cjS>ru3x`C${CZl-+o)Q>_ctKJH|#^cf0StI~yl&k-B8XDpaV@xN+m_uYbS( z+H0?cwGGf^+qP}hIpKs8-g@h;)clP%-iWq^VYLgxDt0}&bf-oQUcI+T^Acsdm8n<+ z=#(EjcI>|U?)%Y4AK6H`J@?*wZ!((SefQk~2OPkZLH5u?51ln@mbK3gP;jGq=9y>S zefM3mx$?>@m9y7J9B~9m807x@?|(me@?ei>@zQ?g7sT%7Ki_Jco zxwH@eZFu&}&ChYl^!=t(UZkSJU-et%LHk@wzv zudt(&&78^3L-1{$_@CTc{(>;_kKuTI`32iSS?WCUyvq^`@I{ z;+FCDg9{Z7WLu$x5Ps$$uZ|dmJih+=>j=!GZ@>K(r;`O5D*|Z!gE%xX0gW393MD$x zGs?mvbSuIZttcTjeq?gwF<;0NuCZ=01R`6PS{V#3OeQS_EY;OlUw<9*LPcqq$VhyH zOC!OnV6U{@RzRD{^Uptz2f*ymUo4Lx&_4vNdSRPEFd*hhYs7M?p?Dp~B+@lpFkz-t zwl;&1dhx{AU_+rAn2u9&f+> zcC9cw`|Y=1sC;mB@B$pYvj=#Yz#xFwgXlQ8tt3{+%zq7rg4;aE>G8)O=l1Ni*IqUW zwup@akj^cC1XJu2XmM&0?8mw1o~y9P9QA3nJ@wR6M8f%Tsq;#eD%lS#!64r~hy^op zs$Kj*+gpqvZ32`uhY_#nTOjWB^<;-M_@^QImL3c7m~oTLA_c zW8-zisPcg2zcN1Wyz|%=h%-2Zoyyr?8+#rZ7F*&wve6u}?B)S_`*R+G=OK8Wa-eq} zphalHF!9)yt8sz(M#Vn32=zq64(44J3_uMm_o=p?aDZQU=*sx)- zPg_{epW|2YskbS5jPxMidGSxUrG(uQK-_4jh@be-7C#x7^|pTEU%k{TDfkW=YXIe!5P$x_oMx z7=NTd$z{ZiBJX+g=EdFUP{w#fH_p}=o56FvjoH|ua~b2|8mj5J9+cHW0~W7Rh2jTV z)Qf5IKHsn$G(ZR>x=S)#!CNppkx)_2l8FyRcMiA+7v&L5!f1zDK5b5#OC1Vog8dNJ2o9a#;S>TIQRh^J!H8oC#vwOs6i?CN8!jUfCeD7Cg_sDVKo3Zw zn1M!I;EBvU_}Cz7Zy_*wQ3 zS;2P%b#O6%ad2pB5Jnh5g@SUbjU^XD8yk81hb(TukQ@(#IJ1z9o{7i=?|})?Pb^0# zo@5~Fl!y#=A>Vv!o6yb(;nh@wCNYV>UA!3Q5~U2E&n2qVVCp+y;c zqy7{3W=J&PlTSVg>3Rga8AJ_{BjhL@L+lakQnIIvm}fwv8fx|RAKhVN! zM`2>Zs4vLEJ*$74$RM(lqncRC0MRa zfz}&MPnY9{^n3BV`_m_g&3aBtw{R@jsCg|+T|;<6}*3<^*PXa s%7M;-&VkNT4s;H5o^qgbpm){uA18ae;+D633IG5A07*qoM6N<$f_EJ|?EnA( diff --git a/tests/ref/show-text-regex-word-boundary.png b/tests/ref/show-text-regex-word-boundary.png index c171ac0270dbf4f6ad5ae50eae82e9a3edbd6125..011d9935d91203ed83ac1f2079a4103754851c94 100644 GIT binary patch delta 1261 zcmVlql>ipy5(y z+Rd%n`_RzP&(F`g*wfC=&dtru%*@QD*7~K^`pe79%F4>g$;r9XyT~=j$hFeI$H&LU z#>U0P#l*zK!^6YE!otD9!N9=4zrVk}zP`P^y}Z1%^r>Cc;rlzH(rHZzYq@<*yqobmtqLYXLJx!-~k&%&Xrh<@=kdKd# zj*gCvjg4xecWR<`jEszni;IeiiiwGdXP;__h=_)UhJ}TNgoK1-n_h#1gMxyBfq{X5 zfPi6`Qh$Gce0+S99s<38ftwR}000A4NklaOEHx#Uk}hKP&*Yh% zGmbOr!0b9Y17q$HxuaM%)D}gdW`t zK9$G0?g=5@+Ac}2gQDV{Z<&aFQ(XPwJ={*81#p!kOWB6uM^H=?;)Ax@Hc47I8=sJ~ zOl4xqI^nsjReFs*2a?z7vx*XnUevD5V{wrVIdEH;S?ci>*PqBYoKA2|MPpwC7{!|A z64AWp%lDQ58S7$yag5|MiQ;V|^~s)?1J-9hG@OqbH`KXlH`P?n`$Z=QZcFUYg9EqT; z`ErS3sZEG~=rBkXb<<`k zgEX7K@&b%#xCLXbGDu7C%jd_^&e_tBrr8_Yvt!SHIZxsxrmN*Y&HTn*6Tpm9`N5wy zl>x6fIsvm#25BjN3BQErwdtF57tvqlKxHsq22%M|)6^QTL$<#l<;?VSU<1P$a64*U}M$r3NYp>gER;KWL}9ovJ>rC zjQ)FghE~Hr4A#&6H^;>{g@rQ!k8Pc4*?tJQ6m(>@e7TfC3bRGv7YA7>kmzzoChl$t zS=G5R>Wd9kD~jqiBsj8AOjJex|Ns8}{{8*^{QUg*{Qvv=`}+F&`T6lq$+m# z%jf*z;o;!m;NRch-rnBCQYiFNoh=_=3 zonMBAhJ}TNgoK1-n_py`UW0>!f`Wp9fq{U4fMS?ae}8{`e0*P&N0I`)e;i(o8~^|V zfJsC_RCwC$)$3DKWdO(VFDxK($zWArtu!viB_JY60kZ5++LqAPEy_^90s@tl zz=#bDB&#hYf+VSf$`Z1IBwAjx)RbIGx`@^P(Vlh~-lzk!TfJ~U^UQhkJ2Ss|UYzs8 z&CSj2cINl+)0_70}3fAYP?o$uFh3t^Yk;Cp-%IFy`Z!krrNa>K{cM-$D(Z+^*S z9GGV6^Y7+XdNTo25wf`LsJ?}9UK&x_Sz9GZZ-ayPd8RWt1?})y>T0DzmkzOOl&N{q zdC&N)$zVxdAaY>IWOlyQQCxj0OLaC%KOKc`F%T|9>E>L6WR~s$f0EaQq94v;3dP%p zt7EMpht!Y0tGXCGrmD2i5?N6`?}tD+uw;e~-qr6Ye(tbR^{Z}o8`hBCY)BA>y^^%A zwra~mkkb+TeRSFx3uht%5$$VoQb&g{70aWLB z#2P|L7S4pjsf7C3*EErnCTlG=jFj*TrK;2E^HDI6|P+lzgtSg4gaqL&y1A3mQ z>EfLdKz>m!UVJ!y3|Y9yT`TrURYLedzn031m=jD=x(q<--m5EnFd%CV093q;4+Z zckli4{cyFE*MX>Q-rUrDjc9G&`x-8$8Uq4;^I=?D8WSA5(9mp_q*rk<8^ExFo3Rg} zSr|Gc=>eSbf60l&3)-aPiMqO$w9pI2)7S{mrvO1n(;z$Luj0d5``XLTvy}(UTen z(I#V1^qxlV)$OZ-KV4t8GOuzI`VBB|k15$?3(*PMiK_YS(b{onouD_#V diff --git a/tests/ref/stack-fr.png b/tests/ref/stack-fr.png index e34dd9b11afe0f80415112ce8b98da903c0a5cbd..40685731f6b0d523f814afdf9bdbbe0be5f9613c 100644 GIT binary patch delta 1846 zcmV-62g&%I5t|W^HGlj2`}h3+`uh6$`T6+x`1kks@c91r_V)Gl_4M@g?Dqci^YiHS z{_^tj@$vES@bK^N@8k0R;qw0O?(X67{q61T?Ck98>+9<3>e}x8>FMd{=;+_<=GN@} z=jZ3<=H}()<$@($Ue;k=+K_O#KgqI!^5D?;KIVf!NI}6z`(!1zrMb{y}iA>yu66X`MbNjy1Kf# zxw*KwxVN{rg~jc*wzinWypu@*Bp!Xc$*QWVsi~=`sHmr>r>3T+infrXq@<&xqoSgs zlZycxe{!vwnVFfGn3$KBmzI{6m6ermsg9GAlai8>k&%&Xrh<@=kdKd#j*gCvjg4xe zcWR<`i;Ihjii(MeiD#c`h=_=XhlhrShJ}TNgoK1-n_h#1gMxyBfq{X5fPi6`Qh$Gc zetv#^eSLgp zWgNiq@3I7J?`|Z!g@9oJK^z$xr5m7#LL14Ec&&n(;w|D4ilRs=-bWrOT1jH2n+A$& zW^6%5e;7oW9K3H zf1)QIa}jm?hH6DC6r_KI$nhp{FdWHmcGP6@qI$#>W&og?SD2m|fW?q$nN1P7=Y29P zt2Gk+iiM+=8vqb;3nHjCWMS_?PKYwhJ&7xvr2rt+JXjtaGMP;F&m?wEK;N~SJwX0A zzimO-h~FPxXetH*kg|v^!Pv?LU`sdzf2&E#V`DH5O_bQewejP}p9*JLRH=^`c3T&{ zwUO;s99@=P&_WDP%c9##+l4#r0+JS0g>%OcW41xp@K*$W=cWp$vif;0+~HF>)OSz# ziZC058#W2XOCM_I6y-{&-d7)j#-Nj@j7%nz$z(E_OeVX(q?|R}U%+7>9#XSYe^4zY zrw}0MJ6~$F4;}ZD2RrNYu#VsT;$7a`rQQi% z5D;&V6-7dCkJ5_J%qbOwUrl+JB+O%2B)L&J&pXS)|Q38@HZ{LAD) zX%j=rg#2yE6_Y!aW6ky4%l4&He>%&uDQC`tdxcd8;4B=|u{t?<5o;o;xNGxDy{Tm1=*(QKDJvAaGrM}X z`HVWJDR-ul;|eM`G9qg>hg@;njdl;c=iMtT9E3_Z;?J_So%3#oE5TYNf9&+4lEbmw zVex{PeObhBLis)WHB@pgfD=?i0*6(&e~Zh0`sK32o0_Cuk7)I0BJE4M7b;fm_Wogd z*sZ2m+dr%EPyK^`@0fBlGYPEum$SA#Y#x%E)bLtLvs=x^l#acc-v983j*V{&XZV#c z_U9Cxiny3fO`NrAjf*ole|dvW>!7Wp(h67;1V^pLNvlC{;|?lR0QCi`?h9H04|^S` zT~TRWs8e;0n`70}F7nKh$z-zst0!K1z?q~Q%2)EGe^!enVzPWe$ZxOf%md@H(|Eexsp7Mg55!)v+PA&F5flJ|dgxsbolD(~|d0rjf=5)Vd$@nYAO3 zvn~|0YqRWjc=nvzd|@5pR`=~Pz+~!Se1=-Ld^pb?fKkgD6+2AZ<*eSD8G5q&w(Sn9 zRMO97>|vT|G(JkLb60{V5YonxR)%_RhG+t!ynG1<;Ki!)gdWBQ*xZvXjmDNwVtbD~ kUbpV!kH`~TCi}Pf8}PrYOwhk#xBvhE07*qoM6N<$f~B78tpET3 delta 1828 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 LNkvXXu0mjfk4Pr$ delta 1475 zcmV;!1w8tQ42BGlB!90^OjJex|Ns8}{{8*^{QUg;`}_I%`S|$w_xSkt_xJYp_W$+9&~=;!C>=H}-A z<)-E3<>ch#;^N}}}d)4R4-rnBb-GANx+=SfR+}qpR+S=OL z+1dZtdDz(4|J+^7;=k9|*Vfk7|J85R)z#G0)YH?`_ts$F&BoEu(Zt=g*wDY<%)rpl z(9h7(|Ib*@&(F@z&i~9!@6ck*%*@Nn%e&aB%F4>g$;rsb$mh#t$H&LU#>U0P#k16) z!^6Yt#8k7;qJP4|!oR=2-@stLzP`x5m+QMkySux_xuCkby1BWzx3{;pwzj~xm9@3C zw6wIdv$L|YvazwT^`TI(u&~vyQ<=qo*sV*huCA@Et*or9;G$NZxQ^MXI;*Rz*s40J zs;a4}si&!{r>Cc;rlzH(rKF^!&!RP>qobmurLUxVpMRjCi?Vm0pP!zdo}HbYr=EG7 zoSc8QUz?ko#hEagnwsx@NST?Ln3$M{r*oH=mzI{6m6es0l$4W`lai8>cc@yBkdSh! zQ;(02j*gCvjg5PrWQ>fAi;Ii1i6?TQRf>v=h>DA{h$nELREUU(bDLR*hlhuUgNBBN zg@uJ`oqtP%gM)3BQ-XqmfP#gAfq{R2e|~;`eSLj=e0+L(dSQ=5czAeQk~v(FJ9l?? zc6N4Jk2!U9b#!!eSdBPvaBx+LG*pK*ZEkUHZf;YDGHq>bPlGN^f-GleXG?%9Wo2c7 zV#vmFC@nEn z*_M=}0Ik5#Wg~=C}IW9AeP<4vV$Nv zRZ><^cIWPVm_{$6NlVj^Uf^|N*B)5Qc7mYY`7@h0Ut0)Mv9g^u&DIK{wtd;YZBZ*? z4tGwXTP#IURHJovm-MiJv2}w;s=#l$!oXhlQw?K_hsd`l>Tntl*lAHqo*8EiV{?iW zFVU#OCVwyvfKF}bTB}yWX!=KszVp+|zNCvIcW|`PvS3-j!JH3jj}Vq+14<#>0iyo$=KcDFe)H}i zd9z4oqbMHTZLa$+a9llf=v=hQBNkXOH$Vs{e)2X`K66X9?*34vnzhuQf43srRGAhlqi5M@0&0Yh_uh4>WUx>w>v!1Tjfyij;0V8%^wq)#npDIbgBaJl3%%yno6Ex)`~ z?LCW_gzO>Nky?MJal-ewIi6N!B{H{s;56;*_e2Fwpr_pbI+K(>ByWee@9a^4hn((1 d6h-~VegX4K+;}Uyv$FsI002ovPDHLkV1i|$JFx%& diff --git a/tests/ref/text-copy-paste-ligatures.png b/tests/ref/text-copy-paste-ligatures.png index f0f36a869165b472b2aad34375d927d79485bf81..74f49e27e6aec94ced282636f8c3a695443e1eb6 100644 GIT binary patch delta 670 zcmV;P0%85<2;~TnydKBL$HvCS#l^+K!otD9!N9=4zrVk|y}i4;yVj@Fk=;%h|8RJl zo14Rbz?zzxnVFfGn3$H9mj8AnlRyC;4F6p;h=_=XhlhrShLdUm8U#vHg_@Iv0Vf+- zVyI_lXJcbyVPRntk3L^tUtC;VT3T9JSy_|H0fB$fq-UG}00I62!YQ)WgN^ z(eL&6bI$wO2ZchRP(;DnbE3=Uyv72KzH;U{ckreTBrO-_v|uADm$aQ%NeZ*TGd4E^ zk{y42lq7@Xq(t+FDAn|$3MW^@LUBGoTv-zNpUPZ!x3R_&uP-;1fNIEBwl5>0!c@S< zodP&6A-I_E_r2|2yvgS+zzyBFFin^m2I6u~Cj|6kgB>7x945~J4<=sU7dG z{*|8-v1GpMu1T33R)fa>IPv*|Ez_NJd0IcD!*z)W5k0{yy#Np@p#snXF@Tsq)S`a@ ziVn{yLals~LZSF4%x*B`A-~;26!$kHzwN^_;a$jY(;Wa+X{iCwX+czNKn@wB&}tNC zJ&`l1*gIfg@O=t0Sn(3R#DL+{4=2l~NPCL47CB5l2?v6YCf>GbVCnDB$FUbvX~;pH zwVuR0BLKdp>;#Ol$YoRjkQMYIFTozH)w#-_Dir_GUt73_k+1K8H~;_u07*qoM6N<$ Ef)mD0k^lez delta 672 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