rsvg/
text.rs

1//! Text elements: `text`, `tspan`, `tref`.
2
3use markup5ever::{QualName, expanded_name, local_name, ns};
4use pango::IsAttribute;
5use pango::prelude::FontExt;
6use std::cell::RefCell;
7use std::convert::TryFrom;
8use std::rc::Rc;
9
10use crate::document::{AcquiredNodes, NodeId};
11use crate::drawing_ctx::{DrawingCtx, FontOptions, Viewport, create_pango_context};
12use crate::element::{DrawResult, ElementData, ElementTrait, set_attribute};
13use crate::error::*;
14use crate::layout::{self, FontProperties, Layer, LayerKind, StackingContext, Stroke, TextSpan};
15use crate::length::*;
16use crate::node::{CascadedValues, Node, NodeBorrow};
17use crate::paint_server::PaintSource;
18use crate::parsers::{CommaSeparatedList, Parse, ParseValue};
19use crate::properties::{
20    ComputedValues, Direction, DominantBaseline, FontStretch, FontStyle, FontVariant, FontWeight,
21    PaintOrder, TextAnchor, TextRendering, UnicodeBidi, WritingMode, XmlLang, XmlSpace,
22};
23use crate::rect::Rect;
24use crate::rsvg_log;
25use crate::session::Session;
26use crate::space::{NormalizeDefault, XmlSpaceNormalize, xml_space_normalize};
27use crate::xml::Attributes;
28
29/// The state of a text layout operation.
30struct LayoutContext {
31    /// `writing-mode` property from the `<text>` element.
32    writing_mode: WritingMode,
33
34    /// Font options from the DrawingCtx.
35    font_options: FontOptions,
36
37    /// For normalizing lengths.
38    viewport: Viewport,
39
40    /// Session metadata for the document
41    session: Session,
42}
43
44/// An absolutely-positioned array of `Span`s
45///
46/// SVG defines a "[text chunk]" to occur when a text-related element
47/// has an absolute position adjustment, that is, `x` or `y`
48/// attributes.
49///
50/// A `<text>` element always starts with an absolute position from
51/// such attributes, or (0, 0) if they are not specified.
52///
53/// Subsequent children of the `<text>` element will create new chunks
54/// whenever they have `x` or `y` attributes.
55///
56/// [text chunk]: https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction
57struct Chunk {
58    values: Rc<ComputedValues>,
59    x: Option<f64>,
60    y: Option<f64>,
61    spans: Vec<Span>,
62}
63
64struct MeasuredChunk {
65    values: Rc<ComputedValues>,
66    x: Option<f64>,
67    y: Option<f64>,
68    dx: f64,
69    dy: f64,
70    spans: Vec<MeasuredSpan>,
71}
72
73struct PositionedChunk {
74    next_chunk_x: f64,
75    next_chunk_y: f64,
76    spans: Vec<PositionedSpan>,
77}
78
79struct Span {
80    values: Rc<ComputedValues>,
81    text: String,
82    dx: f64,
83    dy: f64,
84    _depth: usize,
85    link_target: Option<String>,
86    span_element_name: Rc<String>,
87}
88
89struct MeasuredSpan {
90    values: Rc<ComputedValues>,
91    layout: pango::Layout,
92    layout_size: (f64, f64),
93    advance: (f64, f64),
94    dx: f64,
95    dy: f64,
96    link_target: Option<String>,
97    span_element_name: Rc<String>,
98}
99
100struct PositionedSpan {
101    layout: pango::Layout,
102    values: Rc<ComputedValues>,
103    rendered_position: (f64, f64),
104    next_span_position: (f64, f64),
105    link_target: Option<String>,
106    span_element_name: Rc<String>,
107}
108
109/// A laid-out and resolved text span.
110///
111/// The only thing not in user-space units are the `stroke_paint` and `fill_paint`.
112///
113/// This is the non-user-space version of `layout::TextSpan`.
114struct LayoutSpan {
115    layout: pango::Layout,
116    gravity: pango::Gravity,
117    extents: Option<Rect>,
118    is_visible: bool,
119    x: f64,
120    y: f64,
121    paint_order: PaintOrder,
122    stroke: Stroke,
123    stroke_paint: Rc<PaintSource>,
124    fill_paint: Rc<PaintSource>,
125    text_rendering: TextRendering,
126    link_target: Option<String>,
127    values: Rc<ComputedValues>,
128}
129
130impl Chunk {
131    fn new(values: &ComputedValues, x: Option<f64>, y: Option<f64>) -> Chunk {
132        Chunk {
133            values: Rc::new(values.clone()),
134            x,
135            y,
136            spans: Vec::new(),
137        }
138    }
139}
140
141impl MeasuredChunk {
142    fn from_chunk(layout_context: &LayoutContext, chunk: &Chunk) -> MeasuredChunk {
143        let mut measured_spans: Vec<MeasuredSpan> = chunk
144            .spans
145            .iter()
146            .filter_map(|span| MeasuredSpan::from_span(layout_context, span))
147            .collect();
148
149        // The first span contains the (dx, dy) that will be applied to the whole chunk.
150        // Make them 0 in the span, and extract the values to set them on the chunk.
151        // This is a hack until librsvg adds support for multiple dx/dy values per text/tspan.
152
153        let (chunk_dx, chunk_dy) = if let Some(first) = measured_spans.first_mut() {
154            let dx = first.dx;
155            let dy = first.dy;
156            first.dx = 0.0;
157            first.dy = 0.0;
158            (dx, dy)
159        } else {
160            (0.0, 0.0)
161        };
162
163        MeasuredChunk {
164            values: chunk.values.clone(),
165            x: chunk.x,
166            y: chunk.y,
167            dx: chunk_dx,
168            dy: chunk_dy,
169            spans: measured_spans,
170        }
171    }
172}
173
174impl PositionedChunk {
175    fn from_measured(
176        layout_context: &LayoutContext,
177        measured: &MeasuredChunk,
178        chunk_x: f64,
179        chunk_y: f64,
180    ) -> PositionedChunk {
181        let chunk_direction = measured.values.direction();
182
183        // Position the spans relatively to each other, starting at (0, 0)
184
185        let mut positioned = Vec::new();
186
187        // Start position of each span; gets advanced as each span is laid out.
188        // This is the text's start position, not the bounding box.
189        let mut x = 0.0;
190        let mut y = 0.0;
191
192        let mut chunk_bounds: Option<Rect> = None;
193
194        // Find the bounding box of the entire chunk by taking the union of the bounding boxes
195        // of each individual span.
196
197        for mspan in &measured.spans {
198            let params = NormalizeParams::new(&mspan.values, &layout_context.viewport);
199
200            let layout = mspan.layout.clone();
201            let layout_size = mspan.layout_size;
202            let values = mspan.values.clone();
203            let dx = mspan.dx;
204            let dy = mspan.dy;
205            let advance = mspan.advance;
206
207            let baseline_offset = compute_baseline_offset(&layout, &values, &params);
208
209            let start_pos = match chunk_direction {
210                Direction::Ltr => (x, y),
211                Direction::Rtl => (x - advance.0, y),
212            };
213
214            let span_advance = match chunk_direction {
215                Direction::Ltr => (advance.0, advance.1),
216                Direction::Rtl => (-advance.0, advance.1),
217            };
218
219            let rendered_position = if layout_context.writing_mode.is_horizontal() {
220                (start_pos.0 + dx, start_pos.1 - baseline_offset + dy)
221            } else {
222                (start_pos.0 + baseline_offset + dx, start_pos.1 + dy)
223            };
224
225            let span_bounds =
226                Rect::from_size(layout_size.0, layout_size.1).translate(rendered_position);
227
228            // We take the union here
229
230            if let Some(bounds) = chunk_bounds {
231                chunk_bounds = Some(bounds.union(&span_bounds));
232            } else {
233                chunk_bounds = Some(span_bounds);
234            }
235
236            x = x + span_advance.0 + dx;
237            y = y + span_advance.1 + dy;
238
239            let positioned_span = PositionedSpan {
240                layout,
241                values,
242                rendered_position,
243                next_span_position: (x, y),
244                link_target: mspan.link_target.clone(),
245                span_element_name: mspan.span_element_name.clone(),
246            };
247
248            positioned.push(positioned_span);
249        }
250
251        // Compute the offsets needed to align the chunk per the text-anchor property (start, middle, end):
252
253        let anchor_offset = text_anchor_offset(
254            measured.values.text_anchor(),
255            chunk_direction,
256            layout_context.writing_mode,
257            chunk_bounds.unwrap_or_default(),
258        );
259
260        // Apply the text-anchor offset to each individually-positioned span, and compute the
261        // start position of the next chunk.  Also add in the chunk's dx/dy.
262
263        let mut next_chunk_x = chunk_x;
264        let mut next_chunk_y = chunk_y;
265
266        for pspan in &mut positioned {
267            // Add the chunk's position, plus the text-anchor offset, plus the chunk's dx/dy.
268            // This last term is a hack until librsvg adds support for multiple dx/dy values per text/tspan;
269            // see the corresponding part in MeasuredChunk::from_chunk().
270            pspan.rendered_position.0 += chunk_x + anchor_offset.0 + measured.dx;
271            pspan.rendered_position.1 += chunk_y + anchor_offset.1 + measured.dy;
272
273            next_chunk_x = chunk_x + pspan.next_span_position.0 + anchor_offset.0 + measured.dx;
274            next_chunk_y = chunk_y + pspan.next_span_position.1 + anchor_offset.1 + measured.dy;
275        }
276
277        PositionedChunk {
278            next_chunk_x,
279            next_chunk_y,
280            spans: positioned,
281        }
282    }
283}
284
285fn compute_baseline_offset(
286    layout: &pango::Layout,
287    values: &ComputedValues,
288    params: &NormalizeParams,
289) -> f64 {
290    let mut baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE);
291    let dominant_baseline = values.dominant_baseline();
292
293    let mut layout_iter = layout.iter();
294    loop {
295        if let Some(layout_run) = layout_iter.run_readonly() {
296            let item = layout_run.item();
297            unsafe {
298                let analysis = (*item.as_ptr()).analysis;
299                if analysis.font.is_null() {
300                    break;
301                }
302            }
303            let font = item.analysis().font();
304
305            let metrics = font.metrics(None);
306            let ascent = metrics.ascent();
307            let descent = metrics.descent();
308            let height = metrics.height();
309
310            match dominant_baseline {
311                DominantBaseline::Hanging => {
312                    baseline -= f64::from(ascent - descent) / f64::from(pango::SCALE);
313                }
314                DominantBaseline::Middle => {
315                    // Approximate meanline using strikethrough position and thickness
316                    // https://mail.gnome.org/archives/gtk-i18n-list/2012-December/msg00046.html
317                    baseline -= f64::from(
318                        metrics.strikethrough_position() + metrics.strikethrough_thickness() / 2,
319                    ) / f64::from(pango::SCALE);
320                }
321                DominantBaseline::Central => {
322                    baseline = 0.5 * f64::from(ascent + descent) / f64::from(pango::SCALE);
323                }
324                DominantBaseline::TextBeforeEdge | DominantBaseline::TextTop => {
325                    //baseline -= f64::from(ascent) / f64::from(pango::SCALE);
326                    // Bit of a klutch, but leads to better results
327                    baseline -= f64::from(2 * ascent - height) / f64::from(pango::SCALE);
328                }
329                DominantBaseline::TextAfterEdge | DominantBaseline::TextBottom => {
330                    baseline += f64::from(descent) / f64::from(pango::SCALE);
331                }
332                DominantBaseline::Ideographic => {
333                    // Approx
334                    baseline += f64::from(descent) / f64::from(pango::SCALE);
335                }
336                DominantBaseline::Mathematical => {
337                    // Approx
338                    baseline = 0.5 * f64::from(ascent + descent) / f64::from(pango::SCALE);
339                }
340                _ => (),
341            }
342
343            break;
344        }
345
346        if !layout_iter.next_run() {
347            break;
348        }
349    }
350
351    let baseline_shift = values.baseline_shift().0.to_user(params);
352
353    baseline + baseline_shift
354}
355
356/// Computes the (x, y) offsets to be applied to spans after applying the text-anchor property (start, middle, end).
357#[rustfmt::skip]
358fn text_anchor_offset(
359    anchor: TextAnchor,
360    direction: Direction,
361    writing_mode: WritingMode,
362    chunk_bounds: Rect,
363) -> (f64, f64) {
364    let (w, h) = (chunk_bounds.width(), chunk_bounds.height());
365
366    let x0 = chunk_bounds.x0;
367
368    if writing_mode.is_horizontal() {
369        match (anchor, direction) {
370            (TextAnchor::Start,  Direction::Ltr) => (-x0, 0.0),
371            (TextAnchor::Start,  Direction::Rtl) => (-x0 - w, 0.0),
372
373            (TextAnchor::Middle, Direction::Ltr) => (-x0 - w / 2.0, 0.0),
374            (TextAnchor::Middle, Direction::Rtl) => (-x0 - w / 2.0, 0.0),
375
376            (TextAnchor::End,    Direction::Ltr) => (-x0 - w, 0.0),
377            (TextAnchor::End,    Direction::Rtl) => (-x0, 0.0),
378        }
379    } else {
380        // FIXME: we don't deal with text direction for vertical text yet.
381        match anchor {
382            TextAnchor::Start => (0.0, 0.0),
383            TextAnchor::Middle => (0.0, -h / 2.0),
384            TextAnchor::End => (0.0, -h),
385        }
386    }
387}
388
389impl Span {
390    fn new(
391        text: &str,
392        span_element_name: Rc<String>,
393        values: Rc<ComputedValues>,
394        dx: f64,
395        dy: f64,
396        depth: usize,
397        link_target: Option<String>,
398    ) -> Span {
399        Span {
400            values,
401            text: text.to_string(),
402            dx,
403            dy,
404            _depth: depth,
405            link_target,
406            span_element_name,
407        }
408    }
409}
410
411/// Use as `PangoUnits::from_pixels()` so that we can check for overflow.
412struct PangoUnits(i32);
413
414impl PangoUnits {
415    fn from_pixels(v: f64) -> Option<Self> {
416        // We want (v * f64::from(pango::SCALE) + 0.5) as i32
417        //
418        // But check for overflow.
419
420        cast::i32(v * f64::from(pango::SCALE) + 0.5)
421            .ok()
422            .map(PangoUnits)
423    }
424}
425
426impl MeasuredSpan {
427    fn from_span(layout_context: &LayoutContext, span: &Span) -> Option<MeasuredSpan> {
428        let values = span.values.clone();
429
430        let params = NormalizeParams::new(&values, &layout_context.viewport);
431
432        let properties = FontProperties::new(&values, &params);
433
434        let bidi_control = BidiControl::from_unicode_bidi_and_direction(
435            properties.unicode_bidi,
436            properties.direction,
437        );
438
439        let with_control_chars = wrap_with_direction_control_chars(&span.text, &bidi_control);
440
441        if let Some(layout) = create_pango_layout(layout_context, &properties, &with_control_chars)
442        {
443            let (w, h) = layout.size();
444
445            let w = f64::from(w) / f64::from(pango::SCALE);
446            let h = f64::from(h) / f64::from(pango::SCALE);
447
448            let advance = if layout_context.writing_mode.is_horizontal() {
449                (w, 0.0)
450            } else {
451                (0.0, w)
452            };
453
454            Some(MeasuredSpan {
455                values,
456                layout,
457                layout_size: (w, h),
458                advance,
459                dx: span.dx,
460                dy: span.dy,
461                link_target: span.link_target.clone(),
462                span_element_name: span.span_element_name.clone(),
463            })
464        } else {
465            None
466        }
467    }
468}
469
470// FIXME: should the pango crate provide this like PANGO_GRAVITY_IS_VERTICAL() ?
471fn gravity_is_vertical(gravity: pango::Gravity) -> bool {
472    matches!(gravity, pango::Gravity::East | pango::Gravity::West)
473}
474
475fn compute_text_box(
476    layout: &pango::Layout,
477    x: f64,
478    y: f64,
479    gravity: pango::Gravity,
480) -> Option<Rect> {
481    #![allow(clippy::many_single_char_names)]
482
483    let (ink, _) = layout.extents();
484    if ink.width() == 0 || ink.height() == 0 {
485        return None;
486    }
487
488    let ink_x = f64::from(ink.x());
489    let ink_y = f64::from(ink.y());
490    let ink_width = f64::from(ink.width());
491    let ink_height = f64::from(ink.height());
492    let pango_scale = f64::from(pango::SCALE);
493
494    let (x, y, w, h) = if gravity_is_vertical(gravity) {
495        (
496            x + (ink_x - ink_height) / pango_scale,
497            y + ink_y / pango_scale,
498            ink_height / pango_scale,
499            ink_width / pango_scale,
500        )
501    } else {
502        (
503            x + ink_x / pango_scale,
504            y + ink_y / pango_scale,
505            ink_width / pango_scale,
506            ink_height / pango_scale,
507        )
508    };
509
510    Some(Rect::new(x, y, x + w, y + h))
511}
512
513impl PositionedSpan {
514    fn layout(
515        &self,
516        layout_context: &LayoutContext,
517        acquired_nodes: &mut AcquiredNodes<'_>,
518    ) -> LayoutSpan {
519        let params = NormalizeParams::new(&self.values, &layout_context.viewport);
520
521        let layout = self.layout.clone();
522        let is_visible = self.values.is_visible();
523        let (x, y) = self.rendered_position;
524
525        let stroke = Stroke::new(&self.values, &params);
526
527        let gravity = layout.context().gravity();
528
529        let extents = compute_text_box(&layout, x, y, gravity);
530
531        let stroke_paint = self.values.stroke().0.resolve(
532            acquired_nodes,
533            &self.span_element_name,
534            self.values.stroke_opacity().0,
535            self.values.color().0,
536            None,
537            None,
538            &layout_context.session,
539        );
540
541        let fill_paint = self.values.fill().0.resolve(
542            acquired_nodes,
543            &self.span_element_name,
544            self.values.fill_opacity().0,
545            self.values.color().0,
546            None,
547            None,
548            &layout_context.session,
549        );
550
551        let paint_order = self.values.paint_order();
552        let text_rendering = self.values.text_rendering();
553
554        LayoutSpan {
555            layout,
556            gravity,
557            extents,
558            is_visible,
559            x,
560            y,
561            paint_order,
562            stroke,
563            stroke_paint,
564            fill_paint,
565            text_rendering,
566            values: self.values.clone(),
567            link_target: self.link_target.clone(),
568        }
569    }
570}
571
572/// Walks the children of a `<text>`, `<tspan>`, or `<tref>` element
573/// and appends chunks/spans from them into the specified `chunks`
574/// array.
575fn children_to_chunks(
576    chunks: &mut Vec<Chunk>,
577    node: &Node,
578    acquired_nodes: &mut AcquiredNodes<'_>,
579    cascaded: &CascadedValues<'_>,
580    layout_context: &LayoutContext,
581    dx: f64,
582    dy: f64,
583    depth: usize,
584    link: Option<String>,
585) {
586    let mut dx = dx;
587    let mut dy = dy;
588
589    let node_name = Rc::new(format!("{node}"));
590
591    for child in node.children() {
592        if child.is_chars() {
593            let values = cascaded.get();
594            child.borrow_chars().to_chunks(
595                &child,
596                node_name.clone(),
597                Rc::new(values.clone()),
598                chunks,
599                dx,
600                dy,
601                depth,
602                link.clone(),
603            );
604        } else {
605            assert!(child.is_element());
606
607            match *child.borrow_element_data() {
608                ElementData::TSpan(ref tspan) => {
609                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
610                    tspan.to_chunks(
611                        &child,
612                        acquired_nodes,
613                        &cascaded,
614                        layout_context,
615                        chunks,
616                        dx,
617                        dy,
618                        depth + 1,
619                        link.clone(),
620                    );
621                }
622
623                ElementData::Link(ref link) => {
624                    // TSpan::default sets all offsets to 0,
625                    // which is what we want in links.
626                    //
627                    // FIXME: This is the only place in the code where an element's method (TSpan::to_chunks)
628                    // is called with a node that is not the element itself: here, `child` is a Link, not a TSpan.
629                    //
630                    // The code works because the `tspan` is dropped immediately after calling to_chunks and no
631                    // references are retained for it.
632                    let tspan = TSpan::default();
633                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
634                    tspan.to_chunks(
635                        &child,
636                        acquired_nodes,
637                        &cascaded,
638                        layout_context,
639                        chunks,
640                        dx,
641                        dy,
642                        depth + 1,
643                        link.link.clone(),
644                    );
645                }
646
647                ElementData::TRef(ref tref) => {
648                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
649                    tref.to_chunks(
650                        &child,
651                        acquired_nodes,
652                        &cascaded,
653                        chunks,
654                        depth + 1,
655                        layout_context,
656                    );
657                }
658
659                _ => (),
660            }
661        }
662
663        // After the first span, we don't need to carry over the parent's dx/dy.
664        dx = 0.0;
665        dy = 0.0;
666    }
667}
668
669/// In SVG text elements, we use `Chars` to store character data.  For example,
670/// an element like `<text>Foo Bar</text>` will be a `Text` with a single child,
671/// and the child will be a `Chars` with "Foo Bar" for its contents.
672///
673/// Text elements can contain `<tspan>` sub-elements.  In this case,
674/// those `tspan` nodes will also contain `Chars` children.
675///
676/// A text or tspan element can contain more than one `Chars` child, for example,
677/// if there is an XML comment that splits the character contents in two:
678///
679/// ```xml
680/// <text>
681///   This sentence will create a Chars.
682///   <!-- this comment is ignored -->
683///   This sentence will cretea another Chars.
684/// </text>
685/// ```
686///
687/// When rendering a text element, it will take care of concatenating the strings
688/// in its `Chars` children as appropriate, depending on the
689/// `xml:space="preserve"` attribute.  A `Chars` stores the characters verbatim
690/// as they come out of the XML parser, after ensuring that they are valid UTF-8.
691
692#[derive(Default)]
693pub struct Chars {
694    string: RefCell<String>,
695    space_normalized: RefCell<Option<String>>,
696}
697
698impl Chars {
699    pub fn new(initial_text: &str) -> Chars {
700        Chars {
701            string: RefCell::new(String::from(initial_text)),
702            space_normalized: RefCell::new(None),
703        }
704    }
705
706    pub fn is_empty(&self) -> bool {
707        self.string.borrow().is_empty()
708    }
709
710    pub fn append(&self, s: &str) {
711        self.string.borrow_mut().push_str(s);
712        *self.space_normalized.borrow_mut() = None;
713    }
714
715    fn ensure_normalized_string(&self, node: &Node, values: &ComputedValues) {
716        let mut normalized = self.space_normalized.borrow_mut();
717
718        if (*normalized).is_none() {
719            let mode = match values.xml_space() {
720                XmlSpace::Default => XmlSpaceNormalize::Default(NormalizeDefault {
721                    has_element_before: node.previous_sibling().is_some(),
722                    has_element_after: node.next_sibling().is_some(),
723                }),
724
725                XmlSpace::Preserve => XmlSpaceNormalize::Preserve,
726            };
727
728            *normalized = Some(xml_space_normalize(mode, &self.string.borrow()));
729        }
730    }
731
732    fn make_span(
733        &self,
734        node: &Node,
735        span_element_name: Rc<String>,
736        values: Rc<ComputedValues>,
737        dx: f64,
738        dy: f64,
739        depth: usize,
740        link_target: Option<String>,
741    ) -> Span {
742        self.ensure_normalized_string(node, &values);
743
744        Span::new(
745            self.space_normalized.borrow().as_ref().unwrap(),
746            span_element_name,
747            values,
748            dx,
749            dy,
750            depth,
751            link_target,
752        )
753    }
754
755    fn to_chunks(
756        &self,
757        node: &Node,
758        span_element_name: Rc<String>,
759        values: Rc<ComputedValues>,
760        chunks: &mut [Chunk],
761        dx: f64,
762        dy: f64,
763        depth: usize,
764        link_target: Option<String>,
765    ) {
766        let span = self.make_span(node, span_element_name, values, dx, dy, depth, link_target);
767        let num_chunks = chunks.len();
768        assert!(num_chunks > 0);
769
770        chunks[num_chunks - 1].spans.push(span);
771    }
772
773    pub fn get_string(&self) -> String {
774        self.string.borrow().clone()
775    }
776}
777
778#[derive(Default)]
779pub struct Text {
780    x: Length<Horizontal>,
781    y: Length<Vertical>,
782    dx: Length<Horizontal>,
783    dy: Length<Vertical>,
784}
785
786impl Text {
787    fn make_chunks(
788        &self,
789        node: &Node,
790        acquired_nodes: &mut AcquiredNodes<'_>,
791        cascaded: &CascadedValues<'_>,
792        layout_context: &LayoutContext,
793        x: f64,
794        y: f64,
795    ) -> Vec<Chunk> {
796        let mut chunks = Vec::new();
797
798        let values = cascaded.get();
799        let params = NormalizeParams::new(values, &layout_context.viewport);
800
801        chunks.push(Chunk::new(values, Some(x), Some(y)));
802
803        let dx = self.dx.to_user(&params);
804        let dy = self.dy.to_user(&params);
805
806        children_to_chunks(
807            &mut chunks,
808            node,
809            acquired_nodes,
810            cascaded,
811            layout_context,
812            dx,
813            dy,
814            0,
815            None,
816        );
817        chunks
818    }
819
820    pub fn layout_text_spans(
821        &self,
822        node: &Node,
823        acquired_nodes: &mut AcquiredNodes<'_>,
824        cascaded: &CascadedValues<'_>,
825        viewport: &Viewport,
826        font_options: FontOptions,
827        session: &Session,
828    ) -> layout::Text {
829        let values = cascaded.get();
830        let params = NormalizeParams::new(values, viewport);
831
832        let layout_context = LayoutContext {
833            writing_mode: values.writing_mode(),
834            font_options,
835            viewport: *viewport,
836            session: session.clone(),
837        };
838
839        let mut x = self.x.to_user(&params);
840        let mut y = self.y.to_user(&params);
841
842        let chunks = self.make_chunks(node, acquired_nodes, cascaded, &layout_context, x, y);
843
844        let mut measured_chunks = Vec::new();
845        for chunk in &chunks {
846            measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk));
847        }
848
849        let mut positioned_chunks = Vec::new();
850        for chunk in &measured_chunks {
851            let chunk_x = chunk.x.unwrap_or(x);
852            let chunk_y = chunk.y.unwrap_or(y);
853
854            let positioned =
855                PositionedChunk::from_measured(&layout_context, chunk, chunk_x, chunk_y);
856
857            x = positioned.next_chunk_x;
858            y = positioned.next_chunk_y;
859
860            positioned_chunks.push(positioned);
861        }
862
863        let mut layout_spans = Vec::new();
864        for chunk in &positioned_chunks {
865            for span in &chunk.spans {
866                layout_spans.push(span.layout(&layout_context, acquired_nodes));
867            }
868        }
869
870        let text_extents: Option<Rect> = layout_spans
871            .iter()
872            .map(|span| span.extents)
873            .reduce(|a, b| match (a, b) {
874                (None, None) => None,
875                (None, Some(b)) => Some(b),
876                (Some(a), None) => Some(a),
877                (Some(a), Some(b)) => Some(a.union(&b)),
878            })
879            .flatten();
880
881        let mut text_spans = Vec::new();
882        for span in layout_spans {
883            let normalize_values = NormalizeValues::new(&span.values);
884
885            let stroke_paint = span.stroke_paint.to_user_space(
886                &text_extents,
887                &layout_context.viewport,
888                &normalize_values,
889            );
890            let fill_paint = span.fill_paint.to_user_space(
891                &text_extents,
892                &layout_context.viewport,
893                &normalize_values,
894            );
895
896            let text_span = TextSpan {
897                layout: span.layout,
898                gravity: span.gravity,
899                extents: span.extents,
900                is_visible: span.is_visible,
901                x: span.x,
902                y: span.y,
903                paint_order: span.paint_order,
904                stroke: span.stroke,
905                stroke_paint,
906                fill_paint,
907                text_rendering: span.text_rendering,
908                link_target: span.link_target,
909            };
910
911            text_spans.push(text_span);
912        }
913
914        layout::Text {
915            spans: text_spans,
916            extents: text_extents,
917        }
918    }
919}
920
921// Parse an (optionally) comma-separated list and just return the first element.
922//
923// From https://gitlab.gnome.org/GNOME/librsvg/-/issues/183, the current implementation
924// of text layout only supports a single value for the x/y/dx/dy attributes.  However,
925// we need to be able to parse values with multiple lengths.  So, we'll do that, but just
926// use the first value from each attribute.
927fn parse_list_and_extract_first<T: Copy + Default + Parse>(
928    dest: &mut T,
929    attr: QualName,
930    value: &str,
931    session: &Session,
932) {
933    let mut list: CommaSeparatedList<T, 0, 1024> = CommaSeparatedList(Vec::new());
934
935    set_attribute(&mut list, attr.parse(value), session);
936    if list.0.is_empty() {
937        *dest = Default::default();
938    } else {
939        *dest = list.0[0]; // ignore all but the first element
940    }
941}
942
943impl ElementTrait for Text {
944    fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
945        for (attr, value) in attrs.iter() {
946            match attr.expanded() {
947                expanded_name!("", "x") => {
948                    parse_list_and_extract_first(&mut self.x, attr, value, session)
949                }
950                expanded_name!("", "y") => {
951                    parse_list_and_extract_first(&mut self.y, attr, value, session)
952                }
953                expanded_name!("", "dx") => {
954                    parse_list_and_extract_first(&mut self.dx, attr, value, session)
955                }
956                expanded_name!("", "dy") => {
957                    parse_list_and_extract_first(&mut self.dy, attr, value, session)
958                }
959                _ => (),
960            }
961        }
962    }
963
964    fn layout(
965        &self,
966        node: &Node,
967        acquired_nodes: &mut AcquiredNodes<'_>,
968        cascaded: &CascadedValues<'_>,
969        viewport: &Viewport,
970        draw_ctx: &mut DrawingCtx,
971    ) -> Result<Option<Layer>, Box<InternalRenderingError>> {
972        let session = draw_ctx.session();
973
974        let values = cascaded.get();
975
976        let elt = node.borrow_element();
977
978        let stacking_ctx = StackingContext::new(
979            draw_ctx,
980            acquired_nodes,
981            &elt,
982            values.transform(),
983            None,
984            values,
985            viewport,
986        );
987
988        let font_options = draw_ctx.get_font_options();
989        let layout_text = self.layout_text_spans(
990            node,
991            acquired_nodes,
992            cascaded,
993            viewport,
994            font_options,
995            session,
996        );
997
998        Ok(Some(Layer {
999            kind: LayerKind::Text(Box::new(layout_text)),
1000            stacking_ctx,
1001        }))
1002    }
1003
1004    fn draw(
1005        &self,
1006        node: &Node,
1007        acquired_nodes: &mut AcquiredNodes<'_>,
1008        cascaded: &CascadedValues<'_>,
1009        viewport: &Viewport,
1010        draw_ctx: &mut DrawingCtx,
1011        clipping: bool,
1012    ) -> DrawResult {
1013        self.layout(node, acquired_nodes, cascaded, viewport, draw_ctx)
1014            .and_then(|layer| {
1015                draw_ctx.draw_layer(layer.as_ref().unwrap(), acquired_nodes, clipping, viewport)
1016            })
1017    }
1018}
1019
1020#[derive(Default)]
1021pub struct TRef {
1022    link: Option<NodeId>,
1023}
1024
1025impl TRef {
1026    fn to_chunks(
1027        &self,
1028        node: &Node,
1029        acquired_nodes: &mut AcquiredNodes<'_>,
1030        cascaded: &CascadedValues<'_>,
1031        chunks: &mut Vec<Chunk>,
1032        depth: usize,
1033        layout_context: &LayoutContext,
1034    ) {
1035        if self.link.is_none() {
1036            return;
1037        }
1038
1039        let link = self.link.as_ref().unwrap();
1040
1041        let values = cascaded.get();
1042        if !values.is_displayed() {
1043            return;
1044        }
1045
1046        let tref_element_name = format!("{node}");
1047
1048        if let Ok(acquired) = acquired_nodes.acquire(&tref_element_name, link) {
1049            let c = acquired.get();
1050            extract_chars_children_to_chunks_recursively(chunks, c, Rc::new(values.clone()), depth);
1051        } else {
1052            rsvg_log!(
1053                layout_context.session,
1054                "element {} references a nonexistent text source \"{}\"",
1055                node,
1056                link,
1057            );
1058        }
1059    }
1060}
1061
1062fn extract_chars_children_to_chunks_recursively(
1063    chunks: &mut Vec<Chunk>,
1064    node: &Node,
1065    values: Rc<ComputedValues>,
1066    depth: usize,
1067) {
1068    for child in node.children() {
1069        let values = values.clone();
1070
1071        if child.is_chars() {
1072            let span_element_name = Rc::new(format!("{node}"));
1073            child.borrow_chars().to_chunks(
1074                &child,
1075                span_element_name,
1076                values,
1077                chunks,
1078                0.0,
1079                0.0,
1080                depth,
1081                None,
1082            )
1083        } else {
1084            extract_chars_children_to_chunks_recursively(chunks, &child, values, depth + 1)
1085        }
1086    }
1087}
1088
1089impl ElementTrait for TRef {
1090    fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) {
1091        self.link = attrs
1092            .iter()
1093            .find(|(attr, _)| attr.expanded() == expanded_name!(xlink "href"))
1094            // Unlike other elements which use `href` in SVG2 versus `xlink:href` in SVG1.1,
1095            // the <tref> element got removed in SVG2.  So, here we still use a match
1096            // against the full namespaced version of the attribute.
1097            .and_then(|(attr, value)| NodeId::parse(value).attribute(attr).ok());
1098    }
1099}
1100
1101#[derive(Default)]
1102pub struct TSpan {
1103    x: Option<Length<Horizontal>>,
1104    y: Option<Length<Vertical>>,
1105    dx: Length<Horizontal>,
1106    dy: Length<Vertical>,
1107}
1108
1109impl TSpan {
1110    fn to_chunks(
1111        &self,
1112        node: &Node,
1113        acquired_nodes: &mut AcquiredNodes<'_>,
1114        cascaded: &CascadedValues<'_>,
1115        layout_context: &LayoutContext,
1116        chunks: &mut Vec<Chunk>,
1117        dx: f64,
1118        dy: f64,
1119        depth: usize,
1120        link: Option<String>,
1121    ) {
1122        let values = cascaded.get();
1123        if !values.is_displayed() {
1124            return;
1125        }
1126
1127        let params = NormalizeParams::new(values, &layout_context.viewport);
1128
1129        let x = self.x.map(|l| l.to_user(&params));
1130        let y = self.y.map(|l| l.to_user(&params));
1131
1132        let span_dx = dx + self.dx.to_user(&params);
1133        let span_dy = dy + self.dy.to_user(&params);
1134
1135        if x.is_some() || y.is_some() {
1136            chunks.push(Chunk::new(values, x, y));
1137        }
1138
1139        children_to_chunks(
1140            chunks,
1141            node,
1142            acquired_nodes,
1143            cascaded,
1144            layout_context,
1145            span_dx,
1146            span_dy,
1147            depth,
1148            link,
1149        );
1150    }
1151}
1152
1153impl ElementTrait for TSpan {
1154    fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
1155        for (attr, value) in attrs.iter() {
1156            match attr.expanded() {
1157                expanded_name!("", "x") => {
1158                    parse_list_and_extract_first(&mut self.x, attr, value, session)
1159                }
1160                expanded_name!("", "y") => {
1161                    parse_list_and_extract_first(&mut self.y, attr, value, session)
1162                }
1163                expanded_name!("", "dx") => {
1164                    parse_list_and_extract_first(&mut self.dx, attr, value, session)
1165                }
1166                expanded_name!("", "dy") => {
1167                    parse_list_and_extract_first(&mut self.dy, attr, value, session)
1168                }
1169                _ => (),
1170            }
1171        }
1172    }
1173}
1174
1175impl From<FontStyle> for pango::Style {
1176    fn from(s: FontStyle) -> pango::Style {
1177        match s {
1178            FontStyle::Normal => pango::Style::Normal,
1179            FontStyle::Italic => pango::Style::Italic,
1180            FontStyle::Oblique => pango::Style::Oblique,
1181        }
1182    }
1183}
1184
1185impl From<FontVariant> for pango::Variant {
1186    fn from(v: FontVariant) -> pango::Variant {
1187        match v {
1188            FontVariant::Normal => pango::Variant::Normal,
1189            FontVariant::SmallCaps => pango::Variant::SmallCaps,
1190        }
1191    }
1192}
1193
1194impl From<FontStretch> for pango::Stretch {
1195    fn from(s: FontStretch) -> pango::Stretch {
1196        match s {
1197            FontStretch::Normal => pango::Stretch::Normal,
1198            FontStretch::Wider => pango::Stretch::Expanded, // not quite correct
1199            FontStretch::Narrower => pango::Stretch::Condensed, // not quite correct
1200            FontStretch::UltraCondensed => pango::Stretch::UltraCondensed,
1201            FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed,
1202            FontStretch::Condensed => pango::Stretch::Condensed,
1203            FontStretch::SemiCondensed => pango::Stretch::SemiCondensed,
1204            FontStretch::SemiExpanded => pango::Stretch::SemiExpanded,
1205            FontStretch::Expanded => pango::Stretch::Expanded,
1206            FontStretch::ExtraExpanded => pango::Stretch::ExtraExpanded,
1207            FontStretch::UltraExpanded => pango::Stretch::UltraExpanded,
1208        }
1209    }
1210}
1211
1212impl From<FontWeight> for pango::Weight {
1213    fn from(w: FontWeight) -> pango::Weight {
1214        pango::Weight::__Unknown(w.numeric_weight().into())
1215    }
1216}
1217
1218impl From<Direction> for pango::Direction {
1219    fn from(d: Direction) -> pango::Direction {
1220        match d {
1221            Direction::Ltr => pango::Direction::Ltr,
1222            Direction::Rtl => pango::Direction::Rtl,
1223        }
1224    }
1225}
1226
1227impl From<WritingMode> for pango::Direction {
1228    fn from(m: WritingMode) -> pango::Direction {
1229        use WritingMode::*;
1230        match m {
1231            HorizontalTb | VerticalRl | VerticalLr | LrTb | Lr | Tb | TbRl => pango::Direction::Ltr,
1232            RlTb | Rl => pango::Direction::Rtl,
1233        }
1234    }
1235}
1236
1237impl From<WritingMode> for pango::Gravity {
1238    fn from(m: WritingMode) -> pango::Gravity {
1239        use WritingMode::*;
1240        match m {
1241            HorizontalTb | LrTb | Lr | RlTb | Rl => pango::Gravity::South,
1242            VerticalRl | Tb | TbRl => pango::Gravity::East,
1243            VerticalLr => pango::Gravity::West,
1244        }
1245    }
1246}
1247
1248/// Constants with Unicode's directional formatting characters
1249///
1250/// <https://unicode.org/reports/tr9/#Directional_Formatting_Characters>
1251pub mod directional_formatting_characters {
1252    /// Left-to-Right Embedding
1253    ///
1254    /// Treat the following text as embedded left-to-right.
1255    pub const LRE: char = '\u{202a}';
1256
1257    /// Right-to-Left Embedding
1258    ///
1259    /// Treat the following text as embedded right-to-left.
1260    pub const RLE: char = '\u{202b}';
1261
1262    /// Left-to-Right Override
1263    ///
1264    /// Force following characters to be treated as strong left-to-right characters.
1265    pub const LRO: char = '\u{202d}';
1266
1267    /// Right-to-Left Override
1268    ///
1269    /// Force following characters to be treated as strong right-to-left characters.
1270    pub const RLO: char = '\u{202e}';
1271
1272    /// Pop Directional Formatting
1273    ///
1274    /// End the scope of the last LRE, RLE, RLO, or LRO.
1275    pub const PDF: char = '\u{202c}';
1276
1277    /// Left-to-Right Isolate
1278    ///
1279    /// Treat the following text as isolated and left-to-right.
1280    pub const LRI: char = '\u{2066}';
1281
1282    /// Right-to-Left Isolate
1283    ///
1284    /// Treat the following text as isolated and right-to-left.
1285    pub const RLI: char = '\u{2067}';
1286
1287    /// First Strong Isolate
1288    ///
1289    /// Treat the following text as isolated and in the direction of its first strong
1290    /// directional character that is not inside a nested isolate.
1291    pub const FSI: char = '\u{2068}';
1292
1293    /// Pop Directional Isolate
1294    ///
1295    /// End the scope of the last LRI, RLI, or FSI.
1296    pub const PDI: char = '\u{2069}';
1297}
1298
1299/// Unicode control characters to be inserted when `unicode-bidi` is specified.
1300///
1301/// The `unicode-bidi` property is used to change the embedding of a text span within
1302/// another.  This struct contains slices with the control characters that must be
1303/// inserted into the text stream at the span's limits so that the bidi/shaping engine
1304/// will know what to do.
1305pub struct BidiControl {
1306    pub start: &'static [char],
1307    pub end: &'static [char],
1308}
1309
1310impl BidiControl {
1311    /// Creates a `BidiControl` from the properties that determine it.
1312    ///
1313    /// See the table titled "Bidi control codes injected..." in
1314    /// <https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi>
1315    #[rustfmt::skip]
1316    pub fn from_unicode_bidi_and_direction(unicode_bidi: UnicodeBidi, direction: Direction) -> BidiControl {
1317        use UnicodeBidi::*;
1318        use Direction::*;
1319        use directional_formatting_characters::*;
1320
1321        let (start, end) = match (unicode_bidi, direction) {
1322            (Normal,          _)   => (&[][..],         &[][..]),
1323            (Embed,           Ltr) => (&[LRE][..],      &[PDF][..]),
1324            (Embed,           Rtl) => (&[RLE][..],      &[PDF][..]),
1325            (Isolate,         Ltr) => (&[LRI][..],      &[PDI][..]),
1326            (Isolate,         Rtl) => (&[RLI][..],      &[PDI][..]),
1327            (BidiOverride,    Ltr) => (&[LRO][..],      &[PDF][..]),
1328            (BidiOverride,    Rtl) => (&[RLO][..],      &[PDF][..]),
1329            (IsolateOverride, Ltr) => (&[FSI, LRO][..], &[PDF, PDI][..]),
1330            (IsolateOverride, Rtl) => (&[FSI, RLO][..], &[PDF, PDI][..]),
1331            (Plaintext,       Ltr) => (&[FSI][..],      &[PDI][..]),
1332            (Plaintext,       Rtl) => (&[FSI][..],      &[PDI][..]),
1333        };
1334
1335        BidiControl { start, end }
1336    }
1337}
1338
1339/// Prepends and appends Unicode directional formatting characters.
1340fn wrap_with_direction_control_chars(s: &str, bidi_control: &BidiControl) -> String {
1341    let mut res =
1342        String::with_capacity(s.len() + bidi_control.start.len() + bidi_control.end.len());
1343
1344    for &ch in bidi_control.start {
1345        res.push(ch);
1346    }
1347
1348    res.push_str(s);
1349
1350    for &ch in bidi_control.end {
1351        res.push(ch);
1352    }
1353
1354    res
1355}
1356
1357/// Returns `None` if the layout would be invalid due to, for example, out-of-bounds font sizes.
1358fn create_pango_layout(
1359    layout_context: &LayoutContext,
1360    props: &FontProperties,
1361    text: &str,
1362) -> Option<pango::Layout> {
1363    let pango_context = create_pango_context(&layout_context.font_options);
1364
1365    if let XmlLang(Some(ref lang)) = props.xml_lang {
1366        pango_context.set_language(Some(&pango::Language::from_string(lang.as_str())));
1367    }
1368
1369    pango_context.set_base_gravity(pango::Gravity::from(layout_context.writing_mode));
1370
1371    match (props.unicode_bidi, props.direction) {
1372        (UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => {
1373            pango_context.set_base_dir(pango::Direction::from(props.direction));
1374        }
1375
1376        (_, direction) if direction != Direction::Ltr => {
1377            pango_context.set_base_dir(pango::Direction::from(direction));
1378        }
1379
1380        (_, _) => {
1381            pango_context.set_base_dir(pango::Direction::from(layout_context.writing_mode));
1382        }
1383    }
1384
1385    let layout = pango::Layout::new(&pango_context);
1386
1387    let font_size = PangoUnits::from_pixels(props.font_size);
1388    let letter_spacing = PangoUnits::from_pixels(props.letter_spacing);
1389
1390    if font_size.is_none() {
1391        rsvg_log!(
1392            &layout_context.session,
1393            "font-size {} is out of bounds; ignoring span",
1394            props.font_size
1395        );
1396    }
1397
1398    if letter_spacing.is_none() {
1399        rsvg_log!(
1400            &layout_context.session,
1401            "letter-spacing {} is out of bounds; ignoring span",
1402            props.letter_spacing
1403        );
1404    }
1405
1406    if let (Some(font_size), Some(letter_spacing)) = (font_size, letter_spacing) {
1407        let attr_list = pango::AttrList::new();
1408        add_pango_attributes(&attr_list, props, 0, text.len(), font_size, letter_spacing);
1409
1410        layout.set_attributes(Some(&attr_list));
1411        layout.set_text(text);
1412        layout.set_auto_dir(false);
1413
1414        Some(layout)
1415    } else {
1416        None
1417    }
1418}
1419
1420/// Adds Pango attributes, suitable for a span of text, to an `AttrList`.
1421fn add_pango_attributes(
1422    attr_list: &pango::AttrList,
1423    props: &FontProperties,
1424    start_index: usize,
1425    end_index: usize,
1426    font_size: PangoUnits,
1427    letter_spacing: PangoUnits,
1428) {
1429    let start_index = u32::try_from(start_index).expect("Pango attribute index must fit in u32");
1430    let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32");
1431    assert!(start_index <= end_index);
1432
1433    let mut attributes = Vec::new();
1434
1435    let mut font_desc = pango::FontDescription::new();
1436    font_desc.set_family(props.font_family.as_str());
1437    font_desc.set_style(pango::Style::from(props.font_style));
1438
1439    font_desc.set_variant(pango::Variant::from(props.font_variant));
1440
1441    font_desc.set_weight(pango::Weight::from(props.font_weight));
1442    font_desc.set_stretch(pango::Stretch::from(props.font_stretch));
1443
1444    font_desc.set_size(font_size.0);
1445
1446    attributes.push(pango::AttrFontDesc::new(&font_desc).upcast());
1447
1448    attributes.push(pango::AttrInt::new_letter_spacing(letter_spacing.0).upcast());
1449
1450    if props.text_decoration.overline {
1451        attributes.push(pango::AttrInt::new_overline(pango::Overline::Single).upcast());
1452    }
1453
1454    if props.text_decoration.underline {
1455        attributes.push(pango::AttrInt::new_underline(pango::Underline::Single).upcast());
1456    }
1457
1458    if props.text_decoration.strike {
1459        attributes.push(pango::AttrInt::new_strikethrough(true).upcast());
1460    }
1461
1462    // Set the range in each attribute
1463
1464    for attr in &mut attributes {
1465        attr.set_start_index(start_index);
1466        attr.set_end_index(end_index);
1467    }
1468
1469    // Add the attributes to the attr_list
1470
1471    for attr in attributes {
1472        attr_list.insert(attr);
1473    }
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478    use super::*;
1479
1480    #[test]
1481    fn chars_default() {
1482        let c = Chars::default();
1483        assert!(c.is_empty());
1484        assert!(c.space_normalized.borrow().is_none());
1485    }
1486
1487    #[test]
1488    fn chars_new() {
1489        let example = "Test 123";
1490        let c = Chars::new(example);
1491        assert_eq!(c.get_string(), example);
1492        assert!(c.space_normalized.borrow().is_none());
1493    }
1494
1495    // This is called _horizontal because the property value in "CSS Writing Modes 3"
1496    // is `horizontal-tb`.  Eventually we will support that and this will make more sense.
1497    #[test]
1498    fn adjusted_advance_horizontal_ltr() {
1499        use Direction::*;
1500        use TextAnchor::*;
1501
1502        assert_eq!(
1503            text_anchor_offset(
1504                Start,
1505                Ltr,
1506                WritingMode::Lr,
1507                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1508            ),
1509            (-5.0, 0.0)
1510        );
1511
1512        assert_eq!(
1513            text_anchor_offset(
1514                Middle,
1515                Ltr,
1516                WritingMode::Lr,
1517                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1518            ),
1519            (-5.5, 0.0)
1520        );
1521
1522        assert_eq!(
1523            text_anchor_offset(
1524                End,
1525                Ltr,
1526                WritingMode::Lr,
1527                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1528            ),
1529            (-6.0, 0.0)
1530        );
1531    }
1532
1533    #[test]
1534    fn adjusted_advance_horizontal_rtl() {
1535        use Direction::*;
1536        use TextAnchor::*;
1537
1538        assert_eq!(
1539            text_anchor_offset(
1540                Start,
1541                Rtl,
1542                WritingMode::Rl,
1543                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1544            ),
1545            (-6.0, 0.0)
1546        );
1547        assert_eq!(
1548            text_anchor_offset(
1549                Middle,
1550                Rtl,
1551                WritingMode::Rl,
1552                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1553            ),
1554            (-5.5, 0.0)
1555        );
1556        assert_eq!(
1557            text_anchor_offset(
1558                TextAnchor::End,
1559                Direction::Rtl,
1560                WritingMode::Rl,
1561                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
1562            ),
1563            (-5.0, 0.0)
1564        );
1565    }
1566
1567    // This is called _vertical because "CSS Writing Modes 3" has both `vertical-rl` (East
1568    // Asia), and `vertical-lr` (Manchu, Mongolian), but librsvg does not support block
1569    // flow direction properly yet.  Eventually we will support that and this will make
1570    // more sense.
1571    #[test]
1572    fn adjusted_advance_vertical() {
1573        use Direction::*;
1574        use TextAnchor::*;
1575
1576        assert_eq!(
1577            text_anchor_offset(Start, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
1578            (0.0, 0.0)
1579        );
1580
1581        assert_eq!(
1582            text_anchor_offset(Middle, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
1583            (0.0, -2.0)
1584        );
1585
1586        assert_eq!(
1587            text_anchor_offset(End, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
1588            (0.0, -4.0)
1589        );
1590    }
1591
1592    #[test]
1593    fn pango_units_works() {
1594        assert_eq!(PangoUnits::from_pixels(10.0).unwrap().0, pango::SCALE * 10);
1595    }
1596
1597    #[test]
1598    fn pango_units_detects_overflow() {
1599        assert!(PangoUnits::from_pixels(1e7).is_none());
1600    }
1601}