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