rsvg/
text.rs

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