mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
More robust glyph drawing (#5159)
This commit is contained in:
parent
9ee80762a5
commit
6257e4d6cf
@ -12,10 +12,11 @@ use indexmap::IndexMap;
|
|||||||
use pdf_writer::types::UnicodeCmap;
|
use pdf_writer::types::UnicodeCmap;
|
||||||
use pdf_writer::writers::WMode;
|
use pdf_writer::writers::WMode;
|
||||||
use pdf_writer::{Filter, Finish, Name, Rect, Ref};
|
use pdf_writer::{Filter, Finish, Name, Rect, Ref};
|
||||||
use typst::diag::SourceResult;
|
use typst::diag::{bail, error, SourceDiagnostic, SourceResult};
|
||||||
|
use typst::foundations::Repr;
|
||||||
use typst::layout::Em;
|
use typst::layout::Em;
|
||||||
use typst::text::color::frame_for_glyph;
|
use typst::text::color::glyph_frame;
|
||||||
use typst::text::Font;
|
use typst::text::{Font, Glyph, TextItemView};
|
||||||
|
|
||||||
use crate::content;
|
use crate::content;
|
||||||
use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO};
|
use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO};
|
||||||
@ -211,9 +212,10 @@ impl ColorFontMap<()> {
|
|||||||
pub fn get(
|
pub fn get(
|
||||||
&mut self,
|
&mut self,
|
||||||
options: &PdfOptions,
|
options: &PdfOptions,
|
||||||
font: &Font,
|
text: &TextItemView,
|
||||||
gid: u16,
|
glyph: &Glyph,
|
||||||
) -> SourceResult<(usize, u8)> {
|
) -> SourceResult<(usize, u8)> {
|
||||||
|
let font = &text.item.font;
|
||||||
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
|
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
|
||||||
let global_bbox = font.ttf().global_bounding_box();
|
let global_bbox = font.ttf().global_bounding_box();
|
||||||
let bbox = Rect::new(
|
let bbox = Rect::new(
|
||||||
@ -230,7 +232,7 @@ impl ColorFontMap<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) {
|
Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&glyph.id) {
|
||||||
// If we already know this glyph, return it.
|
// If we already know this glyph, return it.
|
||||||
(color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8)
|
(color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8)
|
||||||
} else {
|
} else {
|
||||||
@ -242,9 +244,13 @@ impl ColorFontMap<()> {
|
|||||||
self.total_slice_count += 1;
|
self.total_slice_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame = frame_for_glyph(font, gid);
|
let (frame, tofu) = glyph_frame(font, glyph.id);
|
||||||
let width =
|
if options.standards.pdfa && tofu {
|
||||||
font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em();
|
bail!(failed_to_convert(text, glyph));
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get()
|
||||||
|
* font.units_per_em();
|
||||||
let instructions = content::build(
|
let instructions = content::build(
|
||||||
options,
|
options,
|
||||||
&mut self.resources,
|
&mut self.resources,
|
||||||
@ -252,8 +258,8 @@ impl ColorFontMap<()> {
|
|||||||
None,
|
None,
|
||||||
Some(width as f32),
|
Some(width as f32),
|
||||||
)?;
|
)?;
|
||||||
color_font.glyphs.push(ColorGlyph { gid, instructions });
|
color_font.glyphs.push(ColorGlyph { gid: glyph.id, instructions });
|
||||||
color_font.glyph_indices.insert(gid, index);
|
color_font.glyph_indices.insert(glyph.id, index);
|
||||||
|
|
||||||
(color_font.slice_ids[index / 256], index as u8)
|
(color_font.slice_ids[index / 256], index as u8)
|
||||||
})
|
})
|
||||||
@ -321,3 +327,19 @@ pub struct ColorFontSlice {
|
|||||||
/// represent the subset of the TTF font we are interested in.
|
/// represent the subset of the TTF font we are interested in.
|
||||||
pub subfont: usize,
|
pub subfont: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The error when the glyph could not be converted.
|
||||||
|
#[cold]
|
||||||
|
fn failed_to_convert(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic {
|
||||||
|
let mut diag = error!(
|
||||||
|
glyph.span.0,
|
||||||
|
"the glyph for {} could not be exported",
|
||||||
|
text.glyph_text(glyph).repr()
|
||||||
|
);
|
||||||
|
|
||||||
|
if text.item.font.ttf().tables().cff2.is_some() {
|
||||||
|
diag.hint("CFF2 fonts are not currently supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
diag
|
||||||
|
}
|
||||||
|
@ -10,15 +10,15 @@ use pdf_writer::types::{
|
|||||||
};
|
};
|
||||||
use pdf_writer::writers::PositionedItems;
|
use pdf_writer::writers::PositionedItems;
|
||||||
use pdf_writer::{Content, Finish, Name, Rect, Str};
|
use pdf_writer::{Content, Finish, Name, Rect, Str};
|
||||||
use typst::diag::{bail, SourceResult};
|
use typst::diag::{bail, error, SourceDiagnostic, SourceResult};
|
||||||
use typst::foundations::Repr;
|
use typst::foundations::Repr;
|
||||||
use typst::layout::{
|
use typst::layout::{
|
||||||
Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform,
|
Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform,
|
||||||
};
|
};
|
||||||
use typst::model::Destination;
|
use typst::model::Destination;
|
||||||
use typst::syntax::Span;
|
use typst::syntax::Span;
|
||||||
use typst::text::color::is_color_glyph;
|
use typst::text::color::should_outline;
|
||||||
use typst::text::{Font, TextItem, TextItemView};
|
use typst::text::{Font, Glyph, TextItem, TextItemView};
|
||||||
use typst::utils::{Deferred, Numeric, SliceExt};
|
use typst::utils::{Deferred, Numeric, SliceExt};
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem,
|
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem,
|
||||||
@ -418,46 +418,27 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult
|
|||||||
|
|
||||||
/// Encode a text run into the content stream.
|
/// Encode a text run into the content stream.
|
||||||
fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> {
|
fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> {
|
||||||
if ctx.options.standards.pdfa {
|
if ctx.options.standards.pdfa && text.font.info().is_last_resort() {
|
||||||
let last_resort = text.font.info().is_last_resort();
|
|
||||||
for g in &text.glyphs {
|
|
||||||
if last_resort || g.id == 0 {
|
|
||||||
bail!(
|
bail!(
|
||||||
g.span.0,
|
Span::find(text.glyphs.iter().map(|g| g.span.0)),
|
||||||
"the text {} could not be displayed with any font",
|
"the text {} could not be displayed with any font",
|
||||||
TextItemView::full(text).glyph_text(g).repr(),
|
&text.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ttf = text.font.ttf();
|
let outline_glyphs =
|
||||||
let tables = ttf.tables();
|
text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count();
|
||||||
|
|
||||||
// If the text run contains either only color glyphs (used for emojis for
|
if outline_glyphs == text.glyphs.len() {
|
||||||
// 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::full(text))?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
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::full(text))?;
|
|
||||||
} else if color_glyph_count == 0 {
|
|
||||||
write_normal_text(ctx, pos, TextItemView::full(text))?;
|
write_normal_text(ctx, pos, TextItemView::full(text))?;
|
||||||
|
} else if outline_glyphs == 0 {
|
||||||
|
write_complex_glyphs(ctx, pos, TextItemView::full(text))?;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we need to split it in smaller text runs
|
// Otherwise we need to split it into smaller text runs.
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
let mut position_in_run = Abs::zero();
|
let mut position_in_run = Abs::zero();
|
||||||
for (color, sub_run) in
|
for (should_outline, sub_run) in
|
||||||
text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g))
|
text.glyphs.group_by_key(|g| should_outline(&text.font, g))
|
||||||
{
|
{
|
||||||
let end = offset + sub_run.len();
|
let end = offset + sub_run.len();
|
||||||
|
|
||||||
@ -468,11 +449,12 @@ fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()
|
|||||||
let pos = pos + Point::new(position_in_run, Abs::zero());
|
let pos = pos + Point::new(position_in_run, Abs::zero());
|
||||||
position_in_run += text_item_view.width();
|
position_in_run += text_item_view.width();
|
||||||
offset = end;
|
offset = end;
|
||||||
// Actually write the sub text-run
|
|
||||||
if color {
|
// Actually write the sub text-run.
|
||||||
write_color_glyphs(ctx, pos, text_item_view)?;
|
if should_outline {
|
||||||
} else {
|
|
||||||
write_normal_text(ctx, pos, text_item_view)?;
|
write_normal_text(ctx, pos, text_item_view)?;
|
||||||
|
} else {
|
||||||
|
write_complex_glyphs(ctx, pos, text_item_view)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -534,6 +516,10 @@ fn write_normal_text(
|
|||||||
|
|
||||||
// Write the glyphs with kerning adjustments.
|
// Write the glyphs with kerning adjustments.
|
||||||
for glyph in text.glyphs() {
|
for glyph in text.glyphs() {
|
||||||
|
if ctx.options.standards.pdfa && glyph.id == 0 {
|
||||||
|
bail!(tofu(&text, glyph));
|
||||||
|
}
|
||||||
|
|
||||||
adjustment += glyph.x_offset;
|
adjustment += glyph.x_offset;
|
||||||
|
|
||||||
if !adjustment.is_zero() {
|
if !adjustment.is_zero() {
|
||||||
@ -596,7 +582,7 @@ fn show_text(items: &mut PositionedItems, encoded: &[u8]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes a text run made only of color glyphs into the content stream
|
/// Encodes a text run made only of color glyphs into the content stream
|
||||||
fn write_color_glyphs(
|
fn write_complex_glyphs(
|
||||||
ctx: &mut Builder,
|
ctx: &mut Builder,
|
||||||
pos: Point,
|
pos: Point,
|
||||||
text: TextItemView,
|
text: TextItemView,
|
||||||
@ -621,12 +607,17 @@ fn write_color_glyphs(
|
|||||||
.or_default();
|
.or_default();
|
||||||
|
|
||||||
for glyph in text.glyphs() {
|
for glyph in text.glyphs() {
|
||||||
|
if ctx.options.standards.pdfa && glyph.id == 0 {
|
||||||
|
bail!(tofu(&text, glyph));
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the Type3 font reference and the glyph index in the font.
|
// Retrieve the Type3 font reference and the glyph index in the font.
|
||||||
let color_fonts = ctx
|
let color_fonts = ctx
|
||||||
.resources
|
.resources
|
||||||
.color_fonts
|
.color_fonts
|
||||||
.get_or_insert_with(|| Box::new(ColorFontMap::new()));
|
.get_or_insert_with(|| Box::new(ColorFontMap::new()));
|
||||||
let (font, index) = color_fonts.get(ctx.options, &text.item.font, glyph.id)?;
|
|
||||||
|
let (font, index) = color_fonts.get(ctx.options, &text, glyph)?;
|
||||||
|
|
||||||
if last_font != Some(font) {
|
if last_font != Some(font) {
|
||||||
ctx.content.set_font(
|
ctx.content.set_font(
|
||||||
@ -824,3 +815,13 @@ fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle {
|
|||||||
LineJoin::Bevel => LineJoinStyle::BevelJoin,
|
LineJoin::Bevel => LineJoinStyle::BevelJoin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The error when there is a tofu glyph.
|
||||||
|
#[cold]
|
||||||
|
fn tofu(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic {
|
||||||
|
error!(
|
||||||
|
glyph.span.0,
|
||||||
|
"the text {} could not be displayed with any font",
|
||||||
|
text.glyph_text(glyph).repr(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ use pixglyph::Bitmap;
|
|||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use typst::layout::{Abs, Axes, Point, Size};
|
use typst::layout::{Abs, Axes, Point, Size};
|
||||||
use typst::text::color::{frame_for_glyph, is_color_glyph};
|
use typst::text::color::{glyph_frame, should_outline};
|
||||||
use typst::text::{Font, TextItem};
|
use typst::text::{Font, TextItem};
|
||||||
use typst::visualize::{FixedStroke, Paint};
|
use typst::visualize::{FixedStroke, Paint};
|
||||||
|
|
||||||
@ -18,20 +18,19 @@ pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
|
|||||||
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();
|
||||||
|
|
||||||
if is_color_glyph(&text.font, glyph) {
|
if should_outline(&text.font, glyph) {
|
||||||
|
let state =
|
||||||
|
state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
|
||||||
|
render_outline_glyph(canvas, state, text, id);
|
||||||
|
} else {
|
||||||
let upem = text.font.units_per_em();
|
let upem = text.font.units_per_em();
|
||||||
let text_scale = Abs::raw(text.size.to_raw() / upem);
|
let text_scale = Abs::raw(text.size.to_raw() / upem);
|
||||||
let state = state
|
let state = state
|
||||||
.pre_translate(Point::new(Abs::raw(offset as _), -text.size))
|
.pre_translate(Point::new(Abs::raw(offset as _), -text.size))
|
||||||
.pre_scale(Axes::new(text_scale, text_scale));
|
.pre_scale(Axes::new(text_scale, text_scale));
|
||||||
|
|
||||||
let glyph_frame = frame_for_glyph(&text.font, glyph.id);
|
let (glyph_frame, _) = glyph_frame(&text.font, glyph.id);
|
||||||
|
|
||||||
crate::render_frame(canvas, state, &glyph_frame);
|
crate::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();
|
||||||
|
@ -92,6 +92,13 @@ impl Span {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the first non-detached span in the iterator.
|
||||||
|
pub fn find(iter: impl IntoIterator<Item = Self>) -> Self {
|
||||||
|
iter.into_iter()
|
||||||
|
.find(|span| !span.is_detached())
|
||||||
|
.unwrap_or(Span::detached())
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a file location relative to this span's source.
|
/// Resolve a file location relative to this span's source.
|
||||||
pub fn resolve_path(self, path: &str) -> Result<FileId, EcoString> {
|
pub fn resolve_path(self, path: &str) -> Result<FileId, EcoString> {
|
||||||
let Some(file) = self.id() else {
|
let Some(file) = self.id() else {
|
||||||
|
@ -1241,9 +1241,5 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) {
|
|||||||
|
|
||||||
/// Finds the first non-detached span in the list.
|
/// Finds the first non-detached span in the list.
|
||||||
fn select_span(children: &[Pair]) -> Span {
|
fn select_span(children: &[Pair]) -> Span {
|
||||||
children
|
Span::find(children.iter().map(|(c, _)| c.span()))
|
||||||
.iter()
|
|
||||||
.map(|(c, _)| c.span())
|
|
||||||
.find(|span| !span.is_detached())
|
|
||||||
.unwrap_or(Span::detached())
|
|
||||||
}
|
}
|
||||||
|
@ -6,51 +6,138 @@ use ttf_parser::{GlyphId, RgbaColor};
|
|||||||
use usvg::tiny_skia_path;
|
use usvg::tiny_skia_path;
|
||||||
use xmlwriter::XmlWriter;
|
use xmlwriter::XmlWriter;
|
||||||
|
|
||||||
use crate::layout::{Abs, Axes, Frame, FrameItem, Point, Size};
|
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||||
use crate::syntax::Span;
|
use crate::syntax::Span;
|
||||||
use crate::text::{Font, Glyph};
|
use crate::text::{Font, Glyph};
|
||||||
use crate::visualize::Image;
|
use crate::visualize::{FixedStroke, Geometry, Image};
|
||||||
|
|
||||||
/// Tells if a glyph is a color glyph or not in a given font.
|
/// Whether this glyph should be rendered via simple outlining instead of via
|
||||||
pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool {
|
/// `glyph_frame`.
|
||||||
|
pub fn should_outline(font: &Font, glyph: &Glyph) -> bool {
|
||||||
let ttf = font.ttf();
|
let ttf = font.ttf();
|
||||||
let glyph_id = GlyphId(g.id);
|
let glyph_id = GlyphId(glyph.id);
|
||||||
ttf.glyph_raster_image(glyph_id, 160).is_some()
|
(ttf.tables().glyf.is_some() || ttf.tables().cff.is_some())
|
||||||
|| ttf.glyph_svg_image(glyph_id).is_some()
|
&& !ttf
|
||||||
|| ttf.is_color_glyph(glyph_id)
|
.glyph_raster_image(glyph_id, u16::MAX)
|
||||||
|
.is_some_and(|img| img.format == ttf_parser::RasterImageFormat::PNG)
|
||||||
|
&& !ttf.is_color_glyph(glyph_id)
|
||||||
|
&& ttf.glyph_svg_image(glyph_id).is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a frame with the glyph drawn inside.
|
/// Returns a frame representing a glyph and whether it is a fallback tofu
|
||||||
|
/// frame.
|
||||||
|
///
|
||||||
|
/// Should only be called on glyphs for which [`should_outline`] returns false.
|
||||||
///
|
///
|
||||||
/// The glyphs are sized in font units, [`text.item.size`] is not taken into
|
/// The glyphs are sized in font units, [`text.item.size`] is not taken into
|
||||||
/// account.
|
/// account.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame {
|
pub fn glyph_frame(font: &Font, glyph_id: u16) -> (Frame, bool) {
|
||||||
let ttf = font.ttf();
|
let upem = Abs::pt(font.units_per_em());
|
||||||
let upem = Abs::pt(ttf.units_per_em() as f64);
|
|
||||||
let glyph_id = GlyphId(glyph_id);
|
let glyph_id = GlyphId(glyph_id);
|
||||||
|
|
||||||
let mut frame = Frame::soft(Size::splat(upem));
|
let mut frame = Frame::soft(Size::splat(upem));
|
||||||
|
let mut tofu = false;
|
||||||
|
|
||||||
if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) {
|
if draw_glyph(&mut frame, font, upem, glyph_id).is_none()
|
||||||
draw_raster_glyph(&mut frame, font, upem, raster_image);
|
&& font.ttf().glyph_index(' ') != Some(glyph_id)
|
||||||
} else if ttf.is_color_glyph(glyph_id) {
|
{
|
||||||
draw_colr_glyph(&mut frame, upem, ttf, glyph_id);
|
// Generate a fallback tofu if the glyph couldn't be drawn, unless it is
|
||||||
} else if ttf.glyph_svg_image(glyph_id).is_some() {
|
// the space glyph. Then, an empty frame does the job. (This happens for
|
||||||
draw_svg_glyph(&mut frame, upem, font, glyph_id);
|
// some rare CBDT fonts, which don't define a bitmap for the space, but
|
||||||
|
// also don't have a glyf or CFF table.)
|
||||||
|
draw_fallback_tofu(&mut frame, font, upem, glyph_id);
|
||||||
|
tofu = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame
|
(frame, tofu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tries to draw a glyph.
|
||||||
|
fn draw_glyph(
|
||||||
|
frame: &mut Frame,
|
||||||
|
font: &Font,
|
||||||
|
upem: Abs,
|
||||||
|
glyph_id: GlyphId,
|
||||||
|
) -> Option<()> {
|
||||||
|
let ttf = font.ttf();
|
||||||
|
if let Some(raster_image) = ttf
|
||||||
|
.glyph_raster_image(glyph_id, u16::MAX)
|
||||||
|
.filter(|img| img.format == ttf_parser::RasterImageFormat::PNG)
|
||||||
|
{
|
||||||
|
draw_raster_glyph(frame, font, upem, raster_image)
|
||||||
|
} else if ttf.is_color_glyph(glyph_id) {
|
||||||
|
draw_colr_glyph(frame, font, upem, glyph_id)
|
||||||
|
} else if ttf.glyph_svg_image(glyph_id).is_some() {
|
||||||
|
draw_svg_glyph(frame, font, upem, glyph_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a fallback tofu box with the advance width of the glyph.
|
||||||
|
fn draw_fallback_tofu(frame: &mut Frame, font: &Font, upem: Abs, glyph_id: GlyphId) {
|
||||||
|
let advance = font
|
||||||
|
.ttf()
|
||||||
|
.glyph_hor_advance(glyph_id)
|
||||||
|
.map(|advance| Abs::pt(advance as f64))
|
||||||
|
.unwrap_or(upem / 3.0);
|
||||||
|
let inset = 0.15 * advance;
|
||||||
|
let height = 0.7 * upem;
|
||||||
|
let pos = Point::new(inset, upem - height);
|
||||||
|
let size = Size::new(advance - inset * 2.0, height);
|
||||||
|
let thickness = upem / 20.0;
|
||||||
|
let stroke = FixedStroke { thickness, ..Default::default() };
|
||||||
|
let shape = Geometry::Rect(size).stroked(stroke);
|
||||||
|
frame.push(pos, FrameItem::Shape(shape, Span::detached()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a raster glyph in a frame.
|
||||||
|
///
|
||||||
|
/// Supports only PNG images.
|
||||||
|
fn draw_raster_glyph(
|
||||||
|
frame: &mut Frame,
|
||||||
|
font: &Font,
|
||||||
|
upem: Abs,
|
||||||
|
raster_image: ttf_parser::RasterGlyphImage,
|
||||||
|
) -> Option<()> {
|
||||||
|
let image = Image::new(
|
||||||
|
raster_image.data.into(),
|
||||||
|
typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
// 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 = Size::new(upem, upem * aspect_ratio);
|
||||||
|
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a glyph from the COLR table into the frame.
|
||||||
fn draw_colr_glyph(
|
fn draw_colr_glyph(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
|
font: &Font,
|
||||||
upem: Abs,
|
upem: Abs,
|
||||||
ttf: &ttf_parser::Face,
|
|
||||||
glyph_id: GlyphId,
|
glyph_id: GlyphId,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let mut svg = XmlWriter::new(xmlwriter::Options::default());
|
let mut svg = XmlWriter::new(xmlwriter::Options::default());
|
||||||
|
|
||||||
|
let ttf = font.ttf();
|
||||||
let width = ttf.global_bounding_box().width() as f64;
|
let width = ttf.global_bounding_box().width() as f64;
|
||||||
let height = ttf.global_bounding_box().height() as f64;
|
let height = ttf.global_bounding_box().height() as f64;
|
||||||
let x_min = ttf.global_bounding_box().x_min as f64;
|
let x_min = ttf.global_bounding_box().x_min as f64;
|
||||||
@ -87,8 +174,7 @@ fn draw_colr_glyph(
|
|||||||
transforms_stack: vec![ttf_parser::Transform::default()],
|
transforms_stack: vec![ttf_parser::Transform::default()],
|
||||||
};
|
};
|
||||||
|
|
||||||
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)
|
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
|
||||||
.unwrap();
|
|
||||||
svg.end_element();
|
svg.end_element();
|
||||||
|
|
||||||
let data = svg.end_document().into_bytes();
|
let data = svg.end_document().into_bytes();
|
||||||
@ -98,54 +184,22 @@ fn draw_colr_glyph(
|
|||||||
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.ok()?;
|
||||||
|
|
||||||
let y_shift = Abs::raw(upem.to_raw() - y_max);
|
let y_shift = Abs::raw(upem.to_raw() - y_max);
|
||||||
|
|
||||||
let position = Point::new(Abs::raw(x_min), y_shift);
|
let position = Point::new(Abs::raw(x_min), y_shift);
|
||||||
let size = Axes::new(Abs::pt(width), Abs::pt(height));
|
let size = Size::new(Abs::pt(width), Abs::pt(height));
|
||||||
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 an SVG glyph in a frame.
|
/// Draws an SVG glyph in a frame.
|
||||||
fn draw_svg_glyph(
|
fn draw_svg_glyph(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
upem: Abs,
|
|
||||||
font: &Font,
|
font: &Font,
|
||||||
|
upem: Abs,
|
||||||
glyph_id: GlyphId,
|
glyph_id: GlyphId,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
// TODO: Our current conversion of the SVG table works for Twitter Color Emoji,
|
// TODO: Our current conversion of the SVG table works for Twitter Color Emoji,
|
||||||
@ -211,9 +265,10 @@ fn draw_svg_glyph(
|
|||||||
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.ok()?;
|
||||||
|
|
||||||
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
||||||
let size = Axes::new(Abs::pt(width), Abs::pt(height));
|
let size = Size::new(Abs::pt(width), Abs::pt(height));
|
||||||
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
frame.push(position, FrameItem::Image(image, size, Span::detached()));
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user