Emojis in PDF (#3853)
8
Cargo.lock
generated
@ -1612,9 +1612,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdf-writer"
|
name = "pdf-writer"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "644b654f2de28457bf1e25a4905a76a563d1128a33ce60cf042f721f6818feaf"
|
checksum = "24e9127455063c816e661caac9ecd9043ad2871f55be93014e6838a8ced2332b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -2525,6 +2525,7 @@ dependencies = [
|
|||||||
"comemo",
|
"comemo",
|
||||||
"csv",
|
"csv",
|
||||||
"ecow",
|
"ecow",
|
||||||
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
"hayagriva",
|
"hayagriva",
|
||||||
"hypher",
|
"hypher",
|
||||||
@ -2571,6 +2572,7 @@ dependencies = [
|
|||||||
"unicode-math-class",
|
"unicode-math-class",
|
||||||
"unicode-script",
|
"unicode-script",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"unscanny",
|
||||||
"usvg",
|
"usvg",
|
||||||
"wasmi",
|
"wasmi",
|
||||||
]
|
]
|
||||||
@ -2702,6 +2704,7 @@ dependencies = [
|
|||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"image",
|
"image",
|
||||||
|
"indexmap 2.2.5",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pdf-writer",
|
"pdf-writer",
|
||||||
@ -2723,7 +2726,6 @@ version = "0.11.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"comemo",
|
"comemo",
|
||||||
"flate2",
|
|
||||||
"image",
|
"image",
|
||||||
"pixglyph",
|
"pixglyph",
|
||||||
"resvg",
|
"resvg",
|
||||||
|
@ -73,7 +73,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p
|
|||||||
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pdf-writer = "0.9.2"
|
pdf-writer = "0.9.3"
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.11", features = ["macros"] }
|
||||||
pixglyph = "0.3"
|
pixglyph = "0.3"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
|
@ -22,6 +22,7 @@ bytemuck = { workspace = true }
|
|||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
miniz_oxide = { workspace = true }
|
miniz_oxide = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
pdf-writer = { workspace = true }
|
pdf-writer = { workspace = true }
|
||||||
|
@ -3,13 +3,16 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
|
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
|
||||||
|
use pdf_writer::writers::FontDescriptor;
|
||||||
use pdf_writer::{Filter, Finish, Name, Rect, Str};
|
use pdf_writer::{Filter, Finish, Name, Rect, Str};
|
||||||
use ttf_parser::{name_id, GlyphId, Tag};
|
use ttf_parser::{name_id, GlyphId, Tag};
|
||||||
|
use typst::layout::{Abs, Em, Ratio, Transform};
|
||||||
use typst::text::Font;
|
use typst::text::Font;
|
||||||
use typst::util::SliceExt;
|
use typst::util::SliceExt;
|
||||||
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
|
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
|
||||||
|
|
||||||
use crate::{deflate, EmExt, PdfContext};
|
use crate::page::{write_frame, PageContext};
|
||||||
|
use crate::{deflate, AbsExt, EmExt, PdfContext};
|
||||||
|
|
||||||
const CFF: Tag = Tag::from_bytes(b"CFF ");
|
const CFF: Tag = Tag::from_bytes(b"CFF ");
|
||||||
const CFF2: Tag = Tag::from_bytes(b"CFF2");
|
const CFF2: Tag = Tag::from_bytes(b"CFF2");
|
||||||
@ -23,6 +26,8 @@ const SYSTEM_INFO: SystemInfo = SystemInfo {
|
|||||||
/// Embed all used fonts into the PDF.
|
/// Embed all used fonts into the PDF.
|
||||||
#[typst_macros::time(name = "write fonts")]
|
#[typst_macros::time(name = "write fonts")]
|
||||||
pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
||||||
|
write_color_fonts(ctx);
|
||||||
|
|
||||||
for font in ctx.font_map.items() {
|
for font in ctx.font_map.items() {
|
||||||
let type0_ref = ctx.alloc.bump();
|
let type0_ref = ctx.alloc.bump();
|
||||||
let cid_ref = ctx.alloc.bump();
|
let cid_ref = ctx.alloc.bump();
|
||||||
@ -32,7 +37,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
ctx.font_refs.push(type0_ref);
|
ctx.font_refs.push(type0_ref);
|
||||||
|
|
||||||
let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
|
let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
|
||||||
let metrics = font.metrics();
|
|
||||||
let ttf = font.ttf();
|
let ttf = font.ttf();
|
||||||
|
|
||||||
// Do we have a TrueType or CFF font?
|
// Do we have a TrueType or CFF font?
|
||||||
@ -103,47 +107,6 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
width_writer.finish();
|
width_writer.finish();
|
||||||
cid.finish();
|
cid.finish();
|
||||||
|
|
||||||
let mut flags = FontFlags::empty();
|
|
||||||
flags.set(FontFlags::SERIF, postscript_name.contains("Serif"));
|
|
||||||
flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
|
|
||||||
flags.set(FontFlags::ITALIC, ttf.is_italic());
|
|
||||||
flags.insert(FontFlags::SYMBOLIC);
|
|
||||||
flags.insert(FontFlags::SMALL_CAP);
|
|
||||||
|
|
||||||
let global_bbox = ttf.global_bounding_box();
|
|
||||||
let bbox = Rect::new(
|
|
||||||
font.to_em(global_bbox.x_min).to_font_units(),
|
|
||||||
font.to_em(global_bbox.y_min).to_font_units(),
|
|
||||||
font.to_em(global_bbox.x_max).to_font_units(),
|
|
||||||
font.to_em(global_bbox.y_max).to_font_units(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
|
|
||||||
let ascender = metrics.ascender.to_font_units();
|
|
||||||
let descender = metrics.descender.to_font_units();
|
|
||||||
let cap_height = metrics.cap_height.to_font_units();
|
|
||||||
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
|
|
||||||
|
|
||||||
// Write the font descriptor (contains metrics about the font).
|
|
||||||
let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref);
|
|
||||||
font_descriptor
|
|
||||||
.name(Name(base_font.as_bytes()))
|
|
||||||
.flags(flags)
|
|
||||||
.bbox(bbox)
|
|
||||||
.italic_angle(italic_angle)
|
|
||||||
.ascent(ascender)
|
|
||||||
.descent(descender)
|
|
||||||
.cap_height(cap_height)
|
|
||||||
.stem_v(stem_v);
|
|
||||||
|
|
||||||
if is_cff {
|
|
||||||
font_descriptor.font_file3(data_ref);
|
|
||||||
} else {
|
|
||||||
font_descriptor.font_file2(data_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
font_descriptor.finish();
|
|
||||||
|
|
||||||
// Write the /ToUnicode character map, which maps glyph ids back to
|
// Write the /ToUnicode character map, which maps glyph ids back to
|
||||||
// unicode codepoints to enable copying out of the PDF.
|
// unicode codepoints to enable copying out of the PDF.
|
||||||
let cmap = create_cmap(font, glyph_set);
|
let cmap = create_cmap(font, glyph_set);
|
||||||
@ -160,9 +123,173 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stream.finish();
|
stream.finish();
|
||||||
|
|
||||||
|
let mut font_descriptor =
|
||||||
|
write_font_descriptor(&mut ctx.pdf, descriptor_ref, font, &base_font);
|
||||||
|
if is_cff {
|
||||||
|
font_descriptor.font_file3(data_ref);
|
||||||
|
} else {
|
||||||
|
font_descriptor.font_file2(data_ref);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes color fonts as Type3 fonts
|
||||||
|
fn write_color_fonts(ctx: &mut PdfContext) {
|
||||||
|
let color_font_map = ctx.color_font_map.take_map();
|
||||||
|
for (font, color_font) in color_font_map {
|
||||||
|
// For each Type3 font that is part of this family…
|
||||||
|
for (font_index, subfont_id) in color_font.refs.iter().enumerate() {
|
||||||
|
// Allocate some IDs.
|
||||||
|
let cmap_ref = ctx.alloc.bump();
|
||||||
|
let descriptor_ref = ctx.alloc.bump();
|
||||||
|
let widths_ref = ctx.alloc.bump();
|
||||||
|
// And a map between glyph IDs and the instructions to draw this
|
||||||
|
// glyph.
|
||||||
|
let mut glyphs_to_instructions = Vec::new();
|
||||||
|
|
||||||
|
let start = font_index * 256;
|
||||||
|
let end = (start + 256).min(color_font.glyphs.len());
|
||||||
|
let glyph_count = end - start;
|
||||||
|
let subset = &color_font.glyphs[start..end];
|
||||||
|
let mut widths = Vec::new();
|
||||||
|
|
||||||
|
let scale_factor = font.ttf().units_per_em() as f32;
|
||||||
|
|
||||||
|
// Write the instructions for each glyph.
|
||||||
|
for color_glyph in subset {
|
||||||
|
let instructions_stream_ref = ctx.alloc.bump();
|
||||||
|
let width =
|
||||||
|
font.advance(color_glyph.gid).unwrap_or(Em::new(0.0)).to_font_units();
|
||||||
|
widths.push(width);
|
||||||
|
// Create a fake page context for `write_frame`. We are only
|
||||||
|
// interested in the contents of the page.
|
||||||
|
let size = color_glyph.frame.size();
|
||||||
|
let mut page_ctx = PageContext::new(ctx, size);
|
||||||
|
page_ctx.bottom = size.y.to_f32();
|
||||||
|
page_ctx.content.start_color_glyph(width);
|
||||||
|
page_ctx.transform(
|
||||||
|
// Make the Y axis go upwards, while preserving aspect ratio
|
||||||
|
Transform::scale(Ratio::one(), -size.aspect_ratio())
|
||||||
|
// Also move the origin to the top left corner
|
||||||
|
.post_concat(Transform::translate(Abs::zero(), size.y)),
|
||||||
|
);
|
||||||
|
write_frame(&mut page_ctx, &color_glyph.frame);
|
||||||
|
|
||||||
|
// Retrieve the stream of the page and write it.
|
||||||
|
let stream = page_ctx.content.finish();
|
||||||
|
ctx.pdf.stream(instructions_stream_ref, &stream);
|
||||||
|
|
||||||
|
// Use this stream as instructions to draw the glyph.
|
||||||
|
glyphs_to_instructions.push(instructions_stream_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the Type3 font object.
|
||||||
|
let mut pdf_font = ctx.pdf.type3_font(*subfont_id);
|
||||||
|
pdf_font.pair(Name(b"Resources"), ctx.type3_font_resources_ref);
|
||||||
|
pdf_font.bbox(color_font.bbox);
|
||||||
|
pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]);
|
||||||
|
pdf_font.first_char(0);
|
||||||
|
pdf_font.last_char((glyph_count - 1) as u8);
|
||||||
|
pdf_font.pair(Name(b"Widths"), widths_ref);
|
||||||
|
pdf_font.to_unicode(cmap_ref);
|
||||||
|
pdf_font.font_descriptor(descriptor_ref);
|
||||||
|
|
||||||
|
// Write the /CharProcs dictionary, that maps glyph names to
|
||||||
|
// drawing instructions.
|
||||||
|
let mut char_procs = pdf_font.char_procs();
|
||||||
|
for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() {
|
||||||
|
char_procs
|
||||||
|
.pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref);
|
||||||
|
}
|
||||||
|
char_procs.finish();
|
||||||
|
|
||||||
|
// Write the /Encoding dictionary.
|
||||||
|
let names = (0..glyph_count)
|
||||||
|
.map(|gid| eco_format!("glyph{gid}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
pdf_font
|
||||||
|
.encoding_custom()
|
||||||
|
.differences()
|
||||||
|
.consecutive(0, names.iter().map(|name| Name(name.as_bytes())));
|
||||||
|
pdf_font.finish();
|
||||||
|
|
||||||
|
// Encode a CMAP to make it possible to search or copy glyphs.
|
||||||
|
let glyph_set = ctx.glyph_sets.get_mut(&font).unwrap();
|
||||||
|
let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
|
||||||
|
for (index, glyph) in subset.iter().enumerate() {
|
||||||
|
let Some(text) = glyph_set.get(&glyph.gid) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
cmap.pair_with_multiple(index as u8, text.chars());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.pdf.cmap(cmap_ref, &cmap.finish());
|
||||||
|
|
||||||
|
// Write the font descriptor.
|
||||||
|
let postscript_name = font
|
||||||
|
.find_name(name_id::POST_SCRIPT_NAME)
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
let base_font = eco_format!("COLOR{font_index:x}+{postscript_name}");
|
||||||
|
write_font_descriptor(&mut ctx.pdf, descriptor_ref, &font, &base_font);
|
||||||
|
|
||||||
|
// Write the widths array
|
||||||
|
ctx.pdf.indirect(widths_ref).array().items(widths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a FontDescriptor dictionary.
|
||||||
|
fn write_font_descriptor<'a>(
|
||||||
|
pdf: &'a mut pdf_writer::Pdf,
|
||||||
|
descriptor_ref: pdf_writer::Ref,
|
||||||
|
font: &'a Font,
|
||||||
|
base_font: &EcoString,
|
||||||
|
) -> FontDescriptor<'a> {
|
||||||
|
let ttf = font.ttf();
|
||||||
|
let metrics = font.metrics();
|
||||||
|
let postscript_name = font
|
||||||
|
.find_name(name_id::POST_SCRIPT_NAME)
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let mut flags = FontFlags::empty();
|
||||||
|
flags.set(FontFlags::SERIF, postscript_name.contains("Serif"));
|
||||||
|
flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
|
||||||
|
flags.set(FontFlags::ITALIC, ttf.is_italic());
|
||||||
|
flags.insert(FontFlags::SYMBOLIC);
|
||||||
|
flags.insert(FontFlags::SMALL_CAP);
|
||||||
|
|
||||||
|
let global_bbox = ttf.global_bounding_box();
|
||||||
|
let bbox = Rect::new(
|
||||||
|
font.to_em(global_bbox.x_min).to_font_units(),
|
||||||
|
font.to_em(global_bbox.y_min).to_font_units(),
|
||||||
|
font.to_em(global_bbox.x_max).to_font_units(),
|
||||||
|
font.to_em(global_bbox.y_max).to_font_units(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
|
||||||
|
let ascender = metrics.ascender.to_font_units();
|
||||||
|
let descender = metrics.descender.to_font_units();
|
||||||
|
let cap_height = metrics.cap_height.to_font_units();
|
||||||
|
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
|
||||||
|
|
||||||
|
// Write the font descriptor (contains metrics about the font).
|
||||||
|
let mut font_descriptor = pdf.font_descriptor(descriptor_ref);
|
||||||
|
font_descriptor
|
||||||
|
.name(Name(base_font.as_bytes()))
|
||||||
|
.flags(flags)
|
||||||
|
.bbox(bbox)
|
||||||
|
.italic_angle(italic_angle)
|
||||||
|
.ascent(ascender)
|
||||||
|
.descent(descender)
|
||||||
|
.cap_height(cap_height)
|
||||||
|
.stem_v(stem_v);
|
||||||
|
|
||||||
|
font_descriptor
|
||||||
|
}
|
||||||
|
|
||||||
/// Subset a font to the given glyphs.
|
/// Subset a font to the given glyphs.
|
||||||
///
|
///
|
||||||
/// - For a font with TrueType outlines, this returns the whole OpenType font.
|
/// - For a font with TrueType outlines, this returns the whole OpenType font.
|
||||||
|
@ -15,13 +15,15 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
use indexmap::IndexMap;
|
||||||
use pdf_writer::types::Direction;
|
use pdf_writer::types::Direction;
|
||||||
use pdf_writer::writers::Destination;
|
use pdf_writer::writers::Destination;
|
||||||
use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr};
|
use pdf_writer::{Finish, Name, Pdf, Rect, Ref, Str, TextStr};
|
||||||
use typst::foundations::{Datetime, Label, NativeElement, Smart};
|
use typst::foundations::{Datetime, Label, NativeElement, Smart};
|
||||||
use typst::introspection::Location;
|
use typst::introspection::Location;
|
||||||
use typst::layout::{Abs, Dir, Em, Transform};
|
use typst::layout::{Abs, Dir, Em, Frame, Transform};
|
||||||
use typst::model::{Document, HeadingElem};
|
use typst::model::{Document, HeadingElem};
|
||||||
|
use typst::text::color::frame_for_glyph;
|
||||||
use typst::text::{Font, Lang};
|
use typst::text::{Font, Lang};
|
||||||
use typst::util::Deferred;
|
use typst::util::Deferred;
|
||||||
use typst::visualize::Image;
|
use typst::visualize::Image;
|
||||||
@ -68,6 +70,7 @@ pub fn pdf(
|
|||||||
pattern::write_patterns(&mut ctx);
|
pattern::write_patterns(&mut ctx);
|
||||||
write_named_destinations(&mut ctx);
|
write_named_destinations(&mut ctx);
|
||||||
page::write_page_tree(&mut ctx);
|
page::write_page_tree(&mut ctx);
|
||||||
|
page::write_global_resources(&mut ctx);
|
||||||
write_catalog(&mut ctx, ident, timestamp);
|
write_catalog(&mut ctx, ident, timestamp);
|
||||||
ctx.pdf.finish()
|
ctx.pdf.finish()
|
||||||
}
|
}
|
||||||
@ -96,6 +99,15 @@ struct PdfContext<'a> {
|
|||||||
alloc: Ref,
|
alloc: Ref,
|
||||||
/// The ID of the page tree.
|
/// The ID of the page tree.
|
||||||
page_tree_ref: Ref,
|
page_tree_ref: Ref,
|
||||||
|
/// The ID of the globally shared Resources dictionary.
|
||||||
|
global_resources_ref: Ref,
|
||||||
|
/// The ID of the resource dictionary shared by Type3 fonts.
|
||||||
|
///
|
||||||
|
/// Type3 fonts cannot use the global resources, as it would create some
|
||||||
|
/// kind of infinite recursion (they are themselves present in that
|
||||||
|
/// dictionary), which Acrobat doesn't appreciate (it fails to parse the
|
||||||
|
/// font) even if the specification seems to allow it.
|
||||||
|
type3_font_resources_ref: Ref,
|
||||||
/// The IDs of written pages.
|
/// The IDs of written pages.
|
||||||
page_refs: Vec<Ref>,
|
page_refs: Vec<Ref>,
|
||||||
/// The IDs of written fonts.
|
/// The IDs of written fonts.
|
||||||
@ -123,6 +135,8 @@ struct PdfContext<'a> {
|
|||||||
pattern_map: Remapper<PdfPattern>,
|
pattern_map: Remapper<PdfPattern>,
|
||||||
/// Deduplicates external graphics states used across the document.
|
/// Deduplicates external graphics states used across the document.
|
||||||
extg_map: Remapper<ExtGState>,
|
extg_map: Remapper<ExtGState>,
|
||||||
|
/// Deduplicates color glyphs.
|
||||||
|
color_font_map: ColorFontMap,
|
||||||
|
|
||||||
/// A sorted list of all named destinations.
|
/// A sorted list of all named destinations.
|
||||||
dests: Vec<(Label, Ref)>,
|
dests: Vec<(Label, Ref)>,
|
||||||
@ -134,6 +148,8 @@ impl<'a> PdfContext<'a> {
|
|||||||
fn new(document: &'a Document) -> Self {
|
fn new(document: &'a Document) -> Self {
|
||||||
let mut alloc = Ref::new(1);
|
let mut alloc = Ref::new(1);
|
||||||
let page_tree_ref = alloc.bump();
|
let page_tree_ref = alloc.bump();
|
||||||
|
let global_resources_ref = alloc.bump();
|
||||||
|
let type3_font_resources_ref = alloc.bump();
|
||||||
Self {
|
Self {
|
||||||
document,
|
document,
|
||||||
pdf: Pdf::new(),
|
pdf: Pdf::new(),
|
||||||
@ -142,6 +158,8 @@ impl<'a> PdfContext<'a> {
|
|||||||
languages: BTreeMap::new(),
|
languages: BTreeMap::new(),
|
||||||
alloc,
|
alloc,
|
||||||
page_tree_ref,
|
page_tree_ref,
|
||||||
|
global_resources_ref,
|
||||||
|
type3_font_resources_ref,
|
||||||
page_refs: vec![],
|
page_refs: vec![],
|
||||||
font_refs: vec![],
|
font_refs: vec![],
|
||||||
image_refs: vec![],
|
image_refs: vec![],
|
||||||
@ -155,6 +173,7 @@ impl<'a> PdfContext<'a> {
|
|||||||
gradient_map: Remapper::new(),
|
gradient_map: Remapper::new(),
|
||||||
pattern_map: Remapper::new(),
|
pattern_map: Remapper::new(),
|
||||||
extg_map: Remapper::new(),
|
extg_map: Remapper::new(),
|
||||||
|
color_font_map: ColorFontMap::new(),
|
||||||
dests: vec![],
|
dests: vec![],
|
||||||
loc_to_dest: HashMap::new(),
|
loc_to_dest: HashMap::new(),
|
||||||
}
|
}
|
||||||
@ -455,6 +474,98 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A mapping between `Font`s and all the corresponding `ColorFont`s.
|
||||||
|
///
|
||||||
|
/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3
|
||||||
|
/// font, and fonts generally have more color glyphs than that.
|
||||||
|
struct ColorFontMap {
|
||||||
|
/// The mapping itself
|
||||||
|
map: IndexMap<Font, ColorFont>,
|
||||||
|
/// A list of all PDF indirect references to Type3 font objects.
|
||||||
|
all_refs: Vec<Ref>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of Type3 font, belonging to the same TTF font.
|
||||||
|
struct ColorFont {
|
||||||
|
/// A list of references to Type3 font objects for this font family.
|
||||||
|
refs: Vec<Ref>,
|
||||||
|
/// The list of all color glyphs in this family.
|
||||||
|
///
|
||||||
|
/// The index in this vector modulo 256 corresponds to the index in one of
|
||||||
|
/// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the
|
||||||
|
/// quotient of the index divided by 256).
|
||||||
|
glyphs: Vec<ColorGlyph>,
|
||||||
|
/// The global bounding box of the font.
|
||||||
|
bbox: Rect,
|
||||||
|
/// A mapping between glyph IDs and character indices in the `glyphs`
|
||||||
|
/// vector.
|
||||||
|
glyph_indices: HashMap<u16, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single color glyph.
|
||||||
|
struct ColorGlyph {
|
||||||
|
/// The ID of the glyph.
|
||||||
|
gid: u16,
|
||||||
|
/// A frame that contains the glyph.
|
||||||
|
frame: Frame,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorFontMap {
|
||||||
|
/// Creates a new empty mapping
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { map: IndexMap::new(), all_refs: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the contents of the mapping.
|
||||||
|
///
|
||||||
|
/// After calling this function, the mapping will be empty.
|
||||||
|
fn take_map(&mut self) -> IndexMap<Font, ColorFont> {
|
||||||
|
std::mem::take(&mut self.map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtains the reference to a Type3 font, and an index in this font
|
||||||
|
/// that can be used to draw a color glyph.
|
||||||
|
///
|
||||||
|
/// The glyphs will be de-duplicated if needed.
|
||||||
|
fn get(&mut self, alloc: &mut Ref, font: &Font, gid: u16) -> (Ref, u8) {
|
||||||
|
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
|
||||||
|
let global_bbox = font.ttf().global_bounding_box();
|
||||||
|
let bbox = Rect::new(
|
||||||
|
font.to_em(global_bbox.x_min).to_font_units(),
|
||||||
|
font.to_em(global_bbox.y_min).to_font_units(),
|
||||||
|
font.to_em(global_bbox.x_max).to_font_units(),
|
||||||
|
font.to_em(global_bbox.y_max).to_font_units(),
|
||||||
|
);
|
||||||
|
ColorFont {
|
||||||
|
bbox,
|
||||||
|
refs: Vec::new(),
|
||||||
|
glyphs: Vec::new(),
|
||||||
|
glyph_indices: HashMap::new(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) {
|
||||||
|
// If we already know this glyph, return it.
|
||||||
|
(color_font.refs[index_of_glyph / 256], *index_of_glyph as u8)
|
||||||
|
} else {
|
||||||
|
// Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font
|
||||||
|
// if needed
|
||||||
|
let index = color_font.glyphs.len();
|
||||||
|
if index % 256 == 0 {
|
||||||
|
let new_ref = alloc.bump();
|
||||||
|
self.all_refs.push(new_ref);
|
||||||
|
color_font.refs.push(new_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
let instructions = frame_for_glyph(font, gid);
|
||||||
|
color_font.glyphs.push(ColorGlyph { gid, frame: instructions });
|
||||||
|
color_font.glyph_indices.insert(gid, index);
|
||||||
|
|
||||||
|
(color_font.refs[index / 256], index as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Additional methods for [`Abs`].
|
/// Additional methods for [`Abs`].
|
||||||
trait AbsExt {
|
trait AbsExt {
|
||||||
/// Convert an to a number of points.
|
/// Convert an to a number of points.
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use crate::color::PaintEncode;
|
||||||
|
use crate::extg::ExtGState;
|
||||||
|
use crate::image::deferred_image;
|
||||||
|
use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::{
|
use pdf_writer::types::{
|
||||||
ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle,
|
ActionType, AnnotationFlags, AnnotationType, ColorSpaceOperand, LineCapStyle,
|
||||||
@ -13,17 +17,13 @@ use typst::layout::{
|
|||||||
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
|
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
|
||||||
};
|
};
|
||||||
use typst::model::{Destination, Numbering};
|
use typst::model::{Destination, Numbering};
|
||||||
use typst::text::{Case, Font, TextItem};
|
use typst::text::color::is_color_glyph;
|
||||||
use typst::util::{Deferred, Numeric};
|
use typst::text::{Case, Font, TextItem, TextItemView};
|
||||||
|
use typst::util::{Deferred, Numeric, SliceExt};
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
|
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::color::PaintEncode;
|
|
||||||
use crate::extg::ExtGState;
|
|
||||||
use crate::image::deferred_image;
|
|
||||||
use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
|
|
||||||
|
|
||||||
/// Construct page objects.
|
/// Construct page objects.
|
||||||
#[typst_macros::time(name = "construct pages")]
|
#[typst_macros::time(name = "construct pages")]
|
||||||
pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) {
|
pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) {
|
||||||
@ -44,17 +44,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
|
|||||||
let page_ref = ctx.alloc.bump();
|
let page_ref = ctx.alloc.bump();
|
||||||
|
|
||||||
let size = frame.size();
|
let size = frame.size();
|
||||||
let mut ctx = PageContext {
|
let mut ctx = PageContext::new(ctx, size);
|
||||||
parent: ctx,
|
|
||||||
page_ref,
|
|
||||||
uses_opacities: false,
|
|
||||||
content: Content::new(),
|
|
||||||
state: State::new(size),
|
|
||||||
saves: vec![],
|
|
||||||
bottom: 0.0,
|
|
||||||
links: vec![],
|
|
||||||
resources: HashMap::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make the coordinate system start at the top-left.
|
// Make the coordinate system start at the top-left.
|
||||||
ctx.bottom = size.y.to_f32();
|
ctx.bottom = size.y.to_f32();
|
||||||
@ -73,7 +63,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
|
|||||||
let page = EncodedPage {
|
let page = EncodedPage {
|
||||||
size,
|
size,
|
||||||
content: deflate_deferred(ctx.content.finish()),
|
content: deflate_deferred(ctx.content.finish()),
|
||||||
id: ctx.page_ref,
|
id: page_ref,
|
||||||
uses_opacities: ctx.uses_opacities,
|
uses_opacities: ctx.uses_opacities,
|
||||||
links: ctx.links,
|
links: ctx.links,
|
||||||
label: None,
|
label: None,
|
||||||
@ -85,10 +75,8 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
|
|||||||
|
|
||||||
/// Write the page tree.
|
/// Write the page tree.
|
||||||
pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
|
pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
|
||||||
let resources_ref = write_global_resources(ctx);
|
|
||||||
|
|
||||||
for i in 0..ctx.pages.len() {
|
for i in 0..ctx.pages.len() {
|
||||||
write_page(ctx, i, resources_ref);
|
write_page(ctx, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.pdf
|
ctx.pdf
|
||||||
@ -102,30 +90,20 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
|
|||||||
/// We add a reference to this dictionary to each page individually instead of
|
/// We add a reference to this dictionary to each page individually instead of
|
||||||
/// to the root node of the page tree because using the resource inheritance
|
/// to the root node of the page tree because using the resource inheritance
|
||||||
/// feature breaks PDF merging with Apple Preview.
|
/// feature breaks PDF merging with Apple Preview.
|
||||||
fn write_global_resources(ctx: &mut PdfContext) -> Ref {
|
pub(crate) fn write_global_resources(ctx: &mut PdfContext) {
|
||||||
let resource_ref = ctx.alloc.bump();
|
let images_ref = ctx.alloc.bump();
|
||||||
|
let patterns_ref = ctx.alloc.bump();
|
||||||
|
let ext_gs_states_ref = ctx.alloc.bump();
|
||||||
|
let color_spaces_ref = ctx.alloc.bump();
|
||||||
|
|
||||||
let mut resources = ctx.pdf.indirect(resource_ref).start::<Resources>();
|
let mut images = ctx.pdf.indirect(images_ref).dict();
|
||||||
ctx.colors
|
|
||||||
.write_color_spaces(resources.color_spaces(), &mut ctx.alloc);
|
|
||||||
|
|
||||||
let mut fonts = resources.fonts();
|
|
||||||
for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) {
|
|
||||||
let name = eco_format!("F{}", f);
|
|
||||||
fonts.pair(Name(name.as_bytes()), font_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
fonts.finish();
|
|
||||||
|
|
||||||
let mut images = resources.x_objects();
|
|
||||||
for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) {
|
for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) {
|
||||||
let name = eco_format!("Im{}", im);
|
let name = eco_format!("Im{}", im);
|
||||||
images.pair(Name(name.as_bytes()), image_ref);
|
images.pair(Name(name.as_bytes()), image_ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
images.finish();
|
images.finish();
|
||||||
|
|
||||||
let mut patterns = resources.patterns();
|
let mut patterns = ctx.pdf.indirect(patterns_ref).dict();
|
||||||
for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) {
|
for (gradient_ref, gr) in ctx.gradient_map.pdf_indices(&ctx.gradient_refs) {
|
||||||
let name = eco_format!("Gr{}", gr);
|
let name = eco_format!("Gr{}", gr);
|
||||||
patterns.pair(Name(name.as_bytes()), gradient_ref);
|
patterns.pair(Name(name.as_bytes()), gradient_ref);
|
||||||
@ -135,26 +113,64 @@ fn write_global_resources(ctx: &mut PdfContext) -> Ref {
|
|||||||
let name = eco_format!("P{}", p);
|
let name = eco_format!("P{}", p);
|
||||||
patterns.pair(Name(name.as_bytes()), pattern_ref);
|
patterns.pair(Name(name.as_bytes()), pattern_ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
patterns.finish();
|
patterns.finish();
|
||||||
|
|
||||||
let mut ext_gs_states = resources.ext_g_states();
|
let mut ext_gs_states = ctx.pdf.indirect(ext_gs_states_ref).dict();
|
||||||
for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) {
|
for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) {
|
||||||
let name = eco_format!("Gs{}", gs);
|
let name = eco_format!("Gs{}", gs);
|
||||||
ext_gs_states.pair(Name(name.as_bytes()), gs_ref);
|
ext_gs_states.pair(Name(name.as_bytes()), gs_ref);
|
||||||
}
|
}
|
||||||
ext_gs_states.finish();
|
ext_gs_states.finish();
|
||||||
|
|
||||||
|
let color_spaces = ctx.pdf.indirect(color_spaces_ref).dict();
|
||||||
|
ctx.colors.write_color_spaces(color_spaces, &mut ctx.alloc);
|
||||||
|
|
||||||
|
let mut resources = ctx.pdf.indirect(ctx.global_resources_ref).start::<Resources>();
|
||||||
|
resources.pair(Name(b"XObject"), images_ref);
|
||||||
|
resources.pair(Name(b"Pattern"), patterns_ref);
|
||||||
|
resources.pair(Name(b"ExtGState"), ext_gs_states_ref);
|
||||||
|
resources.pair(Name(b"ColorSpace"), color_spaces_ref);
|
||||||
|
|
||||||
|
let mut fonts = resources.fonts();
|
||||||
|
for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) {
|
||||||
|
let name = eco_format!("F{}", f);
|
||||||
|
fonts.pair(Name(name.as_bytes()), font_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
for font in &ctx.color_font_map.all_refs {
|
||||||
|
let name = eco_format!("Cf{}", font.get());
|
||||||
|
fonts.pair(Name(name.as_bytes()), font);
|
||||||
|
}
|
||||||
|
fonts.finish();
|
||||||
|
|
||||||
resources.finish();
|
resources.finish();
|
||||||
|
|
||||||
|
// Also write the resources for Type3 fonts, that only contains images,
|
||||||
|
// color spaces and regular fonts (COLR glyphs depend on them).
|
||||||
|
if !ctx.color_font_map.all_refs.is_empty() {
|
||||||
|
let mut resources =
|
||||||
|
ctx.pdf.indirect(ctx.type3_font_resources_ref).start::<Resources>();
|
||||||
|
resources.pair(Name(b"XObject"), images_ref);
|
||||||
|
resources.pair(Name(b"Pattern"), patterns_ref);
|
||||||
|
resources.pair(Name(b"ExtGState"), ext_gs_states_ref);
|
||||||
|
resources.pair(Name(b"ColorSpace"), color_spaces_ref);
|
||||||
|
|
||||||
|
let mut fonts = resources.fonts();
|
||||||
|
for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) {
|
||||||
|
let name = eco_format!("F{}", f);
|
||||||
|
fonts.pair(Name(name.as_bytes()), font_ref);
|
||||||
|
}
|
||||||
|
fonts.finish();
|
||||||
|
|
||||||
|
resources.finish();
|
||||||
|
}
|
||||||
|
|
||||||
// Write all of the functions used by the document.
|
// Write all of the functions used by the document.
|
||||||
ctx.colors.write_functions(&mut ctx.pdf);
|
ctx.colors.write_functions(&mut ctx.pdf);
|
||||||
|
|
||||||
resource_ref
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a page tree node.
|
/// Write a page tree node.
|
||||||
fn write_page(ctx: &mut PdfContext, i: usize, resources_ref: Ref) {
|
fn write_page(ctx: &mut PdfContext, i: usize) {
|
||||||
let page = &ctx.pages[i];
|
let page = &ctx.pages[i];
|
||||||
let content_id = ctx.alloc.bump();
|
let content_id = ctx.alloc.bump();
|
||||||
|
|
||||||
@ -165,7 +181,7 @@ fn write_page(ctx: &mut PdfContext, i: usize, resources_ref: Ref) {
|
|||||||
let h = page.size.y.to_f32();
|
let h = page.size.y.to_f32();
|
||||||
page_writer.media_box(Rect::new(0.0, 0.0, w, h));
|
page_writer.media_box(Rect::new(0.0, 0.0, w, h));
|
||||||
page_writer.contents(content_id);
|
page_writer.contents(content_id);
|
||||||
page_writer.pair(Name(b"Resources"), resources_ref);
|
page_writer.pair(Name(b"Resources"), ctx.global_resources_ref);
|
||||||
|
|
||||||
if page.uses_opacities {
|
if page.uses_opacities {
|
||||||
page_writer
|
page_writer
|
||||||
@ -434,17 +450,31 @@ impl PageResource {
|
|||||||
/// An exporter for the contents of a single PDF page.
|
/// An exporter for the contents of a single PDF page.
|
||||||
pub struct PageContext<'a, 'b> {
|
pub struct PageContext<'a, 'b> {
|
||||||
pub(crate) parent: &'a mut PdfContext<'b>,
|
pub(crate) parent: &'a mut PdfContext<'b>,
|
||||||
page_ref: Ref,
|
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
state: State,
|
state: State,
|
||||||
saves: Vec<State>,
|
saves: Vec<State>,
|
||||||
bottom: f32,
|
pub bottom: f32,
|
||||||
uses_opacities: bool,
|
uses_opacities: bool,
|
||||||
links: Vec<(Destination, Rect)>,
|
links: Vec<(Destination, Rect)>,
|
||||||
/// Keep track of the resources being used in the page.
|
/// Keep track of the resources being used in the page.
|
||||||
pub resources: HashMap<PageResource, usize>,
|
pub resources: HashMap<PageResource, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> PageContext<'a, 'b> {
|
||||||
|
pub fn new(parent: &'a mut PdfContext<'b>, size: Size) -> Self {
|
||||||
|
PageContext {
|
||||||
|
parent,
|
||||||
|
uses_opacities: false,
|
||||||
|
content: Content::new(),
|
||||||
|
state: State::new(size),
|
||||||
|
saves: vec![],
|
||||||
|
bottom: 0.0,
|
||||||
|
links: vec![],
|
||||||
|
resources: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A simulated graphics state used to deduplicate graphics state changes and
|
/// A simulated graphics state used to deduplicate graphics state changes and
|
||||||
/// keep track of the current transformation matrix for link annotations.
|
/// keep track of the current transformation matrix for link annotations.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -555,7 +585,7 @@ impl PageContext<'_, '_> {
|
|||||||
self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity });
|
self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform(&mut self, transform: Transform) {
|
pub fn transform(&mut self, transform: Transform) {
|
||||||
let Transform { sx, ky, kx, sy, tx, ty } = transform;
|
let Transform { sx, ky, kx, sy, tx, ty } = transform;
|
||||||
self.state.transform = self.state.transform.pre_concat(transform);
|
self.state.transform = self.state.transform.pre_concat(transform);
|
||||||
if self.state.container_transform.is_identity() {
|
if self.state.container_transform.is_identity() {
|
||||||
@ -670,7 +700,7 @@ impl PageContext<'_, '_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encode a frame into the content stream.
|
/// Encode a frame into the content stream.
|
||||||
fn write_frame(ctx: &mut PageContext, frame: &Frame) {
|
pub(crate) fn write_frame(ctx: &mut PageContext, frame: &Frame) {
|
||||||
for &(pos, ref item) in frame.items() {
|
for &(pos, ref item) in frame.items() {
|
||||||
let x = pos.x.to_f32();
|
let x = pos.x.to_f32();
|
||||||
let y = pos.y.to_f32();
|
let y = pos.y.to_f32();
|
||||||
@ -718,21 +748,71 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
|
|||||||
|
|
||||||
/// Encode a text run into the content stream.
|
/// Encode a text run into the content stream.
|
||||||
fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
||||||
|
let ttf = text.font.ttf();
|
||||||
|
let tables = ttf.tables();
|
||||||
|
|
||||||
|
// If the text run contains either only color glyphs (used for emojis for
|
||||||
|
// example) or normal text we can render it directly
|
||||||
|
let has_color_glyphs = tables.sbix.is_some()
|
||||||
|
|| tables.cbdt.is_some()
|
||||||
|
|| tables.svg.is_some()
|
||||||
|
|| tables.colr.is_some();
|
||||||
|
if !has_color_glyphs {
|
||||||
|
write_normal_text(ctx, pos, TextItemView::all_of(text));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_glyph_count =
|
||||||
|
text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count();
|
||||||
|
|
||||||
|
if color_glyph_count == text.glyphs.len() {
|
||||||
|
write_color_glyphs(ctx, pos, TextItemView::all_of(text));
|
||||||
|
} else if color_glyph_count == 0 {
|
||||||
|
write_normal_text(ctx, pos, TextItemView::all_of(text));
|
||||||
|
} else {
|
||||||
|
// Otherwise we need to split it in smaller text runs
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut position_in_run = Abs::zero();
|
||||||
|
for (color, sub_run) in
|
||||||
|
text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g))
|
||||||
|
{
|
||||||
|
let end = offset + sub_run.len();
|
||||||
|
|
||||||
|
// Build a sub text-run
|
||||||
|
let text_item_view = TextItemView::from_glyph_range(text, offset..end);
|
||||||
|
|
||||||
|
// Adjust the position of the run on the line
|
||||||
|
let pos = pos + Point::new(position_in_run, Abs::zero());
|
||||||
|
position_in_run += text_item_view.width();
|
||||||
|
offset = end;
|
||||||
|
// Actually write the sub text-run
|
||||||
|
if color {
|
||||||
|
write_color_glyphs(ctx, pos, text_item_view);
|
||||||
|
} else {
|
||||||
|
write_normal_text(ctx, pos, text_item_view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encodes a text run (without any color glyph) into the content stream.
|
||||||
|
fn write_normal_text(ctx: &mut PageContext, pos: Point, text: TextItemView) {
|
||||||
let x = pos.x.to_f32();
|
let x = pos.x.to_f32();
|
||||||
let y = pos.y.to_f32();
|
let y = pos.y.to_f32();
|
||||||
|
|
||||||
*ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
|
*ctx.parent.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len();
|
||||||
|
|
||||||
let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default();
|
let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default();
|
||||||
for g in &text.glyphs {
|
for g in text.glyphs() {
|
||||||
let segment = &text.text[g.range()];
|
let t = text.text();
|
||||||
|
let segment = &t[g.range()];
|
||||||
glyph_set.entry(g.id).or_insert_with(|| segment.into());
|
glyph_set.entry(g.id).or_insert_with(|| segment.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let fill_transform = ctx.state.transforms(Size::zero(), pos);
|
let fill_transform = ctx.state.transforms(Size::zero(), pos);
|
||||||
ctx.set_fill(&text.fill, true, fill_transform);
|
ctx.set_fill(&text.item.fill, true, fill_transform);
|
||||||
|
|
||||||
let stroke = text.stroke.as_ref().and_then(|stroke| {
|
let stroke = text.item.stroke.as_ref().and_then(|stroke| {
|
||||||
if stroke.thickness.to_f32() > 0.0 {
|
if stroke.thickness.to_f32() > 0.0 {
|
||||||
Some(stroke)
|
Some(stroke)
|
||||||
} else {
|
} else {
|
||||||
@ -747,8 +827,8 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
|||||||
ctx.set_text_rendering_mode(TextRenderingMode::Fill);
|
ctx.set_text_rendering_mode(TextRenderingMode::Fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.set_font(&text.font, text.size);
|
ctx.set_font(&text.item.font, text.item.size);
|
||||||
ctx.set_opacities(text.stroke.as_ref(), Some(&text.fill));
|
ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill));
|
||||||
ctx.content.begin_text();
|
ctx.content.begin_text();
|
||||||
|
|
||||||
// Position the text.
|
// Position the text.
|
||||||
@ -760,7 +840,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
|||||||
let mut encoded = vec![];
|
let mut encoded = vec![];
|
||||||
|
|
||||||
// Write the glyphs with kerning adjustments.
|
// Write the glyphs with kerning adjustments.
|
||||||
for glyph in &text.glyphs {
|
for glyph in text.glyphs() {
|
||||||
adjustment += glyph.x_offset;
|
adjustment += glyph.x_offset;
|
||||||
|
|
||||||
if !adjustment.is_zero() {
|
if !adjustment.is_zero() {
|
||||||
@ -773,11 +853,11 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
|||||||
adjustment = Em::zero();
|
adjustment = Em::zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
let cid = crate::font::glyph_cid(&text.font, glyph.id);
|
let cid = crate::font::glyph_cid(&text.item.font, glyph.id);
|
||||||
encoded.push((cid >> 8) as u8);
|
encoded.push((cid >> 8) as u8);
|
||||||
encoded.push((cid & 0xff) as u8);
|
encoded.push((cid & 0xff) as u8);
|
||||||
|
|
||||||
if let Some(advance) = text.font.advance(glyph.id) {
|
if let Some(advance) = text.item.font.advance(glyph.id) {
|
||||||
adjustment += glyph.x_advance - advance;
|
adjustment += glyph.x_advance - advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -793,6 +873,46 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
|
|||||||
ctx.content.end_text();
|
ctx.content.end_text();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encodes a text run made only of color glyphs into the content stream
|
||||||
|
fn write_color_glyphs(ctx: &mut PageContext, pos: Point, text: TextItemView) {
|
||||||
|
let x = pos.x.to_f32();
|
||||||
|
let y = pos.y.to_f32();
|
||||||
|
|
||||||
|
let mut last_font = None;
|
||||||
|
|
||||||
|
ctx.content.begin_text();
|
||||||
|
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
|
||||||
|
// So that the next call to ctx.set_font() will change the font to one that
|
||||||
|
// displays regular glyphs and not color glyphs.
|
||||||
|
ctx.state.font = None;
|
||||||
|
|
||||||
|
let glyph_set = ctx.parent.glyph_sets.entry(text.item.font.clone()).or_default();
|
||||||
|
|
||||||
|
for glyph in text.glyphs() {
|
||||||
|
// Retrieve the Type3 font reference and the glyph index in the font.
|
||||||
|
let (font, index) = ctx.parent.color_font_map.get(
|
||||||
|
&mut ctx.parent.alloc,
|
||||||
|
&text.item.font,
|
||||||
|
glyph.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if last_font != Some(font.get()) {
|
||||||
|
ctx.content.set_font(
|
||||||
|
Name(eco_format!("Cf{}", font.get()).as_bytes()),
|
||||||
|
text.item.size.to_f32(),
|
||||||
|
);
|
||||||
|
last_font = Some(font.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.content.show(Str(&[index]));
|
||||||
|
|
||||||
|
glyph_set
|
||||||
|
.entry(glyph.id)
|
||||||
|
.or_insert_with(|| text.text()[glyph.range()].into());
|
||||||
|
}
|
||||||
|
ctx.content.end_text();
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode a geometrical shape into the content stream.
|
/// Encode a geometrical shape into the content stream.
|
||||||
fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
|
fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
|
||||||
let x = pos.x.to_f32();
|
let x = pos.x.to_f32();
|
||||||
|
@ -18,7 +18,6 @@ typst-macros = { workspace = true }
|
|||||||
typst-timing = { workspace = true }
|
typst-timing = { workspace = true }
|
||||||
bytemuck = { workspace = true }
|
bytemuck = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
flate2 = { workspace = true }
|
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
pixglyph = { workspace = true }
|
pixglyph = { workspace = true }
|
||||||
resvg = { workspace = true }
|
resvg = { workspace = true }
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
//! Rendering of Typst documents into raster images.
|
//! Rendering of Typst documents into raster images.
|
||||||
|
|
||||||
use std::io::Read;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::{GenericImageView, Rgba};
|
use image::{GenericImageView, Rgba};
|
||||||
use pixglyph::Bitmap;
|
use pixglyph::Bitmap;
|
||||||
use resvg::tiny_skia::IntRect;
|
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use typst::introspection::Meta;
|
use typst::introspection::Meta;
|
||||||
@ -14,12 +12,12 @@ use typst::layout::{
|
|||||||
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
|
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
|
||||||
};
|
};
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
|
use typst::text::color::{frame_for_glyph, is_color_glyph};
|
||||||
use typst::text::{Font, TextItem};
|
use typst::text::{Font, TextItem};
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap,
|
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap,
|
||||||
LineJoin, Paint, Path, PathItem, Pattern, RasterFormat, RelativeTo, Shape,
|
LineJoin, Paint, Path, PathItem, Pattern, RelativeTo, Shape,
|
||||||
};
|
};
|
||||||
use usvg::TreeParsing;
|
|
||||||
|
|
||||||
/// Export a frame into a raster image.
|
/// Export a frame into a raster image.
|
||||||
///
|
///
|
||||||
@ -115,6 +113,13 @@ impl<'a> State<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pre_scale(self, scale: Axes<Abs>) -> Self {
|
||||||
|
Self {
|
||||||
|
transform: self.transform.pre_scale(scale.x.to_f32(), scale.y.to_f32()),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre concat the current item's transform.
|
/// Pre concat the current item's transform.
|
||||||
fn pre_concat(self, transform: sk::Transform) -> Self {
|
fn pre_concat(self, transform: sk::Transform) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -236,132 +241,27 @@ fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
|
|||||||
for glyph in &text.glyphs {
|
for glyph in &text.glyphs {
|
||||||
let id = GlyphId(glyph.id);
|
let id = GlyphId(glyph.id);
|
||||||
let offset = x + glyph.x_offset.at(text.size).to_f32();
|
let offset = x + glyph.x_offset.at(text.size).to_f32();
|
||||||
let state = state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
|
|
||||||
|
|
||||||
render_svg_glyph(canvas, state, text, id)
|
if is_color_glyph(&text.font, glyph) {
|
||||||
.or_else(|| render_bitmap_glyph(canvas, state, text, id))
|
let upem = text.font.units_per_em();
|
||||||
.or_else(|| render_outline_glyph(canvas, state, text, id));
|
let text_scale = Abs::raw(text.size.to_raw() / upem);
|
||||||
|
let state = state
|
||||||
|
.pre_translate(Point::new(Abs::raw(offset as _), -text.size))
|
||||||
|
.pre_scale(Axes::new(text_scale, text_scale));
|
||||||
|
|
||||||
|
let glyph_frame = frame_for_glyph(&text.font, glyph.id);
|
||||||
|
|
||||||
|
render_frame(canvas, state, &glyph_frame);
|
||||||
|
} else {
|
||||||
|
let state =
|
||||||
|
state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
|
||||||
|
render_outline_glyph(canvas, state, text, id);
|
||||||
|
}
|
||||||
|
|
||||||
x += glyph.x_advance.at(text.size).to_f32();
|
x += glyph.x_advance.at(text.size).to_f32();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render an SVG glyph into the canvas.
|
|
||||||
fn render_svg_glyph(
|
|
||||||
canvas: &mut sk::Pixmap,
|
|
||||||
state: State,
|
|
||||||
text: &TextItem,
|
|
||||||
id: GlyphId,
|
|
||||||
) -> Option<()> {
|
|
||||||
let ts = &state.transform;
|
|
||||||
let mut data = text.font.ttf().glyph_svg_image(id)?.data;
|
|
||||||
|
|
||||||
// Decompress SVGZ.
|
|
||||||
let mut decoded = vec![];
|
|
||||||
if data.starts_with(&[0x1f, 0x8b]) {
|
|
||||||
let mut decoder = flate2::read::GzDecoder::new(data);
|
|
||||||
decoder.read_to_end(&mut decoded).ok()?;
|
|
||||||
data = &decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse XML.
|
|
||||||
let xml = std::str::from_utf8(data).ok()?;
|
|
||||||
let document = roxmltree::Document::parse(xml).ok()?;
|
|
||||||
let root = document.root_element();
|
|
||||||
|
|
||||||
// Parse SVG.
|
|
||||||
let opts = usvg::Options::default();
|
|
||||||
let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
|
|
||||||
tree.calculate_bounding_boxes();
|
|
||||||
let view_box = tree.view_box.rect;
|
|
||||||
|
|
||||||
// If there's no viewbox defined, use the em square for our scale
|
|
||||||
// transformation ...
|
|
||||||
let upem = text.font.units_per_em() as f32;
|
|
||||||
let (mut width, mut height) = (upem, upem);
|
|
||||||
|
|
||||||
// ... but if there's a viewbox or width, use that.
|
|
||||||
if root.has_attribute("viewBox") || root.has_attribute("width") {
|
|
||||||
width = view_box.width();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as for width.
|
|
||||||
if root.has_attribute("viewBox") || root.has_attribute("height") {
|
|
||||||
height = view_box.height();
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = text.size.to_f32();
|
|
||||||
let ts = ts.pre_scale(size / width, size / height);
|
|
||||||
|
|
||||||
// Compute the space we need to draw our glyph.
|
|
||||||
// See https://github.com/RazrFalcon/resvg/issues/602 for why
|
|
||||||
// using the svg size is problematic here.
|
|
||||||
let mut bbox = usvg::BBox::default();
|
|
||||||
if let Some(tree_bbox) = tree.root.bounding_box {
|
|
||||||
bbox = bbox.expand(tree_bbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the bbox after the transform is applied.
|
|
||||||
// We add a nice 5px border along the bounding box to
|
|
||||||
// be on the safe size. We also compute the intersection
|
|
||||||
// with the canvas rectangle
|
|
||||||
let bbox = bbox.transform(ts)?.to_rect()?.round_out()?;
|
|
||||||
let bbox = IntRect::from_xywh(
|
|
||||||
bbox.left() - 5,
|
|
||||||
bbox.y() - 5,
|
|
||||||
bbox.width() + 10,
|
|
||||||
bbox.height() + 10,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?;
|
|
||||||
|
|
||||||
// We offset our transform so that the pixmap starts at the edge of the bbox.
|
|
||||||
let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32);
|
|
||||||
resvg::render(&tree, ts, &mut pixmap.as_mut());
|
|
||||||
|
|
||||||
canvas.draw_pixmap(
|
|
||||||
bbox.left(),
|
|
||||||
bbox.top(),
|
|
||||||
pixmap.as_ref(),
|
|
||||||
&sk::PixmapPaint::default(),
|
|
||||||
sk::Transform::identity(),
|
|
||||||
state.mask,
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a bitmap glyph into the canvas.
|
|
||||||
fn render_bitmap_glyph(
|
|
||||||
canvas: &mut sk::Pixmap,
|
|
||||||
state: State,
|
|
||||||
text: &TextItem,
|
|
||||||
id: GlyphId,
|
|
||||||
) -> Option<()> {
|
|
||||||
let ts = state.transform;
|
|
||||||
let size = text.size.to_f32();
|
|
||||||
let ppem = size * ts.sy;
|
|
||||||
let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?;
|
|
||||||
if raster.format != ttf_parser::RasterImageFormat::PNG {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?;
|
|
||||||
|
|
||||||
// FIXME: Vertical alignment isn't quite right for Apple Color Emoji,
|
|
||||||
// and maybe also for Noto Color Emoji. And: Is the size calculation
|
|
||||||
// correct?
|
|
||||||
let h = text.size;
|
|
||||||
let w = (image.width() / image.height()) * h;
|
|
||||||
let dx = (raster.x as f32) / (image.width() as f32) * size;
|
|
||||||
let dy = (raster.y as f32) / (image.height() as f32) * size;
|
|
||||||
render_image(
|
|
||||||
canvas,
|
|
||||||
state.pre_translate(Point::new(Abs::raw(dx as _), Abs::raw((-size - dy) as _))),
|
|
||||||
&image,
|
|
||||||
Size::new(w, h),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render an outline glyph into the canvas. This is the "normal" case.
|
/// Render an outline glyph into the canvas. This is the "normal" case.
|
||||||
fn render_outline_glyph(
|
fn render_outline_glyph(
|
||||||
canvas: &mut sk::Pixmap,
|
canvas: &mut sk::Pixmap,
|
||||||
|
@ -24,6 +24,7 @@ ciborium = { workspace = true }
|
|||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
csv = { workspace = true }
|
csv = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
|
flate2 = { workspace = true }
|
||||||
fontdb = { workspace = true }
|
fontdb = { workspace = true }
|
||||||
hayagriva = { workspace = true }
|
hayagriva = { workspace = true }
|
||||||
hypher = { workspace = true }
|
hypher = { workspace = true }
|
||||||
@ -64,6 +65,7 @@ unicode-bidi = { workspace = true }
|
|||||||
unicode-math-class = { workspace = true }
|
unicode-math-class = { workspace = true }
|
||||||
unicode-script = { workspace = true }
|
unicode-script = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
|
unscanny = { workspace = true }
|
||||||
usvg = { workspace = true }
|
usvg = { workspace = true }
|
||||||
wasmi = { workspace = true }
|
wasmi = { workspace = true }
|
||||||
|
|
||||||
|
272
crates/typst/src/text/font/color.rs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
//! Utilities for color font handling
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use ecow::EcoString;
|
||||||
|
use ttf_parser::GlyphId;
|
||||||
|
use usvg::{TreeParsing, TreeWriting};
|
||||||
|
|
||||||
|
use crate::layout::{Abs, Axes, Em, Frame, FrameItem, Point, Size};
|
||||||
|
use crate::syntax::Span;
|
||||||
|
use crate::text::{Font, Glyph, Lang, TextItem};
|
||||||
|
use crate::visualize::{Color, Image, Paint, Rgb};
|
||||||
|
|
||||||
|
/// Tells if a glyph is a color glyph or not in a given font.
|
||||||
|
pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool {
|
||||||
|
let ttf = font.ttf();
|
||||||
|
let glyph_id = GlyphId(g.id);
|
||||||
|
ttf.glyph_raster_image(glyph_id, 160).is_some()
|
||||||
|
|| ttf.glyph_svg_image(glyph_id).is_some()
|
||||||
|
|| ttf.is_color_glyph(glyph_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a frame with the glyph drawn inside.
|
||||||
|
///
|
||||||
|
/// The glyphs are sized in font units, [`text.item.size`] is not taken into
|
||||||
|
/// account.
|
||||||
|
#[comemo::memoize]
|
||||||
|
pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame {
|
||||||
|
let ttf = font.ttf();
|
||||||
|
let upem = Abs::pt(ttf.units_per_em() as f64);
|
||||||
|
let glyph_id = GlyphId(glyph_id);
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(Size::splat(upem));
|
||||||
|
|
||||||
|
if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) {
|
||||||
|
draw_raster_glyph(&mut frame, font, upem, raster_image);
|
||||||
|
} else if ttf.glyph_svg_image(glyph_id).is_some() {
|
||||||
|
draw_svg_glyph(&mut frame, upem, font, glyph_id);
|
||||||
|
} else if ttf.is_color_glyph(glyph_id) {
|
||||||
|
draw_colr_glyph(&mut frame, font, glyph_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a raster glyph in a frame.
|
||||||
|
fn draw_raster_glyph(
|
||||||
|
frame: &mut Frame,
|
||||||
|
font: &Font,
|
||||||
|
upem: Abs,
|
||||||
|
raster_image: ttf_parser::RasterGlyphImage,
|
||||||
|
) {
|
||||||
|
let image = Image::new(
|
||||||
|
raster_image.data.into(),
|
||||||
|
typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Apple Color emoji doesn't provide offset information (or at least
|
||||||
|
// not in a way ttf-parser understands), so we artificially shift their
|
||||||
|
// baseline to make it look good.
|
||||||
|
let y_offset = if font.info().family.to_lowercase() == "apple color emoji" {
|
||||||
|
20.0
|
||||||
|
} else {
|
||||||
|
-(raster_image.y as f64)
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = Point::new(
|
||||||
|
upem * raster_image.x as f64 / raster_image.pixels_per_em as f64,
|
||||||
|
upem * y_offset / raster_image.pixels_per_em as f64,
|
||||||
|
);
|
||||||
|
let aspect_ratio = image.width() / image.height();
|
||||||
|
let size = Axes::new(upem, upem * aspect_ratio);
|
||||||
|
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a COLR glyph in a frame.
|
||||||
|
fn draw_colr_glyph(frame: &mut Frame, font: &Font, glyph_id: GlyphId) {
|
||||||
|
let mut painter = ColrPainter { font, current_glyph: glyph_id, frame };
|
||||||
|
font.ttf().paint_color_glyph(glyph_id, 0, &mut painter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws COLR glyphs in a frame.
|
||||||
|
struct ColrPainter<'f, 't> {
|
||||||
|
/// The frame in which to draw.
|
||||||
|
frame: &'f mut Frame,
|
||||||
|
/// The font of the text.
|
||||||
|
font: &'t Font,
|
||||||
|
/// The glyph that will be drawn the next time `ColrPainter::paint` is called.
|
||||||
|
current_glyph: GlyphId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'f, 't> ColrPainter<'f, 't> {
|
||||||
|
fn paint(&mut self, fill: Paint) {
|
||||||
|
self.frame.push(
|
||||||
|
// With images, the position corresponds to the top-left corner, but
|
||||||
|
// in the case of text it matches the baseline-left point. Here, we
|
||||||
|
// move the glyph one unit down to compensate for that.
|
||||||
|
Point::new(Abs::zero(), Abs::pt(self.font.units_per_em())),
|
||||||
|
FrameItem::Text(TextItem {
|
||||||
|
font: self.font.clone(),
|
||||||
|
size: Abs::pt(self.font.units_per_em()),
|
||||||
|
fill,
|
||||||
|
stroke: None,
|
||||||
|
lang: Lang::ENGLISH,
|
||||||
|
text: EcoString::new(),
|
||||||
|
glyphs: vec![Glyph {
|
||||||
|
id: self.current_glyph.0,
|
||||||
|
// Advance is not relevant here as we will draw glyph on top
|
||||||
|
// of each other anyway
|
||||||
|
x_advance: Em::zero(),
|
||||||
|
x_offset: Em::zero(),
|
||||||
|
range: 0..0,
|
||||||
|
span: (Span::detached(), 0),
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'f, 't> ttf_parser::colr::Painter for ColrPainter<'f, 't> {
|
||||||
|
fn outline(&mut self, glyph_id: GlyphId) {
|
||||||
|
self.current_glyph = glyph_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_foreground(&mut self) {
|
||||||
|
// Default to black if no color was specified
|
||||||
|
self.paint(Paint::Solid(Color::BLACK))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_color(&mut self, color: ttf_parser::RgbaColor) {
|
||||||
|
let color = Color::Rgb(Rgb::new(
|
||||||
|
color.red as f32 / 255.0,
|
||||||
|
color.green as f32 / 255.0,
|
||||||
|
color.blue as f32 / 255.0,
|
||||||
|
color.alpha as f32 / 255.0,
|
||||||
|
));
|
||||||
|
self.paint(Paint::Solid(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws an SVG glyph in a frame.
|
||||||
|
fn draw_svg_glyph(
|
||||||
|
frame: &mut Frame,
|
||||||
|
upem: Abs,
|
||||||
|
font: &Font,
|
||||||
|
glyph_id: GlyphId,
|
||||||
|
) -> Option<()> {
|
||||||
|
let mut data = font.ttf().glyph_svg_image(glyph_id)?.data;
|
||||||
|
|
||||||
|
// Decompress SVGZ.
|
||||||
|
let mut decoded = vec![];
|
||||||
|
if data.starts_with(&[0x1f, 0x8b]) {
|
||||||
|
let mut decoder = flate2::read::GzDecoder::new(data);
|
||||||
|
decoder.read_to_end(&mut decoded).ok()?;
|
||||||
|
data = &decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse XML.
|
||||||
|
let xml = std::str::from_utf8(data).ok()?;
|
||||||
|
let document = roxmltree::Document::parse(xml).ok()?;
|
||||||
|
|
||||||
|
// Parse SVG.
|
||||||
|
let opts = usvg::Options::default();
|
||||||
|
let mut tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
|
||||||
|
|
||||||
|
// Compute the space we need to draw our glyph.
|
||||||
|
// See https://github.com/RazrFalcon/resvg/issues/602 for why
|
||||||
|
// using the svg size is problematic here.
|
||||||
|
tree.calculate_bounding_boxes();
|
||||||
|
let mut bbox = usvg::BBox::default();
|
||||||
|
if let Some(tree_bbox) = tree.root.bounding_box {
|
||||||
|
bbox = bbox.expand(tree_bbox);
|
||||||
|
}
|
||||||
|
let bbox = bbox.to_rect()?;
|
||||||
|
|
||||||
|
let mut data = tree.to_string(&usvg::XmlOptions::default());
|
||||||
|
|
||||||
|
let width = bbox.width() as f64;
|
||||||
|
let height = bbox.height() as f64;
|
||||||
|
let left = bbox.left() as f64;
|
||||||
|
let top = bbox.top() as f64;
|
||||||
|
|
||||||
|
// The SVG coordinates and the font coordinates are not the same: the Y axis
|
||||||
|
// is mirrored. But the origin of the axes are the same (which means that
|
||||||
|
// the horizontal axis in the SVG document corresponds to the baseline). See
|
||||||
|
// the reference for more details:
|
||||||
|
// https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
|
||||||
|
//
|
||||||
|
// If we used the SVG document as it is, svg2pdf would produce a cropped
|
||||||
|
// glyph (only what is under the baseline would be visible). So we need to
|
||||||
|
// embed the original SVG in another one that has the exact dimensions of
|
||||||
|
// the glyph, with a transform to make it fit. We also need to remove the
|
||||||
|
// viewBox, height and width attributes from the inner SVG, otherwise usvg
|
||||||
|
// takes into account these values to clip the embedded SVG.
|
||||||
|
make_svg_unsized(&mut data);
|
||||||
|
let wrapper_svg = format!(
|
||||||
|
r#"
|
||||||
|
<svg
|
||||||
|
width="{width}"
|
||||||
|
height="{height}"
|
||||||
|
viewBox="0 0 {width} {height}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1 0 0 1 {tx} {ty})">
|
||||||
|
{inner}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
"#,
|
||||||
|
inner = data,
|
||||||
|
tx = -left,
|
||||||
|
ty = -top,
|
||||||
|
);
|
||||||
|
|
||||||
|
let image = Image::new(
|
||||||
|
wrapper_svg.into_bytes().into(),
|
||||||
|
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
||||||
|
let size = Axes::new(Abs::pt(width), Abs::pt(height));
|
||||||
|
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all size specifications (viewBox, width and height attributes) from a
|
||||||
|
/// SVG document.
|
||||||
|
fn make_svg_unsized(svg: &mut String) {
|
||||||
|
let mut viewbox_range = None;
|
||||||
|
let mut width_range = None;
|
||||||
|
let mut height_range = None;
|
||||||
|
|
||||||
|
let mut s = unscanny::Scanner::new(svg);
|
||||||
|
|
||||||
|
s.eat_until("<svg");
|
||||||
|
s.eat_if("<svg");
|
||||||
|
while !s.eat_if('>') && !s.done() {
|
||||||
|
s.eat_whitespace();
|
||||||
|
let start = s.cursor();
|
||||||
|
let attr_name = s.eat_until('=').trim();
|
||||||
|
// Eat the equal sign and the quote.
|
||||||
|
s.eat();
|
||||||
|
s.eat();
|
||||||
|
let mut escaped = false;
|
||||||
|
while (escaped || !s.eat_if('"')) && !s.done() {
|
||||||
|
escaped = s.eat() == Some('\\');
|
||||||
|
}
|
||||||
|
match attr_name {
|
||||||
|
"viewBox" => viewbox_range = Some(start..s.cursor()),
|
||||||
|
"width" => width_range = Some(start..s.cursor()),
|
||||||
|
"height" => height_range = Some(start..s.cursor()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the `viewBox` attribute.
|
||||||
|
if let Some(range) = viewbox_range {
|
||||||
|
svg.replace_range(range.clone(), &" ".repeat(range.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the `width` attribute.
|
||||||
|
if let Some(range) = width_range {
|
||||||
|
svg.replace_range(range.clone(), &" ".repeat(range.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the `height` attribute.
|
||||||
|
if let Some(range) = height_range {
|
||||||
|
svg.replace_range(range, "");
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
//! Font handling.
|
//! Font handling.
|
||||||
|
|
||||||
|
pub mod color;
|
||||||
|
|
||||||
mod book;
|
mod book;
|
||||||
mod exceptions;
|
mod exceptions;
|
||||||
mod variant;
|
mod variant;
|
||||||
|
@ -65,3 +65,65 @@ impl Glyph {
|
|||||||
usize::from(self.range.start)..usize::from(self.range.end)
|
usize::from(self.range.start)..usize::from(self.range.end)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A slice of a [`TextItem`].
|
||||||
|
pub struct TextItemView<'a> {
|
||||||
|
/// The whole item this is a part of
|
||||||
|
pub item: &'a TextItem,
|
||||||
|
/// The glyphs of this slice
|
||||||
|
pub glyph_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TextItemView<'a> {
|
||||||
|
/// Build a TextItemView for the whole contents of a TextItem.
|
||||||
|
pub fn all_of(text: &'a TextItem) -> Self {
|
||||||
|
Self::from_glyph_range(text, 0..text.glyphs.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs.
|
||||||
|
pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self {
|
||||||
|
TextItemView { item: text, glyph_range }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtains a glyph in this slice, remapping the range that it represents in
|
||||||
|
/// the original text so that it is relative to the start of the slice
|
||||||
|
pub fn glyph_at(&self, index: usize) -> Glyph {
|
||||||
|
let g = &self.item.glyphs[self.glyph_range.start + index];
|
||||||
|
let text_range = self.text_range();
|
||||||
|
Glyph {
|
||||||
|
range: (g.range.start - text_range.start as u16)
|
||||||
|
..(g.range.end - text_range.start as u16),
|
||||||
|
..*g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the glyphs of the slice.
|
||||||
|
///
|
||||||
|
/// The range of text that each glyph represents is remapped to be relative
|
||||||
|
/// to the start of the slice.
|
||||||
|
pub fn glyphs(&self) -> impl Iterator<Item = Glyph> + '_ {
|
||||||
|
(0..self.glyph_range.len()).map(|index| self.glyph_at(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The plain text that this slice represents
|
||||||
|
pub fn text(&self) -> &str {
|
||||||
|
&self.item.text[self.text_range()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total width of this text slice
|
||||||
|
pub fn width(&self) -> Abs {
|
||||||
|
self.item.glyphs[self.glyph_range.clone()]
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.x_advance)
|
||||||
|
.sum::<Em>()
|
||||||
|
.at(self.item.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The range of text in the original TextItem that this slice corresponds
|
||||||
|
/// to.
|
||||||
|
fn text_range(&self) -> Range<usize> {
|
||||||
|
let text_start = self.item.glyphs[self.glyph_range.start].range().start;
|
||||||
|
let text_end = self.item.glyphs[self.glyph_range.end - 1].range().end;
|
||||||
|
text_start..text_end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,7 +30,9 @@ impl SvgImage {
|
|||||||
/// Decode an SVG image without fonts.
|
/// Decode an SVG image without fonts.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub fn new(data: Bytes) -> StrResult<SvgImage> {
|
pub fn new(data: Bytes) -> StrResult<SvgImage> {
|
||||||
let tree = usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
|
let mut tree =
|
||||||
|
usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
|
||||||
|
tree.calculate_bounding_boxes();
|
||||||
Ok(Self(Arc::new(Repr {
|
Ok(Self(Arc::new(Repr {
|
||||||
data,
|
data,
|
||||||
size: tree_size(&tree),
|
size: tree_size(&tree),
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 685 B |
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 948 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 640 B |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 758 B After Width: | Height: | Size: 735 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |