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;
25pub use lru;
26
27use std::collections::hash_map::DefaultHasher;
28use std::convert::TryFrom;
29use std::hash::{Hash, Hasher};
30use std::num::NonZeroU16;
31use std::rc::Rc;
32use std::{collections::HashMap, num::NonZeroUsize};
33
34use fontdb::{Database, ID};
35use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
36use rustybuzz::ttf_parser;
37use ttf_parser::GlyphId;
38use unicode_script::UnicodeScript;
39use usvgr::*;
40
41pub trait TreeTextToPath {
43 fn convert_text(&mut self, fontdb: &fontdb::Database, keep_named_groups: bool);
48
49 fn convert_text_with_cache(
51 &mut self,
52 fontdb: &fontdb::Database,
53 cache: &mut UsvgrTextLayoutCache,
54 fonts_cache: &mut FontsCache,
55 keep_named_groups: bool,
56 );
57}
58
59#[derive(Debug)]
61pub struct UsvgrTextLayoutCache(Option<lru::LruCache<u64, (Vec<Path>, PathBbox)>>);
62
63impl UsvgrTextLayoutCache {
64 pub fn new(size: usize) -> Self {
67 if size > 0 {
68 Self(Some(lru::LruCache::new(NonZeroUsize::new(size).unwrap())))
69 } else {
70 Self::none()
71 }
72 }
73
74 pub fn none() -> Self {
76 UsvgrTextLayoutCache(None)
77 }
78
79 fn get_or_insert(
80 &mut self,
81 key: u64,
82 f: impl FnOnce() -> (Vec<Path>, PathBbox),
83 ) -> (Vec<Path>, PathBbox) {
84 if let Some(cache) = self.0.as_mut() {
85 cache.get_or_insert(key, f).to_owned()
86 } else {
87 f()
88 }
89 }
90}
91
92impl TreeTextToPath for usvgr::Tree {
93 fn convert_text(&mut self, fontdb: &fontdb::Database, keep_named_groups: bool) {
94 convert_text(
95 self.root.clone(),
96 fontdb,
97 keep_named_groups,
98 &mut UsvgrTextLayoutCache::none(),
99 &mut FontsCache(HashMap::new()),
100 );
101 }
102
103 fn convert_text_with_cache(
104 &mut self,
105 fontdb: &fontdb::Database,
106 text_layouts_cache: &mut UsvgrTextLayoutCache,
107 fonts_cache: &mut FontsCache,
108 keep_named_groups: bool,
109 ) {
110 convert_text(
111 self.root.clone(),
112 fontdb,
113 keep_named_groups,
114 text_layouts_cache,
115 fonts_cache,
116 );
117 }
118}
119
120pub trait TextToPath {
122 fn convert(
126 &self,
127 fontdb: &fontdb::Database,
128 absolute_ts: Transform,
129 cache: &mut UsvgrTextLayoutCache,
130 fonts_cache: &mut FontsCache,
131 ) -> Option<Node>;
132}
133
134impl TextToPath for Text {
135 fn convert(
136 &self,
137 fontdb: &fontdb::Database,
138 absolute_ts: Transform,
139 cache: &mut UsvgrTextLayoutCache,
140 fonts_cache: &mut FontsCache,
141 ) -> Option<Node> {
142 let mut hasher = DefaultHasher::new();
143 self.hash(&mut hasher);
144 let hash = hasher.finish();
145
146 let (new_paths, bbox) = cache.get_or_insert(hash, || {
147 text_to_paths(self, fontdb, absolute_ts, fonts_cache)
148 });
149
150 if new_paths.is_empty() {
151 return None;
152 }
153
154 let group = Node::new(NodeKind::Group(Group {
156 id: self.id.clone(),
157 transform: self.transform,
158 ..Group::default()
159 }));
160
161 let rendering_mode = resolve_rendering_mode(self);
162 for mut path in new_paths {
163 fix_obj_bounding_box(&mut path, bbox);
164 path.rendering_mode = rendering_mode;
165 group.append_kind(NodeKind::Path(path));
166 }
167
168 Some(group)
169 }
170}
171
172fn convert_text(
173 root: Node,
174 fontdb: &fontdb::Database,
175 keep_named_groups: bool,
176 cache: &mut UsvgrTextLayoutCache,
177 fonts_cache: &mut FontsCache,
178) {
179 let mut text_nodes = Vec::new();
180 for node in root.descendants() {
182 match *node.borrow() {
183 NodeKind::Group(ref g) => {
184 if let Some(ref clip) = g.clip_path {
185 convert_text(
186 clip.root.clone(),
187 fontdb,
188 keep_named_groups,
189 cache,
190 fonts_cache,
191 );
192 }
193
194 if let Some(ref mask) = g.mask {
195 convert_text(
196 mask.root.clone(),
197 fontdb,
198 keep_named_groups,
199 cache,
200 fonts_cache,
201 );
202 }
203 }
204 NodeKind::Path(ref path) => {
205 if let Some(ref fill) = path.fill {
206 if let Paint::Pattern(ref p) = fill.paint {
207 convert_text(
208 p.root.clone(),
209 fontdb,
210 keep_named_groups,
211 cache,
212 fonts_cache,
213 );
214 }
215 }
216 if let Some(ref stroke) = path.stroke {
217 if let Paint::Pattern(ref p) = stroke.paint {
218 convert_text(
219 p.root.clone(),
220 fontdb,
221 keep_named_groups,
222 cache,
223 fonts_cache,
224 );
225 }
226 }
227 }
228 NodeKind::Image(_) => {}
229 NodeKind::Text(ref text) => {
230 text_nodes.push(node.clone());
231
232 for chunk in &text.chunks {
233 for span in &chunk.spans {
234 if let Some(ref fill) = span.fill {
235 if let Paint::Pattern(ref p) = fill.paint {
236 convert_text(
237 p.root.clone(),
238 fontdb,
239 keep_named_groups,
240 cache,
241 fonts_cache,
242 );
243 }
244 }
245 if let Some(ref stroke) = span.stroke {
246 if let Paint::Pattern(ref p) = stroke.paint {
247 convert_text(
248 p.root.clone(),
249 fontdb,
250 keep_named_groups,
251 cache,
252 fonts_cache,
253 );
254 }
255 }
256 }
257 }
258 }
259 }
260 }
261
262 if text_nodes.is_empty() {
263 return;
264 }
265
266 for node in &text_nodes {
267 let mut new_node = None;
268 if let NodeKind::Text(ref text) = *node.borrow() {
269 let mut absolute_ts = node.parent().unwrap().abs_transform();
270 absolute_ts.append(&text.transform);
271 new_node = text.convert(fontdb, absolute_ts, cache, fonts_cache);
272 }
273
274 if let Some(new_node) = new_node {
275 node.insert_after(new_node);
276 }
277 }
278
279 text_nodes.iter().for_each(|n| n.detach());
280 Tree::ungroup_groups(root, keep_named_groups);
281}
282
283trait DatabaseExt {
284 fn load_font(&self, id: ID) -> Option<ResolvedFont>;
285 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<PathData>;
286 fn has_char(&self, id: ID, c: char) -> bool;
287}
288
289impl DatabaseExt for Database {
290 #[inline(never)]
291 fn load_font(&self, id: ID) -> Option<ResolvedFont> {
292 self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
293 let font = ttf_parser::Face::parse(data, face_index).ok()?;
294
295 let units_per_em = NonZeroU16::new(font.units_per_em())?;
296
297 let ascent = font.ascender();
298 let descent = font.descender();
299
300 let x_height = font
301 .x_height()
302 .and_then(|x| u16::try_from(x).ok())
303 .and_then(NonZeroU16::new);
304 let x_height = match x_height {
305 Some(height) => height,
306 None => {
307 u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
310 .ok()
311 .and_then(NonZeroU16::new)?
312 }
313 };
314
315 let line_through = font.strikeout_metrics();
316 let line_through_position = match line_through {
317 Some(metrics) => metrics.position,
318 None => x_height.get() as i16 / 2,
319 };
320
321 let (underline_position, underline_thickness) = match font.underline_metrics() {
322 Some(metrics) => {
323 let thickness = u16::try_from(metrics.thickness)
324 .ok()
325 .and_then(NonZeroU16::new)
326 .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
328
329 (metrics.position, thickness)
330 }
331 None => (
332 -(units_per_em.get() as i16) / 9,
333 NonZeroU16::new(units_per_em.get() / 12).unwrap(),
334 ),
335 };
336
337 let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16;
339 let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16;
340 if let Some(metrics) = font.subscript_metrics() {
341 subscript_offset = metrics.y_offset;
342 }
343
344 if let Some(metrics) = font.superscript_metrics() {
345 superscript_offset = metrics.y_offset;
346 }
347
348 Some(ResolvedFont {
349 id,
350 units_per_em,
351 ascent,
352 descent,
353 x_height,
354 underline_position,
355 underline_thickness,
356 line_through_position,
357 subscript_offset,
358 superscript_offset,
359 })
360 })?
361 }
362
363 #[inline(never)]
364 fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<PathData> {
365 self.with_face_data(id, |data, face_index| -> Option<PathData> {
366 let font = ttf_parser::Face::parse(data, face_index).ok()?;
367
368 let mut builder = PathBuilder {
369 path: PathData::new(),
370 };
371 font.outline_glyph(glyph_id, &mut builder)?;
372 Some(builder.path)
373 })?
374 }
375
376 #[inline(never)]
377 fn has_char(&self, id: ID, c: char) -> bool {
378 let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
379 let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
380 font.glyph_index(c)?;
381 Some(true)
382 });
383
384 res == Some(Some(true))
385 }
386}
387
388#[derive(Clone, Copy, Debug)]
389struct ResolvedFont {
390 id: ID,
391
392 units_per_em: NonZeroU16,
393
394 ascent: i16,
396 descent: i16,
397 x_height: NonZeroU16,
398
399 underline_position: i16,
400 underline_thickness: NonZeroU16,
401
402 line_through_position: i16,
406
407 subscript_offset: i16,
408 superscript_offset: i16,
409}
410
411impl ResolvedFont {
412 #[inline]
413 fn scale(&self, font_size: f64) -> f64 {
414 font_size / self.units_per_em.get() as f64
415 }
416
417 #[inline]
418 fn ascent(&self, font_size: f64) -> f64 {
419 self.ascent as f64 * self.scale(font_size)
420 }
421
422 #[inline]
423 fn descent(&self, font_size: f64) -> f64 {
424 self.descent as f64 * self.scale(font_size)
425 }
426
427 #[inline]
428 fn height(&self, font_size: f64) -> f64 {
429 self.ascent(font_size) - self.descent(font_size)
430 }
431
432 #[inline]
433 fn x_height(&self, font_size: f64) -> f64 {
434 self.x_height.get() as f64 * self.scale(font_size)
435 }
436
437 #[inline]
438 fn underline_position(&self, font_size: f64) -> f64 {
439 self.underline_position as f64 * self.scale(font_size)
440 }
441
442 #[inline]
443 fn underline_thickness(&self, font_size: f64) -> f64 {
444 self.underline_thickness.get() as f64 * self.scale(font_size)
445 }
446
447 #[inline]
448 fn line_through_position(&self, font_size: f64) -> f64 {
449 self.line_through_position as f64 * self.scale(font_size)
450 }
451
452 #[inline]
453 fn subscript_offset(&self, font_size: f64) -> f64 {
454 self.subscript_offset as f64 * self.scale(font_size)
455 }
456
457 #[inline]
458 fn superscript_offset(&self, font_size: f64) -> f64 {
459 self.superscript_offset as f64 * self.scale(font_size)
460 }
461
462 fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f64) -> f64 {
463 let alignment = match baseline {
464 DominantBaseline::Auto => AlignmentBaseline::Auto,
465 DominantBaseline::UseScript => AlignmentBaseline::Auto, DominantBaseline::NoChange => AlignmentBaseline::Auto, DominantBaseline::ResetSize => AlignmentBaseline::Auto, DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
469 DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
470 DominantBaseline::Hanging => AlignmentBaseline::Hanging,
471 DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
472 DominantBaseline::Central => AlignmentBaseline::Central,
473 DominantBaseline::Middle => AlignmentBaseline::Middle,
474 DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
475 DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
476 };
477
478 self.alignment_baseline_shift(alignment, font_size)
479 }
480
481 fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f64) -> f64 {
511 match alignment {
512 AlignmentBaseline::Auto => 0.0,
513 AlignmentBaseline::Baseline => 0.0,
514 AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
515 self.ascent(font_size)
516 }
517 AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
518 AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
519 AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
520 self.descent(font_size)
521 }
522 AlignmentBaseline::Ideographic => self.descent(font_size),
523 AlignmentBaseline::Alphabetic => 0.0,
524 AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
525 AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
526 }
527 }
528}
529
530struct PathBuilder {
531 path: PathData,
532}
533
534impl ttf_parser::OutlineBuilder for PathBuilder {
535 fn move_to(&mut self, x: f32, y: f32) {
536 self.path.push_move_to(x as f64, y as f64);
537 }
538
539 fn line_to(&mut self, x: f32, y: f32) {
540 self.path.push_line_to(x as f64, y as f64);
541 }
542
543 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
544 self.path
545 .push_quad_to(x1 as f64, y1 as f64, x as f64, y as f64);
546 }
547
548 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
549 self.path.push_curve_to(
550 x1 as f64, y1 as f64, x2 as f64, y2 as f64, x as f64, y as f64,
551 );
552 }
553
554 fn close(&mut self) {
555 self.path.push_close_path();
556 }
557}
558
559#[derive(Clone, Copy, PartialEq)]
563struct ByteIndex(usize);
564
565impl ByteIndex {
566 fn new(i: usize) -> Self {
567 ByteIndex(i)
568 }
569
570 fn value(&self) -> usize {
571 self.0
572 }
573
574 fn code_point_at(&self, text: &str) -> usize {
576 text.char_indices()
577 .take_while(|(i, _)| *i != self.0)
578 .count()
579 }
580
581 fn char_from(&self, text: &str) -> char {
583 text[self.0..].chars().next().unwrap()
584 }
585}
586
587fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
588 match text.rendering_mode {
589 TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
590 TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
591 TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
592 }
593}
594
595fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
596 for span in &chunk.spans {
597 if span_contains(span, byte_offset) {
598 return Some(span);
599 }
600 }
601
602 None
603}
604
605fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
606 byte_offset.value() >= span.start && byte_offset.value() < span.end
607}
608
609fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingMode) -> f64 {
618 let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
619
620 if writing_mode == WritingMode::LeftToRight {
622 if span.alignment_baseline == AlignmentBaseline::Auto
623 || span.alignment_baseline == AlignmentBaseline::Baseline
624 {
625 shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
626 } else {
627 shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
628 }
629 }
630
631 return shift;
632}
633
634type FontsCacheInner = HashMap<Font, Rc<ResolvedFont>>;
635
636#[derive(Debug)]
637pub struct FontsCache(FontsCacheInner);
641
642impl FontsCache {
643 pub fn new() -> Self {
645 FontsCache(HashMap::new())
646 }
647}
648
649fn text_to_paths(
650 text_node: &Text,
651 fontdb: &fontdb::Database,
652 abs_ts: Transform,
653 fonts_cache: &mut FontsCache,
654) -> (Vec<Path>, PathBbox) {
655 let fonts_cache = &mut fonts_cache.0;
656
657 for chunk in &text_node.chunks {
658 for span in &chunk.spans {
659 if !fonts_cache.contains_key(&span.font) {
660 if let Some(font) = resolve_font(&span.font, fontdb) {
661 fonts_cache.insert(span.font.clone(), Rc::new(font));
662 }
663 }
664 }
665 }
666
667 let mut bbox = PathBbox::new_bbox();
668 let mut char_offset = 0;
669 let mut last_x = 0.0;
670 let mut last_y = 0.0;
671 let mut new_paths = Vec::new();
672 for chunk in &text_node.chunks {
673 let (x, y) = match chunk.text_flow {
674 TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
675 TextFlow::Path(_) => (0.0, 0.0),
676 };
677
678 let mut clusters = outline_chunk(chunk, &fonts_cache, fontdb);
679 if clusters.is_empty() {
680 char_offset += chunk.text.chars().count();
681 continue;
682 }
683
684 apply_writing_mode(text_node.writing_mode, &mut clusters);
685 apply_letter_spacing(chunk, &mut clusters);
686 apply_word_spacing(chunk, &mut clusters);
687 apply_length_adjust(chunk, &mut clusters);
688 let mut curr_pos = resolve_clusters_positions(
689 chunk,
690 char_offset,
691 &text_node.positions,
692 &text_node.rotate,
693 text_node.writing_mode,
694 abs_ts,
695 &fonts_cache,
696 &mut clusters,
697 );
698
699 let mut text_ts = Transform::default();
700 if text_node.writing_mode == WritingMode::TopToBottom {
701 if let TextFlow::Linear = chunk.text_flow {
702 text_ts.rotate_at(90.0, x, y);
703 }
704 }
705
706 for span in &chunk.spans {
707 let font = match fonts_cache.get(&span.font) {
708 Some(v) => v,
709 None => continue,
710 };
711
712 let decoration_spans = collect_decoration_spans(span, &clusters);
713
714 let mut span_ts = text_ts;
715 span_ts.translate(x, y);
716 if let TextFlow::Linear = chunk.text_flow {
717 let shift = resolve_baseline(span, font, text_node.writing_mode);
718
719 span_ts.translate(0.0, shift);
723 }
724
725 if let Some(decoration) = span.decoration.underline.clone() {
726 let offset = match text_node.writing_mode {
731 WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
732 WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
733 };
734
735 let path =
736 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts);
737
738 if let Some(r) = path.data.bbox() {
739 bbox = bbox.expand(r);
740 }
741
742 new_paths.push(path);
743 }
744
745 if let Some(decoration) = span.decoration.overline.clone() {
746 let offset = match text_node.writing_mode {
747 WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
748 WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
749 };
750
751 let path =
752 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts);
753
754 if let Some(r) = path.data.bbox() {
755 bbox = bbox.expand(r);
756 }
757
758 new_paths.push(path);
759 }
760
761 if let Some(path) = convert_span(span, &mut clusters, &span_ts) {
762 if let Some(r) = path.text_bbox {
764 bbox = bbox.expand(r.to_path_bbox());
765 }
766
767 new_paths.push(path);
768 }
769
770 if let Some(decoration) = span.decoration.line_through.clone() {
771 let offset = match text_node.writing_mode {
772 WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
773 WritingMode::TopToBottom => 0.0,
774 };
775
776 let path =
777 convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts);
778
779 if let Some(r) = path.data.bbox() {
780 bbox = bbox.expand(r);
781 }
782
783 new_paths.push(path);
784 }
785 }
786
787 char_offset += chunk.text.chars().count();
788
789 if text_node.writing_mode == WritingMode::TopToBottom {
790 if let TextFlow::Linear = chunk.text_flow {
791 std::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
792 }
793 }
794
795 last_x = x + curr_pos.0;
796 last_y = y + curr_pos.1;
797 }
798
799 (new_paths, bbox)
800}
801
802fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option<ResolvedFont> {
803 let mut name_list = Vec::new();
804 for family in &font.families {
805 name_list.push(match family.as_str() {
806 "serif" => fontdb::Family::Serif,
807 "sans-serif" => fontdb::Family::SansSerif,
808 "cursive" => fontdb::Family::Cursive,
809 "fantasy" => fontdb::Family::Fantasy,
810 "monospace" => fontdb::Family::Monospace,
811 _ => fontdb::Family::Name(family),
812 });
813 }
814
815 name_list.push(fontdb::Family::Serif);
817
818 let stretch = match font.stretch {
819 Stretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
820 Stretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
821 Stretch::Condensed => fontdb::Stretch::Condensed,
822 Stretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
823 Stretch::Normal => fontdb::Stretch::Normal,
824 Stretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
825 Stretch::Expanded => fontdb::Stretch::Expanded,
826 Stretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
827 Stretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
828 };
829
830 let style = match font.style {
831 Style::Normal => fontdb::Style::Normal,
832 Style::Italic => fontdb::Style::Italic,
833 Style::Oblique => fontdb::Style::Oblique,
834 };
835
836 let query = fontdb::Query {
837 families: &name_list,
838 weight: fontdb::Weight(font.weight),
839 stretch,
840 style,
841 };
842
843 let id = fontdb.query(&query);
844 if id.is_none() {
845 log::warn!("No match for '{}' font-family.", font.families.join(", "));
846 }
847
848 fontdb.load_font(id?)
849}
850
851fn convert_span(
852 span: &TextSpan,
853 clusters: &mut [OutlinedCluster],
854 text_ts: &Transform,
855) -> Option<Path> {
856 let mut path_data = PathData::new();
857 let mut bboxes_data = PathData::new();
858
859 for cluster in clusters {
860 if !cluster.visible {
861 continue;
862 }
863
864 if span_contains(span, cluster.byte_idx) {
865 let mut path = std::mem::replace(&mut cluster.path, PathData::new());
866 path.transform(cluster.transform);
867
868 path_data.push_path(&path);
869
870 if let Some(r) = Rect::new(0.0, -cluster.ascent, cluster.advance, cluster.height()) {
872 if let Some(r) = r.transform(&cluster.transform) {
873 bboxes_data.push_rect(r);
874 }
875 }
876 }
877 }
878
879 if path_data.is_empty() {
880 return None;
881 }
882
883 path_data.transform(*text_ts);
884 bboxes_data.transform(*text_ts);
885
886 let mut fill = span.fill.clone();
887 if let Some(ref mut fill) = fill {
888 fill.rule = FillRule::NonZero;
894 }
895
896 let path = Path {
897 id: String::new(),
898 transform: Transform::default(),
899 visibility: span.visibility,
900 fill,
901 stroke: span.stroke.clone(),
902 paint_order: span.paint_order,
903 rendering_mode: ShapeRendering::default(),
904 text_bbox: bboxes_data.bbox().and_then(|r| r.to_rect()),
905 data: Rc::new(path_data),
906 };
907
908 Some(path)
909}
910
911fn collect_decoration_spans(span: &TextSpan, clusters: &[OutlinedCluster]) -> Vec<DecorationSpan> {
912 let mut spans = Vec::new();
913
914 let mut started = false;
915 let mut width = 0.0;
916 let mut transform = Transform::default();
917 for cluster in clusters {
918 if span_contains(span, cluster.byte_idx) {
919 if started && cluster.has_relative_shift {
920 started = false;
921 spans.push(DecorationSpan { width, transform });
922 }
923
924 if !started {
925 width = cluster.advance;
926 started = true;
927 transform = cluster.transform;
928 } else {
929 width += cluster.advance;
930 }
931 } else if started {
932 spans.push(DecorationSpan { width, transform });
933 started = false;
934 }
935 }
936
937 if started {
938 spans.push(DecorationSpan { width, transform });
939 }
940
941 spans
942}
943
944fn convert_decoration(
945 dy: f64,
946 span: &TextSpan,
947 font: &ResolvedFont,
948 mut decoration: TextDecorationStyle,
949 decoration_spans: &[DecorationSpan],
950 transform: Transform,
951) -> Path {
952 debug_assert!(!decoration_spans.is_empty());
953
954 let thickness = font.underline_thickness(span.font_size.get());
955
956 let mut path = PathData::new();
957 for dec_span in decoration_spans {
958 let rect = match Rect::new(0.0, -thickness / 2.0, dec_span.width, thickness) {
959 Some(v) => v,
960 None => {
961 log::warn!("a decoration span has a malformed bbox");
962 continue;
963 }
964 };
965
966 let start_idx = path.len();
967 path.push_rect(rect);
968
969 let mut ts = dec_span.transform;
970 ts.translate(0.0, dy);
971 path.transform_from(start_idx, ts);
972 }
973
974 path.transform(transform);
975
976 Path {
977 visibility: span.visibility,
978 fill: decoration.fill.take(),
979 stroke: decoration.stroke.take(),
980 data: Rc::new(path),
981 ..Path::default()
982 }
983}
984
985fn fix_obj_bounding_box(path: &mut Path, bbox: PathBbox) {
989 if let Some(ref mut fill) = path.fill {
990 if let Some(new_paint) = paint_server_to_user_space_on_use(fill.paint.clone(), bbox) {
991 fill.paint = new_paint;
992 }
993 }
994
995 if let Some(ref mut stroke) = path.stroke {
996 if let Some(new_paint) = paint_server_to_user_space_on_use(stroke.paint.clone(), bbox) {
997 stroke.paint = new_paint;
998 }
999 }
1000}
1001
1002fn paint_server_to_user_space_on_use(paint: Paint, bbox: PathBbox) -> Option<Paint> {
1008 if paint.units() != Some(Units::ObjectBoundingBox) {
1009 return None;
1010 }
1011
1012 let ts = Transform::from_bbox(bbox.to_rect()?);
1018 let paint = match paint {
1019 Paint::Color(_) => paint,
1020 Paint::LinearGradient(ref lg) => {
1021 let mut transform = lg.transform;
1022 transform.prepend(&ts);
1023 Paint::LinearGradient(Rc::new(LinearGradient {
1024 id: String::new(),
1025 x1: lg.x1,
1026 y1: lg.y1,
1027 x2: lg.x2,
1028 y2: lg.y2,
1029 base: BaseGradient {
1030 units: Units::UserSpaceOnUse,
1031 transform,
1032 spread_method: lg.spread_method,
1033 stops: lg.stops.clone(),
1034 },
1035 }))
1036 }
1037 Paint::RadialGradient(ref rg) => {
1038 let mut transform = rg.transform;
1039 transform.prepend(&ts);
1040 Paint::RadialGradient(Rc::new(RadialGradient {
1041 id: String::new(),
1042 cx: rg.cx,
1043 cy: rg.cy,
1044 r: rg.r,
1045 fx: rg.fx,
1046 fy: rg.fy,
1047 base: BaseGradient {
1048 units: Units::UserSpaceOnUse,
1049 transform,
1050 spread_method: rg.spread_method,
1051 stops: rg.stops.clone(),
1052 },
1053 }))
1054 }
1055 Paint::Pattern(ref patt) => {
1056 let mut transform = patt.transform;
1057 transform.prepend(&ts);
1058 Paint::Pattern(Rc::new(Pattern {
1059 id: String::new(),
1060 units: Units::UserSpaceOnUse,
1061 content_units: patt.content_units,
1062 transform: transform,
1063 rect: patt.rect,
1064 view_box: patt.view_box,
1065 root: patt.root.clone().make_deep_copy(),
1066 }))
1067 }
1068 };
1069
1070 Some(paint)
1071}
1072
1073#[derive(Clone, Copy)]
1078struct DecorationSpan {
1079 width: f64,
1080 transform: Transform,
1081}
1082
1083#[derive(Clone)]
1087struct Glyph {
1088 id: GlyphId,
1090
1091 byte_idx: ByteIndex,
1095
1096 dx: i32,
1098
1099 dy: i32,
1101
1102 width: i32,
1104
1105 font: Rc<ResolvedFont>,
1109}
1110
1111impl Glyph {
1112 fn is_missing(&self) -> bool {
1113 self.id.0 == 0
1114 }
1115}
1116
1117#[derive(Clone)]
1126struct OutlinedCluster {
1127 byte_idx: ByteIndex,
1131
1132 codepoint: char,
1137
1138 width: f64,
1142
1143 advance: f64,
1147
1148 ascent: f64,
1150
1151 descent: f64,
1153
1154 x_height: f64,
1156
1157 has_relative_shift: bool,
1162
1163 path: PathData,
1165
1166 transform: Transform,
1168
1169 visible: bool,
1173}
1174
1175impl OutlinedCluster {
1176 fn height(&self) -> f64 {
1177 self.ascent - self.descent
1178 }
1179}
1180
1181struct GlyphClusters<'a> {
1186 data: &'a [Glyph],
1187 idx: usize,
1188}
1189
1190impl<'a> GlyphClusters<'a> {
1191 fn new(data: &'a [Glyph]) -> Self {
1192 GlyphClusters { data, idx: 0 }
1193 }
1194}
1195
1196impl<'a> Iterator for GlyphClusters<'a> {
1197 type Item = (std::ops::Range<usize>, ByteIndex);
1198
1199 fn next(&mut self) -> Option<Self::Item> {
1200 if self.idx == self.data.len() {
1201 return None;
1202 }
1203
1204 let start = self.idx;
1205 let cluster = self.data[self.idx].byte_idx;
1206 for g in &self.data[self.idx..] {
1207 if g.byte_idx != cluster {
1208 break;
1209 }
1210
1211 self.idx += 1;
1212 }
1213
1214 Some((start..self.idx, cluster))
1215 }
1216}
1217
1218fn outline_chunk(
1223 chunk: &TextChunk,
1224 fonts_cache: &FontsCacheInner,
1225 fontdb: &fontdb::Database,
1226) -> Vec<OutlinedCluster> {
1227 let mut glyphs = Vec::new();
1228 for span in &chunk.spans {
1229 let font = match fonts_cache.get(&span.font) {
1230 Some(v) => v.clone(),
1231 None => continue,
1232 };
1233
1234 let tmp_glyphs = shape_text(
1235 &chunk.text,
1236 font,
1237 span.small_caps,
1238 span.apply_kerning,
1239 fontdb,
1240 );
1241
1242 if glyphs.is_empty() {
1244 glyphs = tmp_glyphs;
1245 continue;
1246 }
1247
1248 if glyphs.len() != tmp_glyphs.len() {
1251 log::warn!("Text layouting failed.");
1252 return Vec::new();
1253 }
1254
1255 for (i, glyph) in tmp_glyphs.iter().enumerate() {
1257 if span_contains(span, glyph.byte_idx) {
1258 glyphs[i] = glyph.clone();
1259 }
1260 }
1261 }
1262
1263 let mut clusters = Vec::new();
1265 for (range, byte_idx) in GlyphClusters::new(&glyphs) {
1266 if let Some(span) = chunk_span_at(chunk, byte_idx) {
1267 clusters.push(outline_cluster(
1268 &glyphs[range],
1269 &chunk.text,
1270 span.font_size.get(),
1271 fontdb,
1272 ));
1273 }
1274 }
1275
1276 clusters
1277}
1278
1279fn shape_text(
1281 text: &str,
1282 font: Rc<ResolvedFont>,
1283 small_caps: bool,
1284 apply_kerning: bool,
1285 fontdb: &fontdb::Database,
1286) -> Vec<Glyph> {
1287 let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb)
1288 .unwrap_or_default();
1289
1290 let mut used_fonts = vec![font.id];
1292
1293 'outer: loop {
1295 let mut missing = None;
1296 for glyph in &glyphs {
1297 if glyph.is_missing() {
1298 missing = Some(glyph.byte_idx.char_from(text));
1299 break;
1300 }
1301 }
1302
1303 if let Some(c) = missing {
1304 let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) {
1305 Some(v) => Rc::new(v),
1306 None => break 'outer,
1307 };
1308
1309 let fallback_glyphs = shape_text_with_font(
1311 text,
1312 fallback_font.clone(),
1313 small_caps,
1314 apply_kerning,
1315 fontdb,
1316 )
1317 .unwrap_or_default();
1318
1319 let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
1320 if all_matched {
1321 glyphs = fallback_glyphs;
1323 break 'outer;
1324 }
1325
1326 if glyphs.len() != fallback_glyphs.len() {
1329 break 'outer;
1330 }
1331
1332 for i in 0..glyphs.len() {
1336 if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1337 glyphs[i] = fallback_glyphs[i].clone();
1338 }
1339 }
1340
1341 used_fonts.push(fallback_font.id);
1343 } else {
1344 break 'outer;
1345 }
1346 }
1347
1348 for glyph in &glyphs {
1350 if glyph.is_missing() {
1351 let c = glyph.byte_idx.char_from(text);
1352 log::warn!(
1354 "No fonts with a {}/U+{:X} character were found.",
1355 c,
1356 c as u32
1357 );
1358 }
1359 }
1360
1361 glyphs
1362}
1363
1364fn shape_text_with_font(
1368 text: &str,
1369 font: Rc<ResolvedFont>,
1370 small_caps: bool,
1371 apply_kerning: bool,
1372 fontdb: &fontdb::Database,
1373) -> Option<Vec<Glyph>> {
1374 fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1375 let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1376
1377 let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1378 let paragraph = &bidi_info.paragraphs[0];
1379 let line = paragraph.range.clone();
1380
1381 let mut glyphs = Vec::new();
1382
1383 let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1384 for run in runs.iter() {
1385 let sub_text = &text[run.clone()];
1386 if sub_text.is_empty() {
1387 continue;
1388 }
1389
1390 let hb_direction = if levels[run.start].is_rtl() {
1391 rustybuzz::Direction::RightToLeft
1392 } else {
1393 rustybuzz::Direction::LeftToRight
1394 };
1395
1396 let mut buffer = rustybuzz::UnicodeBuffer::new();
1397 buffer.push_str(sub_text);
1398 buffer.set_direction(hb_direction);
1399
1400 let mut features = Vec::new();
1401 if small_caps {
1402 features.push(rustybuzz::Feature::new(
1403 rustybuzz::Tag::from_bytes(b"smcp"),
1404 1,
1405 ..,
1406 ));
1407 }
1408
1409 if !apply_kerning {
1410 features.push(rustybuzz::Feature::new(
1411 rustybuzz::Tag::from_bytes(b"kern"),
1412 0,
1413 ..,
1414 ));
1415 }
1416
1417 let output = rustybuzz::shape(&rb_font, &features, buffer);
1418
1419 let positions = output.glyph_positions();
1420 let infos = output.glyph_infos();
1421
1422 for (pos, info) in positions.iter().zip(infos) {
1423 let idx = run.start + info.cluster as usize;
1424 debug_assert!(text.get(idx..).is_some());
1425
1426 glyphs.push(Glyph {
1427 byte_idx: ByteIndex::new(idx),
1428 id: GlyphId(info.glyph_id as u16),
1429 dx: pos.x_offset,
1430 dy: pos.y_offset,
1431 width: pos.x_advance,
1432 font: font.clone(),
1433 });
1434 }
1435 }
1436
1437 Some(glyphs)
1438 })?
1439}
1440
1441fn outline_cluster(
1445 glyphs: &[Glyph],
1446 text: &str,
1447 font_size: f64,
1448 db: &fontdb::Database,
1449) -> OutlinedCluster {
1450 debug_assert!(!glyphs.is_empty());
1451
1452 let mut path = PathData::new();
1453 let mut width = 0.0;
1454 let mut x = 0.0;
1455
1456 for glyph in glyphs {
1457 let mut outline = db.outline(glyph.font.id, glyph.id).unwrap_or_default();
1458
1459 let sx = glyph.font.scale(font_size);
1460
1461 if !outline.is_empty() {
1462 let mut ts = Transform::new_scale(1.0, -1.0);
1464
1465 ts.scale(sx, sx);
1467
1468 ts.translate(x + glyph.dx as f64, glyph.dy as f64);
1475
1476 outline.transform(ts);
1477
1478 path.push_path(&outline);
1479 }
1480
1481 x += glyph.width as f64;
1482
1483 let glyph_width = glyph.width as f64 * sx;
1484 if glyph_width > width {
1485 width = glyph_width;
1486 }
1487 }
1488
1489 let byte_idx = glyphs[0].byte_idx;
1490 let font = glyphs[0].font.clone();
1491 OutlinedCluster {
1492 byte_idx,
1493 codepoint: byte_idx.char_from(text),
1494 width,
1495 advance: width,
1496 ascent: font.ascent(font_size),
1497 descent: font.descent(font_size),
1498 x_height: font.x_height(font_size),
1499 has_relative_shift: false,
1500 path,
1501 transform: Transform::default(),
1502 visible: true,
1503 }
1504}
1505
1506fn find_font_for_char(
1510 c: char,
1511 exclude_fonts: &[fontdb::ID],
1512 fontdb: &fontdb::Database,
1513) -> Option<ResolvedFont> {
1514 let base_font_id = exclude_fonts[0];
1515
1516 for face in fontdb.faces() {
1518 if exclude_fonts.contains(&face.id) {
1520 continue;
1521 }
1522
1523 let base_face = fontdb.face(base_font_id)?;
1525 if base_face.style != face.style
1526 && base_face.weight != face.weight
1527 && base_face.stretch != face.stretch
1528 {
1529 continue;
1530 }
1531
1532 if !fontdb.has_char(face.id, c) {
1533 continue;
1534 }
1535
1536 let base_family = base_face
1537 .families
1538 .iter()
1539 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1540 .unwrap_or(&base_face.families[0]);
1541
1542 let new_family = face
1543 .families
1544 .iter()
1545 .find(|f| f.1 == fontdb::Language::English_UnitedStates)
1546 .unwrap_or(&base_face.families[0]);
1547
1548 log::warn!("Fallback from {} to {}.", base_family.0, new_family.0);
1549 return fontdb.load_font(face.id);
1550 }
1551
1552 None
1553}
1554
1555fn resolve_clusters_positions(
1561 chunk: &TextChunk,
1562 char_offset: usize,
1563 pos_list: &[CharacterPosition],
1564 rotate_list: &[f64],
1565 writing_mode: WritingMode,
1566 ts: Transform,
1567 fonts_cache: &FontsCacheInner,
1568 clusters: &mut [OutlinedCluster],
1569) -> (f64, f64) {
1570 match chunk.text_flow {
1571 TextFlow::Linear => resolve_clusters_positions_horizontal(
1572 chunk,
1573 char_offset,
1574 pos_list,
1575 rotate_list,
1576 writing_mode,
1577 clusters,
1578 ),
1579 TextFlow::Path(ref path) => resolve_clusters_positions_path(
1580 chunk,
1581 char_offset,
1582 path,
1583 pos_list,
1584 rotate_list,
1585 writing_mode,
1586 ts,
1587 fonts_cache,
1588 clusters,
1589 ),
1590 }
1591}
1592
1593fn resolve_clusters_positions_horizontal(
1594 chunk: &TextChunk,
1595 offset: usize,
1596 pos_list: &[CharacterPosition],
1597 rotate_list: &[f64],
1598 writing_mode: WritingMode,
1599 clusters: &mut [OutlinedCluster],
1600) -> (f64, f64) {
1601 let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
1602 let mut y = 0.0;
1603
1604 for cluster in clusters {
1605 let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
1606 if let Some(pos) = pos_list.get(cp) {
1607 if writing_mode == WritingMode::LeftToRight {
1608 x += pos.dx.unwrap_or(0.0);
1609 y += pos.dy.unwrap_or(0.0);
1610 } else {
1611 y -= pos.dx.unwrap_or(0.0);
1612 x += pos.dy.unwrap_or(0.0);
1613 }
1614 cluster.has_relative_shift = pos.dx.is_some() || pos.dy.is_some();
1615 }
1616
1617 cluster.transform.translate(x, y);
1618
1619 if let Some(angle) = rotate_list.get(cp).cloned() {
1620 if !angle.is_fuzzy_zero() {
1621 cluster.transform.rotate(angle);
1622 cluster.has_relative_shift = true;
1623 }
1624 }
1625
1626 x += cluster.advance;
1627 }
1628
1629 (x, y)
1630}
1631
1632fn resolve_clusters_positions_path(
1633 chunk: &TextChunk,
1634 char_offset: usize,
1635 path: &TextPath,
1636 pos_list: &[CharacterPosition],
1637 rotate_list: &[f64],
1638 writing_mode: WritingMode,
1639 ts: Transform,
1640 fonts_cache: &FontsCacheInner,
1641 clusters: &mut [OutlinedCluster],
1642) -> (f64, f64) {
1643 let mut last_x = 0.0;
1644 let mut last_y = 0.0;
1645
1646 let mut dy = 0.0;
1647
1648 let chunk_offset = match writing_mode {
1651 WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
1652 WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
1653 };
1654
1655 let start_offset =
1656 chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
1657
1658 let normals = collect_normals(
1659 chunk,
1660 clusters,
1661 &path.path,
1662 pos_list,
1663 char_offset,
1664 start_offset,
1665 ts,
1666 );
1667 for (cluster, normal) in clusters.iter_mut().zip(normals) {
1668 let (x, y, angle) = match normal {
1669 Some(normal) => (normal.x, normal.y, normal.angle),
1670 None => {
1671 cluster.visible = false;
1673 continue;
1674 }
1675 };
1676
1677 cluster.has_relative_shift = true;
1679
1680 let orig_ts = cluster.transform;
1681
1682 let half_width = cluster.width / 2.0;
1684 cluster.transform = Transform::default();
1685 cluster.transform.translate(x - half_width, y);
1686 cluster.transform.rotate_at(angle, half_width, 0.0);
1687
1688 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1689 if let Some(pos) = pos_list.get(cp) {
1690 dy += pos.dy.unwrap_or(0.0);
1691 }
1692
1693 let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
1694 .map(|span| {
1695 let font = match fonts_cache.get(&span.font) {
1696 Some(v) => v,
1697 None => return 0.0,
1698 };
1699 -resolve_baseline(span, font, writing_mode)
1700 })
1701 .unwrap_or(0.0);
1702
1703 if !dy.is_fuzzy_zero() || !baseline_shift.is_fuzzy_zero() {
1706 let shift = kurbo::Vec2::new(0.0, dy - baseline_shift);
1707 cluster.transform.translate(shift.x, shift.y);
1708 }
1709
1710 if let Some(angle) = rotate_list.get(cp).cloned() {
1711 if !angle.is_fuzzy_zero() {
1712 cluster.transform.rotate(angle);
1713 }
1714 }
1715
1716 cluster.transform.append(&orig_ts);
1718
1719 last_x = x + cluster.advance;
1720 last_y = y;
1721 }
1722
1723 (last_x, last_y)
1724}
1725
1726fn clusters_length(clusters: &[OutlinedCluster]) -> f64 {
1727 clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
1728}
1729
1730fn process_anchor(a: TextAnchor, text_width: f64) -> f64 {
1731 match a {
1732 TextAnchor::Start => 0.0, TextAnchor::Middle => -text_width / 2.0,
1734 TextAnchor::End => -text_width,
1735 }
1736}
1737
1738struct PathNormal {
1739 x: f64,
1740 y: f64,
1741 angle: f64,
1742}
1743
1744fn collect_normals(
1745 chunk: &TextChunk,
1746 clusters: &[OutlinedCluster],
1747 path: &PathData,
1748 pos_list: &[CharacterPosition],
1749 char_offset: usize,
1750 offset: f64,
1751 ts: Transform,
1752) -> Vec<Option<PathNormal>> {
1753 debug_assert!(!path.is_empty());
1754
1755 let mut offsets = Vec::with_capacity(clusters.len());
1756 let mut normals = Vec::with_capacity(clusters.len());
1757 {
1758 let mut advance = offset;
1759 for cluster in clusters {
1760 let half_width = cluster.width / 2.0;
1762
1763 let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
1765 if let Some(pos) = pos_list.get(cp) {
1766 advance += pos.dx.unwrap_or(0.0);
1767 }
1768
1769 let offset = advance + half_width;
1770
1771 if offset < 0.0 {
1773 normals.push(None);
1774 }
1775
1776 offsets.push(offset);
1777 advance += cluster.advance;
1778 }
1779 }
1780
1781 let mut prev_mx = path.points()[0];
1782 let mut prev_my = path.points()[1];
1783 let mut prev_x = prev_mx;
1784 let mut prev_y = prev_my;
1785
1786 fn create_curve_from_line(px: f64, py: f64, x: f64, y: f64) -> kurbo::CubicBez {
1787 let line = kurbo::Line::new(kurbo::Point::new(px, py), kurbo::Point::new(x, y));
1788 let p1 = line.eval(0.33);
1789 let p2 = line.eval(0.66);
1790 cubic_from_points(px, py, p1.x, p1.y, p2.x, p2.y, x, y)
1791 }
1792
1793 let mut length = 0.0;
1794 for seg in path.segments() {
1795 let curve = match seg {
1796 PathSegment::MoveTo { x, y } => {
1797 prev_mx = x;
1798 prev_my = y;
1799 prev_x = x;
1800 prev_y = y;
1801 continue;
1802 }
1803 PathSegment::LineTo { x, y } => create_curve_from_line(prev_x, prev_y, x, y),
1804 PathSegment::CurveTo {
1805 x1,
1806 y1,
1807 x2,
1808 y2,
1809 x,
1810 y,
1811 } => cubic_from_points(prev_x, prev_y, x1, y1, x2, y2, x, y),
1812 PathSegment::ClosePath => create_curve_from_line(prev_x, prev_y, prev_mx, prev_my),
1813 };
1814
1815 let arclen_accuracy = {
1816 let base_arclen_accuracy = 0.5;
1817 let (sx, sy) = ts.get_scale();
1821 base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
1823 };
1824
1825 let curve_len = curve.arclen(arclen_accuracy);
1826
1827 for offset in &offsets[normals.len()..] {
1828 if *offset >= length && *offset <= length + curve_len {
1829 let mut offset = curve.inv_arclen(offset - length, arclen_accuracy);
1830 debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
1832 offset = offset.min(1.0).max(0.0);
1833
1834 let pos = curve.eval(offset);
1835 let d = curve.deriv().eval(offset);
1836 let d = kurbo::Vec2::new(-d.y, d.x); let angle = d.atan2().to_degrees() - 90.0;
1838
1839 normals.push(Some(PathNormal {
1840 x: pos.x,
1841 y: pos.y,
1842 angle,
1843 }));
1844
1845 if normals.len() == offsets.len() {
1846 break;
1847 }
1848 }
1849 }
1850
1851 length += curve_len;
1852 prev_x = curve.p3.x;
1853 prev_y = curve.p3.y;
1854 }
1855
1856 for _ in 0..(offsets.len() - normals.len()) {
1858 normals.push(None);
1859 }
1860
1861 normals
1862}
1863
1864fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1868 if !chunk
1870 .spans
1871 .iter()
1872 .any(|span| !span.letter_spacing.is_fuzzy_zero())
1873 {
1874 return;
1875 }
1876
1877 let num_clusters = clusters.len();
1878 for (i, cluster) in clusters.iter_mut().enumerate() {
1879 let script = cluster.codepoint.script();
1884 if script_supports_letter_spacing(script) {
1885 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1886 if i != num_clusters - 1 {
1889 cluster.advance += span.letter_spacing;
1890 }
1891
1892 if !cluster.advance.is_valid_length() {
1895 cluster.width = 0.0;
1896 cluster.advance = 0.0;
1897 cluster.path.clear();
1898 }
1899 }
1900 }
1901 }
1902}
1903
1904fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1910 use unicode_script::Script;
1911
1912 !matches!(
1913 script,
1914 Script::Arabic
1915 | Script::Syriac
1916 | Script::Nko
1917 | Script::Manichaean
1918 | Script::Psalter_Pahlavi
1919 | Script::Mandaic
1920 | Script::Mongolian
1921 | Script::Phags_Pa
1922 | Script::Devanagari
1923 | Script::Bengali
1924 | Script::Gurmukhi
1925 | Script::Modi
1926 | Script::Sharada
1927 | Script::Syloti_Nagri
1928 | Script::Tirhuta
1929 | Script::Ogham
1930 )
1931}
1932
1933fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1937 if !chunk
1939 .spans
1940 .iter()
1941 .any(|span| !span.word_spacing.is_fuzzy_zero())
1942 {
1943 return;
1944 }
1945
1946 for cluster in clusters {
1947 if is_word_separator_characters(cluster.codepoint) {
1948 if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1949 cluster.advance += span.word_spacing;
1953
1954 }
1956 }
1957 }
1958}
1959
1960fn is_word_separator_characters(c: char) -> bool {
1964 matches!(
1965 c as u32,
1966 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1967 )
1968}
1969
1970fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
1971 let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
1972
1973 for span in &chunk.spans {
1974 let target_width = if let Some(w) = span.text_length {
1975 w
1976 } else {
1977 continue;
1978 };
1979
1980 let mut width = 0.0;
1981 let mut cluster_indexes = Vec::new();
1982 for i in span.start..span.end {
1983 if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
1984 cluster_indexes.push(index);
1985 }
1986 }
1987 cluster_indexes.sort();
1989 cluster_indexes.dedup();
1990
1991 for i in &cluster_indexes {
1992 width += clusters[*i].width;
1995 }
1996
1997 if cluster_indexes.is_empty() {
1998 continue;
1999 }
2000
2001 if span.length_adjust == LengthAdjust::Spacing {
2002 let factor = if cluster_indexes.len() > 1 {
2003 (target_width - width) / (cluster_indexes.len() - 1) as f64
2004 } else {
2005 0 as f64
2006 };
2007
2008 for i in cluster_indexes {
2009 clusters[i].advance = clusters[i].width + factor;
2010 }
2011 } else {
2012 let factor = target_width / width;
2013 if factor < 0.001 {
2015 continue;
2016 }
2017
2018 for i in cluster_indexes {
2019 clusters[i].transform.scale(factor, 1.0);
2020
2021 if !is_horizontal {
2023 clusters[i].advance *= factor;
2024 clusters[i].width *= factor;
2025 }
2026 }
2027 }
2028 }
2029}
2030
2031fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [OutlinedCluster]) {
2034 if writing_mode != WritingMode::TopToBottom {
2035 return;
2036 }
2037
2038 for cluster in clusters {
2039 let orientation = unicode_vo::char_orientation(cluster.codepoint);
2040 if orientation == unicode_vo::Orientation::Upright {
2041 let dy = cluster.width - cluster.height();
2043
2044 let mut ts = Transform::default();
2046 ts.translate(cluster.width / 2.0, 0.0);
2047 ts.rotate(-90.0);
2048 ts.translate(-cluster.width / 2.0, -dy);
2049 cluster.path.transform(ts);
2050
2051 cluster.ascent = cluster.width / 2.0;
2053 cluster.descent = -cluster.width / 2.0;
2054 } else {
2055 cluster.transform.translate(0.0, cluster.x_height / 2.0);
2059 }
2060 }
2061}
2062
2063fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f64) -> f64 {
2064 let mut shift = 0.0;
2065 for baseline in baselines.iter().rev() {
2066 match baseline {
2067 BaselineShift::Baseline => {}
2068 BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
2069 BaselineShift::Superscript => shift += font.superscript_offset(font_size),
2070 BaselineShift::Number(n) => shift += n,
2071 }
2072 }
2073
2074 shift
2075}
2076
2077fn cubic_from_points(
2078 px: f64,
2079 py: f64,
2080 x1: f64,
2081 y1: f64,
2082 x2: f64,
2083 y2: f64,
2084 x: f64,
2085 y: f64,
2086) -> kurbo::CubicBez {
2087 kurbo::CubicBez {
2088 p0: kurbo::Point::new(px, py),
2089 p1: kurbo::Point::new(x1, y1),
2090 p2: kurbo::Point::new(x2, y2),
2091 p3: kurbo::Point::new(x, y),
2092 }
2093}