1#![forbid(unsafe_code)]
13#![warn(missing_docs)]
14#![warn(missing_debug_implementations)]
15#![warn(missing_copy_implementations)]
16#![allow(clippy::many_single_char_names)]
17#![allow(clippy::collapsible_else_if)]
18#![allow(clippy::too_many_arguments)]
19#![allow(clippy::neg_cmp_op_on_partial_ord)]
20#![allow(clippy::identity_op)]
21#![allow(clippy::question_mark)]
22#![allow(clippy::upper_case_acronyms)]
23
24pub use fontdb;
25
26use std::cell::RefCell;
27use std::collections::HashMap;
28use std::num::NonZeroU16;
29use std::rc::Rc;
30
31use fontdb::{Database, ID};
32use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
33use rustybuzz::ttf_parser;
34use ttf_parser::GlyphId;
35use unicode_script::UnicodeScript;
36use usvg_tree::*;
37
38pub fn convert_text(root: &mut Group, fontdb: &fontdb::Database) {
40 for node in &mut root.children {
41 if let Node::Text(ref mut text) = node {
42 if let Some((node, bbox, stroke_bbox)) = convert_node(text, fontdb) {
43 text.bounding_box = Some(bbox);
44 text.stroke_bounding_box = Some(stroke_bbox);
46 text.flattened = Some(Box::new(node));
47 }
48 }
49
50 if let Node::Group(ref mut g) = node {
51 convert_text(g, fontdb);
52 }
53
54 node.subroots_mut(|subroot| convert_text(subroot, fontdb))
56 }
57}
58
59fn convert_node(
60 text: &Text,
61 fontdb: &fontdb::Database,
62) -> Option<(Group, NonZeroRect, NonZeroRect)> {
63 let (new_paths, bbox, stroke_bbox) = text_to_paths(text, fontdb)?;
64
65 let mut group = Group {
66 id: text.id.clone(),
67 ..Group::default()
68 };
69
70 let rendering_mode = resolve_rendering_mode(text);
71 for mut path in new_paths {
72 fix_obj_bounding_box(&mut path, bbox);
73 path.rendering_mode = rendering_mode;
74 group.children.push(Node::Path(Box::new(path)));
75 }
76
77 group.calculate_abs_transforms(Transform::identity());
78
79 Some((group, bbox, stroke_bbox))
80}
81
82trait DatabaseExt {
83 fn load_font(&self, id: ID) -> Option<ResolvedFont>;
84 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
85 fn has_char(&self, id: ID, c: char) -> bool;
86}
87
88impl DatabaseExt for Database {
89 #[inline(never)]
90 fn load_font(&self, id: ID) -> Option<ResolvedFont> {
91 self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
92 let font = ttf_parser::Face::parse(data, face_index).ok()?;
93
94 let units_per_em = NonZeroU16::new(font.units_per_em())?;
95
96 let ascent = font.ascender();
97 let descent = font.descender();
98
99 let x_height = font
100 .x_height()
101 .and_then(|x| u16::try_from(x).ok())
102 .and_then(NonZeroU16::new);
103 let x_height = match x_height {
104 Some(height) => height,
105 None => {
106 u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
109 .ok()
110 .and_then(NonZeroU16::new)?
111 }
112 };
113
114 let line_through = font.strikeout_metrics();
115 let line_through_position = match line_through {
116 Some(metrics) => metrics.position,
117 None => x_height.get() as i16 / 2,
118 };
119
120 let (underline_position, underline_thickness) = match font.underline_metrics() {
121 Some(metrics) => {
122 let thickness = u16::try_from(metrics.thickness)
123 .ok()
124 .and_then(NonZeroU16::new)
125 .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
127
128 (metrics.position, thickness)
129 }
130 None => (
131 -(units_per_em.get() as i16) / 9,
132 NonZeroU16::new(units_per_em.get() / 12).unwrap(),
133 ),
134 };
135
136 let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16;
138 let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16;
139 if let Some(metrics) = font.subscript_metrics() {
140 subscript_offset = metrics.y_offset;
141 }
142
143 if let Some(metrics) = font.superscript_metrics() {
144 superscript_offset = metrics.y_offset;
145 }
146
147 Some(ResolvedFont {
148 id,
149 units_per_em,
150 ascent,
151 descent,
152 x_height,
153 underline_position,
154 underline_thickness,
155 line_through_position,
156 subscript_offset,
157 superscript_offset,
158 })
159 })?
160 }
161
162 #[inline(never)]
163 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path> {
164 self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
165 let font = ttf_parser::Face::parse(data, face_index).ok()?;
166
167 let mut builder = PathBuilder {
168 builder: tiny_skia_path::PathBuilder::new(),
169 };
170 font.outline_glyph(glyph_id, &mut builder)?;
171 builder.builder.finish()
172 })?
173 }
174
175 #[inline(never)]
176 fn has_char(&self, id: ID, c: char) -> bool {
177 let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
178 let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
179 font.glyph_index(c)?;
180 Some(true)
181 });
182
183 res == Some(Some(true))
184 }
185}
186
187#[derive(Clone, Copy, Debug)]
188struct ResolvedFont {
189 id: ID,
190
191 units_per_em: NonZeroU16,
192
193 ascent: i16,
195 descent: i16,
196 x_height: NonZeroU16,
197
198 underline_position: i16,
199 underline_thickness: NonZeroU16,
200
201 line_through_position: i16,
205
206 subscript_offset: i16,
207 superscript_offset: i16,
208}
209
210impl ResolvedFont {
211 #[inline]
212 fn scale(&self, font_size: f32) -> f32 {
213 font_size / self.units_per_em.get() as f32
214 }
215
216 #[inline]
217 fn ascent(&self, font_size: f32) -> f32 {
218 self.ascent as f32 * self.scale(font_size)
219 }
220
221 #[inline]
222 fn descent(&self, font_size: f32) -> f32 {
223 self.descent as f32 * self.scale(font_size)
224 }
225
226 #[inline]
227 fn height(&self, font_size: f32) -> f32 {
228 self.ascent(font_size) - self.descent(font_size)
229 }
230
231 #[inline]
232 fn x_height(&self, font_size: f32) -> f32 {
233 self.x_height.get() as f32 * self.scale(font_size)
234 }
235
236 #[inline]
237 fn underline_position(&self, font_size: f32) -> f32 {
238 self.underline_position as f32 * self.scale(font_size)
239 }
240
241 #[inline]
242 fn underline_thickness(&self, font_size: f32) -> f32 {
243 self.underline_thickness.get() as f32 * self.scale(font_size)
244 }
245
246 #[inline]
247 fn line_through_position(&self, font_size: f32) -> f32 {
248 self.line_through_position as f32 * self.scale(font_size)
249 }
250
251 #[inline]
252 fn subscript_offset(&self, font_size: f32) -> f32 {
253 self.subscript_offset as f32 * self.scale(font_size)
254 }
255
256 #[inline]
257 fn superscript_offset(&self, font_size: f32) -> f32 {
258 self.superscript_offset as f32 * self.scale(font_size)
259 }
260
261 fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
262 let alignment = match baseline {
263 DominantBaseline::Auto => AlignmentBaseline::Auto,
264 DominantBaseline::UseScript => AlignmentBaseline::Auto, DominantBaseline::NoChange => AlignmentBaseline::Auto, DominantBaseline::ResetSize => AlignmentBaseline::Auto, DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
268 DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
269 DominantBaseline::Hanging => AlignmentBaseline::Hanging,
270 DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
271 DominantBaseline::Central => AlignmentBaseline::Central,
272 DominantBaseline::Middle => AlignmentBaseline::Middle,
273 DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
274 DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
275 };
276
277 self.alignment_baseline_shift(alignment, font_size)
278 }
279
280 fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
310 match alignment {
311 AlignmentBaseline::Auto => 0.0,
312 AlignmentBaseline::Baseline => 0.0,
313 AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
314 self.ascent(font_size)
315 }
316 AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
317 AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
318 AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
319 self.descent(font_size)
320 }
321 AlignmentBaseline::Ideographic => self.descent(font_size),
322 AlignmentBaseline::Alphabetic => 0.0,
323 AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
324 AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
325 }
326 }
327}
328
329struct PathBuilder {
330 builder: tiny_skia_path::PathBuilder,
331}
332
333impl ttf_parser::OutlineBuilder for PathBuilder {
334 fn move_to(&mut self, x: f32, y: f32) {
335 self.builder.move_to(x, y);
336 }
337
338 fn line_to(&mut self, x: f32, y: f32) {
339 self.builder.line_to(x, y);
340 }
341
342 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
343 self.builder.quad_to(x1, y1, x, y);
344 }
345
346 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
347 self.builder.cubic_to(x1, y1, x2, y2, x, y);
348 }
349
350 fn close(&mut self) {
351 self.builder.close();
352 }
353}
354
355#[derive(Clone, Copy, PartialEq)]
359struct ByteIndex(usize);
360
361impl ByteIndex {
362 fn new(i: usize) -> Self {
363 ByteIndex(i)
364 }
365
366 fn value(&self) -> usize {
367 self.0
368 }
369
370 fn code_point_at(&self, text: &str) -> usize {
372 text.char_indices()
373 .take_while(|(i, _)| *i != self.0)
374 .count()
375 }
376
377 fn char_from(&self, text: &str) -> char {
379 text[self.0..].chars().next().unwrap()
380 }
381}
382
383fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
384 match text.rendering_mode {
385 TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
386 TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
387 TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
388 }
389}
390
391fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
392 chunk
393 .spans
394 .iter()
395 .find(|&span| span_contains(span, byte_offset))
396}
397
398fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
399 byte_offset.value() >= span.start && byte_offset.value() < span.end
400}
401
402fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingMode) -> f32 {
411 let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
412
413 if writing_mode == WritingMode::LeftToRight {
415 if span.alignment_baseline == AlignmentBaseline::Auto
416 || span.alignment_baseline == AlignmentBaseline::Baseline
417 {
418 shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
419 } else {
420 shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
421 }
422 }
423
424 shift
425}
426
427type FontsCache = HashMap<Font, Rc<ResolvedFont>>;
428
429fn text_to_paths(
430 text_node: &Text,
431 fontdb: &fontdb::Database,
432) -> Option<(Vec<Path>, NonZeroRect, NonZeroRect)> {
433 let mut fonts_cache: FontsCache = HashMap::new();
434 for chunk in &text_node.chunks {
435 for span in &chunk.spans {
436 if !fonts_cache.contains_key(&span.font) {
437 if let Some(font) = resolve_font(&span.font, fontdb) {
438 fonts_cache.insert(span.font.clone(), Rc::new(font));
439 }
440 }
441 }
442 }
443
444 let mut bbox = BBox::default();
445 let mut stroke_bbox = BBox::default();
446 let mut char_offset = 0;
447 let mut last_x = 0.0;
448 let mut last_y = 0.0;
449 let mut new_paths = Vec::new();
450 for chunk in &text_node.chunks {
451 let (x, y) = match chunk.text_flow {
452 TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
453 TextFlow::Path(_) => (0.0, 0.0),
454 };
455
456 let mut clusters = outline_chunk(chunk, &fonts_cache, fontdb);
457 if clusters.is_empty() {
458 char_offset += chunk.text.chars().count();
459 continue;
460 }
461
462 apply_writing_mode(text_node.writing_mode, &mut clusters);
463 apply_letter_spacing(chunk, &mut clusters);
464 apply_word_spacing(chunk, &mut clusters);
465 apply_length_adjust(chunk, &mut clusters);
466 let mut curr_pos = resolve_clusters_positions(
467 text_node,
468 chunk,
469 char_offset,
470 text_node.writing_mode,
471 &fonts_cache,
472 &mut clusters,
473 );
474
475 let mut text_ts = Transform::default();
476 if text_node.writing_mode == WritingMode::TopToBottom {
477 if let TextFlow::Linear = chunk.text_flow {
478 text_ts = text_ts.pre_rotate_at(90.0, x, y);
479 }
480 }
481
482 for span in &chunk.spans {
483 let font = match fonts_cache.get(&span.font) {
484 Some(v) => v,
485 None => continue,
486 };
487
488 let decoration_spans = collect_decoration_spans(span, &clusters);
489
490 let mut span_ts = text_ts;
491 span_ts = span_ts.pre_translate(x, y);
492 if let TextFlow::Linear = chunk.text_flow {
493 let shift = resolve_baseline(span, font, text_node.writing_mode);
494
495 span_ts = span_ts.pre_translate(0.0, shift);
499 }
500
501 if let Some(decoration) = span.decoration.underline.clone() {
502 let offset = match text_node.writing_mode {
507 WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
508 WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
509 };
510
511 if let Some(path) =
512 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
513 {
514 bbox = bbox.expand(path.data.bounds());
515 stroke_bbox = stroke_bbox.expand(path.data.bounds());
516 new_paths.push(path);
517 }
518 }
519
520 if let Some(decoration) = span.decoration.overline.clone() {
521 let offset = match text_node.writing_mode {
522 WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
523 WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
524 };
525
526 if let Some(path) =
527 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
528 {
529 bbox = bbox.expand(path.data.bounds());
530 stroke_bbox = stroke_bbox.expand(path.data.bounds());
531 new_paths.push(path);
532 }
533 }
534
535 if let Some((path, span_bbox)) = convert_span(span, &mut clusters, span_ts) {
536 bbox = bbox.expand(span_bbox);
537
538 if let Some(s_bbox) = path.calculate_stroke_bounding_box() {
540 stroke_bbox = stroke_bbox.expand(s_bbox)
541 }
542
543 new_paths.push(path);
544 }
545
546 if let Some(decoration) = span.decoration.line_through.clone() {
547 let offset = match text_node.writing_mode {
548 WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
549 WritingMode::TopToBottom => 0.0,
550 };
551
552 if let Some(path) =
553 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
554 {
555 bbox = bbox.expand(path.data.bounds());
556 stroke_bbox = stroke_bbox.expand(path.data.bounds());
557 new_paths.push(path);
558 }
559 }
560 }
561
562 char_offset += chunk.text.chars().count();
563
564 if text_node.writing_mode == WritingMode::TopToBottom {
565 if let TextFlow::Linear = chunk.text_flow {
566 std::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
567 }
568 }
569
570 last_x = x + curr_pos.0;
571 last_y = y + curr_pos.1;
572 }
573
574 let bbox = bbox.to_non_zero_rect()?;
575 let stroke_bbox = stroke_bbox.to_non_zero_rect().unwrap_or(bbox);
576 Some((new_paths, bbox, stroke_bbox))
577}
578
579fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option<ResolvedFont> {
580 let mut name_list = Vec::new();
581 for family in &font.families {
582 name_list.push(match family.as_str() {
583 "serif" => fontdb::Family::Serif,
584 "sans-serif" => fontdb::Family::SansSerif,
585 "cursive" => fontdb::Family::Cursive,
586 "fantasy" => fontdb::Family::Fantasy,
587 "monospace" => fontdb::Family::Monospace,
588 _ => fontdb::Family::Name(family),
589 });
590 }
591
592 name_list.push(fontdb::Family::Serif);
594
595 let stretch = match font.stretch {
596 FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
597 FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
598 FontStretch::Condensed => fontdb::Stretch::Condensed,
599 FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
600 FontStretch::Normal => fontdb::Stretch::Normal,
601 FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
602 FontStretch::Expanded => fontdb::Stretch::Expanded,
603 FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
604 FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
605 };
606
607 let style = match font.style {
608 FontStyle::Normal => fontdb::Style::Normal,
609 FontStyle::Italic => fontdb::Style::Italic,
610 FontStyle::Oblique => fontdb::Style::Oblique,
611 };
612
613 let query = fontdb::Query {
614 families: &name_list,
615 weight: fontdb::Weight(font.weight),
616 stretch,
617 style,
618 };
619
620 let id = fontdb.query(&query);
621 if id.is_none() {
622 log::warn!("No match for '{}' font-family.", font.families.join(", "));
623 }
624
625 fontdb.load_font(id?)
626}
627
628fn convert_span(
629 span: &TextSpan,
630 clusters: &mut [OutlinedCluster],
631 text_ts: Transform,
632) -> Option<(Path, NonZeroRect)> {
633 let mut path_builder = tiny_skia_path::PathBuilder::new();
634 let mut bboxes_builder = tiny_skia_path::PathBuilder::new();
635
636 for cluster in clusters {
637 if !cluster.visible {
638 continue;
639 }
640
641 if span_contains(span, cluster.byte_idx) {
642 let path = cluster
643 .path
644 .take()
645 .and_then(|p| p.transform(cluster.transform));
646
647 if let Some(path) = path {
648 path_builder.push_path(&path);
649 }
650
651 let mut advance = cluster.advance;
653 if advance <= 0.0 {
654 advance = 1.0;
655 }
656
657 if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height())
659 {
660 if let Some(r) = r.transform(cluster.transform) {
661 bboxes_builder.push_rect(r.to_rect());
662 }
663 }
664 }
665 }
666
667 let mut path = path_builder.finish()?;
668 path = path.transform(text_ts)?;
669
670 let mut bboxes = bboxes_builder.finish()?;
671 bboxes = bboxes.transform(text_ts)?;
672
673 let mut fill = span.fill.clone();
674 if let Some(ref mut fill) = fill {
675 fill.rule = FillRule::NonZero;
681 }
682
683 let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?;
684
685 let path = Path {
686 id: String::new(),
687 visibility: span.visibility,
688 fill,
689 stroke: span.stroke.clone(),
690 paint_order: span.paint_order,
691 rendering_mode: ShapeRendering::default(),
692 data: Rc::new(path),
693 abs_transform: Transform::default(),
694 bounding_box: None,
695 stroke_bounding_box: None,
696 };
697
698 Some((path, bbox))
699}
700
701fn collect_decoration_spans(span: &TextSpan, clusters: &[OutlinedCluster]) -> Vec<DecorationSpan> {
702 let mut spans = Vec::new();
703
704 let mut started = false;
705 let mut width = 0.0;
706 let mut transform = Transform::default();
707 for cluster in clusters {
708 if span_contains(span, cluster.byte_idx) {
709 if started && cluster.has_relative_shift {
710 started = false;
711 spans.push(DecorationSpan { width, transform });
712 }
713
714 if !started {
715 width = cluster.advance;
716 started = true;
717 transform = cluster.transform;
718 } else {
719 width += cluster.advance;
720 }
721 } else if started {
722 spans.push(DecorationSpan { width, transform });
723 started = false;
724 }
725 }
726
727 if started {
728 spans.push(DecorationSpan { width, transform });
729 }
730
731 spans
732}
733
734fn convert_decoration(
735 dy: f32,
736 span: &TextSpan,
737 font: &ResolvedFont,
738 mut decoration: TextDecorationStyle,
739 decoration_spans: &[DecorationSpan],
740 transform: Transform,
741) -> Option<Path> {
742 debug_assert!(!decoration_spans.is_empty());
743
744 let thickness = font.underline_thickness(span.font_size.get());
745
746 let mut builder = tiny_skia_path::PathBuilder::new();
747 for dec_span in decoration_spans {
748 let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) {
749 Some(v) => v,
750 None => {
751 log::warn!("a decoration span has a malformed bbox");
752 continue;
753 }
754 };
755
756 let ts = dec_span.transform.pre_translate(0.0, dy);
757
758 let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect());
759 path = match path.transform(ts) {
760 Some(v) => v,
761 None => continue,
762 };
763
764 builder.push_path(&path);
765 }
766
767 let mut path_data = builder.finish()?;
768 path_data = path_data.transform(transform)?;
769
770 let mut path = Path::new(Rc::new(path_data));
771 path.visibility = span.visibility;
772 path.fill = decoration.fill.take();
773 path.stroke = decoration.stroke.take();
774 Some(path)
775}
776
777fn fix_obj_bounding_box(path: &mut Path, bbox: NonZeroRect) {
781 if let Some(ref mut fill) = path.fill {
782 if let Some(new_paint) = paint_server_to_user_space_on_use(fill.paint.clone(), bbox) {
783 fill.paint = new_paint;
784 }
785 }
786
787 if let Some(ref mut stroke) = path.stroke {
788 if let Some(new_paint) = paint_server_to_user_space_on_use(stroke.paint.clone(), bbox) {
789 stroke.paint = new_paint;
790 }
791 }
792}
793
794fn paint_server_to_user_space_on_use(paint: Paint, bbox: NonZeroRect) -> Option<Paint> {
800 if paint.units() != Some(Units::ObjectBoundingBox) {
801 return None;
802 }
803
804 let ts = Transform::from_bbox(bbox);
809 let paint = match paint {
810 Paint::Color(_) => paint,
811 Paint::LinearGradient(ref lg) => {
812 let transform = lg.transform.post_concat(ts);
813 Paint::LinearGradient(Rc::new(LinearGradient {
814 x1: lg.x1,
815 y1: lg.y1,
816 x2: lg.x2,
817 y2: lg.y2,
818 base: BaseGradient {
819 id: String::new(),
820 units: Units::UserSpaceOnUse,
821 transform,
822 spread_method: lg.spread_method,
823 stops: lg.stops.clone(),
824 },
825 }))
826 }
827 Paint::RadialGradient(ref rg) => {
828 let transform = rg.transform.post_concat(ts);
829 Paint::RadialGradient(Rc::new(RadialGradient {
830 cx: rg.cx,
831 cy: rg.cy,
832 r: rg.r,
833 fx: rg.fx,
834 fy: rg.fy,
835 base: BaseGradient {
836 id: String::new(),
837 units: Units::UserSpaceOnUse,
838 transform,
839 spread_method: rg.spread_method,
840 stops: rg.stops.clone(),
841 },
842 }))
843 }
844 Paint::Pattern(ref patt) => {
845 let transform = patt.borrow().transform.post_concat(ts);
846 Paint::Pattern(Rc::new(RefCell::new(Pattern {
847 id: String::new(),
848 units: Units::UserSpaceOnUse,
849 content_units: patt.borrow().content_units,
850 transform,
851 rect: patt.borrow().rect,
852 view_box: patt.borrow().view_box,
853 root: patt.borrow().root.clone(),
854 })))
855 }
856 };
857
858 Some(paint)
859}
860
861#[derive(Clone, Copy)]
866struct DecorationSpan {
867 width: f32,
868 transform: Transform,
869}
870
871#[derive(Clone)]
875struct Glyph {
876 id: GlyphId,
878
879 byte_idx: ByteIndex,
883
884 dx: i32,
886
887 dy: i32,
889
890 width: i32,
892
893 font: Rc<ResolvedFont>,
897}
898
899impl Glyph {
900 fn is_missing(&self) -> bool {
901 self.id.0 == 0
902 }
903}
904
905#[derive(Clone)]
914struct OutlinedCluster {
915 byte_idx: ByteIndex,
919
920 codepoint: char,
925
926 width: f32,
930
931 advance: f32,
935
936 ascent: f32,
938
939 descent: f32,
941
942 x_height: f32,
944
945 has_relative_shift: bool,
950
951 path: Option<tiny_skia_path::Path>,
953
954 transform: Transform,
956
957 visible: bool,
961}
962
963impl OutlinedCluster {
964 fn height(&self) -> f32 {
965 self.ascent - self.descent
966 }
967}
968
969struct GlyphClusters<'a> {
974 data: &'a [Glyph],
975 idx: usize,
976}
977
978impl<'a> GlyphClusters<'a> {
979 fn new(data: &'a [Glyph]) -> Self {
980 GlyphClusters { data, idx: 0 }
981 }
982}
983
984impl<'a> Iterator for GlyphClusters<'a> {
985 type Item = (std::ops::Range<usize>, ByteIndex);
986
987 fn next(&mut self) -> Option<Self::Item> {
988 if self.idx == self.data.len() {
989 return None;
990 }
991
992 let start = self.idx;
993 let cluster = self.data[self.idx].byte_idx;
994 for g in &self.data[self.idx..] {
995 if g.byte_idx != cluster {
996 break;
997 }
998
999 self.idx += 1;
1000 }
1001
1002 Some((start..self.idx, cluster))
1003 }
1004}
1005
1006fn outline_chunk(
1011 chunk: &TextChunk,
1012 fonts_cache: &FontsCache,
1013 fontdb: &fontdb::Database,
1014) -> Vec<OutlinedCluster> {
1015 let mut glyphs = Vec::new();
1016 for span in &chunk.spans {
1017 let font = match fonts_cache.get(&span.font) {
1018 Some(v) => v.clone(),
1019 None => continue,
1020 };
1021
1022 let tmp_glyphs = shape_text(
1023 &chunk.text,
1024 font,
1025 span.small_caps,
1026 span.apply_kerning,
1027 fontdb,
1028 );
1029
1030 if glyphs.is_empty() {
1032 glyphs = tmp_glyphs;
1033 continue;
1034 }
1035
1036 if glyphs.len() != tmp_glyphs.len() {
1039 log::warn!("Text layouting failed.");
1040 return Vec::new();
1041 }
1042
1043 for (i, glyph) in tmp_glyphs.iter().enumerate() {
1045 if span_contains(span, glyph.byte_idx) {
1046 glyphs[i] = glyph.clone();
1047 }
1048 }
1049 }
1050
1051 let mut clusters = Vec::new();
1053 for (range, byte_idx) in GlyphClusters::new(&glyphs) {
1054 if let Some(span) = chunk_span_at(chunk, byte_idx) {
1055 clusters.push(outline_cluster(
1056 &glyphs[range],
1057 &chunk.text,
1058 span.font_size.get(),
1059 fontdb,
1060 ));
1061 }
1062 }
1063
1064 clusters
1065}
1066
1067fn shape_text(
1069 text: &str,
1070 font: Rc<ResolvedFont>,
1071 small_caps: bool,
1072 apply_kerning: bool,
1073 fontdb: &fontdb::Database,
1074) -> Vec<Glyph> {
1075 let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb)
1076 .unwrap_or_default();
1077
1078 let mut used_fonts = vec![font.id];
1080
1081 'outer: loop {
1083 let mut missing = None;
1084 for glyph in &glyphs {
1085 if glyph.is_missing() {
1086 missing = Some(glyph.byte_idx.char_from(text));
1087 break;
1088 }
1089 }
1090
1091 if let Some(c) = missing {
1092 let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) {
1093 Some(v) => Rc::new(v),
1094 None => break 'outer,
1095 };
1096
1097 let fallback_glyphs = shape_text_with_font(
1099 text,
1100 fallback_font.clone(),
1101 small_caps,
1102 apply_kerning,
1103 fontdb,
1104 )
1105 .unwrap_or_default();
1106
1107 let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
1108 if all_matched {
1109 glyphs = fallback_glyphs;
1111 break 'outer;
1112 }
1113
1114 if glyphs.len() != fallback_glyphs.len() {
1117 break 'outer;
1118 }
1119
1120 for i in 0..glyphs.len() {
1124 if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1125 glyphs[i] = fallback_glyphs[i].clone();
1126 }
1127 }
1128
1129 used_fonts.push(fallback_font.id);
1131 } else {
1132 break 'outer;
1133 }
1134 }
1135
1136 for glyph in &glyphs {
1138 if glyph.is_missing() {
1139 let c = glyph.byte_idx.char_from(text);
1140 log::warn!(
1142 "No fonts with a {}/U+{:X} character were found.",
1143 c,
1144 c as u32
1145 );
1146 }
1147 }
1148
1149 glyphs
1150}
1151
1152fn shape_text_with_font(
1156 text: &str,
1157 font: Rc<ResolvedFont>,
1158 small_caps: bool,
1159 apply_kerning: bool,
1160 fontdb: &fontdb::Database,
1161) -> Option<Vec<Glyph>> {
1162 fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1163 let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1164
1165 let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1166 let paragraph = &bidi_info.paragraphs[0];
1167 let line = paragraph.range.clone();
1168
1169 let mut glyphs = Vec::new();
1170
1171 let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1172 for run in runs.iter() {
1173 let sub_text = &text[run.clone()];
1174 if sub_text.is_empty() {
1175 continue;
1176 }
1177
1178 let hb_direction = if levels[run.start].is_rtl() {
1179 rustybuzz::Direction::RightToLeft
1180 } else {
1181 rustybuzz::Direction::LeftToRight
1182 };
1183
1184 let mut buffer = rustybuzz::UnicodeBuffer::new();
1185 buffer.push_str(sub_text);
1186 buffer.set_direction(hb_direction);
1187
1188 let mut features = Vec::new();
1189 if small_caps {
1190 features.push(rustybuzz::Feature::new(
1191 rustybuzz::Tag::from_bytes(b"smcp"),
1192 1,
1193 ..,
1194 ));
1195 }
1196
1197 if !apply_kerning {
1198 features.push(rustybuzz::Feature::new(
1199 rustybuzz::Tag::from_bytes(b"kern"),
1200 0,
1201 ..,
1202 ));
1203 }
1204
1205 let output = rustybuzz::shape(&rb_font, &features, buffer);
1206
1207 let positions = output.glyph_positions();
1208 let infos = output.glyph_infos();
1209
1210 for (pos, info) in positions.iter().zip(infos) {
1211 let idx = run.start + info.cluster as usize;
1212 debug_assert!(text.get(idx..).is_some());
1213
1214 glyphs.push(Glyph {
1215 byte_idx: ByteIndex::new(idx),
1216 id: GlyphId(info.glyph_id as u16),
1217 dx: pos.x_offset,
1218 dy: pos.y_offset,
1219 width: pos.x_advance,
1220 font: font.clone(),
1221 });
1222 }
1223 }
1224
1225 Some(glyphs)
1226 })?
1227}
1228
1229fn outline_cluster(
1233 glyphs: &[Glyph],
1234 text: &str,
1235 font_size: f32,
1236 db: &fontdb::Database,
1237) -> OutlinedCluster {
1238 debug_assert!(!glyphs.is_empty());
1239
1240 let mut builder = tiny_skia_path::PathBuilder::new();
1241 let mut width = 0.0;
1242 let mut x: f32 = 0.0;
1243
1244 for glyph in glyphs {
1245 let sx = glyph.font.scale(font_size);
1246
1247 if let Some(outline) = db.outline(glyph.font.id, glyph.id) {
1248 let mut ts = Transform::from_scale(1.0, -1.0);
1250
1251 ts = ts.pre_scale(sx, sx);
1253
1254 ts = ts.pre_translate(x + glyph.dx as f32, glyph.dy as f32);
1261
1262 if let Some(outline) = outline.transform(ts) {
1263 builder.push_path(&outline);
1264 }
1265 }
1266
1267 x += glyph.width as f32;
1268
1269 let glyph_width = glyph.width as f32 * sx;
1270 if glyph_width > width {
1271 width = glyph_width;
1272 }
1273 }
1274
1275 let byte_idx = glyphs[0].byte_idx;
1276 let font = glyphs[0].font.clone();
1277 OutlinedCluster {
1278 byte_idx,
1279 codepoint: byte_idx.char_from(text),
1280 width,
1281 advance: width,
1282 ascent: font.ascent(font_size),
1283 descent: font.descent(font_size),
1284 x_height: font.x_height(font_size),
1285 has_relative_shift: false,
1286 path: builder.finish(),
1287 transform: Transform::default(),
1288 visible: true,
1289 }
1290}
1291
1292fn find_font_for_char(
1296 c: char,
1297 exclude_fonts: &[fontdb::ID],
1298 fontdb: &fontdb::Database,
1299) -> Option<ResolvedFont> {
1300 let base_font_id = exclude_fonts[0];
1301
1302 for face in fontdb.faces() {
1304 if exclude_fonts.contains(&face.id) {
1306 continue;
1307 }
1308
1309 let base_face = fontdb.face(base_font_id)?;
1311 if base_face.style != face.style
1312 && base_face.weight != face.weight
1313 && base_face.stretch != face.stretch
1314 {
1315 continue;
1316 }
1317
1318 if !fontdb.has_char(face.id, c) {
1319 continue;
1320 }
1321
1322 let base_family = base_face
1323 .families
1324 .iter()
1325 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1326 .unwrap_or(&base_face.families[0]);
1327
1328 let new_family = face
1329 .families
1330 .iter()
1331 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1332 .unwrap_or(&base_face.families[0]);
1333
1334 log::warn!("Fallback from {} to {}.", base_family.0, new_family.0);
1335 return fontdb.load_font(face.id);
1336 }
1337
1338 None
1339}
1340
1341fn resolve_clusters_positions(
1347 text: &Text,
1348 chunk: &TextChunk,
1349 char_offset: usize,
1350 writing_mode: WritingMode,
1351 fonts_cache: &FontsCache,
1352 clusters: &mut [OutlinedCluster],
1353) -> (f32, f32) {
1354 match chunk.text_flow {
1355 TextFlow::Linear => {
1356 resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters)
1357 }
1358 TextFlow::Path(ref path) => resolve_clusters_positions_path(
1359 text,
1360 chunk,
1361 char_offset,
1362 path,
1363 writing_mode,
1364 fonts_cache,
1365 clusters,
1366 ),
1367 }
1368}
1369
1370fn resolve_clusters_positions_horizontal(
1371 text: &Text,
1372 chunk: &TextChunk,
1373 offset: usize,
1374 writing_mode: WritingMode,
1375 clusters: &mut [OutlinedCluster],
1376) -> (f32, f32) {
1377 let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
1378 let mut y = 0.0;
1379
1380 for cluster in clusters {
1381 let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
1382 if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
1383 if writing_mode == WritingMode::LeftToRight {
1384 x += dx;
1385 y += dy;
1386 } else {
1387 y -= dx;
1388 x += dy;
1389 }
1390 cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
1391 }
1392
1393 cluster.transform = cluster.transform.pre_translate(x, y);
1394
1395 if let Some(angle) = text.rotate.get(cp).cloned() {
1396 if !angle.approx_zero_ulps(4) {
1397 cluster.transform = cluster.transform.pre_rotate(angle);
1398 cluster.has_relative_shift = true;
1399 }
1400 }
1401
1402 x += cluster.advance;
1403 }
1404
1405 (x, y)
1406}
1407
1408fn resolve_clusters_positions_path(
1409 text: &Text,
1410 chunk: &TextChunk,
1411 char_offset: usize,
1412 path: &TextPath,
1413 writing_mode: WritingMode,
1414 fonts_cache: &FontsCache,
1415 clusters: &mut [OutlinedCluster],
1416) -> (f32, f32) {
1417 let mut last_x = 0.0;
1418 let mut last_y = 0.0;
1419
1420 let mut dy = 0.0;
1421
1422 let chunk_offset = match writing_mode {
1425 WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
1426 WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
1427 };
1428
1429 let start_offset =
1430 chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
1431
1432 let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
1433 for (cluster, normal) in clusters.iter_mut().zip(normals) {
1434 let (x, y, angle) = match normal {
1435 Some(normal) => (normal.x, normal.y, normal.angle),
1436 None => {
1437 cluster.visible = false;
1439 continue;
1440 }
1441 };
1442
1443 cluster.has_relative_shift = true;
1445
1446 let orig_ts = cluster.transform;
1447
1448 let half_width = cluster.width / 2.0;
1450 cluster.transform = Transform::default();
1451 cluster.transform = cluster.transform.pre_translate(x - half_width, y);
1452 cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
1453
1454 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1455 dy += text.dy.get(cp).cloned().unwrap_or(0.0);
1456
1457 let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
1458 .map(|span| {
1459 let font = match fonts_cache.get(&span.font) {
1460 Some(v) => v,
1461 None => return 0.0,
1462 };
1463 -resolve_baseline(span, font, writing_mode)
1464 })
1465 .unwrap_or(0.0);
1466
1467 if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
1470 let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
1471 cluster.transform = cluster
1472 .transform
1473 .pre_translate(shift.x as f32, shift.y as f32);
1474 }
1475
1476 if let Some(angle) = text.rotate.get(cp).cloned() {
1477 if !angle.approx_zero_ulps(4) {
1478 cluster.transform = cluster.transform.pre_rotate(angle);
1479 }
1480 }
1481
1482 cluster.transform = cluster.transform.pre_concat(orig_ts);
1484
1485 last_x = x + cluster.advance;
1486 last_y = y;
1487 }
1488
1489 (last_x, last_y)
1490}
1491
1492fn clusters_length(clusters: &[OutlinedCluster]) -> f32 {
1493 clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
1494}
1495
1496fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
1497 match a {
1498 TextAnchor::Start => 0.0, TextAnchor::Middle => -text_width / 2.0,
1500 TextAnchor::End => -text_width,
1501 }
1502}
1503
1504struct PathNormal {
1505 x: f32,
1506 y: f32,
1507 angle: f32,
1508}
1509
1510fn collect_normals(
1511 text: &Text,
1512 chunk: &TextChunk,
1513 clusters: &[OutlinedCluster],
1514 path: &tiny_skia_path::Path,
1515 char_offset: usize,
1516 offset: f32,
1517) -> Vec<Option<PathNormal>> {
1518 let mut offsets = Vec::with_capacity(clusters.len());
1519 let mut normals = Vec::with_capacity(clusters.len());
1520 {
1521 let mut advance = offset;
1522 for cluster in clusters {
1523 let half_width = cluster.width / 2.0;
1525
1526 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1528 advance += text.dx.get(cp).cloned().unwrap_or(0.0);
1529
1530 let offset = advance + half_width;
1531
1532 if offset < 0.0 {
1534 normals.push(None);
1535 }
1536
1537 offsets.push(offset as f64);
1538 advance += cluster.advance;
1539 }
1540 }
1541
1542 let mut prev_mx = path.points()[0].x;
1543 let mut prev_my = path.points()[0].y;
1544 let mut prev_x = prev_mx;
1545 let mut prev_y = prev_my;
1546
1547 fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
1548 let line = kurbo::Line::new(
1549 kurbo::Point::new(px as f64, py as f64),
1550 kurbo::Point::new(x as f64, y as f64),
1551 );
1552 let p1 = line.eval(0.33);
1553 let p2 = line.eval(0.66);
1554 kurbo::CubicBez {
1555 p0: line.p0,
1556 p1,
1557 p2,
1558 p3: line.p1,
1559 }
1560 }
1561
1562 let mut length: f64 = 0.0;
1563 for seg in path.segments() {
1564 let curve = match seg {
1565 tiny_skia_path::PathSegment::MoveTo(p) => {
1566 prev_mx = p.x;
1567 prev_my = p.y;
1568 prev_x = p.x;
1569 prev_y = p.y;
1570 continue;
1571 }
1572 tiny_skia_path::PathSegment::LineTo(p) => {
1573 create_curve_from_line(prev_x, prev_y, p.x, p.y)
1574 }
1575 tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
1576 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
1577 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
1578 p2: kurbo::Point::new(p.x as f64, p.y as f64),
1579 }
1580 .raise(),
1581 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
1582 p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
1583 p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
1584 p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
1585 p3: kurbo::Point::new(p.x as f64, p.y as f64),
1586 },
1587 tiny_skia_path::PathSegment::Close => {
1588 create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
1589 }
1590 };
1591
1592 let arclen_accuracy = {
1593 let base_arclen_accuracy = 0.5;
1594 let (sx, sy) = text.abs_transform.get_scale();
1598 base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
1600 };
1601
1602 let curve_len = curve.arclen(arclen_accuracy as f64);
1603
1604 for offset in &offsets[normals.len()..] {
1605 if *offset >= length && *offset <= length + curve_len {
1606 let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
1607 debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
1609 offset = offset.min(1.0).max(0.0);
1610
1611 let pos = curve.eval(offset);
1612 let d = curve.deriv().eval(offset);
1613 let d = kurbo::Vec2::new(-d.y, d.x); let angle = d.atan2().to_degrees() - 90.0;
1615
1616 normals.push(Some(PathNormal {
1617 x: pos.x as f32,
1618 y: pos.y as f32,
1619 angle: angle as f32,
1620 }));
1621
1622 if normals.len() == offsets.len() {
1623 break;
1624 }
1625 }
1626 }
1627
1628 length += curve_len;
1629 prev_x = curve.p3.x as f32;
1630 prev_y = curve.p3.y as f32;
1631 }
1632
1633 for _ in 0..(offsets.len() - normals.len()) {
1635 normals.push(None);
1636 }
1637
1638 normals
1639}
1640
1641fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1645 if !chunk
1647 .spans
1648 .iter()
1649 .any(|span| !span.letter_spacing.approx_zero_ulps(4))
1650 {
1651 return;
1652 }
1653
1654 let num_clusters = clusters.len();
1655 for (i, cluster) in clusters.iter_mut().enumerate() {
1656 let script = cluster.codepoint.script();
1661 if script_supports_letter_spacing(script) {
1662 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1663 if i != num_clusters - 1 {
1666 cluster.advance += span.letter_spacing;
1667 }
1668
1669 if !cluster.advance.is_valid_length() {
1672 cluster.width = 0.0;
1673 cluster.advance = 0.0;
1674 cluster.path = None;
1675 }
1676 }
1677 }
1678 }
1679}
1680
1681fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1687 use unicode_script::Script;
1688
1689 !matches!(
1690 script,
1691 Script::Arabic
1692 | Script::Syriac
1693 | Script::Nko
1694 | Script::Manichaean
1695 | Script::Psalter_Pahlavi
1696 | Script::Mandaic
1697 | Script::Mongolian
1698 | Script::Phags_Pa
1699 | Script::Devanagari
1700 | Script::Bengali
1701 | Script::Gurmukhi
1702 | Script::Modi
1703 | Script::Sharada
1704 | Script::Syloti_Nagri
1705 | Script::Tirhuta
1706 | Script::Ogham
1707 )
1708}
1709
1710fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1714 if !chunk
1716 .spans
1717 .iter()
1718 .any(|span| !span.word_spacing.approx_zero_ulps(4))
1719 {
1720 return;
1721 }
1722
1723 for cluster in clusters {
1724 if is_word_separator_characters(cluster.codepoint) {
1725 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1726 cluster.advance += span.word_spacing;
1730
1731 }
1733 }
1734 }
1735}
1736
1737fn is_word_separator_characters(c: char) -> bool {
1741 matches!(
1742 c as u32,
1743 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1744 )
1745}
1746
1747fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1748 let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
1749
1750 for span in &chunk.spans {
1751 let target_width = match span.text_length {
1752 Some(v) => v,
1753 None => continue,
1754 };
1755
1756 let mut width = 0.0;
1757 let mut cluster_indexes = Vec::new();
1758 for i in span.start..span.end {
1759 if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
1760 cluster_indexes.push(index);
1761 }
1762 }
1763 cluster_indexes.sort();
1765 cluster_indexes.dedup();
1766
1767 for i in &cluster_indexes {
1768 width += clusters[*i].width;
1771 }
1772
1773 if cluster_indexes.is_empty() {
1774 continue;
1775 }
1776
1777 if span.length_adjust == LengthAdjust::Spacing {
1778 let factor = if cluster_indexes.len() > 1 {
1779 (target_width - width) / (cluster_indexes.len() - 1) as f32
1780 } else {
1781 0.0
1782 };
1783
1784 for i in cluster_indexes {
1785 clusters[i].advance = clusters[i].width + factor;
1786 }
1787 } else {
1788 let factor = target_width / width;
1789 if factor < 0.001 {
1791 continue;
1792 }
1793
1794 for i in cluster_indexes {
1795 clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
1796
1797 if !is_horizontal {
1799 clusters[i].advance *= factor;
1800 clusters[i].width *= factor;
1801 }
1802 }
1803 }
1804 }
1805}
1806
1807fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [OutlinedCluster]) {
1810 if writing_mode != WritingMode::TopToBottom {
1811 return;
1812 }
1813
1814 for cluster in clusters {
1815 let orientation = unicode_vo::char_orientation(cluster.codepoint);
1816 if orientation == unicode_vo::Orientation::Upright {
1817 let dy = cluster.width - cluster.height();
1819
1820 let mut ts = Transform::default();
1822 ts = ts.pre_translate(cluster.width / 2.0, 0.0);
1823 ts = ts.pre_rotate(-90.0);
1824 ts = ts.pre_translate(-cluster.width / 2.0, -dy);
1825
1826 if let Some(path) = cluster.path.take() {
1827 cluster.path = path.transform(ts);
1828 }
1829
1830 cluster.ascent = cluster.width / 2.0;
1832 cluster.descent = -cluster.width / 2.0;
1833 } else {
1834 cluster.transform = cluster.transform.pre_translate(0.0, cluster.x_height / 2.0);
1838 }
1839 }
1840}
1841
1842fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
1843 let mut shift = 0.0;
1844 for baseline in baselines.iter().rev() {
1845 match baseline {
1846 BaselineShift::Baseline => {}
1847 BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
1848 BaselineShift::Superscript => shift += font.superscript_offset(font_size),
1849 BaselineShift::Number(n) => shift += n,
1850 }
1851 }
1852
1853 shift
1854}