1
//! Text elements: `text`, `tspan`, `tref`.
2

            
3
use markup5ever::{expanded_name, local_name, namespace_url, ns};
4
use pango::IsAttribute;
5
use std::cell::RefCell;
6
use std::convert::TryFrom;
7
use std::rc::Rc;
8

            
9
use crate::bbox::BoundingBox;
10
use crate::document::{AcquiredNodes, NodeId};
11
use crate::drawing_ctx::{create_pango_context, DrawingCtx, FontOptions, Viewport};
12
use crate::element::{set_attribute, ElementData, ElementTrait};
13
use crate::error::*;
14
use crate::layout::{self, FontProperties, Layer, LayerKind, StackingContext, Stroke, TextSpan};
15
use crate::length::*;
16
use crate::node::{CascadedValues, Node, NodeBorrow};
17
use crate::paint_server::PaintSource;
18
use crate::parsers::ParseValue;
19
use crate::properties::{
20
    ComputedValues, Direction, FontStretch, FontStyle, FontVariant, FontWeight, PaintOrder,
21
    TextAnchor, TextRendering, UnicodeBidi, WritingMode, XmlLang, XmlSpace,
22
};
23
use crate::rect::Rect;
24
use crate::rsvg_log;
25
use crate::session::Session;
26
use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize};
27
use crate::transform::{Transform, ValidTransform};
28
use crate::xml::Attributes;
29

            
30
/// The state of a text layout operation.
31
struct LayoutContext {
32
    /// `writing-mode` property from the `<text>` element.
33
    writing_mode: WritingMode,
34

            
35
    /// Current transform in the DrawingCtx.
36
    transform: ValidTransform,
37

            
38
    /// Font options from the DrawingCtx.
39
    font_options: FontOptions,
40

            
41
    /// For normalizing lengths.
42
    viewport: Viewport,
43

            
44
    /// Session metadata for the document
45
    session: Session,
46
}
47

            
48
/// An absolutely-positioned array of `Span`s
49
///
50
/// SVG defines a "[text chunk]" to occur when a text-related element
51
/// has an absolute position adjustment, that is, `x` or `y`
52
/// attributes.
53
///
54
/// A `<text>` element always starts with an absolute position from
55
/// such attributes, or (0, 0) if they are not specified.
56
///
57
/// Subsequent children of the `<text>` element will create new chunks
58
/// whenever they have `x` or `y` attributes.
59
///
60
/// [text chunk]: https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction
61
struct Chunk {
62
    values: Rc<ComputedValues>,
63
    x: Option<f64>,
64
    y: Option<f64>,
65
    spans: Vec<Span>,
66
}
67

            
68
struct MeasuredChunk {
69
    values: Rc<ComputedValues>,
70
    x: Option<f64>,
71
    y: Option<f64>,
72
    dx: f64,
73
    dy: f64,
74
    spans: Vec<MeasuredSpan>,
75
}
76

            
77
struct PositionedChunk {
78
    next_chunk_x: f64,
79
    next_chunk_y: f64,
80
    spans: Vec<PositionedSpan>,
81
}
82

            
83
struct Span {
84
    values: Rc<ComputedValues>,
85
    text: String,
86
    dx: f64,
87
    dy: f64,
88
    _depth: usize,
89
    link_target: Option<String>,
90
}
91

            
92
struct MeasuredSpan {
93
    values: Rc<ComputedValues>,
94
    layout: pango::Layout,
95
    layout_size: (f64, f64),
96
    advance: (f64, f64),
97
    dx: f64,
98
    dy: f64,
99
    link_target: Option<String>,
100
}
101

            
102
struct PositionedSpan {
103
    layout: pango::Layout,
104
    values: Rc<ComputedValues>,
105
    rendered_position: (f64, f64),
106
    next_span_position: (f64, f64),
107
    link_target: Option<String>,
108
}
109

            
110
/// A laid-out and resolved text span.
111
///
112
/// The only thing not in user-space units are the `stroke_paint` and `fill_paint`.
113
///
114
/// This is the non-user-space version of `layout::TextSpan`.
115
struct LayoutSpan {
116
    layout: pango::Layout,
117
    gravity: pango::Gravity,
118
    bbox: Option<BoundingBox>,
119
    is_visible: bool,
120
    x: f64,
121
    y: f64,
122
    paint_order: PaintOrder,
123
    stroke: Stroke,
124
    stroke_paint: Rc<PaintSource>,
125
    fill_paint: Rc<PaintSource>,
126
    text_rendering: TextRendering,
127
    link_target: Option<String>,
128
    values: Rc<ComputedValues>,
129
}
130

            
131
impl Chunk {
132
961
    fn new(values: &ComputedValues, x: Option<f64>, y: Option<f64>) -> Chunk {
133
961
        Chunk {
134
961
            values: Rc::new(values.clone()),
135
            x,
136
            y,
137
961
            spans: Vec::new(),
138
        }
139
961
    }
140
}
141

            
142
impl MeasuredChunk {
143
961
    fn from_chunk(layout_context: &LayoutContext, chunk: &Chunk) -> MeasuredChunk {
144
1922
        let mut measured_spans: Vec<MeasuredSpan> = chunk
145
            .spans
146
            .iter()
147
2000
            .filter_map(|span| MeasuredSpan::from_span(layout_context, span))
148
            .collect();
149

            
150
        // The first span contains the (dx, dy) that will be applied to the whole chunk.
151
        // Make them 0 in the span, and extract the values to set them on the chunk.
152
        // This is a hack until librsvg adds support for multiple dx/dy values per text/tspan.
153

            
154
961
        let (chunk_dx, chunk_dy) = if let Some(first) = measured_spans.first_mut() {
155
943
            let dx = first.dx;
156
943
            let dy = first.dy;
157
943
            first.dx = 0.0;
158
943
            first.dy = 0.0;
159
943
            (dx, dy)
160
        } else {
161
18
            (0.0, 0.0)
162
        };
163

            
164
961
        MeasuredChunk {
165
961
            values: chunk.values.clone(),
166
961
            x: chunk.x,
167
961
            y: chunk.y,
168
            dx: chunk_dx,
169
            dy: chunk_dy,
170
961
            spans: measured_spans,
171
        }
172
961
    }
173
}
174

            
175
impl PositionedChunk {
176
960
    fn from_measured(
177
        layout_context: &LayoutContext,
178
        measured: &MeasuredChunk,
179
        chunk_x: f64,
180
        chunk_y: f64,
181
    ) -> PositionedChunk {
182
960
        let chunk_direction = measured.values.direction();
183

            
184
        // Position the spans relatively to each other, starting at (0, 0)
185

            
186
960
        let mut positioned = Vec::new();
187

            
188
        // Start position of each span; gets advanced as each span is laid out.
189
        // This is the text's start position, not the bounding box.
190
960
        let mut x = 0.0;
191
960
        let mut y = 0.0;
192

            
193
960
        let mut chunk_bounds: Option<Rect> = None;
194

            
195
1997
        for mspan in &measured.spans {
196
1036
            let params = NormalizeParams::new(&mspan.values, &layout_context.viewport);
197

            
198
1036
            let layout = mspan.layout.clone();
199
1037
            let layout_size = mspan.layout_size;
200
1037
            let values = mspan.values.clone();
201
1037
            let dx = mspan.dx;
202
1037
            let dy = mspan.dy;
203
1037
            let advance = mspan.advance;
204

            
205
1037
            let baseline_offset = compute_baseline_offset(&layout, &values, &params);
206

            
207
1037
            let start_pos = match chunk_direction {
208
1031
                Direction::Ltr => (x, y),
209
6
                Direction::Rtl => (x - advance.0, y),
210
            };
211

            
212
1037
            let span_advance = match chunk_direction {
213
1031
                Direction::Ltr => (advance.0, advance.1),
214
6
                Direction::Rtl => (-advance.0, advance.1),
215
            };
216

            
217
1037
            let rendered_position = if layout_context.writing_mode.is_horizontal() {
218
1037
                (start_pos.0 + dx, start_pos.1 - baseline_offset + dy)
219
            } else {
220
                (start_pos.0 + baseline_offset + dx, start_pos.1 + dy)
221
            };
222

            
223
            let span_bounds =
224
1037
                Rect::from_size(layout_size.0, layout_size.1).translate(rendered_position);
225

            
226
1037
            if let Some(bounds) = chunk_bounds {
227
94
                chunk_bounds = Some(bounds.union(&span_bounds));
228
            } else {
229
943
                chunk_bounds = Some(span_bounds);
230
            }
231

            
232
1037
            x = x + span_advance.0 + dx;
233
1037
            y = y + span_advance.1 + dy;
234

            
235
1037
            let positioned_span = PositionedSpan {
236
1037
                layout,
237
1037
                values,
238
1037
                rendered_position,
239
1037
                next_span_position: (x, y),
240
1037
                link_target: mspan.link_target.clone(),
241
            };
242

            
243
1037
            positioned.push(positioned_span);
244
1037
        }
245

            
246
        // Compute the offsets needed to align the chunk per the text-anchor property (start, middle, end):
247

            
248
961
        let anchor_offset = text_anchor_offset(
249
961
            measured.values.text_anchor(),
250
            chunk_direction,
251
961
            layout_context.writing_mode,
252
961
            chunk_bounds.unwrap_or_default(),
253
        );
254

            
255
        // Apply the text-anchor offset to each individually-positioned span, and compute the
256
        // start position of the next chunk.  Also add in the chunk's dx/dy.
257

            
258
961
        let mut next_chunk_x = chunk_x;
259
961
        let mut next_chunk_y = chunk_y;
260

            
261
1998
        for pspan in &mut positioned {
262
            // Add the chunk's position, plus the text-anchor offset, plus the chunk's dx/dy.
263
            // This last term is a hack until librsvg adds support for multiple dx/dy values per text/tspan;
264
            // see the corresponding part in MeasuredChunk::from_chunk().
265
1037
            pspan.rendered_position.0 += chunk_x + anchor_offset.0 + measured.dx;
266
1037
            pspan.rendered_position.1 += chunk_y + anchor_offset.1 + measured.dy;
267

            
268
1037
            next_chunk_x = chunk_x + pspan.next_span_position.0 + anchor_offset.0 + measured.dx;
269
1037
            next_chunk_y = chunk_y + pspan.next_span_position.1 + anchor_offset.1 + measured.dy;
270
        }
271

            
272
960
        PositionedChunk {
273
960
            next_chunk_x,
274
960
            next_chunk_y,
275
960
            spans: positioned,
276
        }
277
960
    }
278
}
279

            
280
1036
fn compute_baseline_offset(
281
    layout: &pango::Layout,
282
    values: &ComputedValues,
283
    params: &NormalizeParams,
284
) -> f64 {
285
1036
    let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE);
286
1036
    let baseline_shift = values.baseline_shift().0.to_user(params);
287
1036
    baseline + baseline_shift
288
1036
}
289

            
290
/// Computes the (x, y) offsets to be applied to spans after applying the text-anchor property (start, middle, end).
291
#[rustfmt::skip]
292
970
fn text_anchor_offset(
293
    anchor: TextAnchor,
294
    direction: Direction,
295
    writing_mode: WritingMode,
296
    chunk_bounds: Rect,
297
) -> (f64, f64) {
298
970
    let (w, h) = (chunk_bounds.width(), chunk_bounds.height());
299

            
300
970
    let x0 = chunk_bounds.x0;
301

            
302
970
    if writing_mode.is_horizontal() {
303
967
        match (anchor, direction) {
304
762
            (TextAnchor::Start,  Direction::Ltr) => (-x0, 0.0),
305
2
            (TextAnchor::Start,  Direction::Rtl) => (-x0 - w, 0.0),
306

            
307
182
            (TextAnchor::Middle, Direction::Ltr) => (-x0 - w / 2.0, 0.0),
308
2
            (TextAnchor::Middle, Direction::Rtl) => (-x0 - w / 2.0, 0.0),
309

            
310
17
            (TextAnchor::End,    Direction::Ltr) => (-x0 - w, 0.0),
311
2
            (TextAnchor::End,    Direction::Rtl) => (-x0, 0.0),
312
        }
313
    } else {
314
        // FIXME: we don't deal with text direction for vertical text yet.
315
3
        match anchor {
316
1
            TextAnchor::Start => (0.0, 0.0),
317
1
            TextAnchor::Middle => (0.0, -h / 2.0),
318
1
            TextAnchor::End => (0.0, -h),
319
        }
320
    }
321
970
}
322

            
323
impl Span {
324
1039
    fn new(
325
        text: &str,
326
        values: Rc<ComputedValues>,
327
        dx: f64,
328
        dy: f64,
329
        depth: usize,
330
        link_target: Option<String>,
331
    ) -> Span {
332
1039
        Span {
333
1039
            values,
334
1039
            text: text.to_string(),
335
            dx,
336
            dy,
337
            _depth: depth,
338
1039
            link_target,
339
        }
340
1039
    }
341
}
342

            
343
/// Use as `PangoUnits::from_pixels()` so that we can check for overflow.
344
2077
struct PangoUnits(i32);
345

            
346
impl PangoUnits {
347
2080
    fn from_pixels(v: f64) -> Option<Self> {
348
        // We want (v * f64::from(pango::SCALE) + 0.5) as i32
349
        //
350
        // But check for overflow.
351

            
352
2080
        cast::i32(v * f64::from(pango::SCALE) + 0.5)
353
            .ok()
354
            .map(PangoUnits)
355
2080
    }
356
}
357

            
358
impl MeasuredSpan {
359
1039
    fn from_span(layout_context: &LayoutContext, span: &Span) -> Option<MeasuredSpan> {
360
1039
        let values = span.values.clone();
361

            
362
1039
        let params = NormalizeParams::new(&values, &layout_context.viewport);
363

            
364
1039
        let properties = FontProperties::new(&values, &params);
365

            
366
1039
        let bidi_control = BidiControl::from_unicode_bidi_and_direction(
367
1039
            properties.unicode_bidi,
368
1039
            properties.direction,
369
        );
370

            
371
1039
        let with_control_chars = wrap_with_direction_control_chars(&span.text, &bidi_control);
372

            
373
2076
        if let Some(layout) = create_pango_layout(layout_context, &properties, &with_control_chars)
374
        {
375
1037
            let (w, h) = layout.size();
376

            
377
1037
            let w = f64::from(w) / f64::from(pango::SCALE);
378
1037
            let h = f64::from(h) / f64::from(pango::SCALE);
379

            
380
1037
            let advance = if layout_context.writing_mode.is_horizontal() {
381
1037
                (w, 0.0)
382
            } else {
383
                (0.0, w)
384
            };
385

            
386
1037
            Some(MeasuredSpan {
387
1037
                values,
388
1037
                layout,
389
1037
                layout_size: (w, h),
390
1037
                advance,
391
1037
                dx: span.dx,
392
1037
                dy: span.dy,
393
1037
                link_target: span.link_target.clone(),
394
            })
395
1037
        } else {
396
2
            None
397
        }
398
1039
    }
399
}
400

            
401
// FIXME: should the pango crate provide this like PANGO_GRAVITY_IS_VERTICAL() ?
402
976
fn gravity_is_vertical(gravity: pango::Gravity) -> bool {
403
976
    matches!(gravity, pango::Gravity::East | pango::Gravity::West)
404
976
}
405

            
406
1037
fn compute_text_box(
407
    layout: &pango::Layout,
408
    x: f64,
409
    y: f64,
410
    transform: Transform,
411
    gravity: pango::Gravity,
412
) -> Option<BoundingBox> {
413
    #![allow(clippy::many_single_char_names)]
414

            
415
1037
    let (ink, _) = layout.extents();
416
1037
    if ink.width() == 0 || ink.height() == 0 {
417
61
        return None;
418
    }
419

            
420
976
    let ink_x = f64::from(ink.x());
421
976
    let ink_y = f64::from(ink.y());
422
976
    let ink_width = f64::from(ink.width());
423
976
    let ink_height = f64::from(ink.height());
424
976
    let pango_scale = f64::from(pango::SCALE);
425

            
426
976
    let (x, y, w, h) = if gravity_is_vertical(gravity) {
427
        (
428
            x + (ink_x - ink_height) / pango_scale,
429
            y + ink_y / pango_scale,
430
            ink_height / pango_scale,
431
            ink_width / pango_scale,
432
        )
433
    } else {
434
976
        (
435
976
            x + ink_x / pango_scale,
436
976
            y + ink_y / pango_scale,
437
976
            ink_width / pango_scale,
438
976
            ink_height / pango_scale,
439
        )
440
    };
441

            
442
976
    let r = Rect::new(x, y, x + w, y + h);
443
976
    let bbox = BoundingBox::new()
444
        .with_transform(transform)
445
        .with_rect(r)
446
        .with_ink_rect(r);
447

            
448
976
    Some(bbox)
449
1037
}
450

            
451
impl PositionedSpan {
452
1037
    fn layout(
453
        &self,
454
        layout_context: &LayoutContext,
455
        acquired_nodes: &mut AcquiredNodes<'_>,
456
    ) -> LayoutSpan {
457
1037
        let params = NormalizeParams::new(&self.values, &layout_context.viewport);
458

            
459
1037
        let layout = self.layout.clone();
460
1037
        let is_visible = self.values.is_visible();
461
1036
        let (x, y) = self.rendered_position;
462

            
463
1036
        let stroke = Stroke::new(&self.values, &params);
464

            
465
1035
        let gravity = layout.context().gravity();
466

            
467
1036
        let bbox = compute_text_box(&layout, x, y, *layout_context.transform, gravity);
468

            
469
2074
        let stroke_paint = self.values.stroke().0.resolve(
470
            acquired_nodes,
471
1033
            self.values.stroke_opacity().0,
472
1037
            self.values.color().0,
473
1037
            None,
474
1037
            None,
475
1037
            &layout_context.session,
476
1033
        );
477

            
478
2074
        let fill_paint = self.values.fill().0.resolve(
479
            acquired_nodes,
480
1033
            self.values.fill_opacity().0,
481
1037
            self.values.color().0,
482
1037
            None,
483
1037
            None,
484
1037
            &layout_context.session,
485
1033
        );
486

            
487
1037
        let paint_order = self.values.paint_order();
488
1037
        let text_rendering = self.values.text_rendering();
489

            
490
1033
        LayoutSpan {
491
1033
            layout,
492
            gravity,
493
            bbox,
494
            is_visible,
495
            x,
496
            y,
497
            paint_order,
498
1033
            stroke,
499
1033
            stroke_paint,
500
1033
            fill_paint,
501
            text_rendering,
502
1033
            values: self.values.clone(),
503
1033
            link_target: self.link_target.clone(),
504
        }
505
1033
    }
506
}
507

            
508
/// Walks the children of a `<text>`, `<tspan>`, or `<tref>` element
509
/// and appends chunks/spans from them into the specified `chunks`
510
/// array.
511
1008
fn children_to_chunks(
512
    chunks: &mut Vec<Chunk>,
513
    node: &Node,
514
    acquired_nodes: &mut AcquiredNodes<'_>,
515
    cascaded: &CascadedValues<'_>,
516
    layout_context: &LayoutContext,
517
    dx: f64,
518
    dy: f64,
519
    depth: usize,
520
    link: Option<String>,
521
) {
522
1008
    let mut dx = dx;
523
1008
    let mut dy = dy;
524

            
525
1008
    for child in node.children() {
526
1128
        if child.is_chars() {
527
1026
            let values = cascaded.get();
528
2052
            child.borrow_chars().to_chunks(
529
                &child,
530
1026
                Rc::new(values.clone()),
531
1027
                chunks,
532
1026
                dx,
533
1026
                dy,
534
                depth,
535
1026
                link.clone(),
536
1027
            );
537
        } else {
538
103
            assert!(child.is_element());
539

            
540
103
            match *child.borrow_element_data() {
541
91
                ElementData::TSpan(ref tspan) => {
542
91
                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
543
91
                    tspan.to_chunks(
544
                        &child,
545
                        acquired_nodes,
546
                        &cascaded,
547
                        layout_context,
548
                        chunks,
549
91
                        dx,
550
91
                        dy,
551
91
                        depth + 1,
552
91
                        link.clone(),
553
                    );
554
91
                }
555

            
556
8
                ElementData::Link(ref link) => {
557
                    // TSpan::default sets all offsets to 0,
558
                    // which is what we want in links.
559
                    //
560
                    // FIXME: This is the only place in the code where an element's method (TSpan::to_chunks)
561
                    // is called with a node that is not the element itself: here, `child` is a Link, not a TSpan.
562
                    //
563
                    // The code works because the `tspan` is dropped immediately after calling to_chunks and no
564
                    // references are retained for it.
565
8
                    let tspan = TSpan::default();
566
8
                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
567
8
                    tspan.to_chunks(
568
                        &child,
569
                        acquired_nodes,
570
                        &cascaded,
571
                        layout_context,
572
                        chunks,
573
8
                        dx,
574
8
                        dy,
575
8
                        depth + 1,
576
8
                        link.link.clone(),
577
                    );
578
8
                }
579

            
580
4
                ElementData::TRef(ref tref) => {
581
4
                    let cascaded = CascadedValues::clone_with_node(cascaded, &child);
582
8
                    tref.to_chunks(
583
                        &child,
584
                        acquired_nodes,
585
                        &cascaded,
586
                        chunks,
587
4
                        depth + 1,
588
                        layout_context,
589
                    );
590
4
                }
591

            
592
                _ => (),
593
            }
594
95
        }
595

            
596
        // After the first span, we don't need to carry over the parent's dx/dy.
597
934
        dx = 0.0;
598
934
        dy = 0.0;
599
934
    }
600
1054
}
601

            
602
/// In SVG text elements, we use `Chars` to store character data.  For example,
603
/// an element like `<text>Foo Bar</text>` will be a `Text` with a single child,
604
/// and the child will be a `Chars` with "Foo Bar" for its contents.
605
///
606
/// Text elements can contain `<tspan>` sub-elements.  In this case,
607
/// those `tspan` nodes will also contain `Chars` children.
608
///
609
/// A text or tspan element can contain more than one `Chars` child, for example,
610
/// if there is an XML comment that splits the character contents in two:
611
///
612
/// ```xml
613
/// <text>
614
///   This sentence will create a Chars.
615
///   <!-- this comment is ignored -->
616
///   This sentence will cretea another Chars.
617
/// </text>
618
/// ```
619
///
620
/// When rendering a text element, it will take care of concatenating the strings
621
/// in its `Chars` children as appropriate, depending on the
622
/// `xml:space="preserve"` attribute.  A `Chars` stores the characters verbatim
623
/// as they come out of the XML parser, after ensuring that they are valid UTF-8.
624

            
625
1
#[derive(Default)]
626
pub struct Chars {
627
1
    string: RefCell<String>,
628
1
    space_normalized: RefCell<Option<String>>,
629
}
630

            
631
impl Chars {
632
1031550
    pub fn new(initial_text: &str) -> Chars {
633
1031550
        Chars {
634
1031550
            string: RefCell::new(String::from(initial_text)),
635
1031550
            space_normalized: RefCell::new(None),
636
        }
637
1031550
    }
638

            
639
2
    pub fn is_empty(&self) -> bool {
640
2
        self.string.borrow().is_empty()
641
2
    }
642

            
643
2529
    pub fn append(&self, s: &str) {
644
2529
        self.string.borrow_mut().push_str(s);
645
2529
        *self.space_normalized.borrow_mut() = None;
646
2529
    }
647

            
648
1038
    fn ensure_normalized_string(&self, node: &Node, values: &ComputedValues) {
649
1038
        let mut normalized = self.space_normalized.borrow_mut();
650

            
651
1038
        if (*normalized).is_none() {
652
1033
            let mode = match values.xml_space() {
653
1018
                XmlSpace::Default => XmlSpaceNormalize::Default(NormalizeDefault {
654
1018
                    has_element_before: node.previous_sibling().is_some(),
655
1018
                    has_element_after: node.next_sibling().is_some(),
656
1018
                }),
657

            
658
15
                XmlSpace::Preserve => XmlSpaceNormalize::Preserve,
659
            };
660

            
661
15
            *normalized = Some(xml_space_normalize(mode, &self.string.borrow()));
662
        }
663
1008
    }
664

            
665
1038
    fn make_span(
666
        &self,
667
        node: &Node,
668
        values: Rc<ComputedValues>,
669
        dx: f64,
670
        dy: f64,
671
        depth: usize,
672
        link_target: Option<String>,
673
    ) -> Span {
674
1038
        self.ensure_normalized_string(node, &values);
675

            
676
1039
        Span::new(
677
1039
            self.space_normalized.borrow().as_ref().unwrap(),
678
1039
            values,
679
            dx,
680
            dy,
681
            depth,
682
1039
            link_target,
683
        )
684
1039
    }
685

            
686
1039
    fn to_chunks(
687
        &self,
688
        node: &Node,
689
        values: Rc<ComputedValues>,
690
        chunks: &mut [Chunk],
691
        dx: f64,
692
        dy: f64,
693
        depth: usize,
694
        link_target: Option<String>,
695
    ) {
696
1039
        let span = self.make_span(node, values, dx, dy, depth, link_target);
697
1039
        let num_chunks = chunks.len();
698
1039
        assert!(num_chunks > 0);
699

            
700
1039
        chunks[num_chunks - 1].spans.push(span);
701
1039
    }
702

            
703
43
    pub fn get_string(&self) -> String {
704
43
        self.string.borrow().clone()
705
43
    }
706
}
707

            
708
1870
#[derive(Default)]
709
pub struct Text {
710
935
    x: Length<Horizontal>,
711
935
    y: Length<Vertical>,
712
935
    dx: Length<Horizontal>,
713
935
    dy: Length<Vertical>,
714
}
715

            
716
impl Text {
717
914
    fn make_chunks(
718
        &self,
719
        node: &Node,
720
        acquired_nodes: &mut AcquiredNodes<'_>,
721
        cascaded: &CascadedValues<'_>,
722
        layout_context: &LayoutContext,
723
        x: f64,
724
        y: f64,
725
    ) -> Vec<Chunk> {
726
914
        let mut chunks = Vec::new();
727

            
728
914
        let values = cascaded.get();
729
914
        let params = NormalizeParams::new(values, &layout_context.viewport);
730

            
731
914
        chunks.push(Chunk::new(values, Some(x), Some(y)));
732

            
733
914
        let dx = self.dx.to_user(&params);
734
914
        let dy = self.dy.to_user(&params);
735

            
736
914
        children_to_chunks(
737
            &mut chunks,
738
            node,
739
            acquired_nodes,
740
            cascaded,
741
            layout_context,
742
            dx,
743
            dy,
744
            0,
745
914
            None,
746
        );
747
914
        chunks
748
914
    }
749
}
750

            
751
impl ElementTrait for Text {
752
935
    fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
753
4170
        for (attr, value) in attrs.iter() {
754
3235
            match attr.expanded() {
755
802
                expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
756
871
                expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
757
1
                expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session),
758
1
                expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session),
759
                _ => (),
760
            }
761
3235
        }
762
935
    }
763

            
764
916
    fn draw(
765
        &self,
766
        node: &Node,
767
        acquired_nodes: &mut AcquiredNodes<'_>,
768
        cascaded: &CascadedValues<'_>,
769
        viewport: &Viewport,
770
        draw_ctx: &mut DrawingCtx,
771
        clipping: bool,
772
    ) -> Result<BoundingBox, InternalRenderingError> {
773
916
        let values = cascaded.get();
774
916
        let params = NormalizeParams::new(values, viewport);
775

            
776
916
        let elt = node.borrow_element();
777

            
778
918
        let stacking_ctx = StackingContext::new(
779
916
            draw_ctx.session(),
780
            acquired_nodes,
781
914
            &elt,
782
914
            values.transform(),
783
918
            None,
784
            values,
785
916
        );
786

            
787
        let layout_text = {
788
916
            let transform = draw_ctx.get_transform_for_stacking_ctx(&stacking_ctx, clipping)?;
789

            
790
916
            let layout_context = LayoutContext {
791
914
                writing_mode: values.writing_mode(),
792
918
                transform,
793
914
                font_options: draw_ctx.get_font_options(),
794
916
                viewport: viewport.clone(),
795
913
                session: draw_ctx.session().clone(),
796
            };
797

            
798
916
            let mut x = self.x.to_user(&params);
799
914
            let mut y = self.y.to_user(&params);
800

            
801
918
            let chunks = self.make_chunks(node, acquired_nodes, cascaded, &layout_context, x, y);
802

            
803
916
            let mut measured_chunks = Vec::new();
804
1876
            for chunk in &chunks {
805
961
                measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk));
806
            }
807

            
808
915
            let mut positioned_chunks = Vec::new();
809
1875
            for chunk in &measured_chunks {
810
960
                let chunk_x = chunk.x.unwrap_or(x);
811
960
                let chunk_y = chunk.y.unwrap_or(y);
812

            
813
                let positioned =
814
962
                    PositionedChunk::from_measured(&layout_context, chunk, chunk_x, chunk_y);
815

            
816
961
                x = positioned.next_chunk_x;
817
961
                y = positioned.next_chunk_y;
818

            
819
961
                positioned_chunks.push(positioned);
820
            }
821

            
822
914
            let mut layout_spans = Vec::new();
823
1875
            for chunk in &positioned_chunks {
824
1998
                for span in &chunk.spans {
825
1037
                    layout_spans.push(span.layout(&layout_context, acquired_nodes));
826
                }
827
            }
828

            
829
914
            let empty_bbox = BoundingBox::new().with_transform(*transform);
830

            
831
1951
            let text_bbox = layout_spans.iter().fold(empty_bbox, |mut bbox, span| {
832
1037
                if let Some(ref span_bbox) = span.bbox {
833
976
                    bbox.insert(span_bbox);
834
                }
835

            
836
1037
                bbox
837
1037
            });
838

            
839
914
            let mut text_spans = Vec::new();
840
1951
            for span in layout_spans {
841
1037
                let normalize_values = NormalizeValues::new(&span.values);
842

            
843
1037
                let stroke_paint = span.stroke_paint.to_user_space(
844
                    &text_bbox.rect,
845
1037
                    &layout_context.viewport,
846
                    &normalize_values,
847
1037
                );
848
1037
                let fill_paint = span.fill_paint.to_user_space(
849
                    &text_bbox.rect,
850
1037
                    &layout_context.viewport,
851
                    &normalize_values,
852
                );
853

            
854
1037
                let text_span = TextSpan {
855
1037
                    layout: span.layout,
856
1037
                    gravity: span.gravity,
857
1037
                    bbox: span.bbox,
858
1037
                    is_visible: span.is_visible,
859
1037
                    x: span.x,
860
1037
                    y: span.y,
861
1037
                    paint_order: span.paint_order,
862
1037
                    stroke: span.stroke,
863
1037
                    stroke_paint,
864
                    fill_paint,
865
1037
                    text_rendering: span.text_rendering,
866
1037
                    link_target: span.link_target,
867
                };
868

            
869
1037
                text_spans.push(text_span);
870
1037
            }
871

            
872
914
            layout::Text { spans: text_spans }
873
914
        };
874

            
875
914
        let layer = Layer {
876
914
            kind: LayerKind::Text(Box::new(layout_text)),
877
914
            stacking_ctx,
878
        };
879

            
880
914
        draw_ctx.draw_layer(&layer, acquired_nodes, clipping, viewport)
881
914
    }
882
}
883

            
884
8
#[derive(Default)]
885
pub struct TRef {
886
4
    link: Option<NodeId>,
887
}
888

            
889
impl TRef {
890
4
    fn to_chunks(
891
        &self,
892
        node: &Node,
893
        acquired_nodes: &mut AcquiredNodes<'_>,
894
        cascaded: &CascadedValues<'_>,
895
        chunks: &mut Vec<Chunk>,
896
        depth: usize,
897
        layout_context: &LayoutContext,
898
    ) {
899
4
        if self.link.is_none() {
900
            return;
901
        }
902

            
903
4
        let link = self.link.as_ref().unwrap();
904

            
905
4
        let values = cascaded.get();
906
4
        if !values.is_displayed() {
907
            return;
908
        }
909

            
910
4
        if let Ok(acquired) = acquired_nodes.acquire(link) {
911
4
            let c = acquired.get();
912
4
            extract_chars_children_to_chunks_recursively(chunks, c, Rc::new(values.clone()), depth);
913
4
        } else {
914
            rsvg_log!(
915
                layout_context.session,
916
                "element {} references a nonexistent text source \"{}\"",
917
                node,
918
                link,
919
            );
920
        }
921
4
    }
922
}
923

            
924
12
fn extract_chars_children_to_chunks_recursively(
925
    chunks: &mut Vec<Chunk>,
926
    node: &Node,
927
    values: Rc<ComputedValues>,
928
    depth: usize,
929
) {
930
12
    for child in node.children() {
931
20
        let values = values.clone();
932

            
933
20
        if child.is_chars() {
934
24
            child
935
                .borrow_chars()
936
12
                .to_chunks(&child, values, chunks, 0.0, 0.0, depth, None)
937
12
        } else {
938
8
            extract_chars_children_to_chunks_recursively(chunks, &child, values, depth + 1)
939
        }
940
20
    }
941
28
}
942

            
943
impl ElementTrait for TRef {
944
4
    fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) {
945
4
        self.link = attrs
946
            .iter()
947
4
            .find(|(attr, _)| attr.expanded() == expanded_name!(xlink "href"))
948
            // Unlike other elements which use `href` in SVG2 versus `xlink:href` in SVG1.1,
949
            // the <tref> element got removed in SVG2.  So, here we still use a match
950
            // against the full namespaced version of the attribute.
951
4
            .and_then(|(attr, value)| NodeId::parse(value).attribute(attr).ok());
952
4
    }
953
}
954

            
955
214
#[derive(Default)]
956
pub struct TSpan {
957
107
    x: Option<Length<Horizontal>>,
958
107
    y: Option<Length<Vertical>>,
959
107
    dx: Length<Horizontal>,
960
107
    dy: Length<Vertical>,
961
}
962

            
963
impl TSpan {
964
193
    fn to_chunks(
965
        &self,
966
        node: &Node,
967
        acquired_nodes: &mut AcquiredNodes<'_>,
968
        cascaded: &CascadedValues<'_>,
969
        layout_context: &LayoutContext,
970
        chunks: &mut Vec<Chunk>,
971
        dx: f64,
972
        dy: f64,
973
        depth: usize,
974
        link: Option<String>,
975
    ) {
976
193
        let values = cascaded.get();
977
99
        if !values.is_displayed() {
978
            return;
979
        }
980

            
981
94
        let params = NormalizeParams::new(values, &layout_context.viewport);
982

            
983
141
        let x = self.x.map(|l| l.to_user(&params));
984
132
        let y = self.y.map(|l| l.to_user(&params));
985

            
986
94
        let span_dx = dx + self.dx.to_user(&params);
987
94
        let span_dy = dy + self.dy.to_user(&params);
988

            
989
94
        if x.is_some() || y.is_some() {
990
94
            chunks.push(Chunk::new(values, x, y));
991
        }
992

            
993
47
        children_to_chunks(
994
            chunks,
995
            node,
996
            acquired_nodes,
997
            cascaded,
998
            layout_context,
999
            span_dx,
            span_dy,
            depth,
141
            link,
        );
99
    }
}
impl ElementTrait for TSpan {
99
    fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
265
        for (attr, value) in attrs.iter() {
166
            match attr.expanded() {
47
                expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
36
                expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
3
                expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session),
15
                expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session),
                _ => (),
            }
166
        }
99
    }
}
impl From<FontStyle> for pango::Style {
1037
    fn from(s: FontStyle) -> pango::Style {
1037
        match s {
1026
            FontStyle::Normal => pango::Style::Normal,
11
            FontStyle::Italic => pango::Style::Italic,
            FontStyle::Oblique => pango::Style::Oblique,
        }
1037
    }
}
impl From<FontVariant> for pango::Variant {
1037
    fn from(v: FontVariant) -> pango::Variant {
1037
        match v {
1036
            FontVariant::Normal => pango::Variant::Normal,
1
            FontVariant::SmallCaps => pango::Variant::SmallCaps,
        }
1037
    }
}
impl From<FontStretch> for pango::Stretch {
1037
    fn from(s: FontStretch) -> pango::Stretch {
1037
        match s {
1037
            FontStretch::Normal => pango::Stretch::Normal,
            FontStretch::Wider => pango::Stretch::Expanded, // not quite correct
            FontStretch::Narrower => pango::Stretch::Condensed, // not quite correct
            FontStretch::UltraCondensed => pango::Stretch::UltraCondensed,
            FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed,
            FontStretch::Condensed => pango::Stretch::Condensed,
            FontStretch::SemiCondensed => pango::Stretch::SemiCondensed,
            FontStretch::SemiExpanded => pango::Stretch::SemiExpanded,
            FontStretch::Expanded => pango::Stretch::Expanded,
            FontStretch::ExtraExpanded => pango::Stretch::ExtraExpanded,
            FontStretch::UltraExpanded => pango::Stretch::UltraExpanded,
        }
1037
    }
}
impl From<FontWeight> for pango::Weight {
1037
    fn from(w: FontWeight) -> pango::Weight {
1037
        pango::Weight::__Unknown(w.numeric_weight().into())
1037
    }
}
impl From<Direction> for pango::Direction {
7
    fn from(d: Direction) -> pango::Direction {
7
        match d {
            Direction::Ltr => pango::Direction::Ltr,
7
            Direction::Rtl => pango::Direction::Rtl,
        }
7
    }
}
impl From<WritingMode> for pango::Direction {
1032
    fn from(m: WritingMode) -> pango::Direction {
        use WritingMode::*;
1032
        match m {
1032
            HorizontalTb | VerticalRl | VerticalLr | LrTb | Lr | Tb | TbRl => pango::Direction::Ltr,
            RlTb | Rl => pango::Direction::Rtl,
        }
1032
    }
}
impl From<WritingMode> for pango::Gravity {
1039
    fn from(m: WritingMode) -> pango::Gravity {
        use WritingMode::*;
1039
        match m {
1039
            HorizontalTb | LrTb | Lr | RlTb | Rl => pango::Gravity::South,
            VerticalRl | Tb | TbRl => pango::Gravity::East,
            VerticalLr => pango::Gravity::West,
        }
1039
    }
}
/// Constants with Unicode's directional formatting characters
///
/// <https://unicode.org/reports/tr9/#Directional_Formatting_Characters>
mod directional_formatting_characters {
    /// Left-to-Right Embedding
    ///
    /// Treat the following text as embedded left-to-right.
    pub const LRE: char = '\u{202a}';
    /// Right-to-Left Embedding
    ///
    /// Treat the following text as embedded right-to-left.
    pub const RLE: char = '\u{202b}';
    /// Left-to-Right Override
    ///
    /// Force following characters to be treated as strong left-to-right characters.
    pub const LRO: char = '\u{202d}';
    /// Right-to-Left Override
    ///
    /// Force following characters to be treated as strong right-to-left characters.
    pub const RLO: char = '\u{202e}';
    /// Pop Directional Formatting
    ///
    /// End the scope of the last LRE, RLE, RLO, or LRO.
    pub const PDF: char = '\u{202c}';
    /// Left-to-Right Isolate
    ///
    /// Treat the following text as isolated and left-to-right.
    pub const LRI: char = '\u{2066}';
    /// Right-to-Left Isolate
    ///
    /// Treat the following text as isolated and right-to-left.
    pub const RLI: char = '\u{2067}';
    /// First Strong Isolate
    ///
    /// Treat the following text as isolated and in the direction of its first strong
    /// directional character that is not inside a nested isolate.
    pub const FSI: char = '\u{2068}';
    /// Pop Directional Isolate
    ///
    /// End the scope of the last LRI, RLI, or FSI.
    pub const PDI: char = '\u{2069}';
}
/// Unicode control characters to be inserted when `unicode-bidi` is specified.
///
/// The `unicode-bidi` property is used to change the embedding of a text span within
/// another.  This struct contains slices with the control characters that must be
/// inserted into the text stream at the span's limits so that the bidi/shaping engine
/// will know what to do.
struct BidiControl {
    start: &'static [char],
    end: &'static [char],
}
impl BidiControl {
    /// Creates a `BidiControl` from the properties that determine it.
    ///
    /// See the table titled "Bidi control codes injected..." in
    /// <https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi>
    #[rustfmt::skip]
1039
    fn from_unicode_bidi_and_direction(unicode_bidi: UnicodeBidi, direction: Direction) -> BidiControl {
        use UnicodeBidi::*;
        use Direction::*;
        use directional_formatting_characters::*;
2078
        let (start, end) = match (unicode_bidi, direction) {
1038
            (Normal,          _)   => (&[][..],         &[][..]),
            (Embed,           Ltr) => (&[LRE][..],      &[PDF][..]),
            (Embed,           Rtl) => (&[RLE][..],      &[PDF][..]),
            (Isolate,         Ltr) => (&[LRI][..],      &[PDI][..]),
            (Isolate,         Rtl) => (&[RLI][..],      &[PDI][..]),
            (BidiOverride,    Ltr) => (&[LRO][..],      &[PDF][..]),
1
            (BidiOverride,    Rtl) => (&[RLO][..],      &[PDF][..]),
            (IsolateOverride, Ltr) => (&[FSI, LRO][..], &[PDF, PDI][..]),
            (IsolateOverride, Rtl) => (&[FSI, RLO][..], &[PDF, PDI][..]),
            (Plaintext,       Ltr) => (&[FSI][..],      &[PDI][..]),
            (Plaintext,       Rtl) => (&[FSI][..],      &[PDI][..]),
        };
1039
        BidiControl { start, end }
1039
    }
}
/// Prepends and appends Unicode directional formatting characters.
1039
fn wrap_with_direction_control_chars(s: &str, bidi_control: &BidiControl) -> String {
    let mut res =
1039
        String::with_capacity(s.len() + bidi_control.start.len() + bidi_control.end.len());
1040
    for &ch in bidi_control.start {
1
        res.push(ch);
    }
1039
    res.push_str(s);
1040
    for &ch in bidi_control.end {
1
        res.push(ch);
    }
1039
    res
1039
}
/// Returns `None` if the layout would be invalid due to, for example, out-of-bounds font sizes.
1039
fn create_pango_layout(
    layout_context: &LayoutContext,
    props: &FontProperties,
    text: &str,
) -> Option<pango::Layout> {
1039
    let pango_context =
1039
        create_pango_context(&layout_context.font_options, &layout_context.transform);
1039
    if let XmlLang(Some(ref lang)) = props.xml_lang {
13
        pango_context.set_language(Some(&pango::Language::from_string(lang.as_str())));
    }
1026
    pango_context.set_base_gravity(pango::Gravity::from(layout_context.writing_mode));
1039
    match (props.unicode_bidi, props.direction) {
        (UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => {
1
            pango_context.set_base_dir(pango::Direction::from(props.direction));
        }
1038
        (_, direction) if direction != Direction::Ltr => {
6
            pango_context.set_base_dir(pango::Direction::from(direction));
        }
        (_, _) => {
1032
            pango_context.set_base_dir(pango::Direction::from(layout_context.writing_mode));
        }
    }
1039
    let layout = pango::Layout::new(&pango_context);
1039
    let font_size = PangoUnits::from_pixels(props.font_size);
1039
    let letter_spacing = PangoUnits::from_pixels(props.letter_spacing);
1039
    if font_size.is_none() {
1
        rsvg_log!(
1
            &layout_context.session,
            "font-size {} is out of bounds; ignoring span",
            props.font_size
        );
    }
1039
    if letter_spacing.is_none() {
1
        rsvg_log!(
1
            &layout_context.session,
            "letter-spacing {} is out of bounds; ignoring span",
            props.letter_spacing
        );
    }
1039
    if let (Some(font_size), Some(letter_spacing)) = (font_size, letter_spacing) {
1037
        let attr_list = pango::AttrList::new();
1038
        add_pango_attributes(&attr_list, props, 0, text.len(), font_size, letter_spacing);
1037
        layout.set_attributes(Some(&attr_list));
1037
        layout.set_text(text);
1037
        layout.set_auto_dir(false);
1036
        Some(layout)
1036
    } else {
2
        None
    }
1037
}
/// Adds Pango attributes, suitable for a span of text, to an `AttrList`.
1037
fn add_pango_attributes(
    attr_list: &pango::AttrList,
    props: &FontProperties,
    start_index: usize,
    end_index: usize,
    font_size: PangoUnits,
    letter_spacing: PangoUnits,
) {
1037
    let start_index = u32::try_from(start_index).expect("Pango attribute index must fit in u32");
1037
    let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32");
1037
    assert!(start_index <= end_index);
1037
    let mut attributes = Vec::new();
1037
    let mut font_desc = pango::FontDescription::new();
1037
    font_desc.set_family(props.font_family.as_str());
1037
    font_desc.set_style(pango::Style::from(props.font_style));
1037
    font_desc.set_variant(pango::Variant::from(props.font_variant));
1037
    font_desc.set_weight(pango::Weight::from(props.font_weight));
1037
    font_desc.set_stretch(pango::Stretch::from(props.font_stretch));
1037
    font_desc.set_size(font_size.0);
1037
    attributes.push(pango::AttrFontDesc::new(&font_desc).upcast());
1037
    attributes.push(pango::AttrInt::new_letter_spacing(letter_spacing.0).upcast());
1037
    if props.text_decoration.overline {
        attributes.push(pango::AttrInt::new_overline(pango::Overline::Single).upcast());
    }
1037
    if props.text_decoration.underline {
2
        attributes.push(pango::AttrInt::new_underline(pango::Underline::Single).upcast());
    }
1037
    if props.text_decoration.strike {
1
        attributes.push(pango::AttrInt::new_strikethrough(true).upcast());
    }
    // Set the range in each attribute
3113
    for attr in &mut attributes {
2076
        attr.set_start_index(start_index);
2076
        attr.set_end_index(end_index);
    }
    // Add the attributes to the attr_list
3114
    for attr in attributes {
2077
        attr_list.insert(attr);
    }
1037
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
2
    fn chars_default() {
1
        let c = Chars::default();
1
        assert!(c.is_empty());
1
        assert!(c.space_normalized.borrow().is_none());
2
    }
    #[test]
2
    fn chars_new() {
1
        let example = "Test 123";
1
        let c = Chars::new(example);
2
        assert_eq!(c.get_string(), example);
1
        assert!(c.space_normalized.borrow().is_none());
2
    }
    // This is called _horizontal because the property value in "CSS Writing Modes 3"
    // is `horizontal-tb`.  Eventually we will support that and this will make more sense.
    #[test]
2
    fn adjusted_advance_horizontal_ltr() {
        use Direction::*;
        use TextAnchor::*;
1
        assert_eq!(
1
            text_anchor_offset(
1
                Start,
1
                Ltr,
1
                WritingMode::Lr,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-5.0, 0.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(
1
                Middle,
1
                Ltr,
1
                WritingMode::Lr,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-5.5, 0.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(
1
                End,
1
                Ltr,
1
                WritingMode::Lr,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-6.0, 0.0)
        );
2
    }
    #[test]
2
    fn adjusted_advance_horizontal_rtl() {
        use Direction::*;
        use TextAnchor::*;
1
        assert_eq!(
1
            text_anchor_offset(
1
                Start,
1
                Rtl,
1
                WritingMode::Rl,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-6.0, 0.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(
1
                Middle,
1
                Rtl,
1
                WritingMode::Rl,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-5.5, 0.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(
1
                TextAnchor::End,
1
                Direction::Rtl,
1
                WritingMode::Rl,
1
                Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
            ),
            (-5.0, 0.0)
        );
2
    }
    // This is called _vertical because "CSS Writing Modes 3" has both `vertical-rl` (East
    // Asia), and `vertical-lr` (Manchu, Mongolian), but librsvg does not support block
    // flow direction properly yet.  Eventually we will support that and this will make
    // more sense.
    #[test]
2
    fn adjusted_advance_vertical() {
        use Direction::*;
        use TextAnchor::*;
1
        assert_eq!(
1
            text_anchor_offset(Start, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
            (0.0, 0.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(Middle, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
            (0.0, -2.0)
        );
1
        assert_eq!(
1
            text_anchor_offset(End, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
            (0.0, -4.0)
        );
2
    }
    #[test]
2
    fn pango_units_works() {
1
        assert_eq!(PangoUnits::from_pixels(10.0).unwrap().0, pango::SCALE * 10);
2
    }
    #[test]
2
    fn pango_units_detects_overflow() {
1
        assert!(PangoUnits::from_pixels(1e7).is_none());
2
    }
}