1
//! Calculate glyph positions.
2
//!
3
//! [GlyphLayout] is used to obtain the positions for a collection of shaped glyphs. The position
4
//! for each glyph includes its horizontal and vertical advance as well as any `(x, y)` offset from
5
//! the origin. Horizontal layout in left-to-right and right-to-left directions is supported. Basic
6
//! (but incomplete) support for vertical text is present too.
7
//!
8
//! The position of a series of glyphs is determined from an initial pen position, which is
9
//! incremented by the advance of each glyph as they are processed. The position of a particular
10
//! glyph is the current pen position plus `x_offset` and `y_offset`.
11

            
12
use crate::context::Glyph;
13
use crate::error::ParseError;
14
use crate::gpos::{Info, Placement};
15
use crate::tables::FontTableProvider;
16
use crate::unicode::codepoint::is_upright_char;
17
use crate::Font;
18

            
19
/// Used to calculate the position of shaped glyphs.
20
pub struct GlyphLayout<'f, 'i, T>
21
where
22
    T: FontTableProvider,
23
{
24
    font: &'f mut Font<T>,
25
    infos: &'i [Info],
26
    direction: TextDirection,
27
    vertical: bool,
28
}
29

            
30
/// The position and advance of a glyph.
31
#[derive(Copy, Clone, Eq, Debug, Default)]
32
pub struct GlyphPosition {
33
    /// Horizontal advance
34
    pub hori_advance: i32,
35
    /// Vertical advance
36
    pub vert_advance: i32,
37
    /// Offset in the X (horizontal) direction of this glyph
38
    pub x_offset: i32,
39
    /// Offset in the Y (vertical) direction of this glyph
40
    pub y_offset: i32,
41
    cursive_attachment: Option<u16>,
42
}
43

            
44
/// The horizontal text layout direction.
45
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
46
pub enum TextDirection {
47
    LeftToRight,
48
    RightToLeft,
49
}
50

            
51
impl<'f, 'i, T: FontTableProvider> GlyphLayout<'f, 'i, T> {
52
    /// Construct a new `GlyphLayout` instance.
53
    ///
54
    /// **Arguments**
55
    ///
56
    /// * `font` — the font that the glyphs belong to.
57
    /// * `infos` — the shaped glyphs to lay out.
58
    /// * `direction` — the horizontal text layout direction.
59
    /// * `vertical` — `true` if the text is being laid out top to bottom.
60
    pub fn new(
61
        font: &'f mut Font<T>,
62
        infos: &'i [Info],
63
        direction: TextDirection,
64
        vertical: bool,
65
    ) -> Self {
66
        GlyphLayout {
67
            font,
68
            infos,
69
            direction,
70
            vertical,
71
        }
72
    }
73

            
74
    /// Retrieve the glyphs positions.
75
    pub fn glyph_positions(&mut self) -> Result<Vec<GlyphPosition>, ParseError> {
76
        let mut has_marks = false;
77
        let mut has_cursive_connection = false;
78
        let mut positions = vec![GlyphPosition::default(); self.infos.len()];
79

            
80
        for (i, info) in self.infos.iter().enumerate() {
81
            let (hori_advance, vert_advance) = glyph_advance(self.font, info, self.vertical)?;
82
            match info.placement {
83
                Placement::None => positions[i].update(hori_advance, vert_advance, 0, 0),
84
                Placement::Distance(dx, dy) => {
85
                    positions[i].update(hori_advance, vert_advance, dx, dy)
86
                }
87
                Placement::MarkAnchor(base_index, base_anchor, mark_anchor) => {
88
                    has_marks = true;
89
                    match self.infos.get(base_index) {
90
                        Some(base_info) => {
91
                            let (dx, dy) = match base_info.placement {
92
                                Placement::Distance(dx, dy) => (dx, dy),
93
                                _ => (0, 0),
94
                            };
95
                            let offset_x = i32::from(base_anchor.x) - i32::from(mark_anchor.x) + dx;
96
                            let offset_y = i32::from(base_anchor.y) - i32::from(mark_anchor.y) + dy;
97
                            positions[i].update(hori_advance, vert_advance, offset_x, offset_y);
98
                        }
99
                        None => {
100
                            return Err(ParseError::BadIndex);
101
                        }
102
                    }
103
                }
104
                Placement::MarkOverprint(base_index) => {
105
                    has_marks = true;
106
                    positions[i].update_advance(0, 0);
107
                    self.infos.get(base_index).ok_or(ParseError::BadIndex)?;
108
                }
109
                Placement::CursiveAnchor(exit_glyph_index, _, _, _) => {
110
                    has_cursive_connection = true;
111
                    // Validate index
112
                    self.infos
113
                        .get(exit_glyph_index)
114
                        .ok_or(ParseError::BadIndex)?;
115

            
116
                    // Link to exit glyph
117
                    positions[exit_glyph_index].cursive_attachment = Some(u16::try_from(i)?);
118
                    let new_glyph = GlyphPosition {
119
                        hori_advance,
120
                        vert_advance,
121
                        ..positions[i]
122
                    };
123
                    positions[i] = new_glyph;
124
                }
125
            };
126
        }
127

            
128
        if has_cursive_connection {
129
            // Now that we know all base glyphs are positioned we do a second pass to apply
130
            // cursive attachment adjustments
131
            self.adjust_cursive_connections(&mut positions);
132
        }
133

            
134
        if has_marks {
135
            // Now that cursive connected glyphs are positioned, ensure marks are positioned on their
136
            // base properly.
137
            self.position_marks(&mut positions);
138
        }
139

            
140
        Ok(positions)
141
    }
142

            
143
    fn adjust_cursive_connections(&self, positions: &mut [GlyphPosition]) {
144
        for (i, info) in self.infos.iter().enumerate() {
145
            match info.placement {
146
                Placement::None
147
                | Placement::Distance(_, _)
148
                | Placement::MarkAnchor(_, _, _)
149
                | Placement::MarkOverprint(_) => {}
150
                Placement::CursiveAnchor(
151
                    exit_glyph_index,
152
                    rtl_flag,
153
                    exit_glyph_anchor,
154
                    entry_glyph_anchor,
155
                ) => {
156
                    // Anchor alignment can result in horizontal or vertical positioning adjustments,
157
                    // or both. Note that the positioning effects in the text-layout direction
158
                    // (horizontal, for horizontal layout) work differently than for the cross-stream
159
                    // direction (vertical, in horizontal layout):
160
                    //
161
                    // * For adjustments in the line-layout direction, the layout engine adjusts the
162
                    //   advance of the first glyph (in logical order). This effectively moves the
163
                    //   second glyph relative to the first so that the anchors are aligned in that
164
                    //   direction.
165
                    // * For the cross-stream direction, placement of one glyph is adjusted to make
166
                    //   the anchors align. Which glyph is adjusted is determined by the RIGHT_TO_LEFT
167
                    //   flag in the parent lookup table: if the RIGHT_TO_LEFT flag is clear, the
168
                    //   second glyph is adjusted to align anchors with the first glyph; if the
169
                    //   RIGHT_TO_LEFT flag is set, the first glyph is adjusted to align anchors with
170
                    //   the second glyph.
171
                    //
172
                    // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-3-cursive-attachment-positioning-subtable
173

            
174
                    // First glyph in logical order is the one with the lower index
175
                    let (first_glyph_index, second_glyph_index) = if i < exit_glyph_index {
176
                        (i, exit_glyph_index)
177
                    } else {
178
                        (exit_glyph_index, i)
179
                    };
180

            
181
                    // Line-layout direction
182
                    // TODO: Handle vertical text
183
                    match self.direction {
184
                        TextDirection::LeftToRight => {
185
                            positions[first_glyph_index].hori_advance =
186
                                i32::from(entry_glyph_anchor.x)
187
                        }
188
                        TextDirection::RightToLeft => {
189
                            positions[first_glyph_index].hori_advance +=
190
                                i32::from(entry_glyph_anchor.x)
191
                        }
192
                    }
193

            
194
                    // Cross-stream direction
195
                    let dy = i32::from(exit_glyph_anchor.y) - i32::from(entry_glyph_anchor.y);
196
                    if rtl_flag {
197
                        positions[first_glyph_index].y_offset +=
198
                            dy + positions[second_glyph_index].y_offset;
199
                        if let Some(linked_index) = positions[first_glyph_index].cursive_attachment
200
                        {
201
                            adjust_cursive_chain(
202
                                dy,
203
                                self.direction,
204
                                usize::from(linked_index),
205
                                self.infos,
206
                                positions,
207
                            );
208
                        }
209
                    } else {
210
                        positions[second_glyph_index].y_offset +=
211
                            dy + positions[first_glyph_index].y_offset;
212
                        if let Some(linked_index) = positions[second_glyph_index].cursive_attachment
213
                        {
214
                            adjust_cursive_chain(
215
                                dy,
216
                                self.direction,
217
                                usize::from(linked_index),
218
                                self.infos,
219
                                positions,
220
                            );
221
                        }
222
                    }
223
                }
224
            }
225
        }
226
    }
227

            
228
    fn position_marks(&self, positions: &mut [GlyphPosition]) {
229
        for (i, info) in self.infos.iter().enumerate() {
230
            match info.placement {
231
                Placement::None
232
                | Placement::Distance(_, _)
233
                | Placement::CursiveAnchor(_, _, _, _) => {}
234
                Placement::MarkAnchor(base_index, _, _) => {
235
                    let base_pos = positions[base_index];
236
                    let (hori_advance_offset, vert_advance_offset) = match self.direction {
237
                        TextDirection::LeftToRight => sum_advance(positions.get(base_index..i)),
238
                        TextDirection::RightToLeft => sum_advance(positions.get(i..base_index)),
239
                    };
240

            
241
                    // Add the x & y offset of the base glyph to the mark
242
                    let position = &mut positions[i];
243
                    position.x_offset += base_pos.x_offset;
244
                    position.y_offset += base_pos.y_offset;
245

            
246
                    // Shift the mark back the advance of the base glyph and glyphs leading to it
247
                    // so that it is positioned above it
248
                    match self.direction {
249
                        TextDirection::LeftToRight => {
250
                            position.x_offset -= hori_advance_offset;
251
                            position.y_offset -= vert_advance_offset;
252
                        }
253
                        TextDirection::RightToLeft => {
254
                            position.x_offset += hori_advance_offset;
255
                            position.y_offset += vert_advance_offset;
256
                        }
257
                    }
258
                }
259
                Placement::MarkOverprint(base_index) => {
260
                    let base_pos = positions[base_index];
261
                    let position = &mut positions[i];
262
                    position.x_offset = base_pos.x_offset;
263
                    position.y_offset = base_pos.y_offset;
264
                }
265
            }
266
        }
267
    }
268
}
269

            
270
impl GlyphPosition {
271
    pub const fn new(hori_advance: i32, vert_advance: i32, x_offset: i32, y_offset: i32) -> Self {
272
        GlyphPosition {
273
            hori_advance,
274
            vert_advance,
275
            x_offset,
276
            y_offset,
277
            cursive_attachment: None,
278
        }
279
    }
280

            
281
    pub fn update(&mut self, hori_advance: i32, vert_advance: i32, x_offset: i32, y_offset: i32) {
282
        self.hori_advance = hori_advance;
283
        self.vert_advance = vert_advance;
284
        self.x_offset = x_offset;
285
        self.y_offset = y_offset;
286
    }
287

            
288
    pub fn update_advance(&mut self, hori_advance: i32, vert_advance: i32) {
289
        self.hori_advance = hori_advance;
290
        self.vert_advance = vert_advance;
291
    }
292
}
293

            
294
impl PartialEq for GlyphPosition {
295
    fn eq(&self, other: &Self) -> bool {
296
        self.hori_advance == other.hori_advance
297
            && self.vert_advance == other.vert_advance
298
            && self.x_offset == other.x_offset
299
            && self.y_offset == other.y_offset
300
    }
301
}
302

            
303
fn adjust_cursive_chain(
304
    delta: i32,
305
    direction: TextDirection,
306
    index: usize,
307
    infos: &[Info],
308
    positions: &mut [GlyphPosition],
309
) {
310
    let position = &mut positions[index];
311
    position.y_offset += delta;
312
    if let Some(next_index) = position.cursive_attachment {
313
        // TODO: prevent cycles
314
        adjust_cursive_chain(delta, direction, usize::from(next_index), infos, positions)
315
    }
316
}
317

            
318
fn sum_advance(positions: Option<&[GlyphPosition]>) -> (i32, i32) {
319
    positions.map_or((0, 0), |p| {
320
        p.iter().fold((0, 0), |(hori, vert), &pos| {
321
            (hori + pos.hori_advance, vert + pos.vert_advance)
322
        })
323
    })
324
}
325

            
326
fn glyph_advance<T: FontTableProvider>(
327
    font: &mut Font<T>,
328
    info: &Info,
329
    vertical: bool,
330
) -> Result<(i32, i32), ParseError> {
331
    let advance = if vertical && is_upright_glyph(info) {
332
        font.vertical_advance(info.get_glyph_index())
333
            .map(i32::from)
334
            .unwrap_or_else(|| {
335
                i32::from(font.hhea_table.ascender) - i32::from(font.hhea_table.descender)
336
            })
337
            + i32::from(info.kerning)
338
    } else {
339
        font.horizontal_advance(info.get_glyph_index())
340
            .map(i32::from)
341
            .ok_or(ParseError::MissingValue)?
342
            + i32::from(info.kerning)
343
    };
344
    Ok(if vertical { (0, advance) } else { (advance, 0) })
345
}
346

            
347
fn is_upright_glyph(info: &Info) -> bool {
348
    info.glyph.is_vert_alt()
349
        || info
350
            .glyph
351
            .unicodes
352
            .first()
353
            .is_some_and(|&ch| is_upright_char(ch))
354
}
355

            
356
#[cfg(test)]
357
mod tests {
358
    use std::error::Error;
359
    use std::path::Path;
360

            
361
    use super::*;
362
    use crate::binary::read::ReadScope;
363
    use crate::font::MatchingPresentation;
364
    use crate::font_data::FontData;
365
    use crate::gsub::{Feature, FeatureMask, FeatureMaskExt};
366
    use crate::tag;
367
    use crate::tests::read_fixture;
368

            
369
    fn get_positions(
370
        text: &str,
371
        font: &str,
372
        script: u32,
373
        lang: u32,
374
        direction: TextDirection,
375
    ) -> Result<Vec<GlyphPosition>, Box<dyn Error>> {
376
        get_positions_with_gpos_features(
377
            text,
378
            font,
379
            script,
380
            lang,
381
            FeatureMask::default_mask(),
382
            direction,
383
            false,
384
        )
385
    }
386

            
387
    fn get_positions_with_gpos_features(
388
        text: &str,
389
        font: &str,
390
        script: u32,
391
        lang: u32,
392
        feature_mask: FeatureMask,
393
        direction: TextDirection,
394
        vertical: bool,
395
    ) -> Result<Vec<GlyphPosition>, Box<dyn Error>> {
396
        let path = Path::new("tests/fonts").join(font);
397
        let data = read_fixture(&path);
398
        let scope = ReadScope::new(&data);
399
        let font_file = scope.read::<FontData<'_>>()?;
400
        let provider = font_file.table_provider(0)?;
401
        let mut font = Font::new(provider)?;
402

            
403
        // Map text to glyphs and then apply font shaping
404
        let glyphs = font.map_glyphs(text, script, MatchingPresentation::NotRequired);
405
        let infos = font
406
            .shape(glyphs, script, Some(lang), feature_mask, &[], None, true)
407
            .map_err(|(err, _info)| err)?;
408

            
409
        let mut layout = GlyphLayout::new(&mut font, &infos, direction, vertical);
410
        layout.glyph_positions().map_err(|err| err.into())
411
    }
412

            
413
    #[test]
414
    fn ltr_kerning() -> Result<(), Box<dyn Error>> {
415
        let script = tag::LATN;
416
        let lang = tag!(b"ENG ");
417
        // V gets kerned closer to A in AV
418
        let positions = get_positions(
419
            "AV AA",
420
            "opentype/Klei.otf",
421
            script,
422
            lang,
423
            TextDirection::LeftToRight,
424
        )?;
425
        let expected = &[
426
            GlyphPosition {
427
                hori_advance: 597,
428
                ..Default::default()
429
            },
430
            GlyphPosition {
431
                hori_advance: 758,
432
                ..Default::default()
433
            },
434
            GlyphPosition {
435
                hori_advance: 280,
436
                ..Default::default()
437
            },
438
            GlyphPosition {
439
                hori_advance: 777,
440
                ..Default::default()
441
            },
442
            GlyphPosition {
443
                hori_advance: 777,
444
                ..Default::default()
445
            },
446
        ];
447
        assert_eq!(positions, expected);
448
        Ok(())
449
    }
450

            
451
    #[test]
452
    fn ltr_mark_attach() -> Result<(), Box<dyn Error>> {
453
        let script = tag::KNDA;
454
        let lang = tag!(b"KAN ");
455
        // U+0CBC is a mark on U+0C9F
456
        let positions = get_positions(
457
            "\u{0C9F}\u{0CBC}",
458
            "noto/NotoSansKannada-Regular.ttf",
459
            script,
460
            lang,
461
            TextDirection::LeftToRight,
462
        )?;
463
        let expected = &[
464
            GlyphPosition {
465
                hori_advance: 1669,
466
                ..Default::default()
467
            },
468
            GlyphPosition {
469
                x_offset: -260,
470
                ..Default::default()
471
            },
472
        ];
473
        assert_eq!(positions, expected);
474
        Ok(())
475
    }
476

            
477
    #[test]
478
    fn ltr_attach_distance() -> Result<(), Box<dyn Error>> {
479
        let script = tag!(b"latn");
480
        let lang = tag!(b"ENG ");
481
        let feature_mask = FeatureMask::default_mask() | Feature::FRAC;
482
        // '⁄' is U+2044 FRACTION SLASH, which when the `frac` GPOS feature is enabled is
483
        // positioned to be under the previous character and above the next.
484
        let positions = get_positions_with_gpos_features(
485
            "1⁄99",
486
            "opentype/SourceCodePro-Regular.otf",
487
            script,
488
            lang,
489
            feature_mask,
490
            TextDirection::LeftToRight,
491
            false,
492
        )?;
493
        let expected = &[
494
            GlyphPosition {
495
                hori_advance: 600,
496
                ..Default::default()
497
            },
498
            GlyphPosition {
499
                hori_advance: 0,
500
                x_offset: -300,
501
                ..Default::default()
502
            },
503
            GlyphPosition {
504
                hori_advance: 600,
505
                ..Default::default()
506
            },
507
            GlyphPosition {
508
                hori_advance: 600,
509
                ..Default::default()
510
            },
511
        ];
512
        assert_eq!(positions, expected);
513
        Ok(())
514
    }
515

            
516
    #[test]
517
    fn ltr_mark_overprint() -> Result<(), Box<dyn Error>> {
518
        let script = tag::LATN;
519
        let lang = tag!(b"ENG ");
520
        // TerminusTTF does not have GPOS or GSUB tables. As a result it hits the fallback mark
521
        // handling code that results in characters belonging to the Nonspacing Mark General
522
        // Category to be Mark::Overprint. Combining characters are examples of such characters.
523
        // This test is 'a' followed by COMBINING TILDE.
524
        let positions = get_positions(
525
            "a\u{0303}",
526
            "opentype/TerminusTTF-4.47.0.ttf",
527
            script,
528
            lang,
529
            TextDirection::LeftToRight,
530
        )?;
531
        let expected = &[
532
            GlyphPosition {
533
                hori_advance: 500,
534
                ..Default::default()
535
            },
536
            GlyphPosition::default(),
537
        ];
538
        assert_eq!(positions, expected);
539
        Ok(())
540
    }
541

            
542
    #[test]
543
    fn ltr_cursive() -> Result<(), Box<dyn Error>> {
544
        let script = tag::KNDA;
545
        let lang = tag!(b"KAN ");
546
        // Text is RTL Arabic with cursive connections
547
        let positions = get_positions(
548
            "ಇನ್ಫ್ಲೆಕ್ಷನ್",
549
            "noto/NotoSansKannada-Regular.ttf",
550
            script,
551
            lang,
552
            TextDirection::LeftToRight,
553
        )?;
554
        // [8=0+1457|256=1+1456|118=1+346|335=1+791|282=7+1176|186=10+2096]
555
        let expected = &[
556
            GlyphPosition {
557
                hori_advance: 1457,
558
                ..Default::default()
559
            },
560
            GlyphPosition {
561
                hori_advance: 1456,
562
                ..Default::default()
563
            },
564
            GlyphPosition {
565
                hori_advance: 346, // This glyph's advance gets adjusted to align with the next one
566
                ..Default::default()
567
            },
568
            GlyphPosition {
569
                hori_advance: 791,
570
                ..Default::default()
571
            },
572
            GlyphPosition {
573
                hori_advance: 1176,
574
                ..Default::default()
575
            },
576
            GlyphPosition {
577
                hori_advance: 2096,
578
                ..Default::default()
579
            },
580
        ];
581
        assert_eq!(positions, expected);
582
        Ok(())
583
    }
584

            
585
    #[test]
586
    fn rtl_cursive() -> Result<(), Box<dyn Error>> {
587
        let script = tag::ARAB;
588
        let lang = tag!(b"URD ");
589
        // Text is RTL Arabic with cursive connections
590
        let positions = get_positions(
591
            "لسان",
592
            "arabic/NafeesNastaleeq.ttf",
593
            script,
594
            lang,
595
            TextDirection::RightToLeft,
596
        )?;
597
        let expected = &[
598
            GlyphPosition {
599
                hori_advance: 391,
600
                y_offset: -409,
601
                ..Default::default()
602
            },
603
            GlyphPosition {
604
                hori_advance: 989,
605
                ..Default::default()
606
            },
607
            GlyphPosition {
608
                hori_advance: 213,
609
                ..Default::default()
610
            },
611
            GlyphPosition {
612
                hori_advance: 1561,
613
                ..Default::default()
614
            },
615
        ];
616
        assert_eq!(positions, expected);
617
        Ok(())
618
    }
619
}