Skip to content

Commit b060dd7

Browse files
committed
More robust glyph drawing (#5159)
1 parent 5df07ee commit b060dd7

File tree

6 files changed

+203
-123
lines changed

6 files changed

+203
-123
lines changed

crates/typst-pdf/src/color_font.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ use indexmap::IndexMap;
1212
use pdf_writer::types::UnicodeCmap;
1313
use pdf_writer::writers::WMode;
1414
use pdf_writer::{Filter, Finish, Name, Rect, Ref};
15-
use typst::diag::SourceResult;
15+
use typst::diag::{bail, error, SourceDiagnostic, SourceResult};
16+
use typst::foundations::Repr;
1617
use typst::layout::Em;
17-
use typst::text::color::frame_for_glyph;
18-
use typst::text::Font;
18+
use typst::text::color::glyph_frame;
19+
use typst::text::{Font, Glyph, TextItemView};
1920

2021
use crate::content;
2122
use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO};
@@ -211,9 +212,10 @@ impl ColorFontMap<()> {
211212
pub fn get(
212213
&mut self,
213214
options: &PdfOptions,
214-
font: &Font,
215-
gid: u16,
215+
text: &TextItemView,
216+
glyph: &Glyph,
216217
) -> SourceResult<(usize, u8)> {
218+
let font = &text.item.font;
217219
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
218220
let global_bbox = font.ttf().global_bounding_box();
219221
let bbox = Rect::new(
@@ -230,7 +232,7 @@ impl ColorFontMap<()> {
230232
}
231233
});
232234

233-
Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) {
235+
Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&glyph.id) {
234236
// If we already know this glyph, return it.
235237
(color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8)
236238
} else {
@@ -242,18 +244,22 @@ impl ColorFontMap<()> {
242244
self.total_slice_count += 1;
243245
}
244246

245-
let frame = frame_for_glyph(font, gid);
246-
let width =
247-
font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em();
247+
let (frame, tofu) = glyph_frame(font, glyph.id);
248+
if options.standards.pdfa && tofu {
249+
bail!(failed_to_convert(text, glyph));
250+
}
251+
252+
let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get()
253+
* font.units_per_em();
248254
let instructions = content::build(
249255
options,
250256
&mut self.resources,
251257
&frame,
252258
None,
253259
Some(width as f32),
254260
)?;
255-
color_font.glyphs.push(ColorGlyph { gid, instructions });
256-
color_font.glyph_indices.insert(gid, index);
261+
color_font.glyphs.push(ColorGlyph { gid: glyph.id, instructions });
262+
color_font.glyph_indices.insert(glyph.id, index);
257263

258264
(color_font.slice_ids[index / 256], index as u8)
259265
})
@@ -321,3 +327,19 @@ pub struct ColorFontSlice {
321327
/// represent the subset of the TTF font we are interested in.
322328
pub subfont: usize,
323329
}
330+
331+
/// The error when the glyph could not be converted.
332+
#[cold]
333+
fn failed_to_convert(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic {
334+
let mut diag = error!(
335+
glyph.span.0,
336+
"the glyph for {} could not be exported",
337+
text.glyph_text(glyph).repr()
338+
);
339+
340+
if text.item.font.ttf().tables().cff2.is_some() {
341+
diag.hint("CFF2 fonts are not currently supported");
342+
}
343+
344+
diag
345+
}

crates/typst-pdf/src/content.rs

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use pdf_writer::types::{
1010
};
1111
use pdf_writer::writers::PositionedItems;
1212
use pdf_writer::{Content, Finish, Name, Rect, Str};
13-
use typst::diag::{bail, SourceResult};
13+
use typst::diag::{bail, error, SourceDiagnostic, SourceResult};
1414
use typst::foundations::Repr;
1515
use typst::layout::{
1616
Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform,
1717
};
1818
use typst::model::Destination;
1919
use typst::syntax::Span;
20-
use typst::text::color::is_color_glyph;
21-
use typst::text::{Font, TextItem, TextItemView};
20+
use typst::text::color::should_outline;
21+
use typst::text::{Font, Glyph, TextItem, TextItemView};
2222
use typst::utils::{Deferred, Numeric, SliceExt};
2323
use typst::visualize::{
2424
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem,
@@ -418,46 +418,27 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult
418418

419419
/// Encode a text run into the content stream.
420420
fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> {
421-
if ctx.options.standards.pdfa {
422-
let last_resort = text.font.info().is_last_resort();
423-
for g in &text.glyphs {
424-
if last_resort || g.id == 0 {
425-
bail!(
426-
g.span.0,
427-
"the text {} could not be displayed with any font",
428-
TextItemView::full(text).glyph_text(g).repr(),
429-
);
430-
}
431-
}
432-
}
433-
434-
let ttf = text.font.ttf();
435-
let tables = ttf.tables();
436-
437-
// If the text run contains either only color glyphs (used for emojis for
438-
// example) or normal text we can render it directly
439-
let has_color_glyphs = tables.sbix.is_some()
440-
|| tables.cbdt.is_some()
441-
|| tables.svg.is_some()
442-
|| tables.colr.is_some();
443-
if !has_color_glyphs {
444-
write_normal_text(ctx, pos, TextItemView::full(text))?;
445-
return Ok(());
421+
if ctx.options.standards.pdfa && text.font.info().is_last_resort() {
422+
bail!(
423+
Span::find(text.glyphs.iter().map(|g| g.span.0)),
424+
"the text {} could not be displayed with any font",
425+
&text.text,
426+
);
446427
}
447428

448-
let color_glyph_count =
449-
text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count();
429+
let outline_glyphs =
430+
text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count();
450431

451-
if color_glyph_count == text.glyphs.len() {
452-
write_color_glyphs(ctx, pos, TextItemView::full(text))?;
453-
} else if color_glyph_count == 0 {
432+
if outline_glyphs == text.glyphs.len() {
454433
write_normal_text(ctx, pos, TextItemView::full(text))?;
434+
} else if outline_glyphs == 0 {
435+
write_complex_glyphs(ctx, pos, TextItemView::full(text))?;
455436
} else {
456-
// Otherwise we need to split it in smaller text runs
437+
// Otherwise we need to split it into smaller text runs.
457438
let mut offset = 0;
458439
let mut position_in_run = Abs::zero();
459-
for (color, sub_run) in
460-
text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g))
440+
for (should_outline, sub_run) in
441+
text.glyphs.group_by_key(|g| should_outline(&text.font, g))
461442
{
462443
let end = offset + sub_run.len();
463444

@@ -468,11 +449,12 @@ fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()
468449
let pos = pos + Point::new(position_in_run, Abs::zero());
469450
position_in_run += text_item_view.width();
470451
offset = end;
471-
// Actually write the sub text-run
472-
if color {
473-
write_color_glyphs(ctx, pos, text_item_view)?;
474-
} else {
452+
453+
// Actually write the sub text-run.
454+
if should_outline {
475455
write_normal_text(ctx, pos, text_item_view)?;
456+
} else {
457+
write_complex_glyphs(ctx, pos, text_item_view)?;
476458
}
477459
}
478460
}
@@ -534,6 +516,10 @@ fn write_normal_text(
534516

535517
// Write the glyphs with kerning adjustments.
536518
for glyph in text.glyphs() {
519+
if ctx.options.standards.pdfa && glyph.id == 0 {
520+
bail!(tofu(&text, glyph));
521+
}
522+
537523
adjustment += glyph.x_offset;
538524

539525
if !adjustment.is_zero() {
@@ -596,7 +582,7 @@ fn show_text(items: &mut PositionedItems, encoded: &[u8]) {
596582
}
597583

598584
/// Encodes a text run made only of color glyphs into the content stream
599-
fn write_color_glyphs(
585+
fn write_complex_glyphs(
600586
ctx: &mut Builder,
601587
pos: Point,
602588
text: TextItemView,
@@ -621,12 +607,17 @@ fn write_color_glyphs(
621607
.or_default();
622608

623609
for glyph in text.glyphs() {
610+
if ctx.options.standards.pdfa && glyph.id == 0 {
611+
bail!(tofu(&text, glyph));
612+
}
613+
624614
// Retrieve the Type3 font reference and the glyph index in the font.
625615
let color_fonts = ctx
626616
.resources
627617
.color_fonts
628618
.get_or_insert_with(|| Box::new(ColorFontMap::new()));
629-
let (font, index) = color_fonts.get(ctx.options, &text.item.font, glyph.id)?;
619+
620+
let (font, index) = color_fonts.get(ctx.options, &text, glyph)?;
630621

631622
if last_font != Some(font) {
632623
ctx.content.set_font(
@@ -824,3 +815,13 @@ fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle {
824815
LineJoin::Bevel => LineJoinStyle::BevelJoin,
825816
}
826817
}
818+
819+
/// The error when there is a tofu glyph.
820+
#[cold]
821+
fn tofu(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic {
822+
error!(
823+
glyph.span.0,
824+
"the text {} could not be displayed with any font",
825+
text.glyph_text(glyph).repr(),
826+
)
827+
}

crates/typst-render/src/text.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use pixglyph::Bitmap;
44
use tiny_skia as sk;
55
use ttf_parser::{GlyphId, OutlineBuilder};
66
use typst::layout::{Abs, Axes, Point, Size};
7-
use typst::text::color::{frame_for_glyph, is_color_glyph};
7+
use typst::text::color::{glyph_frame, should_outline};
88
use typst::text::{Font, TextItem};
99
use typst::visualize::{FixedStroke, Paint};
1010

@@ -18,20 +18,19 @@ pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) {
1818
let id = GlyphId(glyph.id);
1919
let offset = x + glyph.x_offset.at(text.size).to_f32();
2020

21-
if is_color_glyph(&text.font, glyph) {
21+
if should_outline(&text.font, glyph) {
22+
let state =
23+
state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
24+
render_outline_glyph(canvas, state, text, id);
25+
} else {
2226
let upem = text.font.units_per_em();
2327
let text_scale = Abs::raw(text.size.to_raw() / upem);
2428
let state = state
2529
.pre_translate(Point::new(Abs::raw(offset as _), -text.size))
2630
.pre_scale(Axes::new(text_scale, text_scale));
2731

28-
let glyph_frame = frame_for_glyph(&text.font, glyph.id);
29-
32+
let (glyph_frame, _) = glyph_frame(&text.font, glyph.id);
3033
crate::render_frame(canvas, state, &glyph_frame);
31-
} else {
32-
let state =
33-
state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0)));
34-
render_outline_glyph(canvas, state, text, id);
3534
}
3635

3736
x += glyph.x_advance.at(text.size).to_f32();

crates/typst-syntax/src/span.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ impl Span {
9292
}
9393
}
9494

95+
/// Find the first non-detached span in the iterator.
96+
pub fn find(iter: impl IntoIterator<Item = Self>) -> Self {
97+
iter.into_iter()
98+
.find(|span| !span.is_detached())
99+
.unwrap_or(Span::detached())
100+
}
101+
95102
/// Resolve a file location relative to this span's source.
96103
pub fn resolve_path(self, path: &str) -> Result<FileId, EcoString> {
97104
let Some(file) = self.id() else {

crates/typst/src/realize.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,9 +1241,5 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) {
12411241

12421242
/// Finds the first non-detached span in the list.
12431243
fn select_span(children: &[Pair]) -> Span {
1244-
children
1245-
.iter()
1246-
.map(|(c, _)| c.span())
1247-
.find(|span| !span.is_detached())
1248-
.unwrap_or(Span::detached())
1244+
Span::find(children.iter().map(|(c, _)| c.span()))
12491245
}

0 commit comments

Comments
 (0)