1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use std::collections::HashMap;
6use std::num::NonZeroU16;
7use std::sync::Arc;
8
9use fontdb::{Database, ID};
10use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
11use rustybuzz::ttf_parser;
12use svgtypes::FontFamily;
13use ttf_parser::GlyphId;
14use unicode_script::UnicodeScript;
15
16use crate::*;
17
18pub(crate) fn convert(text: &mut Text, fontdb: &fontdb::Database) -> Option<()> {
19 let (new_paths, bbox, stroke_bbox) = text_to_paths(text, fontdb)?;
20
21 let mut group = Group {
22 id: text.id.clone(),
23 ..Group::empty()
24 };
25
26 let rendering_mode = resolve_rendering_mode(text);
27 for mut path in new_paths {
28 path.rendering_mode = rendering_mode;
29 group.children.push(Node::Path(Box::new(path)));
30 }
31
32 group.calculate_bounding_boxes();
33 text.flattened = Box::new(group);
34
35 text.bounding_box = bbox.to_rect();
36 text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect();
37 // TODO: test
38 // TODO: should we stroke transformed paths?
39 text.stroke_bounding_box = stroke_bbox.to_rect();
40 text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect();
41
42 Some(())
43}
44
45trait DatabaseExt {
46 fn load_font(&self, id: ID) -> Option<ResolvedFont>;
47 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
48 fn has_char(&self, id: ID, c: char) -> bool;
49}
50
51impl DatabaseExt for Database {
52 #[inline(never)]
53 fn load_font(&self, id: ID) -> Option<ResolvedFont> {
54 self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
55 let font = ttf_parser::Face::parse(data, face_index).ok()?;
56
57 let units_per_em = NonZeroU16::new(font.units_per_em())?;
58
59 let ascent = font.ascender();
60 let descent = font.descender();
61
62 let x_height = font
63 .x_height()
64 .and_then(|x| u16::try_from(x).ok())
65 .and_then(NonZeroU16::new);
66 let x_height = match x_height {
67 Some(height) => height,
68 None => {
69 // If not set - fallback to height * 45%.
70 // 45% is what Firefox uses.
71 u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
72 .ok()
73 .and_then(NonZeroU16::new)?
74 }
75 };
76
77 let line_through = font.strikeout_metrics();
78 let line_through_position = match line_through {
79 Some(metrics) => metrics.position,
80 None => x_height.get() as i16 / 2,
81 };
82
83 let (underline_position, underline_thickness) = match font.underline_metrics() {
84 Some(metrics) => {
85 let thickness = u16::try_from(metrics.thickness)
86 .ok()
87 .and_then(NonZeroU16::new)
88 // `ttf_parser` guarantees that units_per_em is >= 16
89 .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
90
91 (metrics.position, thickness)
92 }
93 None => (
94 -(units_per_em.get() as i16) / 9,
95 NonZeroU16::new(units_per_em.get() / 12).unwrap(),
96 ),
97 };
98
99 // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg).
100 let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16;
101 let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16;
102 if let Some(metrics) = font.subscript_metrics() {
103 subscript_offset = metrics.y_offset;
104 }
105
106 if let Some(metrics) = font.superscript_metrics() {
107 superscript_offset = metrics.y_offset;
108 }
109
110 Some(ResolvedFont {
111 id,
112 units_per_em,
113 ascent,
114 descent,
115 x_height,
116 underline_position,
117 underline_thickness,
118 line_through_position,
119 subscript_offset,
120 superscript_offset,
121 })
122 })?
123 }
124
125 #[inline(never)]
126 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path> {
127 self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
128 let font = ttf_parser::Face::parse(data, face_index).ok()?;
129
130 let mut builder = PathBuilder {
131 builder: tiny_skia_path::PathBuilder::new(),
132 };
133 font.outline_glyph(glyph_id, &mut builder)?;
134 builder.builder.finish()
135 })?
136 }
137
138 #[inline(never)]
139 fn has_char(&self, id: ID, c: char) -> bool {
140 let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
141 let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
142 font.glyph_index(c)?;
143 Some(true)
144 });
145
146 res == Some(Some(true))
147 }
148}
149
150#[derive(Clone, Copy, Debug)]
151struct ResolvedFont {
152 id: ID,
153
154 units_per_em: NonZeroU16,
155
156 // All values below are in font units.
157 ascent: i16,
158 descent: i16,
159 x_height: NonZeroU16,
160
161 underline_position: i16,
162 underline_thickness: NonZeroU16,
163
164 // line-through thickness should be the the same as underline thickness
165 // according to the TrueType spec:
166 // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize
167 line_through_position: i16,
168
169 subscript_offset: i16,
170 superscript_offset: i16,
171}
172
173impl ResolvedFont {
174 #[inline]
175 fn scale(&self, font_size: f32) -> f32 {
176 font_size / self.units_per_em.get() as f32
177 }
178
179 #[inline]
180 fn ascent(&self, font_size: f32) -> f32 {
181 self.ascent as f32 * self.scale(font_size)
182 }
183
184 #[inline]
185 fn descent(&self, font_size: f32) -> f32 {
186 self.descent as f32 * self.scale(font_size)
187 }
188
189 #[inline]
190 fn height(&self, font_size: f32) -> f32 {
191 self.ascent(font_size) - self.descent(font_size)
192 }
193
194 #[inline]
195 fn x_height(&self, font_size: f32) -> f32 {
196 self.x_height.get() as f32 * self.scale(font_size)
197 }
198
199 #[inline]
200 fn underline_position(&self, font_size: f32) -> f32 {
201 self.underline_position as f32 * self.scale(font_size)
202 }
203
204 #[inline]
205 fn underline_thickness(&self, font_size: f32) -> f32 {
206 self.underline_thickness.get() as f32 * self.scale(font_size)
207 }
208
209 #[inline]
210 fn line_through_position(&self, font_size: f32) -> f32 {
211 self.line_through_position as f32 * self.scale(font_size)
212 }
213
214 #[inline]
215 fn subscript_offset(&self, font_size: f32) -> f32 {
216 self.subscript_offset as f32 * self.scale(font_size)
217 }
218
219 #[inline]
220 fn superscript_offset(&self, font_size: f32) -> f32 {
221 self.superscript_offset as f32 * self.scale(font_size)
222 }
223
224 fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
225 let alignment = match baseline {
226 DominantBaseline::Auto => AlignmentBaseline::Auto,
227 DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported
228 DominantBaseline::NoChange => AlignmentBaseline::Auto, // already resolved
229 DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported
230 DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
231 DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
232 DominantBaseline::Hanging => AlignmentBaseline::Hanging,
233 DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
234 DominantBaseline::Central => AlignmentBaseline::Central,
235 DominantBaseline::Middle => AlignmentBaseline::Middle,
236 DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
237 DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
238 };
239
240 self.alignment_baseline_shift(alignment, font_size)
241 }
242
243 // The `alignment-baseline` property is a mess.
244 //
245 // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties)
246 // goes on and on about what this property suppose to do, but doesn't actually explain
247 // how it should be implemented. It's just a very verbose overview.
248 //
249 // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't.
250 // Same goes for basically every SVG library in existence.
251 // Meaning we have no idea how exactly it should be implemented.
252 //
253 // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`,
254 // `text-after-edge` and `ideographic` variants. Producing vastly different output.
255 //
256 // As per spec, a proper implementation should get baseline values from the font itself,
257 // using `BASE` and `bsln` TrueType tables. If those tables are not present,
258 // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts).
259 // And in the worst case scenario simply fallback to hardcoded values.
260 //
261 // Also, most fonts do not provide `BASE` and `bsln` tables to begin with.
262 //
263 // Again, as of Nov 2022, Chrome does only the latter:
264 // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153
265 //
266 // Since baseline TrueType tables parsing and baseline synthesis are pretty hard,
267 // we do what Chrome does - use hardcoded values. And it seems like Safari does the same.
268 //
269 //
270 // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul,
271 // and it's far more complex now. Not sure if anyone actually supports it.
272 fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
273 match alignment {
274 AlignmentBaseline::Auto => 0.0,
275 AlignmentBaseline::Baseline => 0.0,
276 AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
277 self.ascent(font_size)
278 }
279 AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
280 AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
281 AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
282 self.descent(font_size)
283 }
284 AlignmentBaseline::Ideographic => self.descent(font_size),
285 AlignmentBaseline::Alphabetic => 0.0,
286 AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
287 AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
288 }
289 }
290}
291
292struct PathBuilder {
293 builder: tiny_skia_path::PathBuilder,
294}
295
296impl ttf_parser::OutlineBuilder for PathBuilder {
297 fn move_to(&mut self, x: f32, y: f32) {
298 self.builder.move_to(x, y);
299 }
300
301 fn line_to(&mut self, x: f32, y: f32) {
302 self.builder.line_to(x, y);
303 }
304
305 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
306 self.builder.quad_to(x1, y1, x, y);
307 }
308
309 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
310 self.builder.cubic_to(x1, y1, x2, y2, x, y);
311 }
312
313 fn close(&mut self) {
314 self.builder.close();
315 }
316}
317
318/// A read-only text index in bytes.
319///
320/// Guarantee to be on a char boundary and in text bounds.
321#[derive(Clone, Copy, PartialEq)]
322struct ByteIndex(usize);
323
324impl ByteIndex {
325 fn new(i: usize) -> Self {
326 ByteIndex(i)
327 }
328
329 fn value(&self) -> usize {
330 self.0
331 }
332
333 /// Converts byte position into a code point position.
334 fn code_point_at(&self, text: &str) -> usize {
335 textimpl Iterator.char_indices()
336 .take_while(|(i: &usize, _)| *i != self.0)
337 .count()
338 }
339
340 /// Converts byte position into a character.
341 fn char_from(&self, text: &str) -> char {
342 text[self.0..].chars().next().unwrap()
343 }
344}
345
346fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
347 match text.rendering_mode {
348 TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
349 TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
350 TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
351 }
352}
353
354fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
355 chunkIter<'_, TextSpan>
356 .spans
357 .iter()
358 .find(|&span: &TextSpan| span_contains(span, byte_offset))
359}
360
361fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
362 byte_offset.value() >= span.start && byte_offset.value() < span.end
363}
364
365// Baseline resolving in SVG is a mess.
366// Not only it's poorly documented, but as soon as you start mixing
367// `dominant-baseline` and `alignment-baseline` each application/browser will produce
368// different results.
369//
370// For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output.
371//
372// See `alignment_baseline_shift` method comment for more details.
373fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingMode) -> f32 {
374 let mut shift: f32 = -resolve_baseline_shift(&span.baseline_shift, font, font_size:span.font_size.get());
375
376 // TODO: support vertical layout as well
377 if writing_mode == WritingMode::LeftToRight {
378 if span.alignment_baseline == AlignmentBaseline::Auto
379 || span.alignment_baseline == AlignmentBaseline::Baseline
380 {
381 shift += font.dominant_baseline_shift(span.dominant_baseline, font_size:span.font_size.get());
382 } else {
383 shift += font.alignment_baseline_shift(span.alignment_baseline, font_size:span.font_size.get());
384 }
385 }
386
387 shift
388}
389
390type FontsCache = HashMap<Font, Arc<ResolvedFont>>;
391
392fn text_to_paths(
393 text_node: &Text,
394 fontdb: &fontdb::Database,
395) -> Option<(Vec<Path>, NonZeroRect, NonZeroRect)> {
396 let mut fonts_cache: FontsCache = HashMap::new();
397 for chunk in &text_node.chunks {
398 for span in &chunk.spans {
399 if !fonts_cache.contains_key(&span.font) {
400 if let Some(font) = resolve_font(&span.font, fontdb) {
401 fonts_cache.insert(span.font.clone(), Arc::new(font));
402 }
403 }
404 }
405 }
406
407 let mut bbox = BBox::default();
408 let mut stroke_bbox = BBox::default();
409 let mut char_offset = 0;
410 let mut last_x = 0.0;
411 let mut last_y = 0.0;
412 let mut new_paths = Vec::new();
413 for chunk in &text_node.chunks {
414 let (x, y) = match chunk.text_flow {
415 TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
416 TextFlow::Path(_) => (0.0, 0.0),
417 };
418
419 let mut clusters = outline_chunk(chunk, &fonts_cache, fontdb);
420 if clusters.is_empty() {
421 char_offset += chunk.text.chars().count();
422 continue;
423 }
424
425 apply_writing_mode(text_node.writing_mode, &mut clusters);
426 apply_letter_spacing(chunk, &mut clusters);
427 apply_word_spacing(chunk, &mut clusters);
428 apply_length_adjust(chunk, &mut clusters);
429 let mut curr_pos = resolve_clusters_positions(
430 text_node,
431 chunk,
432 char_offset,
433 text_node.writing_mode,
434 &fonts_cache,
435 &mut clusters,
436 );
437
438 let mut text_ts = Transform::default();
439 if text_node.writing_mode == WritingMode::TopToBottom {
440 if let TextFlow::Linear = chunk.text_flow {
441 text_ts = text_ts.pre_rotate_at(90.0, x, y);
442 }
443 }
444
445 for span in &chunk.spans {
446 let font = match fonts_cache.get(&span.font) {
447 Some(v) => v,
448 None => continue,
449 };
450
451 let decoration_spans = collect_decoration_spans(span, &clusters);
452
453 let mut span_ts = text_ts;
454 span_ts = span_ts.pre_translate(x, y);
455 if let TextFlow::Linear = chunk.text_flow {
456 let shift = resolve_baseline(span, font, text_node.writing_mode);
457
458 // In case of a horizontal flow, shift transform and not clusters,
459 // because clusters can be rotated and an additional shift will lead
460 // to invalid results.
461 span_ts = span_ts.pre_translate(0.0, shift);
462 }
463
464 if let Some(decoration) = span.decoration.underline.clone() {
465 // TODO: No idea what offset should be used for top-to-bottom layout.
466 // There is
467 // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property
468 // but it doesn't go into details.
469 let offset = match text_node.writing_mode {
470 WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
471 WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
472 };
473
474 if let Some(path) =
475 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
476 {
477 bbox = bbox.expand(path.data.bounds());
478 stroke_bbox = stroke_bbox.expand(path.data.bounds());
479 new_paths.push(path);
480 }
481 }
482
483 if let Some(decoration) = span.decoration.overline.clone() {
484 let offset = match text_node.writing_mode {
485 WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
486 WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
487 };
488
489 if let Some(path) =
490 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
491 {
492 bbox = bbox.expand(path.data.bounds());
493 stroke_bbox = stroke_bbox.expand(path.data.bounds());
494 new_paths.push(path);
495 }
496 }
497
498 if let Some((path, span_bbox)) = convert_span(span, &mut clusters, span_ts) {
499 bbox = bbox.expand(span_bbox);
500 stroke_bbox = stroke_bbox.expand(path.stroke_bounding_box());
501 new_paths.push(path);
502 }
503
504 if let Some(decoration) = span.decoration.line_through.clone() {
505 let offset = match text_node.writing_mode {
506 WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
507 WritingMode::TopToBottom => 0.0,
508 };
509
510 if let Some(path) =
511 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
512 {
513 bbox = bbox.expand(path.data.bounds());
514 stroke_bbox = stroke_bbox.expand(path.data.bounds());
515 new_paths.push(path);
516 }
517 }
518 }
519
520 char_offset += chunk.text.chars().count();
521
522 if text_node.writing_mode == WritingMode::TopToBottom {
523 if let TextFlow::Linear = chunk.text_flow {
524 std::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
525 }
526 }
527
528 last_x = x + curr_pos.0;
529 last_y = y + curr_pos.1;
530 }
531
532 let bbox = bbox.to_non_zero_rect()?;
533 let stroke_bbox = stroke_bbox.to_non_zero_rect().unwrap_or(bbox);
534 Some((new_paths, bbox, stroke_bbox))
535}
536
537fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option<ResolvedFont> {
538 let mut name_list = Vec::new();
539 for family in &font.families {
540 name_list.push(match family {
541 FontFamily::Serif => fontdb::Family::Serif,
542 FontFamily::SansSerif => fontdb::Family::SansSerif,
543 FontFamily::Cursive => fontdb::Family::Cursive,
544 FontFamily::Fantasy => fontdb::Family::Fantasy,
545 FontFamily::Monospace => fontdb::Family::Monospace,
546 FontFamily::Named(s) => fontdb::Family::Name(s),
547 });
548 }
549
550 // Use the default font as fallback.
551 name_list.push(fontdb::Family::Serif);
552
553 let stretch = match font.stretch {
554 FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
555 FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
556 FontStretch::Condensed => fontdb::Stretch::Condensed,
557 FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
558 FontStretch::Normal => fontdb::Stretch::Normal,
559 FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
560 FontStretch::Expanded => fontdb::Stretch::Expanded,
561 FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
562 FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
563 };
564
565 let style = match font.style {
566 FontStyle::Normal => fontdb::Style::Normal,
567 FontStyle::Italic => fontdb::Style::Italic,
568 FontStyle::Oblique => fontdb::Style::Oblique,
569 };
570
571 let query = fontdb::Query {
572 families: &name_list,
573 weight: fontdb::Weight(font.weight),
574 stretch,
575 style,
576 };
577
578 let id = fontdb.query(&query);
579 if id.is_none() {
580 log::warn!(
581 "No match for '{}' font-family.",
582 font.families
583 .iter()
584 .map(|f| f.to_string())
585 .collect::<Vec<_>>()
586 .join(", ")
587 );
588 }
589
590 fontdb.load_font(id?)
591}
592
593fn convert_span(
594 span: &TextSpan,
595 clusters: &mut [OutlinedCluster],
596 text_ts: Transform,
597) -> Option<(Path, NonZeroRect)> {
598 let mut path_builder = tiny_skia_path::PathBuilder::new();
599 let mut bboxes_builder = tiny_skia_path::PathBuilder::new();
600
601 for cluster in clusters {
602 if !cluster.visible {
603 continue;
604 }
605
606 if span_contains(span, cluster.byte_idx) {
607 let path = cluster
608 .path
609 .take()
610 .and_then(|p| p.transform(cluster.transform));
611
612 if let Some(path) = path {
613 path_builder.push_path(&path);
614 }
615
616 // TODO: make sure `advance` is never negative beforehand.
617 let mut advance = cluster.advance;
618 if advance <= 0.0 {
619 advance = 1.0;
620 }
621
622 // We have to calculate text bbox using font metrics and not glyph shape.
623 if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height())
624 {
625 if let Some(r) = r.transform(cluster.transform) {
626 bboxes_builder.push_rect(r.to_rect());
627 }
628 }
629 }
630 }
631
632 let mut path = path_builder.finish()?;
633 path = path.transform(text_ts)?;
634
635 let mut bboxes = bboxes_builder.finish()?;
636 bboxes = bboxes.transform(text_ts)?;
637
638 let mut fill = span.fill.clone();
639 if let Some(ref mut fill) = fill {
640 // The `fill-rule` should be ignored.
641 // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder
642 //
643 // 'Since the fill-rule property does not apply to SVG text elements,
644 // the specific order of the subpaths within the equivalent path does not matter.'
645 fill.rule = FillRule::NonZero;
646 }
647
648 let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?;
649
650 let path = Path::new(
651 String::new(),
652 span.visibility,
653 fill,
654 span.stroke.clone(),
655 span.paint_order,
656 ShapeRendering::default(),
657 Arc::new(path),
658 Transform::default(),
659 )?;
660
661 Some((path, bbox))
662}
663
664fn collect_decoration_spans(span: &TextSpan, clusters: &[OutlinedCluster]) -> Vec<DecorationSpan> {
665 let mut spans = Vec::new();
666
667 let mut started = false;
668 let mut width = 0.0;
669 let mut transform = Transform::default();
670 for cluster in clusters {
671 if span_contains(span, cluster.byte_idx) {
672 if started && cluster.has_relative_shift {
673 started = false;
674 spans.push(DecorationSpan { width, transform });
675 }
676
677 if !started {
678 width = cluster.advance;
679 started = true;
680 transform = cluster.transform;
681 } else {
682 width += cluster.advance;
683 }
684 } else if started {
685 spans.push(DecorationSpan { width, transform });
686 started = false;
687 }
688 }
689
690 if started {
691 spans.push(DecorationSpan { width, transform });
692 }
693
694 spans
695}
696
697fn convert_decoration(
698 dy: f32,
699 span: &TextSpan,
700 font: &ResolvedFont,
701 mut decoration: TextDecorationStyle,
702 decoration_spans: &[DecorationSpan],
703 transform: Transform,
704) -> Option<Path> {
705 debug_assert!(!decoration_spans.is_empty());
706
707 let thickness = font.underline_thickness(span.font_size.get());
708
709 let mut builder = tiny_skia_path::PathBuilder::new();
710 for dec_span in decoration_spans {
711 let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) {
712 Some(v) => v,
713 None => {
714 log::warn!("a decoration span has a malformed bbox");
715 continue;
716 }
717 };
718
719 let ts = dec_span.transform.pre_translate(0.0, dy);
720
721 let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect());
722 path = match path.transform(ts) {
723 Some(v) => v,
724 None => continue,
725 };
726
727 builder.push_path(&path);
728 }
729
730 let mut path_data = builder.finish()?;
731 path_data = path_data.transform(transform)?;
732
733 Path::new(
734 String::new(),
735 span.visibility,
736 decoration.fill.take(),
737 decoration.stroke.take(),
738 PaintOrder::default(),
739 ShapeRendering::default(),
740 Arc::new(path_data),
741 Transform::default(),
742 )
743}
744
745/// A text decoration span.
746///
747/// Basically a horizontal line, that will be used for underline, overline and line-through.
748/// It doesn't have a height, since it depends on the Font metrics.
749#[derive(Clone, Copy)]
750struct DecorationSpan {
751 width: f32,
752 transform: Transform,
753}
754
755/// A glyph.
756///
757/// Basically, a glyph ID and it's metrics.
758#[derive(Clone)]
759struct Glyph {
760 /// The glyph ID in the font.
761 id: GlyphId,
762
763 /// Position in bytes in the original string.
764 ///
765 /// We use it to match a glyph with a character in the text chunk and therefore with the style.
766 byte_idx: ByteIndex,
767
768 /// The glyph offset in font units.
769 dx: i32,
770
771 /// The glyph offset in font units.
772 dy: i32,
773
774 /// The glyph width / X-advance in font units.
775 width: i32,
776
777 /// Reference to the source font.
778 ///
779 /// Each glyph can have it's own source font.
780 font: Arc<ResolvedFont>,
781}
782
783impl Glyph {
784 fn is_missing(&self) -> bool {
785 self.id.0 == 0
786 }
787}
788
789/// An outlined cluster.
790///
791/// Cluster/grapheme is a single, unbroken, renderable character.
792/// It can be positioned, rotated, spaced, etc.
793///
794/// Let's say we have `й` which is *CYRILLIC SMALL LETTER I* and *COMBINING BREVE*.
795/// It consists of two code points, will be shaped (via harfbuzz) as two glyphs into one cluster,
796/// and then will be combined into the one `OutlinedCluster`.
797#[derive(Clone)]
798struct OutlinedCluster {
799 /// Position in bytes in the original string.
800 ///
801 /// We use it to match a cluster with a character in the text chunk and therefore with the style.
802 byte_idx: ByteIndex,
803
804 /// Cluster's original codepoint.
805 ///
806 /// Technically, a cluster can contain multiple codepoints,
807 /// but we are storing only the first one.
808 codepoint: char,
809
810 /// Cluster's width.
811 ///
812 /// It's different from advance in that it's not affected by letter spacing and word spacing.
813 width: f32,
814
815 /// An advance along the X axis.
816 ///
817 /// Can be negative.
818 advance: f32,
819
820 /// An ascent in SVG coordinates.
821 ascent: f32,
822
823 /// A descent in SVG coordinates.
824 descent: f32,
825
826 /// A x-height in SVG coordinates.
827 x_height: f32,
828
829 /// Indicates that this cluster was affected by the relative shift (via dx/dy attributes)
830 /// during the text layouting. Which breaks the `text-decoration` line.
831 ///
832 /// Used during the `text-decoration` processing.
833 has_relative_shift: bool,
834
835 /// An actual outline.
836 path: Option<tiny_skia_path::Path>,
837
838 /// A cluster's transform that contains it's position, rotation, etc.
839 transform: Transform,
840
841 /// Not all clusters should be rendered.
842 ///
843 /// For example, if a cluster is outside the text path than it should not be rendered.
844 visible: bool,
845}
846
847impl OutlinedCluster {
848 fn height(&self) -> f32 {
849 self.ascent - self.descent
850 }
851}
852
853/// An iterator over glyph clusters.
854///
855/// Input: 0 2 2 2 3 4 4 5 5
856/// Result: 0 1 4 5 7
857struct GlyphClusters<'a> {
858 data: &'a [Glyph],
859 idx: usize,
860}
861
862impl<'a> GlyphClusters<'a> {
863 fn new(data: &'a [Glyph]) -> Self {
864 GlyphClusters { data, idx: 0 }
865 }
866}
867
868impl<'a> Iterator for GlyphClusters<'a> {
869 type Item = (std::ops::Range<usize>, ByteIndex);
870
871 fn next(&mut self) -> Option<Self::Item> {
872 if self.idx == self.data.len() {
873 return None;
874 }
875
876 let start: usize = self.idx;
877 let cluster: ByteIndex = self.data[self.idx].byte_idx;
878 for g: &Glyph in &self.data[self.idx..] {
879 if g.byte_idx != cluster {
880 break;
881 }
882
883 self.idx += 1;
884 }
885
886 Some((start..self.idx, cluster))
887 }
888}
889
890/// Converts a text chunk into a list of outlined clusters.
891///
892/// This function will do the BIDI reordering, text shaping and glyphs outlining,
893/// but not the text layouting. So all clusters are in the 0x0 position.
894fn outline_chunk(
895 chunk: &TextChunk,
896 fonts_cache: &FontsCache,
897 fontdb: &fontdb::Database,
898) -> Vec<OutlinedCluster> {
899 let mut glyphs = Vec::new();
900 for span in &chunk.spans {
901 let font = match fonts_cache.get(&span.font) {
902 Some(v) => v.clone(),
903 None => continue,
904 };
905
906 let tmp_glyphs = shape_text(
907 &chunk.text,
908 font,
909 span.small_caps,
910 span.apply_kerning,
911 fontdb,
912 );
913
914 // Do nothing with the first run.
915 if glyphs.is_empty() {
916 glyphs = tmp_glyphs;
917 continue;
918 }
919
920 // We assume, that shaping with an any font will produce the same amount of glyphs.
921 // Otherwise an error.
922 if glyphs.len() != tmp_glyphs.len() {
923 log::warn!("Text layouting failed.");
924 return Vec::new();
925 }
926
927 // Copy span's glyphs.
928 for (i, glyph) in tmp_glyphs.iter().enumerate() {
929 if span_contains(span, glyph.byte_idx) {
930 glyphs[i] = glyph.clone();
931 }
932 }
933 }
934
935 // Convert glyphs to clusters.
936 let mut clusters = Vec::new();
937 for (range, byte_idx) in GlyphClusters::new(&glyphs) {
938 if let Some(span) = chunk_span_at(chunk, byte_idx) {
939 clusters.push(outline_cluster(
940 &glyphs[range],
941 &chunk.text,
942 span.font_size.get(),
943 fontdb,
944 ));
945 }
946 }
947
948 clusters
949}
950
951/// Text shaping with font fallback.
952fn shape_text(
953 text: &str,
954 font: Arc<ResolvedFont>,
955 small_caps: bool,
956 apply_kerning: bool,
957 fontdb: &fontdb::Database,
958) -> Vec<Glyph> {
959 let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb)
960 .unwrap_or_default();
961
962 // Remember all fonts used for shaping.
963 let mut used_fonts = vec![font.id];
964
965 // Loop until all glyphs become resolved or until no more fonts are left.
966 'outer: loop {
967 let mut missing = None;
968 for glyph in &glyphs {
969 if glyph.is_missing() {
970 missing = Some(glyph.byte_idx.char_from(text));
971 break;
972 }
973 }
974
975 if let Some(c) = missing {
976 let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) {
977 Some(v) => Arc::new(v),
978 None => break 'outer,
979 };
980
981 // Shape again, using a new font.
982 let fallback_glyphs = shape_text_with_font(
983 text,
984 fallback_font.clone(),
985 small_caps,
986 apply_kerning,
987 fontdb,
988 )
989 .unwrap_or_default();
990
991 let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
992 if all_matched {
993 // Replace all glyphs when all of them were matched.
994 glyphs = fallback_glyphs;
995 break 'outer;
996 }
997
998 // We assume, that shaping with an any font will produce the same amount of glyphs.
999 // This is incorrect, but good enough for now.
1000 if glyphs.len() != fallback_glyphs.len() {
1001 break 'outer;
1002 }
1003
1004 // TODO: Replace clusters and not glyphs. This should be more accurate.
1005
1006 // Copy new glyphs.
1007 for i in 0..glyphs.len() {
1008 if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1009 glyphs[i] = fallback_glyphs[i].clone();
1010 }
1011 }
1012
1013 // Remember this font.
1014 used_fonts.push(fallback_font.id);
1015 } else {
1016 break 'outer;
1017 }
1018 }
1019
1020 // Warn about missing glyphs.
1021 for glyph in &glyphs {
1022 if glyph.is_missing() {
1023 let c = glyph.byte_idx.char_from(text);
1024 // TODO: print a full grapheme
1025 log::warn!(
1026 "No fonts with a {}/U+{:X} character were found.",
1027 c,
1028 c as u32
1029 );
1030 }
1031 }
1032
1033 glyphs
1034}
1035
1036/// Converts a text into a list of glyph IDs.
1037///
1038/// This function will do the BIDI reordering and text shaping.
1039fn shape_text_with_font(
1040 text: &str,
1041 font: Arc<ResolvedFont>,
1042 small_caps: bool,
1043 apply_kerning: bool,
1044 fontdb: &fontdb::Database,
1045) -> Option<Vec<Glyph>> {
1046 fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1047 let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1048
1049 let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1050 let paragraph = &bidi_info.paragraphs[0];
1051 let line = paragraph.range.clone();
1052
1053 let mut glyphs = Vec::new();
1054
1055 let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1056 for run in runs.iter() {
1057 let sub_text = &text[run.clone()];
1058 if sub_text.is_empty() {
1059 continue;
1060 }
1061
1062 let hb_direction = if levels[run.start].is_rtl() {
1063 rustybuzz::Direction::RightToLeft
1064 } else {
1065 rustybuzz::Direction::LeftToRight
1066 };
1067
1068 let mut buffer = rustybuzz::UnicodeBuffer::new();
1069 buffer.push_str(sub_text);
1070 buffer.set_direction(hb_direction);
1071
1072 let mut features = Vec::new();
1073 if small_caps {
1074 features.push(rustybuzz::Feature::new(
1075 rustybuzz::Tag::from_bytes(b"smcp"),
1076 1,
1077 ..,
1078 ));
1079 }
1080
1081 if !apply_kerning {
1082 features.push(rustybuzz::Feature::new(
1083 rustybuzz::Tag::from_bytes(b"kern"),
1084 0,
1085 ..,
1086 ));
1087 }
1088
1089 let output = rustybuzz::shape(&rb_font, &features, buffer);
1090
1091 let positions = output.glyph_positions();
1092 let infos = output.glyph_infos();
1093
1094 for (pos, info) in positions.iter().zip(infos) {
1095 let idx = run.start + info.cluster as usize;
1096 debug_assert!(text.get(idx..).is_some());
1097
1098 glyphs.push(Glyph {
1099 byte_idx: ByteIndex::new(idx),
1100 id: GlyphId(info.glyph_id as u16),
1101 dx: pos.x_offset,
1102 dy: pos.y_offset,
1103 width: pos.x_advance,
1104 font: font.clone(),
1105 });
1106 }
1107 }
1108
1109 Some(glyphs)
1110 })?
1111}
1112
1113/// Outlines a glyph cluster.
1114///
1115/// Uses one or more `Glyph`s to construct an `OutlinedCluster`.
1116fn outline_cluster(
1117 glyphs: &[Glyph],
1118 text: &str,
1119 font_size: f32,
1120 db: &fontdb::Database,
1121) -> OutlinedCluster {
1122 debug_assert!(!glyphs.is_empty());
1123
1124 let mut builder = tiny_skia_path::PathBuilder::new();
1125 let mut width = 0.0;
1126 let mut x: f32 = 0.0;
1127
1128 for glyph in glyphs {
1129 let sx = glyph.font.scale(font_size);
1130
1131 if let Some(outline) = db.outline(glyph.font.id, glyph.id) {
1132 // By default, glyphs are upside-down, so we have to mirror them.
1133 let mut ts = Transform::from_scale(1.0, -1.0);
1134
1135 // Scale to font-size.
1136 ts = ts.pre_scale(sx, sx);
1137
1138 // Apply offset.
1139 //
1140 // The first glyph in the cluster will have an offset from 0x0,
1141 // but the later one will have an offset from the "current position".
1142 // So we have to keep an advance.
1143 // TODO: should be done only inside a single text span
1144 ts = ts.pre_translate(x + glyph.dx as f32, glyph.dy as f32);
1145
1146 if let Some(outline) = outline.transform(ts) {
1147 builder.push_path(&outline);
1148 }
1149 }
1150
1151 x += glyph.width as f32;
1152
1153 let glyph_width = glyph.width as f32 * sx;
1154 if glyph_width > width {
1155 width = glyph_width;
1156 }
1157 }
1158
1159 let byte_idx = glyphs[0].byte_idx;
1160 let font = glyphs[0].font.clone();
1161 OutlinedCluster {
1162 byte_idx,
1163 codepoint: byte_idx.char_from(text),
1164 width,
1165 advance: width,
1166 ascent: font.ascent(font_size),
1167 descent: font.descent(font_size),
1168 x_height: font.x_height(font_size),
1169 has_relative_shift: false,
1170 path: builder.finish(),
1171 transform: Transform::default(),
1172 visible: true,
1173 }
1174}
1175
1176/// Finds a font with a specified char.
1177///
1178/// This is a rudimentary font fallback algorithm.
1179fn find_font_for_char(
1180 c: char,
1181 exclude_fonts: &[fontdb::ID],
1182 fontdb: &fontdb::Database,
1183) -> Option<ResolvedFont> {
1184 let base_font_id = exclude_fonts[0];
1185
1186 // Iterate over fonts and check if any of them support the specified char.
1187 for face in fontdb.faces() {
1188 // Ignore fonts, that were used for shaping already.
1189 if exclude_fonts.contains(&face.id) {
1190 continue;
1191 }
1192
1193 // Check that the new face has the same style.
1194 let base_face = fontdb.face(base_font_id)?;
1195 if base_face.style != face.style
1196 && base_face.weight != face.weight
1197 && base_face.stretch != face.stretch
1198 {
1199 continue;
1200 }
1201
1202 if !fontdb.has_char(face.id, c) {
1203 continue;
1204 }
1205
1206 let base_family = base_face
1207 .families
1208 .iter()
1209 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1210 .unwrap_or(&base_face.families[0]);
1211
1212 let new_family = face
1213 .families
1214 .iter()
1215 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1216 .unwrap_or(&base_face.families[0]);
1217
1218 log::warn!("Fallback from {} to {}.", base_family.0, new_family.0);
1219 return fontdb.load_font(face.id);
1220 }
1221
1222 None
1223}
1224
1225/// Resolves clusters positions.
1226///
1227/// Mainly sets the `transform` property.
1228///
1229/// Returns the last text position. The next text chunk should start from that position.
1230fn resolve_clusters_positions(
1231 text: &Text,
1232 chunk: &TextChunk,
1233 char_offset: usize,
1234 writing_mode: WritingMode,
1235 fonts_cache: &FontsCache,
1236 clusters: &mut [OutlinedCluster],
1237) -> (f32, f32) {
1238 match chunk.text_flow {
1239 TextFlow::Linear => {
1240 resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters)
1241 }
1242 TextFlow::Path(ref path: &Arc) => resolve_clusters_positions_path(
1243 text,
1244 chunk,
1245 char_offset,
1246 path,
1247 writing_mode,
1248 fonts_cache,
1249 clusters,
1250 ),
1251 }
1252}
1253
1254fn resolve_clusters_positions_horizontal(
1255 text: &Text,
1256 chunk: &TextChunk,
1257 offset: usize,
1258 writing_mode: WritingMode,
1259 clusters: &mut [OutlinedCluster],
1260) -> (f32, f32) {
1261 let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
1262 let mut y = 0.0;
1263
1264 for cluster in clusters {
1265 let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
1266 if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
1267 if writing_mode == WritingMode::LeftToRight {
1268 x += dx;
1269 y += dy;
1270 } else {
1271 y -= dx;
1272 x += dy;
1273 }
1274 cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
1275 }
1276
1277 cluster.transform = cluster.transform.pre_translate(x, y);
1278
1279 if let Some(angle) = text.rotate.get(cp).cloned() {
1280 if !angle.approx_zero_ulps(4) {
1281 cluster.transform = cluster.transform.pre_rotate(angle);
1282 cluster.has_relative_shift = true;
1283 }
1284 }
1285
1286 x += cluster.advance;
1287 }
1288
1289 (x, y)
1290}
1291
1292fn resolve_clusters_positions_path(
1293 text: &Text,
1294 chunk: &TextChunk,
1295 char_offset: usize,
1296 path: &TextPath,
1297 writing_mode: WritingMode,
1298 fonts_cache: &FontsCache,
1299 clusters: &mut [OutlinedCluster],
1300) -> (f32, f32) {
1301 let mut last_x = 0.0;
1302 let mut last_y = 0.0;
1303
1304 let mut dy = 0.0;
1305
1306 // In the text path mode, chunk's x/y coordinates provide an additional offset along the path.
1307 // The X coordinate is used in a horizontal mode, and Y in vertical.
1308 let chunk_offset = match writing_mode {
1309 WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
1310 WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
1311 };
1312
1313 let start_offset =
1314 chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
1315
1316 let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
1317 for (cluster, normal) in clusters.iter_mut().zip(normals) {
1318 let (x, y, angle) = match normal {
1319 Some(normal) => (normal.x, normal.y, normal.angle),
1320 None => {
1321 // Hide clusters that are outside the text path.
1322 cluster.visible = false;
1323 continue;
1324 }
1325 };
1326
1327 // We have to break a decoration line for each cluster during text-on-path.
1328 cluster.has_relative_shift = true;
1329
1330 let orig_ts = cluster.transform;
1331
1332 // Clusters should be rotated by the x-midpoint x baseline position.
1333 let half_width = cluster.width / 2.0;
1334 cluster.transform = Transform::default();
1335 cluster.transform = cluster.transform.pre_translate(x - half_width, y);
1336 cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
1337
1338 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1339 dy += text.dy.get(cp).cloned().unwrap_or(0.0);
1340
1341 let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
1342 .map(|span| {
1343 let font = match fonts_cache.get(&span.font) {
1344 Some(v) => v,
1345 None => return 0.0,
1346 };
1347 -resolve_baseline(span, font, writing_mode)
1348 })
1349 .unwrap_or(0.0);
1350
1351 // Shift only by `dy` since we already applied `dx`
1352 // during offset along the path calculation.
1353 if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
1354 let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
1355 cluster.transform = cluster
1356 .transform
1357 .pre_translate(shift.x as f32, shift.y as f32);
1358 }
1359
1360 if let Some(angle) = text.rotate.get(cp).cloned() {
1361 if !angle.approx_zero_ulps(4) {
1362 cluster.transform = cluster.transform.pre_rotate(angle);
1363 }
1364 }
1365
1366 // The possible `lengthAdjust` transform should be applied after text-on-path positioning.
1367 cluster.transform = cluster.transform.pre_concat(orig_ts);
1368
1369 last_x = x + cluster.advance;
1370 last_y = y;
1371 }
1372
1373 (last_x, last_y)
1374}
1375
1376fn clusters_length(clusters: &[OutlinedCluster]) -> f32 {
1377 clusters.iter().fold(init:0.0, |w: f32, cluster: &OutlinedCluster| w + cluster.advance)
1378}
1379
1380fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
1381 match a {
1382 TextAnchor::Start => 0.0, // Nothing.
1383 TextAnchor::Middle => -text_width / 2.0,
1384 TextAnchor::End => -text_width,
1385 }
1386}
1387
1388struct PathNormal {
1389 x: f32,
1390 y: f32,
1391 angle: f32,
1392}
1393
1394fn collect_normals(
1395 text: &Text,
1396 chunk: &TextChunk,
1397 clusters: &[OutlinedCluster],
1398 path: &tiny_skia_path::Path,
1399 char_offset: usize,
1400 offset: f32,
1401) -> Vec<Option<PathNormal>> {
1402 let mut offsets = Vec::with_capacity(clusters.len());
1403 let mut normals = Vec::with_capacity(clusters.len());
1404 {
1405 let mut advance = offset;
1406 for cluster in clusters {
1407 // Clusters should be rotated by the x-midpoint x baseline position.
1408 let half_width = cluster.width / 2.0;
1409
1410 // Include relative position.
1411 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1412 advance += text.dx.get(cp).cloned().unwrap_or(0.0);
1413
1414 let offset = advance + half_width;
1415
1416 // Clusters outside the path have no normals.
1417 if offset < 0.0 {
1418 normals.push(None);
1419 }
1420
1421 offsets.push(offset as f64);
1422 advance += cluster.advance;
1423 }
1424 }
1425
1426 let mut prev_mx = path.points()[0].x;
1427 let mut prev_my = path.points()[0].y;
1428 let mut prev_x = prev_mx;
1429 let mut prev_y = prev_my;
1430
1431 fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
1432 let line = kurbo::Line::new(
1433 kurbo::Point::new(px as f64, py as f64),
1434 kurbo::Point::new(x as f64, y as f64),
1435 );
1436 let p1 = line.eval(0.33);
1437 let p2 = line.eval(0.66);
1438 kurbo::CubicBez {
1439 p0: line.p0,
1440 p1,
1441 p2,
1442 p3: line.p1,
1443 }
1444 }
1445
1446 let mut length: f64 = 0.0;
1447 for seg in path.segments() {
1448 let curve = match seg {
1449 tiny_skia_path::PathSegment::MoveTo(p) => {
1450 prev_mx = p.x;
1451 prev_my = p.y;
1452 prev_x = p.x;
1453 prev_y = p.y;
1454 continue;
1455 }
1456 tiny_skia_path::PathSegment::LineTo(p) => {
1457 create_curve_from_line(prev_x, prev_y, p.x, p.y)
1458 }
1459 tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
1460 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
1461 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
1462 p2: kurbo::Point::new(p.x as f64, p.y as f64),
1463 }
1464 .raise(),
1465 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
1466 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
1467 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
1468 p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
1469 p3: kurbo::Point::new(p.x as f64, p.y as f64),
1470 },
1471 tiny_skia_path::PathSegment::Close => {
1472 create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
1473 }
1474 };
1475
1476 let arclen_accuracy = {
1477 let base_arclen_accuracy = 0.5;
1478 // Accuracy depends on a current scale.
1479 // When we have a tiny path scaled by a large value,
1480 // we have to increase out accuracy accordingly.
1481 let (sx, sy) = text.abs_transform.get_scale();
1482 // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy.
1483 base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
1484 };
1485
1486 let curve_len = curve.arclen(arclen_accuracy as f64);
1487
1488 for offset in &offsets[normals.len()..] {
1489 if *offset >= length && *offset <= length + curve_len {
1490 let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
1491 // some rounding error may occur, so we give offset a little tolerance
1492 debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
1493 offset = offset.min(1.0).max(0.0);
1494
1495 let pos = curve.eval(offset);
1496 let d = curve.deriv().eval(offset);
1497 let d = kurbo::Vec2::new(-d.y, d.x); // tangent
1498 let angle = d.atan2().to_degrees() - 90.0;
1499
1500 normals.push(Some(PathNormal {
1501 x: pos.x as f32,
1502 y: pos.y as f32,
1503 angle: angle as f32,
1504 }));
1505
1506 if normals.len() == offsets.len() {
1507 break;
1508 }
1509 }
1510 }
1511
1512 length += curve_len;
1513 prev_x = curve.p3.x as f32;
1514 prev_y = curve.p3.y as f32;
1515 }
1516
1517 // If path ended and we still have unresolved normals - set them to `None`.
1518 for _ in 0..(offsets.len() - normals.len()) {
1519 normals.push(None);
1520 }
1521
1522 normals
1523}
1524
1525/// Applies the `letter-spacing` property to a text chunk clusters.
1526///
1527/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property).
1528fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1529 // At least one span should have a non-zero spacing.
1530 if !chunk
1531 .spans
1532 .iter()
1533 .any(|span| !span.letter_spacing.approx_zero_ulps(4))
1534 {
1535 return;
1536 }
1537
1538 let num_clusters = clusters.len();
1539 for (i, cluster) in clusters.iter_mut().enumerate() {
1540 // Spacing must be applied only to characters that belongs to the script
1541 // that supports spacing.
1542 // We are checking only the first code point, since it should be enough.
1543 // https://www.w3.org/TR/css-text-3/#cursive-tracking
1544 let script = cluster.codepoint.script();
1545 if script_supports_letter_spacing(script) {
1546 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1547 // A space after the last cluster should be ignored,
1548 // since it affects the bbox and text alignment.
1549 if i != num_clusters - 1 {
1550 cluster.advance += span.letter_spacing;
1551 }
1552
1553 // If the cluster advance became negative - clear it.
1554 // This is an UB so we can do whatever we want, and we mimic Chrome's behavior.
1555 if !cluster.advance.is_valid_length() {
1556 cluster.width = 0.0;
1557 cluster.advance = 0.0;
1558 cluster.path = None;
1559 }
1560 }
1561 }
1562 }
1563}
1564
1565/// Checks that selected script supports letter spacing.
1566///
1567/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking).
1568///
1569/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64
1570fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1571 use unicode_script::Script;
1572
1573 !matches!(
1574 script,
1575 Script::Arabic
1576 | Script::Syriac
1577 | Script::Nko
1578 | Script::Manichaean
1579 | Script::Psalter_Pahlavi
1580 | Script::Mandaic
1581 | Script::Mongolian
1582 | Script::Phags_Pa
1583 | Script::Devanagari
1584 | Script::Bengali
1585 | Script::Gurmukhi
1586 | Script::Modi
1587 | Script::Sharada
1588 | Script::Syloti_Nagri
1589 | Script::Tirhuta
1590 | Script::Ogham
1591 )
1592}
1593
1594/// Applies the `word-spacing` property to a text chunk clusters.
1595///
1596/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing).
1597fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1598 // At least one span should have a non-zero spacing.
1599 if !chunkIter<'_, TextSpan>
1600 .spans
1601 .iter()
1602 .any(|span: &TextSpan| !span.word_spacing.approx_zero_ulps(4))
1603 {
1604 return;
1605 }
1606
1607 for cluster: &mut OutlinedCluster in clusters {
1608 if is_word_separator_characters(cluster.codepoint) {
1609 if let Some(span: &TextSpan) = chunk_span_at(chunk, byte_offset:cluster.byte_idx) {
1610 // Technically, word spacing 'should be applied half on each
1611 // side of the character', but it doesn't affect us in any way,
1612 // so we are ignoring this.
1613 cluster.advance += span.word_spacing;
1614
1615 // After word spacing, `advance` can be negative.
1616 }
1617 }
1618 }
1619}
1620
1621/// Checks that the selected character is a word separator.
1622///
1623/// According to: https://www.w3.org/TR/css-text-3/#word-separator
1624fn is_word_separator_characters(c: char) -> bool {
1625 matches!(
1626 c as u32,
1627 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1628 )
1629}
1630
1631fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1632 let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
1633
1634 for span in &chunk.spans {
1635 let target_width = match span.text_length {
1636 Some(v) => v,
1637 None => continue,
1638 };
1639
1640 let mut width = 0.0;
1641 let mut cluster_indexes = Vec::new();
1642 for i in span.start..span.end {
1643 if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
1644 cluster_indexes.push(index);
1645 }
1646 }
1647 // Complex scripts can have mutli-codepoint clusters therefore we have to remove duplicates.
1648 cluster_indexes.sort();
1649 cluster_indexes.dedup();
1650
1651 for i in &cluster_indexes {
1652 // Use the original cluster `width` and not `advance`.
1653 // This method essentially discards any `word-spacing` and `letter-spacing`.
1654 width += clusters[*i].width;
1655 }
1656
1657 if cluster_indexes.is_empty() {
1658 continue;
1659 }
1660
1661 if span.length_adjust == LengthAdjust::Spacing {
1662 let factor = if cluster_indexes.len() > 1 {
1663 (target_width - width) / (cluster_indexes.len() - 1) as f32
1664 } else {
1665 0.0
1666 };
1667
1668 for i in cluster_indexes {
1669 clusters[i].advance = clusters[i].width + factor;
1670 }
1671 } else {
1672 let factor = target_width / width;
1673 // Prevent multiplying by zero.
1674 if factor < 0.001 {
1675 continue;
1676 }
1677
1678 for i in cluster_indexes {
1679 clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
1680
1681 // Technically just a hack to support the current text-on-path algorithm.
1682 if !is_horizontal {
1683 clusters[i].advance *= factor;
1684 clusters[i].width *= factor;
1685 }
1686 }
1687 }
1688 }
1689}
1690
1691/// Rotates clusters according to
1692/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html).
1693fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [OutlinedCluster]) {
1694 if writing_mode != WritingMode::TopToBottom {
1695 return;
1696 }
1697
1698 for cluster in clusters {
1699 let orientation = unicode_vo::char_orientation(cluster.codepoint);
1700 if orientation == unicode_vo::Orientation::Upright {
1701 // Additional offset. Not sure why.
1702 let dy = cluster.width - cluster.height();
1703
1704 // Rotate a cluster 90deg counter clockwise by the center.
1705 let mut ts = Transform::default();
1706 ts = ts.pre_translate(cluster.width / 2.0, 0.0);
1707 ts = ts.pre_rotate(-90.0);
1708 ts = ts.pre_translate(-cluster.width / 2.0, -dy);
1709
1710 if let Some(path) = cluster.path.take() {
1711 cluster.path = path.transform(ts);
1712 }
1713
1714 // Move "baseline" to the middle and make height equal to width.
1715 cluster.ascent = cluster.width / 2.0;
1716 cluster.descent = -cluster.width / 2.0;
1717 } else {
1718 // Could not find a spec that explains this,
1719 // but this is how other applications are shifting the "rotated" characters
1720 // in the top-to-bottom mode.
1721 cluster.transform = cluster.transform.pre_translate(0.0, cluster.x_height / 2.0);
1722 }
1723 }
1724}
1725
1726fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
1727 let mut shift: f32 = 0.0;
1728 for baseline: &BaselineShift in baselines.iter().rev() {
1729 match baseline {
1730 BaselineShift::Baseline => {}
1731 BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
1732 BaselineShift::Superscript => shift += font.superscript_offset(font_size),
1733 BaselineShift::Number(n: &f32) => shift += n,
1734 }
1735 }
1736
1737 shift
1738}
1739