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 
5 use std::cmp;
6 use std::rc::Rc;
7 
8 use crate::{fontdb, svgtree, tree};
9 use crate::convert::{prelude::*, style, units};
10 use super::TextNode;
11 
12 
13 /// A read-only text index in bytes.
14 ///
15 /// Guarantee to be on a char boundary and in text bounds.
16 #[derive(Clone, Copy, PartialEq)]
17 pub struct ByteIndex(usize);
18 
19 impl ByteIndex {
new(i: usize) -> Self20     pub fn new(i: usize) -> Self {
21         ByteIndex(i)
22     }
23 
value(&self) -> usize24     pub fn value(&self) -> usize {
25         self.0
26     }
27 
28     /// Converts byte position into a code point position.
code_point_at(&self, text: &str) -> usize29     pub fn code_point_at(&self, text: &str) -> usize {
30         text.char_indices().take_while(|(i, _)| *i != self.0).count()
31     }
32 
33     /// Converts byte position into a character.
char_from(&self, text: &str) -> char34     pub fn char_from(&self, text: &str) -> char {
35         text[self.0..].chars().next().unwrap()
36     }
37 }
38 
39 
40 #[derive(Clone, Copy, PartialEq)]
41 pub enum TextAnchor {
42     Start,
43     Middle,
44     End,
45 }
46 
47 impl_enum_default!(TextAnchor, Start);
48 
49 impl_enum_from_str!(TextAnchor,
50     "start"     => TextAnchor::Start,
51     "middle"    => TextAnchor::Middle,
52     "end"       => TextAnchor::End
53 );
54 
55 
56 pub struct TextPath {
57     /// A text offset in SVG coordinates.
58     ///
59     /// Percentage values already resolved.
60     pub start_offset: f64,
61 
62     pub path: tree::SharedPathData,
63 }
64 
65 
66 #[derive(Clone)]
67 pub enum TextFlow {
68     Horizontal,
69     Path(Rc<TextPath>),
70 }
71 
72 
73 /// A text chunk.
74 ///
75 /// Text alignment and BIDI reordering can be done only inside a text chunk.
76 pub struct TextChunk {
77     pub x: Option<f64>,
78     pub y: Option<f64>,
79     pub anchor: TextAnchor,
80     pub spans: Vec<TextSpan>,
81     pub text_flow: TextFlow,
82     pub text: String,
83 }
84 
85 impl TextChunk {
span_at(&self, byte_offset: ByteIndex) -> Option<&TextSpan>86     pub fn span_at(&self, byte_offset: ByteIndex) -> Option<&TextSpan> {
87         for span in &self.spans {
88             if span.contains(byte_offset) {
89                 return Some(span);
90             }
91         }
92 
93         None
94     }
95 }
96 
97 
98 /// Spans do not overlap.
99 #[derive(Clone)]
100 pub struct TextSpan {
101     pub start: usize,
102     pub end: usize,
103     pub fill: Option<tree::Fill>,
104     pub stroke: Option<tree::Stroke>,
105     pub font: fontdb::Font,
106     pub font_size: f64,
107     pub decoration: TextDecoration,
108     pub baseline_shift: f64,
109     pub visibility: tree::Visibility,
110     pub letter_spacing: f64,
111     pub word_spacing: f64,
112 }
113 
114 impl TextSpan {
contains(&self, byte_offset: ByteIndex) -> bool115     pub fn contains(&self, byte_offset: ByteIndex) -> bool {
116         byte_offset.value() >= self.start && byte_offset.value() < self.end
117     }
118 }
119 
120 
121 #[derive(Clone, Copy, PartialEq)]
122 pub enum WritingMode {
123     LeftToRight,
124     TopToBottom,
125 }
126 
127 
128 struct IterState {
129     chars_count: usize,
130     chunk_bytes_count: usize,
131     split_chunk: bool,
132     text_flow: TextFlow,
133     chunks: Vec<TextChunk>,
134 }
135 
collect_text_chunks( text_node: TextNode, pos_list: &[CharacterPosition], state: &State, tree: &mut tree::Tree, ) -> Vec<TextChunk>136 pub fn collect_text_chunks(
137     text_node: TextNode,
138     pos_list: &[CharacterPosition],
139     state: &State,
140     tree: &mut tree::Tree,
141 ) -> Vec<TextChunk> {
142     let mut iter_state = IterState {
143         chars_count: 0,
144         chunk_bytes_count: 0,
145         split_chunk: false,
146         text_flow: TextFlow::Horizontal,
147         chunks: Vec::new(),
148     };
149 
150     collect_text_chunks_impl(text_node, *text_node, pos_list, state, tree, &mut iter_state);
151 
152     iter_state.chunks
153 }
154 
collect_text_chunks_impl( text_node: TextNode, parent: svgtree::Node, pos_list: &[CharacterPosition], state: &State, tree: &mut tree::Tree, iter_state: &mut IterState, )155 fn collect_text_chunks_impl(
156     text_node: TextNode,
157     parent: svgtree::Node,
158     pos_list: &[CharacterPosition],
159     state: &State,
160     tree: &mut tree::Tree,
161     iter_state: &mut IterState,
162 ) {
163     for child in parent.children() {
164         if child.is_element() {
165             if child.has_tag_name(EId::TextPath) {
166                 if !parent.has_tag_name(EId::Text) {
167                     // `textPath` can be set only as a direct `text` element child.
168                     iter_state.chars_count += count_chars(child);
169                     continue;
170                 }
171 
172                 match resolve_text_flow(child.clone(), state) {
173                     Some(v) => {
174                         iter_state.text_flow = v;
175                     }
176                     None => {
177                         // Skip an invalid text path and all it's children.
178                         // We have to update the chars count,
179                         // because `pos_list` was calculated including this text path.
180                         iter_state.chars_count += count_chars(child);
181                         continue;
182                     }
183                 }
184 
185                 iter_state.split_chunk = true;
186             }
187 
188             collect_text_chunks_impl(text_node, child, pos_list, state, tree, iter_state);
189 
190             iter_state.text_flow = TextFlow::Horizontal;
191 
192             // Next char after `textPath` should be split too.
193             if child.has_tag_name(EId::TextPath) {
194                 iter_state.split_chunk = true;
195             }
196 
197             continue;
198         }
199 
200         if !parent.is_visible_element(state.opt) {
201             iter_state.chars_count += child.text().chars().count();
202             continue;
203         }
204 
205         let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default();
206 
207         // TODO: what to do when <= 0? UB?
208         let font_size = units::resolve_font_size(parent, state);
209         if !font_size.is_valid_length() {
210             // Skip this span.
211             iter_state.chars_count += child.text().chars().count();
212             continue;
213         }
214 
215         let font = match resolve_font(parent, state) {
216             Some(v) => v,
217             None => {
218                 // Skip this span.
219                 iter_state.chars_count += child.text().chars().count();
220                 continue;
221             }
222         };
223 
224         let span = TextSpan {
225             start: 0,
226             end: 0,
227             fill: style::resolve_fill(parent, true, state, tree),
228             stroke: style::resolve_stroke(parent, true, state, tree),
229             font,
230             font_size,
231             decoration: resolve_decoration(text_node, parent, state, tree),
232             visibility: parent.find_attribute(AId::Visibility).unwrap_or_default(),
233             baseline_shift: resolve_baseline_shift(parent, state),
234             letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
235             word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
236         };
237 
238         let mut is_new_span = true;
239         for c in child.text().chars() {
240             let char_len = c.len_utf8();
241 
242             // Create a new chunk if:
243             // - this is the first span (yes, position can be None)
244             // - text character has an absolute coordinate assigned to it (via x/y attribute)
245             // - `c` is the first char of the `textPath`
246             // - `c` is the first char after `textPath`
247             let is_new_chunk =
248                    pos_list[iter_state.chars_count].x.is_some()
249                 || pos_list[iter_state.chars_count].y.is_some()
250                 || iter_state.split_chunk
251                 || iter_state.chunks.is_empty();
252 
253             iter_state.split_chunk = false;
254 
255             if is_new_chunk {
256                 iter_state.chunk_bytes_count = 0;
257 
258                 let mut span2 = span.clone();
259                 span2.start = 0;
260                 span2.end = char_len;
261 
262                 iter_state.chunks.push(TextChunk {
263                     x: pos_list[iter_state.chars_count].x,
264                     y: pos_list[iter_state.chars_count].y,
265                     anchor,
266                     spans: vec![span2],
267                     text_flow: iter_state.text_flow.clone(),
268                     text: c.to_string(),
269                 });
270             } else if is_new_span {
271                 // Add this span to the last text chunk.
272                 let mut span2 = span.clone();
273                 span2.start = iter_state.chunk_bytes_count;
274                 span2.end = iter_state.chunk_bytes_count + char_len;
275 
276                 if let Some(chunk) = iter_state.chunks.last_mut() {
277                     chunk.text.push(c);
278                     chunk.spans.push(span2);
279                 }
280             } else {
281                 // Extend the last span.
282                 if let Some(chunk) = iter_state.chunks.last_mut() {
283                     chunk.text.push(c);
284                     if let Some(span) = chunk.spans.last_mut() {
285                         debug_assert_ne!(span.end, 0);
286                         span.end += char_len;
287                     }
288                 }
289             }
290 
291             is_new_span = false;
292             iter_state.chars_count += 1;
293             iter_state.chunk_bytes_count += char_len;
294         }
295     }
296 }
297 
resolve_text_flow( node: svgtree::Node, state: &State, ) -> Option<TextFlow>298 fn resolve_text_flow(
299     node: svgtree::Node,
300     state: &State,
301 ) -> Option<TextFlow> {
302     let path_node = node.attribute::<svgtree::Node>(AId::Href)?;
303 
304     if !path_node.has_tag_name(EId::Path) {
305         return None;
306     }
307 
308     let path = path_node.attribute::<tree::SharedPathData>(AId::D)?;
309 
310     let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
311     let start_offset = if start_offset.unit == Unit::Percent {
312         // 'If a percentage is given, then the `startOffset` represents
313         // a percentage distance along the entire path.'
314         let path_len = path.length();
315         path_len * (start_offset.num / 100.0)
316     } else {
317         node.resolve_length(AId::StartOffset, state, 0.0)
318     };
319 
320     Some(TextFlow::Path(Rc::new(TextPath {
321         start_offset,
322         path,
323     })))
324 }
325 
resolve_rendering_mode( text_node: TextNode, state: &State, ) -> tree::ShapeRendering326 pub fn resolve_rendering_mode(
327     text_node: TextNode,
328     state: &State,
329 ) -> tree::ShapeRendering {
330     let mode: tree::TextRendering = text_node
331         .find_attribute(AId::TextRendering)
332         .unwrap_or(state.opt.text_rendering);
333 
334     match mode {
335         tree::TextRendering::OptimizeSpeed      => tree::ShapeRendering::CrispEdges,
336         tree::TextRendering::OptimizeLegibility => tree::ShapeRendering::GeometricPrecision,
337         tree::TextRendering::GeometricPrecision => tree::ShapeRendering::GeometricPrecision,
338     }
339 }
340 
resolve_font( node: svgtree::Node, state: &State, ) -> Option<fontdb::Font>341 fn resolve_font(
342     node: svgtree::Node,
343     state: &State,
344 ) -> Option<fontdb::Font> {
345     let style = node.find_attribute(AId::FontStyle).unwrap_or_default();
346     let stretch = conv_font_stretch(node);
347     let weight = resolve_font_weight(node);
348     let properties = fontdb::Properties { style, weight, stretch };
349 
350     let font_family = if let Some(n) = node.find_node_with_attribute(AId::FontFamily) {
351         n.attribute::<&str>(AId::FontFamily).unwrap_or(&state.opt.font_family).to_owned()
352     } else {
353         state.opt.font_family.to_owned()
354     };
355 
356     let mut name_list = Vec::new();
357     for family in font_family.split(',') {
358         // TODO: to a proper parser
359         let family = family.replace('\'', "");
360         let family = family.trim();
361         name_list.push(family.to_string());
362     }
363 
364     // Use the default font as fallback.
365     name_list.push(state.opt.font_family.clone());
366 
367     let name_list: Vec<_> = name_list.iter().map(|s| s.as_str()).collect();
368 
369     let mut db = state.db.borrow_mut();
370     let id = try_opt_warn_or!(
371         db.select_best_match(&name_list, properties), None,
372         "No match for '{}' font-family.", font_family
373     );
374 
375     db.load_font(id)
376 }
377 
conv_font_stretch(node: svgtree::Node) -> fontdb::Stretch378 fn conv_font_stretch(node: svgtree::Node) -> fontdb::Stretch {
379     if let Some(n) = node.find_node_with_attribute(AId::FontStretch) {
380         match n.attribute(AId::FontStretch).unwrap_or("") {
381             "narrower" | "condensed" => fontdb::Stretch::Condensed,
382             "ultra-condensed"        => fontdb::Stretch::UltraCondensed,
383             "extra-condensed"        => fontdb::Stretch::ExtraCondensed,
384             "semi-condensed"         => fontdb::Stretch::SemiCondensed,
385             "semi-expanded"          => fontdb::Stretch::SemiExpanded,
386             "wider" | "expanded"     => fontdb::Stretch::Expanded,
387             "extra-expanded"         => fontdb::Stretch::ExtraExpanded,
388             "ultra-expanded"         => fontdb::Stretch::UltraExpanded,
389             _                        => fontdb::Stretch::Normal,
390         }
391     } else {
392         fontdb::Stretch::Normal
393     }
394 }
395 
396 #[derive(Clone, Copy)]
397 pub struct CharacterPosition {
398     pub x: Option<f64>,
399     pub y: Option<f64>,
400     pub dx: Option<f64>,
401     pub dy: Option<f64>,
402 }
403 
404 /// Resolves text's character positions.
405 ///
406 /// This includes: x, y, dx, dy.
407 ///
408 /// # The character
409 ///
410 /// The first problem with this task is that the *character* itself
411 /// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
412 /// sometimes a *glyph*, and sometimes just a *character*.
413 ///
414 /// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
415 /// on the SVG working group that addresses this by stating that a character
416 /// is a Unicode code point. But it's not final.
417 ///
418 /// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
419 ///
420 /// Anyway, we treat a character as a Unicode code point.
421 ///
422 /// # Algorithm
423 ///
424 /// To resolve positions, we have to iterate over descendant nodes and
425 /// if the current node is a `tspan` and has x/y/dx/dy attribute,
426 /// than the positions from this attribute should be assigned to the characters
427 /// of this `tspan` and it's descendants.
428 ///
429 /// Positions list can have more values than characters in the `tspan`,
430 /// so we have to clamp it, because values should not overlap, e.g.:
431 ///
432 /// (we ignore whitespaces for example purposes,
433 /// so the `text` content is `Text` and not `T ex t`)
434 ///
435 /// ```text
436 /// <text>
437 ///   a
438 ///   <tspan x="10 20 30">
439 ///     bc
440 ///   </tspan>
441 ///   d
442 /// </text>
443 /// ```
444 ///
445 /// In this example, the `d` position should not be set to `30`.
446 /// And the result should be: `[None, 10, 20, None]`
447 ///
448 /// Another example:
449 ///
450 /// ```text
451 /// <text>
452 ///   <tspan x="100 110 120 130">
453 ///     a
454 ///     <tspan x="50">
455 ///       bc
456 ///     </tspan>
457 ///   </tspan>
458 ///   d
459 /// </text>
460 /// ```
461 ///
462 /// The result should be: `[100, 50, 120, None]`
resolve_positions_list( text_node: TextNode, state: &State, ) -> Vec<CharacterPosition>463 pub fn resolve_positions_list(
464     text_node: TextNode,
465     state: &State,
466 ) -> Vec<CharacterPosition> {
467     // Allocate a list that has all characters positions set to `None`.
468     let total_chars = count_chars(*text_node);
469     let mut list = vec![CharacterPosition {
470         x: None,
471         y: None,
472         dx: None,
473         dy: None,
474     }; total_chars];
475 
476     let mut offset = 0;
477     for child in text_node.descendants() {
478         if child.is_element() {
479             let child_chars = count_chars(child);
480             macro_rules! push_list {
481                 ($aid:expr, $field:ident) => {
482                     if let Some(num_list) = units::convert_list(child, $aid, state) {
483                         // Note that we are using not the total count,
484                         // but the amount of characters in the current `tspan` (with children).
485                         let len = cmp::min(num_list.len(), child_chars);
486                         for i in 0..len {
487                             list[offset + i].$field = Some(num_list[i]);
488                         }
489                     }
490                 };
491             }
492 
493             push_list!(AId::X, x);
494             push_list!(AId::Y, y);
495             push_list!(AId::Dx, dx);
496             push_list!(AId::Dy, dy);
497         } else if child.is_text() {
498             // Advance the offset.
499             offset += child.text().chars().count();
500         }
501     }
502 
503     list
504 }
505 
506 /// Resolves characters rotation.
507 ///
508 /// The algorithm is well explained
509 /// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
510 ///
511 /// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
512 ///
513 /// Note: this algorithm differs from the position resolving one.
resolve_rotate_list(text_node: TextNode) -> Vec<f64>514 pub fn resolve_rotate_list(text_node: TextNode) -> Vec<f64> {
515     // Allocate a list that has all characters angles set to `0.0`.
516     let mut list = vec![0.0; count_chars(*text_node)];
517     let mut last = 0.0;
518     let mut offset = 0;
519     for child in text_node.descendants() {
520         if child.is_element() {
521             if let Some(rotate) = child.attribute::<&svgtypes::NumberList>(AId::Rotate) {
522                 for i in 0..count_chars(child) {
523                     if let Some(a) = rotate.get(i).cloned() {
524                         list[offset + i] = a;
525                         last = a;
526                     } else {
527                         // If the rotate list doesn't specify the rotation for
528                         // this character - use the last one.
529                         list[offset + i] = last;
530                     }
531                 }
532             }
533         } else if child.is_text() {
534             // Advance the offset.
535             offset += child.text().chars().count();
536         }
537     }
538 
539     list
540 }
541 
542 #[derive(Clone)]
543 pub struct TextDecorationStyle {
544     pub fill: Option<tree::Fill>,
545     pub stroke: Option<tree::Stroke>,
546 }
547 
548 #[derive(Clone)]
549 pub struct TextDecoration {
550     pub underline: Option<TextDecorationStyle>,
551     pub overline: Option<TextDecorationStyle>,
552     pub line_through: Option<TextDecorationStyle>,
553 }
554 
555 /// Resolves node's `text-decoration` property.
556 ///
557 /// `text` and `tspan` can point to the same node.
resolve_decoration( text_node: TextNode, tspan: svgtree::Node, state: &State, tree: &mut tree::Tree, ) -> TextDecoration558 fn resolve_decoration(
559     text_node: TextNode,
560     tspan: svgtree::Node,
561     state: &State,
562     tree: &mut tree::Tree,
563 ) -> TextDecoration {
564     // TODO: explain the algorithm
565 
566     let text_dec = conv_text_decoration(text_node);
567     let tspan_dec = conv_text_decoration2(tspan);
568 
569     let mut gen_style = |in_tspan: bool, in_text: bool| {
570         let n = if in_tspan {
571             tspan.clone()
572         } else if in_text {
573             (*text_node).clone()
574         } else {
575             return None;
576         };
577 
578         Some(TextDecorationStyle {
579             fill: style::resolve_fill(n, true, state, tree),
580             stroke: style::resolve_stroke(n, true, state, tree),
581         })
582     };
583 
584     TextDecoration {
585         underline:    gen_style(tspan_dec.has_underline,    text_dec.has_underline),
586         overline:     gen_style(tspan_dec.has_overline,     text_dec.has_overline),
587         line_through: gen_style(tspan_dec.has_line_through, text_dec.has_line_through),
588     }
589 }
590 
591 struct TextDecorationTypes {
592     has_underline: bool,
593     has_overline: bool,
594     has_line_through: bool,
595 }
596 
597 /// Resolves the `text` node's `text-decoration` property.
conv_text_decoration(text_node: TextNode) -> TextDecorationTypes598 fn conv_text_decoration(text_node: TextNode) -> TextDecorationTypes {
599     fn find_decoration(node: svgtree::Node, value: &str) -> bool {
600         node.ancestors().any(|n| n.attribute(AId::TextDecoration) == Some(value))
601     }
602 
603     TextDecorationTypes {
604         has_underline: find_decoration(*text_node, "underline"),
605         has_overline: find_decoration(*text_node, "overline"),
606         has_line_through: find_decoration(*text_node, "line-through"),
607     }
608 }
609 
610 /// Resolves the default `text-decoration` property.
conv_text_decoration2(tspan: svgtree::Node) -> TextDecorationTypes611 fn conv_text_decoration2(tspan: svgtree::Node) -> TextDecorationTypes {
612     let s = tspan.attribute(AId::TextDecoration);
613     TextDecorationTypes {
614         has_underline:    s == Some("underline"),
615         has_overline:     s == Some("overline"),
616         has_line_through: s == Some("line-through"),
617     }
618 }
619 
resolve_baseline_shift( node: svgtree::Node, state: &State, ) -> f64620 fn resolve_baseline_shift(
621     node: svgtree::Node,
622     state: &State,
623 ) -> f64 {
624     let mut shift = 0.0;
625     let nodes: Vec<_> = node.ancestors().take_while(|n| !n.has_tag_name(EId::Text)).collect();
626     for n in nodes.iter().rev().cloned() {
627         if let Some(len) = n.attribute::<Length>(AId::BaselineShift) {
628             if len.unit == Unit::Percent {
629                 shift += units::resolve_font_size(n, state) * (len.num / 100.0);
630             } else {
631                 shift += units::convert_length(
632                     len, n, AId::BaselineShift, tree::Units::ObjectBoundingBox, state,
633                 );
634             }
635         } else if let Some(s) = n.attribute(AId::BaselineShift) {
636             match s {
637                 "baseline" => {}
638                 "sub" => {
639                     let font_size = units::resolve_font_size(n, state);
640                     if let Some(font) = resolve_font(n, state) {
641                         shift -= font.subscript_offset(font_size);
642                     }
643                 }
644                 "super" => {
645                     let font_size = units::resolve_font_size(n, state);
646                     if let Some(font) = resolve_font(n, state) {
647                         shift += font.superscript_offset(font_size);
648                     }
649                 }
650                 _ => {}
651             }
652         }
653     }
654 
655     shift
656 }
657 
resolve_font_weight(node: svgtree::Node) -> fontdb::Weight658 fn resolve_font_weight(node: svgtree::Node) -> fontdb::Weight {
659     fn bound(min: usize, val: usize, max: usize) -> usize {
660         cmp::max(min, cmp::min(max, val))
661     }
662 
663     let nodes: Vec<_> = node.ancestors().collect();
664     let mut weight = 400;
665     for n in nodes.iter().rev().skip(1) { // skip Root
666         weight = match n.attribute(AId::FontWeight).unwrap_or("") {
667             "normal" => 400,
668             "bold" => 700,
669             "100" => 100,
670             "200" => 200,
671             "300" => 300,
672             "400" => 400,
673             "500" => 500,
674             "600" => 600,
675             "700" => 700,
676             "800" => 800,
677             "900" => 900,
678             "bolder" => {
679                 // By the CSS2 spec the default value should be 400
680                 // so `bolder` will result in 500.
681                 // But Chrome and Inkscape will give us 700.
682                 // Have no idea is it a bug or something, but
683                 // we will follow such behavior for now.
684                 let step = if weight == 400 { 300 } else { 100 };
685 
686                 bound(100, weight + step, 900)
687             }
688             "lighter" => {
689                 // By the CSS2 spec the default value should be 400
690                 // so `lighter` will result in 300.
691                 // But Chrome and Inkscape will give us 200.
692                 // Have no idea is it a bug or something, but
693                 // we will follow such behavior for now.
694                 let step = if weight == 400 { 200 } else { 100 };
695 
696                 bound(100, weight - step, 900)
697             }
698             _ => weight,
699         };
700     }
701 
702     let weight = fontdb::Weight::from(weight as u16);
703     match weight {
704         fontdb::Weight::Other(_) => fontdb::Weight::Normal,
705         _ => weight,
706     }
707 }
708 
count_chars(node: svgtree::Node) -> usize709 fn count_chars(node: svgtree::Node) -> usize {
710     node.descendants()
711         .filter(|n| n.is_text())
712         .fold(0, |w, n| w + n.text().chars().count())
713 }
714 
715 /// Converts the writing mode.
716 ///
717 /// According to the [SVG 2.0] spec, there are only two writing modes:
718 /// horizontal left-to-right and vertical right-to-left.
719 /// E.g:
720 ///
721 /// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb`
722 /// - `tb`, `tb-rl` => `vertical-rl`
723 ///
724 /// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`.
725 /// And I'm not sure if it's behaviour is correct.
726 ///
727 /// So we will ignore it as well, mainly because I have no idea how exactly
728 /// it should affect the rendering.
729 ///
730 /// [SVG 2.0]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty
convert_writing_mode(text_node: TextNode) -> WritingMode731 pub fn convert_writing_mode(text_node: TextNode) -> WritingMode {
732     if let Some(n) = text_node.find_node_with_attribute(AId::WritingMode) {
733         match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
734             "tb" | "tb-rl" => WritingMode::TopToBottom,
735             _ => WritingMode::LeftToRight,
736         }
737     } else {
738         WritingMode::LeftToRight
739     }
740 }
741