Skip to main content

fmd_font/
shaping.rs

1//! Deterministic shaping of caller-segmented runs. No bidi segmentation,
2//! normalization, font fallback, or host discovery occurs here. Coverage is
3//! Latin and basic Arabic (U+0620..U+064A plus declared combining marks).
4//! GSUB single/ligature and GPOS single/pair/mark-to-base/mark-to-mark are supported;
5//! selected unsupported lookups are refused rather than silently skipped.
6use crate::{Font, be_i16, be_u16, be_u32, find_table_full};
7use std::{collections::BTreeMap, ops::Range};
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Direction {
10    LeftToRight,
11    RightToLeft,
12}
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Feature {
15    pub tag: [u8; 4],
16    pub enabled: bool,
17}
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ShapeOptions<'a> {
20    pub script: [u8; 4],
21    /// OpenType language tag, or `dflt` for the script's default language.
22    pub language: [u8; 4],
23    pub direction: Direction,
24    pub features: &'a [Feature],
25}
26impl Default for ShapeOptions<'_> {
27    fn default() -> Self {
28        Self {
29            script: *b"latn",
30            language: *b"dflt",
31            direction: Direction::LeftToRight,
32            features: &[],
33        }
34    }
35}
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ShapedGlyph {
38    pub glyph_id: u16,
39    /// UTF-8 byte range in the original logical input. A ligature spans every
40    /// contributing scalar; marks share their base's cluster. Visual order is
41    /// increasing for LTR and decreasing for RTL; ranges may repeat.
42    pub cluster: Range<usize>,
43    pub x_advance: i32,
44    pub y_advance: i32,
45    pub x_offset: i32,
46    pub y_offset: i32,
47}
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ShapedRun {
50    pub glyphs: Vec<ShapedGlyph>,
51    pub direction: Direction,
52    source: String,
53}
54impl ShapedRun {
55    /// Original logical text, retained exactly (including ligatures and marks).
56    pub fn logical_text(&self) -> &str {
57        &self.source
58    }
59}
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ShapeErrorKind {
62    UnsupportedScript,
63    UnsupportedLanguage,
64    UnsupportedFeature,
65    UnsupportedLookup,
66    MissingGlyph,
67    UnpositionedMark,
68    MalformedFont,
69    BudgetExceeded,
70}
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ShapeError {
73    pub kind: ShapeErrorKind,
74    pub table: Option<[u8; 4]>,
75    pub offset: Option<usize>,
76    pub text_offset: Option<usize>,
77}
78impl core::fmt::Display for ShapeError {
79    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80        write!(
81            f,
82            "shaping {:?}, table {:?}, offset {:?}, text byte {:?}",
83            self.kind, self.table, self.offset, self.text_offset
84        )
85    }
86}
87impl std::error::Error for ShapeError {}
88fn error(kind: ShapeErrorKind) -> ShapeError {
89    ShapeError {
90        kind,
91        table: None,
92        offset: None,
93        text_offset: None,
94    }
95}
96#[derive(Clone, Copy)]
97struct Table<'a> {
98    d: &'a [u8],
99    tag: [u8; 4],
100    fuel: &'a std::cell::Cell<usize>,
101}
102impl Table<'_> {
103    fn bad(&self, p: usize) -> ShapeError {
104        ShapeError {
105            kind: ShapeErrorKind::MalformedFont,
106            table: Some(self.tag),
107            offset: Some(p),
108            text_offset: None,
109        }
110    }
111    fn u16(&self, p: usize) -> Result<u16, ShapeError> {
112        self.fuel.set(
113            self.fuel
114                .get()
115                .checked_sub(1)
116                .ok_or(error(ShapeErrorKind::BudgetExceeded))?,
117        );
118        be_u16(self.d, p).ok_or(self.bad(p))
119    }
120    fn i16(&self, p: usize) -> Result<i16, ShapeError> {
121        be_i16(self.d, p).ok_or(self.bad(p))
122    }
123    fn offset(&self, base: usize, p: usize) -> Result<usize, ShapeError> {
124        let n = self.u16(p)?;
125        if n == 0 {
126            return Err(self.bad(p));
127        }
128        let o = base + usize::from(n);
129        if o >= self.d.len() {
130            return Err(self.bad(p));
131        }
132        Ok(o)
133    }
134    fn tag(&self, p: usize) -> Result<[u8; 4], ShapeError> {
135        self.d
136            .get(p..p + 4)
137            .and_then(|v| v.try_into().ok())
138            .ok_or(self.bad(p))
139    }
140    fn coverage(&self, p: usize, g: u16) -> Result<Option<usize>, ShapeError> {
141        let count = usize::from(self.u16(p + 2)?);
142        match self.u16(p)? {
143            1 => {
144                let mut last = None;
145                let mut result = None;
146                for i in 0..count {
147                    let id = self.u16(p + 4 + i * 2)?;
148                    if last.is_some_and(|v| v >= id) {
149                        return Err(self.bad(p));
150                    }
151                    if id == g {
152                        result = Some(i);
153                    }
154                    last = Some(id);
155                }
156                Ok(result)
157            }
158            2 => {
159                let mut previous = None;
160                let mut expected = 0usize;
161                let mut result = None;
162                for i in 0..count {
163                    let r = p + 4 + i * 6;
164                    let a = self.u16(r)?;
165                    let b = self.u16(r + 2)?;
166                    let start = usize::from(self.u16(r + 4)?);
167                    if a > b || previous.is_some_and(|v| v >= a) || start != expected {
168                        return Err(self.bad(r));
169                    }
170                    if (a..=b).contains(&g) {
171                        result = Some(start + usize::from(g - a));
172                    }
173                    expected += usize::from(b - a) + 1;
174                    if expected > 65536 {
175                        return Err(self.bad(r));
176                    }
177                    previous = Some(b);
178                }
179                Ok(result)
180            }
181            _ => Err(self.bad(p)),
182        }
183    }
184    fn class(&self, p: usize, gid: u16) -> Result<usize, ShapeError> {
185        match self.u16(p)? {
186            1 => {
187                let first = self.u16(p + 2)?;
188                let count = usize::from(self.u16(p + 4)?);
189                if gid < first || usize::from(gid - first) >= count {
190                    return Ok(0);
191                }
192                Ok(usize::from(self.u16(p + 6 + usize::from(gid - first) * 2)?))
193            }
194            2 => {
195                let mut previous = None;
196                let mut result = 0;
197                for i in 0..usize::from(self.u16(p + 2)?) {
198                    let r = p + 4 + i * 6;
199                    let a = self.u16(r)?;
200                    let b = self.u16(r + 2)?;
201                    if a > b || previous.is_some_and(|v| v >= a) {
202                        return Err(self.bad(r));
203                    }
204                    if (a..=b).contains(&gid) {
205                        result = usize::from(self.u16(r + 4)?);
206                    }
207                    previous = Some(b);
208                }
209                Ok(result)
210            }
211            _ => Err(self.bad(p)),
212        }
213    }
214    fn anchor(&self, p: usize) -> Result<(i32, i32), ShapeError> {
215        if self.u16(p)? != 1 {
216            return Err(ShapeError {
217                kind: ShapeErrorKind::UnsupportedLookup,
218                ..self.bad(p)
219            });
220        }
221        Ok((i32::from(self.i16(p + 2)?), i32::from(self.i16(p + 4)?)))
222    }
223}
224#[derive(Clone)]
225struct Item {
226    g: ShapedGlyph,
227    mark: bool,
228    positioned: bool,
229    form: [u8; 4],
230}
231fn is_mark(c: char) -> bool {
232    matches!(c as u32,0x0300..=0x036f|0x064b..=0x065f|0x0670)
233}
234fn joining(c: char) -> u8 {
235    // Unicode 17 ArabicShaping.txt: basic Arabic right/dual joining repertoire.
236    match c as u32 {
237        0x0620
238        | 0x0626
239        | 0x0628
240        | 0x062a..=0x062e
241        | 0x0633..=0x063f
242        | 0x0640..=0x0647
243        | 0x0649..=0x064a => 2,
244        0x0622..=0x0625 | 0x0627 | 0x0629 | 0x062f..=0x0632 | 0x0648 => 1,
245        _ => 0,
246    }
247}
248fn features(
249    t: Table<'_>,
250    opts: &ShapeOptions<'_>,
251) -> Result<BTreeMap<[u8; 4], Vec<u16>>, ShapeError> {
252    if t.u16(0)? != 1 || t.u16(2)? != 0 {
253        return Err(ShapeError {
254            kind: ShapeErrorKind::UnsupportedLookup,
255            ..t.bad(0)
256        });
257    }
258    let scripts = t.offset(0, 4)?;
259    let list = t.offset(0, 6)?;
260    let mut script = None;
261    for i in 0..usize::from(t.u16(scripts)?) {
262        let r = scripts + 2 + i * 6;
263        if t.tag(r)? == opts.script {
264            script = Some(t.offset(scripts, r + 4)?);
265        }
266    }
267    let Some(script) = script else {
268        return Ok(BTreeMap::new());
269    };
270    let mut lang = if opts.language == *b"dflt" && t.u16(script)? != 0 {
271        Some(t.offset(script, script)?)
272    } else {
273        None
274    };
275    for i in 0..usize::from(t.u16(script + 2)?) {
276        let r = script + 4 + i * 6;
277        if t.tag(r)? == opts.language {
278            lang = Some(t.offset(script, r + 4)?);
279        }
280    }
281    let lang = lang.ok_or(error(ShapeErrorKind::UnsupportedLanguage))?;
282    if t.u16(lang)? != 0 {
283        return Err(t.bad(lang));
284    }
285    let mut ids = Vec::new();
286    let required = t.u16(lang + 2)?;
287    if required != u16::MAX {
288        ids.push(required);
289    }
290    for i in 0..usize::from(t.u16(lang + 4)?) {
291        ids.push(t.u16(lang + 6 + i * 2)?);
292    }
293    let n = t.u16(list)?;
294    let mut out: BTreeMap<[u8; 4], Vec<u16>> = BTreeMap::new();
295    for id in ids {
296        if id >= n {
297            return Err(t.bad(list));
298        }
299        let r = list + 2 + usize::from(id) * 6;
300        let tag = t.tag(r)?;
301        if id == required
302            && (![
303                *b"ccmp", *b"locl", *b"rlig", *b"liga", *b"isol", *b"init", *b"medi", *b"fina",
304                *b"kern", *b"mark", *b"mkmk",
305            ]
306            .contains(&tag)
307                || !enabled(opts, tag))
308        {
309            return Err(ShapeError {
310                kind: ShapeErrorKind::UnsupportedFeature,
311                ..t.bad(r)
312            });
313        }
314        let f = t.offset(list, r + 4)?;
315        if t.u16(f)? != 0 {
316            return Err(ShapeError {
317                kind: ShapeErrorKind::UnsupportedFeature,
318                ..t.bad(f)
319            });
320        }
321        let dest = out.entry(tag).or_default();
322        for i in 0..usize::from(t.u16(f + 2)?) {
323            dest.push(t.u16(f + 4 + i * 2)?);
324        }
325    }
326    Ok(out)
327}
328fn table<'a>(
329    font: &'a Font,
330    tag: [u8; 4],
331    fuel: &'a std::cell::Cell<usize>,
332) -> Result<Option<Table<'a>>, ShapeError> {
333    let Some((o, n)) = find_table_full(&font.data, &tag) else {
334        return Ok(None);
335    };
336    let d = font
337        .data
338        .get(
339            o..o.checked_add(n)
340                .ok_or(error(ShapeErrorKind::MalformedFont))?,
341        )
342        .ok_or(error(ShapeErrorKind::MalformedFont))?;
343    Ok(Some(Table { d, tag, fuel }))
344}
345fn enabled(opts: &ShapeOptions<'_>, tag: [u8; 4]) -> bool {
346    opts.features
347        .iter()
348        .find(|f| f.tag == tag)
349        .is_none_or(|f| f.enabled)
350}
351fn spend(fuel: &mut usize) -> Result<(), ShapeError> {
352    *fuel = fuel
353        .checked_sub(1)
354        .ok_or(error(ShapeErrorKind::BudgetExceeded))?;
355    Ok(())
356}
357fn lookup(t: Table<'_>, id: u16) -> Result<(u16, u16, Vec<usize>), ShapeError> {
358    let list = t.offset(0, 8)?;
359    if id >= t.u16(list)? {
360        return Err(t.bad(list));
361    }
362    let p = t.offset(list, list + 2 + usize::from(id) * 2)?;
363    let kind = t.u16(p)?;
364    let flags = t.u16(p + 2)?;
365    if flags & !8 != 0 {
366        return Err(ShapeError {
367            kind: ShapeErrorKind::UnsupportedLookup,
368            ..t.bad(p + 2)
369        });
370    }
371    let mut subtables = Vec::new();
372    for i in 0..usize::from(t.u16(p + 4)?) {
373        subtables.push(t.offset(p, p + 6 + i * 2)?);
374    }
375    Ok((kind, flags, subtables))
376}
377fn extension(t: Table<'_>, kind: u16, p: usize) -> Result<(u16, usize), ShapeError> {
378    let wrapper = if t.tag == *b"GSUB" { 7 } else { 9 };
379    if kind != wrapper {
380        return Ok((kind, p));
381    }
382    if t.u16(p)? != 1 {
383        return Err(t.bad(p));
384    }
385    let actual = t.u16(p + 2)?;
386    if actual == wrapper {
387        return Err(t.bad(p));
388    }
389    let offset = be_u32(t.d, p + 4).ok_or(t.bad(p + 4))? as usize;
390    let next = p
391        .checked_add(offset)
392        .filter(|&x| x < t.d.len())
393        .ok_or(t.bad(p + 4))?;
394    Ok((actual, next))
395}
396fn substitute(
397    t: Table<'_>,
398    ids: &[u16],
399    items: &mut Vec<Item>,
400    form: Option<[u8; 4]>,
401    fuel: &mut usize,
402) -> Result<(), ShapeError> {
403    for &id in ids {
404        let (kind, flags, subs) = lookup(t, id)?;
405        for item_index in 0..items.len() {
406            // Index may become out of range after ligature contraction.
407            if item_index >= items.len() {
408                break;
409            }
410            spend(fuel)?;
411            if form.is_some_and(|f| items[item_index].form != f)
412                || flags & 8 != 0 && items[item_index].mark
413            {
414                continue;
415            }
416            for &sub in &subs {
417                let (kind, p) = extension(t, kind, sub)?;
418                if !matches!(kind, 1 | 4) {
419                    return Err(ShapeError {
420                        kind: ShapeErrorKind::UnsupportedLookup,
421                        ..t.bad(p)
422                    });
423                }
424                let coverage = t.offset(p, p + 2)?;
425                let Some(ci) = t.coverage(coverage, items[item_index].g.glyph_id)? else {
426                    continue;
427                };
428                if kind == 1 {
429                    let gid = match t.u16(p)? {
430                        1 => items[item_index].g.glyph_id.wrapping_add(t.u16(p + 4)?),
431                        2 => {
432                            if ci >= usize::from(t.u16(p + 4)?) {
433                                return Err(t.bad(p));
434                            }
435                            t.u16(p + 6 + ci * 2)?
436                        }
437                        _ => return Err(t.bad(p)),
438                    };
439                    items[item_index].g.glyph_id = gid;
440                    break;
441                }
442                if t.u16(p)? != 1 || ci >= usize::from(t.u16(p + 4)?) {
443                    return Err(t.bad(p));
444                }
445                let set = t.offset(p, p + 6 + ci * 2)?;
446                let mut matched = false;
447                for n in 0..usize::from(t.u16(set)?) {
448                    spend(fuel)?;
449                    let lig = t.offset(set, set + 2 + n * 2)?;
450                    let count = usize::from(t.u16(lig + 2)?);
451                    if count < 2 {
452                        return Err(t.bad(lig));
453                    }
454                    let mut indices = vec![item_index];
455                    let mut next = item_index + 1;
456                    for j in 1..count {
457                        while next < items.len() && flags & 8 != 0 && items[next].mark {
458                            next += 1;
459                        }
460                        if next >= items.len()
461                            || items[next].g.glyph_id != t.u16(lig + 2 + j * 2)?
462                        {
463                            break;
464                        }
465                        indices.push(next);
466                        next += 1;
467                    }
468                    if indices.len() == count {
469                        let end = items[*indices.last().ok_or(t.bad(lig))?].g.cluster.end;
470                        items[item_index].g.cluster.end = end;
471                        items[item_index].g.glyph_id = t.u16(lig)?;
472                        // Skipped marks retain the merged source cluster; positioning remains mandatory.
473                        let cluster = items[item_index].g.cluster.clone();
474                        for item in &mut items[item_index..next] {
475                            item.g.cluster = cluster.clone();
476                        }
477                        for &i in indices[1..].iter().rev() {
478                            items.remove(i);
479                        }
480                        matched = true;
481                        break;
482                    }
483                }
484                if matched {
485                    break;
486                }
487            }
488        }
489    }
490    Ok(())
491}
492fn value(t: Table<'_>, p: usize, format: u16) -> Result<([i32; 4], usize), ShapeError> {
493    if format & !15 != 0 {
494        return Err(ShapeError {
495            kind: ShapeErrorKind::UnsupportedLookup,
496            ..t.bad(p)
497        });
498    }
499    let mut values = [0; 4];
500    let mut q = p;
501    for (i, v) in values.iter_mut().enumerate() {
502        if format & (1 << i) != 0 {
503            *v = i32::from(t.i16(q)?);
504            q += 2;
505        }
506    }
507    Ok((values, q))
508}
509fn apply(g: &mut ShapedGlyph, v: [i32; 4]) -> Result<(), ShapeError> {
510    for (dst, delta) in [
511        (&mut g.x_offset, v[0]),
512        (&mut g.y_offset, v[1]),
513        (&mut g.x_advance, v[2]),
514        (&mut g.y_advance, v[3]),
515    ] {
516        *dst = dst
517            .checked_add(delta)
518            .ok_or(error(ShapeErrorKind::BudgetExceeded))?;
519    }
520    Ok(())
521}
522
523fn position(
524    t: Table<'_>,
525    ids: &[u16],
526    items: &mut [Item],
527    direction: Direction,
528    fuel: &mut usize,
529) -> Result<(), ShapeError> {
530    for &id in ids {
531        let (kind, flags, subs) = lookup(t, id)?;
532        for step in 0..items.len() {
533            let i = if direction == Direction::RightToLeft {
534                items.len() - 1 - step
535            } else {
536                step
537            };
538            spend(fuel)?;
539            if flags & 8 != 0 && items[i].mark {
540                continue;
541            }
542            for &sub in &subs {
543                let (kind, p) = extension(t, kind, sub)?;
544                if !matches!(kind, 1 | 2 | 4 | 6) {
545                    return Err(ShapeError {
546                        kind: ShapeErrorKind::UnsupportedLookup,
547                        ..t.bad(p)
548                    });
549                }
550                let Some(ci) = t.coverage(t.offset(p, p + 2)?, items[i].g.glyph_id)? else {
551                    continue;
552                };
553                if kind == 1 {
554                    let format = t.u16(p + 4)?;
555                    let q = match t.u16(p)? {
556                        1 => p + 6,
557                        2 => {
558                            if ci >= usize::from(t.u16(p + 6)?) {
559                                return Err(t.bad(p));
560                            }
561                            p + 8 + ci * format.count_ones() as usize * 2
562                        }
563                        _ => return Err(t.bad(p)),
564                    };
565                    let (v, _) = value(t, q, format)?;
566                    apply(&mut items[i].g, v)?;
567                    break;
568                }
569                if kind == 2 {
570                    let pair_format = t.u16(p)?;
571                    if !matches!(pair_format, 1 | 2) {
572                        return Err(t.bad(p));
573                    }
574                    let candidate = match direction {
575                        Direction::LeftToRight => {
576                            (i + 1..items.len()).find(|&j| flags & 8 == 0 || !items[j].mark)
577                        }
578                        Direction::RightToLeft => {
579                            (0..i).rev().find(|&j| flags & 8 == 0 || !items[j].mark)
580                        }
581                    };
582                    let Some(j) = candidate else {
583                        continue;
584                    };
585                    let f1 = t.u16(p + 4)?;
586                    let f2 = t.u16(p + 6)?;
587                    if pair_format == 2 {
588                        let a = t.class(t.offset(p, p + 8)?, items[i].g.glyph_id)?;
589                        let b = t.class(t.offset(p, p + 10)?, items[j].g.glyph_id)?;
590                        let n1 = usize::from(t.u16(p + 12)?);
591                        let n2 = usize::from(t.u16(p + 14)?);
592                        if n1.checked_mul(n2).is_none_or(|n| n > 1_000_000) {
593                            return Err(error(ShapeErrorKind::BudgetExceeded));
594                        }
595                        if a >= n1 || b >= n2 {
596                            return Err(t.bad(p));
597                        }
598                        let size = (f1.count_ones() + f2.count_ones()) as usize * 2;
599                        let q = p + 16 + (a * n2 + b) * size;
600                        let (v, next) = value(t, q, f1)?;
601                        let (w, _) = value(t, next, f2)?;
602                        apply(&mut items[i].g, v)?;
603                        apply(&mut items[j].g, w)?;
604                        break;
605                    }
606                    if ci >= usize::from(t.u16(p + 8)?) {
607                        return Err(t.bad(p));
608                    }
609                    let set = t.offset(p, p + 10 + ci * 2)?;
610                    let mut q = set + 2;
611                    let mut found = false;
612                    for _ in 0..t.u16(set)? {
613                        spend(fuel)?;
614                        let gid = t.u16(q)?;
615                        let (a, next) = value(t, q + 2, f1)?;
616                        let (b, next) = value(t, next, f2)?;
617                        q = next;
618                        if gid == items[j].g.glyph_id {
619                            apply(&mut items[i].g, a)?;
620                            apply(&mut items[j].g, b)?;
621                            found = true;
622                            break;
623                        }
624                    }
625                    if found {
626                        break;
627                    }
628                    continue;
629                }
630                if t.u16(p)? != 1 {
631                    return Err(t.bad(p));
632                }
633                if !items[i].mark {
634                    return Err(t.bad(p));
635                }
636                let candidate = match direction {
637                    Direction::LeftToRight => (0..i).rev().find(|&j| items[j].mark == (kind == 6)),
638                    Direction::RightToLeft => {
639                        (i + 1..items.len()).find(|&j| items[j].mark == (kind == 6))
640                    }
641                };
642                let Some(j) = candidate else {
643                    continue;
644                };
645                if kind == 6 && items[j.min(i) + 1..j.max(i)].iter().any(|v| !v.mark) {
646                    continue;
647                }
648                let Some(bi) = t.coverage(t.offset(p, p + 4)?, items[j].g.glyph_id)? else {
649                    continue;
650                };
651                let classes = usize::from(t.u16(p + 6)?);
652                if classes > 256 {
653                    return Err(error(ShapeErrorKind::BudgetExceeded));
654                }
655                let marks = t.offset(p, p + 8)?;
656                let bases = t.offset(p, p + 10)?;
657                if ci >= usize::from(t.u16(marks)?) || bi >= usize::from(t.u16(bases)?) {
658                    return Err(t.bad(p));
659                }
660                let class = usize::from(t.u16(marks + 2 + ci * 4)?);
661                if class >= classes {
662                    return Err(t.bad(marks));
663                }
664                let ma = t.anchor(t.offset(marks, marks + 4 + ci * 4)?)?;
665                let anchor_slot = bases + 2 + (bi * classes + class) * 2;
666                if t.u16(anchor_slot)? == 0 {
667                    continue;
668                }
669                let ba = t.anchor(t.offset(bases, anchor_slot)?)?;
670                let delta = if j < i {
671                    -items[j..i]
672                        .iter()
673                        .map(|v| i64::from(v.g.x_advance))
674                        .sum::<i64>()
675                } else {
676                    items[i..j]
677                        .iter()
678                        .map(|v| i64::from(v.g.x_advance))
679                        .sum::<i64>()
680                };
681                items[i].g.x_offset = i32::try_from(
682                    delta + i64::from(items[j].g.x_offset) + i64::from(ba.0) - i64::from(ma.0),
683                )
684                .map_err(|_| error(ShapeErrorKind::BudgetExceeded))?;
685                items[i].g.y_offset = i32::try_from(
686                    i64::from(items[j].g.y_offset) + i64::from(ba.1) - i64::from(ma.1),
687                )
688                .map_err(|_| error(ShapeErrorKind::BudgetExceeded))?;
689                items[i].positioned = true;
690                break;
691            }
692        }
693    }
694    Ok(())
695}
696impl Font {
697    /// Shape a single explicitly segmented run. See module docs for exact coverage.
698    pub fn shape(&self, text: &str, opts: &ShapeOptions<'_>) -> Result<ShapedRun, ShapeError> {
699        self.validate_font_metrics().map_err(|e| ShapeError {
700            kind: ShapeErrorKind::MalformedFont,
701            table: Some(e.table),
702            offset: e.offset,
703            text_offset: None,
704        })?;
705        let arabic = opts.script == *b"arab";
706        if opts.script != *b"latn" && !arabic {
707            return Err(error(ShapeErrorKind::UnsupportedScript));
708        }
709        if (arabic && opts.direction != Direction::RightToLeft)
710            || (!arabic && opts.direction != Direction::LeftToRight)
711        {
712            return Err(error(ShapeErrorKind::UnsupportedScript));
713        }
714        if text.len() > 65536 {
715            return Err(error(ShapeErrorKind::BudgetExceeded));
716        }
717        let allowed = [
718            *b"ccmp", *b"locl", *b"rlig", *b"liga", *b"kern", *b"mark", *b"mkmk", *b"isol",
719            *b"init", *b"medi", *b"fina",
720        ];
721        for (i, f) in opts.features.iter().enumerate() {
722            if !allowed.contains(&f.tag) || opts.features[..i].iter().any(|v| v.tag == f.tag) {
723                return Err(error(ShapeErrorKind::UnsupportedFeature));
724            }
725        }
726        let chars: Vec<_> = text.char_indices().collect();
727        if chars.len() > 4096 {
728            return Err(error(ShapeErrorKind::BudgetExceeded));
729        }
730        let mut items: Vec<Item> = Vec::new();
731        for (i, &(offset, ch)) in chars.iter().enumerate() {
732            let cp = ch as u32;
733            let mark = is_mark(ch);
734            let covered = if arabic {
735                matches!(cp,0x20..=0x40|0x5b..=0x60|0x7b..=0x7e|0x0620..=0x064a|0x0660..=0x0669)
736                    || mark
737            } else {
738                matches!(cp,0x20..=0x024f|0x0300..=0x036f)
739            };
740            if !covered {
741                return Err(ShapeError {
742                    text_offset: Some(offset),
743                    ..error(ShapeErrorKind::UnsupportedScript)
744                });
745            }
746            let gid = self.glyph_index(ch);
747            if gid == 0 {
748                return Err(ShapeError {
749                    text_offset: Some(offset),
750                    ..error(ShapeErrorKind::MissingGlyph)
751                });
752            }
753            if gid >= self.num_glyphs {
754                return Err(error(ShapeErrorKind::MalformedFont));
755            }
756            let end = offset + ch.len_utf8();
757            let start = if mark {
758                items.last().map_or(offset, |v| v.g.cluster.start)
759            } else {
760                offset
761            };
762            if mark && i > 64 && chars[i - 64..i].iter().all(|(_, c)| is_mark(*c)) {
763                return Err(error(ShapeErrorKind::BudgetExceeded));
764            }
765            if mark {
766                for prev in items
767                    .iter_mut()
768                    .rev()
769                    .take_while(|v| v.g.cluster.start == start)
770                {
771                    prev.g.cluster.end = end;
772                }
773            }
774            let mut form = *b"isol";
775            if arabic && !mark {
776                let current = joining(ch);
777                let prev = chars[..i]
778                    .iter()
779                    .rev()
780                    .find(|(_, c)| !is_mark(*c))
781                    .map_or(0, |(_, c)| joining(*c));
782                let next = chars[i + 1..]
783                    .iter()
784                    .find(|(_, c)| !is_mark(*c))
785                    .map_or(0, |(_, c)| joining(*c));
786                let before = prev == 2 && current != 0;
787                let after = current == 2 && next != 0;
788                form = match (before, after) {
789                    (true, true) => *b"medi",
790                    (true, false) => *b"fina",
791                    (false, true) => *b"init",
792                    _ => *b"isol",
793                };
794            }
795            items.push(Item {
796                g: ShapedGlyph {
797                    glyph_id: gid,
798                    cluster: start..end,
799                    x_advance: 0,
800                    y_advance: 0,
801                    x_offset: 0,
802                    y_offset: 0,
803                },
804                mark,
805                positioned: false,
806                form,
807            });
808        }
809        let mut fuel = 1_000_000;
810        let read_budget = std::cell::Cell::new(2_000_000);
811        let gsub = table(self, *b"GSUB", &read_budget)?;
812        let gpos = table(self, *b"GPOS", &read_budget)?;
813        let gs = if let Some(t) = gsub {
814            features(t, opts)?
815        } else {
816            BTreeMap::new()
817        };
818        let gp = if let Some(t) = gpos {
819            features(t, opts)?
820        } else {
821            BTreeMap::new()
822        };
823        if opts.language != *b"dflt" && gs.is_empty() && gp.is_empty() {
824            return Err(error(ShapeErrorKind::UnsupportedLanguage));
825        }
826        if arabic && gp.contains_key(b"curs") {
827            return Err(error(ShapeErrorKind::UnsupportedLookup));
828        }
829        if arabic {
830            for item in &items {
831                if item.form != *b"isol"
832                    && !item.mark
833                    && enabled(opts, item.form)
834                    && !gs.contains_key(&item.form)
835                {
836                    return Err(error(ShapeErrorKind::UnsupportedFeature));
837                }
838            }
839        }
840        for f in opts.features {
841            if f.enabled && !gs.contains_key(&f.tag) && !gp.contains_key(&f.tag) {
842                return Err(error(ShapeErrorKind::UnsupportedFeature));
843            }
844        }
845        if let Some(t) = gsub {
846            for tag in [
847                *b"ccmp", *b"locl", *b"isol", *b"fina", *b"medi", *b"init", *b"rlig", *b"liga",
848            ] {
849                if enabled(opts, tag)
850                    && let Some(ids) = gs.get(&tag)
851                {
852                    let form = if [*b"isol", *b"fina", *b"medi", *b"init"].contains(&tag) {
853                        if !arabic {
854                            continue;
855                        }
856                        Some(tag)
857                    } else {
858                        None
859                    };
860                    substitute(t, ids, &mut items, form, &mut fuel)?;
861                }
862            }
863        }
864        for item in &mut items {
865            if item.g.glyph_id >= self.num_glyphs {
866                return Err(error(ShapeErrorKind::MalformedFont));
867            }
868            item.g.x_advance = if item.mark {
869                0
870            } else {
871                i32::from(self.advance_width(item.g.glyph_id))
872            };
873        }
874        if opts.direction == Direction::RightToLeft {
875            items.reverse();
876        }
877        if let Some(t) = gpos {
878            for tag in [*b"kern", *b"mark", *b"mkmk"] {
879                if enabled(opts, tag)
880                    && let Some(ids) = gp.get(&tag)
881                {
882                    position(t, ids, &mut items, opts.direction, &mut fuel)?;
883                }
884            }
885        }
886        for item in &items {
887            if item.mark && !item.positioned {
888                return Err(ShapeError {
889                    text_offset: Some(item.g.cluster.start),
890                    ..error(ShapeErrorKind::UnpositionedMark)
891                });
892            }
893        }
894        Ok(ShapedRun {
895            glyphs: items.into_iter().map(|i| i.g).collect(),
896            direction: opts.direction,
897            source: text.to_owned(),
898        })
899    }
900}