collective

Browse / github.com/storytold/deckcraft / patch

Fonts: draw MicroType Express embedded fonts

Fonts: draw MicroType Express embedded fonts Embedded fonts that PowerPoint writes (Embedded OpenType with MicroType Express compression, flag 0x4, optionally XOR-obscured with 0x10000000) are now decompressed and used to draw, so text in a deck that embeds a font no longer reflows into a stand-in. Closes the "Embedded MicroType Express font subsets aren't drawn" gap (F10). No other open PR covers this (checked open PRs for MicroType, MTX and embedded fonts; #1206 isolates embedded fonts per presentation and is unrelated to decompression). ## What - `crates/fonts/src/mtx.rs`: decoder written from the W3C member submission "MicroType Ex

Status
open
Score
0.00
Adopters
0 other operators
Author
Harrison-Booth (imported from pull request #1463, open)
Base
88c2ceff5fab9d34dd262a8184bbbd61ca369b97
Size
+1256 −23 in 8 files
Review
pending
Shared
2026-10-28
Id
f11516d167f5ba9dd90db421

How it went

No other operator has reported using it yet.

Diff

raw · ROADMAP.md crates/engine/src/fonts.rs crates/fonts/src/lib.rs crates/fonts/src/mtx.rs crates/fonts/src/tests.rs docs/file-format-parity.md docs/gaps.md docs/roadmap.md

diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -120,8 +120,8 @@ Ranked; detail and the full beta plan in [docs/roadmap.md](docs/roadmap.md). 2. **PPTX on real decks:** the owned corpus now round-trips comments, links and actions, custom    shows, rotated groups, inherited placeholders, text effects and body layout, theme effects,    fills, bevels, connectors, tables, charts, animations, media, sections, masters, 3-D models-   and animation bounce end. Still open: a corpus of real decks, #70, #74, #77, EMF+ records,-   MicroType Express font subsets and SVG. 36–64 h.+   and animation bounce end. Still open: a corpus of real decks, #70, #74, #77, EMF+ records+   and SVG. 36–64 h. 3. **Localization:** catalog, system language (#15), zh-Hans, es, fr, de, ja with IME. 27–46 h. 4. **Output:** vector PDF, printing. 10–16 h. 5. **Depth that users hit:** font weights (#69), chart data and elements, Animation Pane timeline,@@ -209,6 +209,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-13 | Video seeks and loops keep newly decoded frames and end-of-stream notifications; stale generations are filtered when consumed (upstream #416). Deterministic queue coverage and repeated seeks across H.264, HEVC, VP9 and AV1 | | 2026-10-14 | Presenter View handles the same keyboard navigation as the audience window, consumes handled keys before focused buttons, and remains available at the end screen. Regression tests cover presenter-only input, single-step navigation and Escape | | 2026-10-13 | EMF and WMF rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing. EMF+ records are still skipped |+| 2026-10-27 | MicroType Express embedded fonts (as PowerPoint writes them) are decompressed and drawn | | 2026-10-13 | Embedded fonts (`p:embeddedFontLst`) are read, written and used to draw. TrueType, OpenType and Embedded OpenType faces load; MicroType Express subsets are kept for the round trip and still draw in a stand-in | | 2026-10-13 | Codec parsers (#426): H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic, so a hostile number is a format error instead of an overflow | | 2026-10-13 | WordArt Transform: Shape Format ▸ Text Effects shows the 40 text warps and No Transform; `format.textWarp` applies one to the selected text. Adjust handles are still open |diff --git a/crates/engine/src/fonts.rs b/crates/engine/src/fonts.rs--- a/crates/engine/src/fonts.rs+++ b/crates/engine/src/fonts.rs@@ -8,15 +8,14 @@ use deckcraft_model::{Presentation, Shape, ShapeKind};  /// Load each embedded face into the font database, so text in that family draws with the file's /// font on a machine that doesn't have it. A face already loaded (bundled or installed) stays;-/// the part bytes are kept either way. MicroType Express subsets have no TrueType or OpenType-/// font to load and are left for the round trip.+/// the part bytes are kept either way. pub fn install_embedded_fonts(p: &Presentation) {     let db = FontDb::global();     for font in &p.embedded_fonts {         for id in font.faces().into_iter().flatten() {             let Some(item) = p.media(id) else { continue };             let Some(sfnt) = deckcraft_fonts::embedded_sfnt(&item.data) else { continue };-            db.add_font(sfnt.to_vec());+            db.add_font(sfnt.into_owned());         }     } }diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs--- a/crates/fonts/src/lib.rs+++ b/crates/fonts/src/lib.rs@@ -8,8 +8,11 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] +use std::borrow::Cow;+ mod fallback; mod fontdb;+mod mtx; pub mod script; mod shape; @@ -44,10 +47,11 @@ pub fn japanese_fonts() -> impl Iterator<Item = &'static CraftFont> { /// (`application/x-fontdata`). /// /// The part is the font itself, or an Embedded OpenType wrapper around it (the `0x504C` magic-/// at byte 34, from the W3C Embedded OpenType submission). A MicroType Express subset is neither,-/// so this returns none; the original part stays in the file for the round trip.-pub fn embedded_sfnt(part: &[u8]) -> Option<&[u8]> {-    eot_payload(part).or_else(|| sfnt_or_collection(part).then_some(part))+/// at byte 34, from the W3C Embedded OpenType submission). The wrapped font may be XOR-obscured+/// and MicroType Express compressed, which is how PowerPoint embeds fonts; both are undone. Data+/// that is none of these returns none, and the original part stays in the file for the round trip.+pub fn embedded_sfnt(part: &[u8]) -> Option<Cow<'_, [u8]>> {+    eot_payload(part).or_else(|| sfnt_or_collection(part).then_some(Cow::Borrowed(part))) }  fn sfnt_or_collection(b: &[u8]) -> bool {@@ -60,7 +64,7 @@ fn sfnt_or_collection(b: &[u8]) -> bool { }  /// Font data at the end of an Embedded OpenType blob, when the header is one.-fn eot_payload(b: &[u8]) -> Option<&[u8]> {+fn eot_payload(b: &[u8]) -> Option<Cow<'_, [u8]>> {     if b.len() < 82 || u16::from_le_bytes([b[34], b[35]]) != 0x504C {         return None;     }@@ -70,11 +74,18 @@ fn eot_payload(b: &[u8]) -> Option<&[u8]> {     }     let eot_size = u32::from_le_bytes(b.get(0..4)?.try_into().ok()?) as usize;     let font_size = u32::from_le_bytes(b.get(4..8)?.try_into().ok()?) as usize;+    let flags = u32::from_le_bytes(b.get(12..16)?.try_into().ok()?);     if font_size < 12 || eot_size < font_size.saturating_add(82) || eot_size > b.len() {         return None;     }-    let font = b.get(eot_size - font_size..eot_size)?;-    sfnt_or_collection(font).then_some(font)+    let mut font = Cow::Borrowed(b.get(eot_size - font_size..eot_size)?);+    if flags & 0x1000_0000 != 0 {+        font = font.iter().map(|x| x ^ 0x50).collect();+    }+    if flags & 0x4 != 0 {+        font = mtx::decompress(&font)?.into();+    }+    sfnt_or_collection(&font).then_some(font) }  /// Japanese faces in document-fallback order: Mincho (serif) families first, matching thediff --git a/crates/fonts/src/mtx.rs b/crates/fonts/src/mtx.rsnew file mode 100644--- /dev/null+++ b/crates/fonts/src/mtx.rs@@ -0,0 +1,1225 @@+//! MicroType Express (MTX) decompression, written from the public W3C member submission+//! "MicroType Express (MTX) Font Format" (2008). An MTX font is a TrueType font in the Compact Table+//! Format (CTF) split into three LZCOMP streams; [`decompress`] turns it back into a TrueType font.+//!+//! Two device-metric tables, `hdmx` and `VDMX`, are left out of the result: they only help hinted+//! rendering at fixed pixel sizes, and nothing here uses them.++/// Bytes the LZ window starts out holding, so copies can reach back before the first byte.+const PRELOAD: usize = 2 * 32 * 96 + 4 * 256;+/// The largest font or stream this reads or builds.+const MAX_OUT: usize = 64 << 20;++/// The TrueType font inside an MTX stream, or none when the data is not a valid MTX font.+pub fn decompress(mtx: &[u8]) -> Option<Vec<u8>> {+    let version = *mtx.first()?;+    if !(1..=3).contains(&version) {+        return None;+    }+    let off2 = u24(mtx.get(4..7)?);+    let off3 = u24(mtx.get(7..10)?);+    if !(10 <= off2 && off2 <= off3 && off3 <= mtx.len()) {+        return None;+    }+    let tables = unpack(mtx.get(10..off2)?, version)?;+    let push = unpack(mtx.get(off2..off3)?, version)?;+    let code = unpack(mtx.get(off3..)?, version)?;+    rebuild(&tables, &push, &code)+}++/// A big-endian unsigned integer of up to eight bytes.+fn u24(b: &[u8]) -> usize {+    b.iter().fold(0, |v, &x| v << 8 | x as usize)+}++/// A reader of big-endian values.+struct Cur<'a> {+    d: &'a [u8],+    p: usize,+}++impl<'a> Cur<'a> {+    /// A reader at the start of `d`.+    fn new(d: &'a [u8]) -> Self {+        Cur { d, p: 0 }+    }+    /// The next `n` bytes, or none if fewer are left.+    fn take(&mut self, n: usize) -> Option<&'a [u8]> {+        let s = self.d.get(self.p..self.p.checked_add(n)?)?;+        self.p += n;+        Some(s)+    }+    /// The next byte.+    fn u8(&mut self) -> Option<u8> {+        self.take(1).map(|b| b[0])+    }+    /// The next unsigned 16-bit value.+    fn u16(&mut self) -> Option<u16> {+        self.take(2).map(|b| u16::from_be_bytes([b[0], b[1]]))+    }+    /// The next signed 16-bit value.+    fn i16(&mut self) -> Option<i16> {+        self.u16().map(|v| v as i16)+    }+    /// A `255USHORT`: one byte, or a marker byte and more.+    fn ushort255(&mut self) -> Option<u16> {+        Some(match self.u8()? {+            253 => self.u16()?,+            255 => 253 + self.u8()? as u16,+            254 => 506 + self.u8()? as u16,+            v => v as u16,+        })+    }+    /// A `255SHORT`: like `255USHORT` with a sign marker.+    fn short255(&mut self) -> Option<i16> {+        let mut code = self.u8()?;+        if code == 253 {+            return self.i16();+        }+        let sign = if code == 250 {+            code = self.u8()?;+            -1+        } else {+            1+        };+        let v = match code {+            255 => 250 + self.u8()? as i16,+            254 => 500 + self.u8()? as i16,+            v => v as i16,+        };+        Some(v * sign)+    }+}++struct Bits<'a> {+    d: &'a [u8],+    pos: usize,+}++impl Bits<'_> {+    /// The next bit, most significant first, or none at the end of the data.+    fn bit(&mut self) -> Option<usize> {+        let byte = *self.d.get(self.pos >> 3)?;+        let bit = (byte >> (7 - (self.pos & 7))) & 1;+        self.pos += 1;+        Some(bit as usize)+    }+    /// The next `n` bits as a number.+    fn value(&mut self, n: u32) -> Option<usize> {+        (0..n).try_fold(0, |v, _| Some(v << 1 | self.bit()?))+    }+}++#[derive(Clone, Copy, Default)]+struct Node {+    up: usize,+    left: usize,+    right: usize,+    /// The symbol of a leaf, negative for an inner node.+    code: i32,+    weight: u32,+}++/// An adaptive Huffman code over the symbols `0..range`. Node 1 is the root and the children of+/// node `i` start out at `2i` and `2i + 1`; the leaves are `range..2 * range`.+struct Ahuff {+    tree: Vec<Node>,+    leaf: Vec<usize>,+}++impl Ahuff {+    /// The code for `range` symbols in the state the format starts every stream in.+    fn new(range: usize) -> Self {+        let mut tree = vec![Node { code: -1, ..Node::default() }; 2 * range];+        for (i, n) in tree.iter_mut().enumerate().skip(2) {+            n.up = i / 2;+            n.weight = 1;+        }+        for (i, n) in tree.iter_mut().enumerate().take(range).skip(1) {+            n.left = 2 * i;+            n.right = 2 * i + 1;+        }+        let mut leaf = vec![0; range];+        for (i, l) in leaf.iter_mut().enumerate() {+            tree[range + i].code = i as i32;+            *l = range + i;+        }+        for i in (1..range).rev() {+            tree[i].weight = tree[2 * i].weight + tree[2 * i + 1].weight;+        }+        let mut t = Ahuff { tree, leaf };+        if range > 256 && range < 512 {+            // The symbol alphabet: literals, then copy commands, then DUP2, DUP4 and DUP6. Start+            // with the first two bytes of a font and the short copies the likelier ones.+            for (sym, times) in [(256, 1), (257, 1), (range - 3, 12), (range - 2, 6)] {+                for _ in 0..times {+                    t.update(t.leaf[sym]);+                }+            }+        } else {+            for _ in 0..2 {+                for sym in 0..range {+                    t.update(t.leaf[sym]);+                }+            }+        }+        t+    }++    /// Exchanges two nodes of equal weight, with their subtrees.+    fn swap(&mut self, a: usize, b: usize) {+        let (upa, upb) = (self.tree[a].up, self.tree[b].up);+        self.tree.swap(a, b);+        self.tree[a].up = upa;+        self.tree[b].up = upb;+        for n in [a, b] {+            if self.tree[n].code < 0 {+                let (l, r) = (self.tree[n].left, self.tree[n].right);+                self.tree[l].up = n;+                self.tree[r].up = n;+            } else {+                self.leaf[self.tree[n].code as usize] = n;+            }+        }+    }++    /// Counts one more use of the node `a` and its ancestors, keeping siblings ordered by weight.+    fn update(&mut self, mut a: usize) {+        while a > 1 {+            let w = self.tree[a].weight;+            if self.tree[a - 1].weight == w {+                let mut b = a - 1;+                while b > 1 && self.tree[b - 1].weight == w {+                    b -= 1;+                }+                if b > 1 {+                    self.swap(a, b);+                    a = b;+                }+            }+            self.tree[a].weight = w + 1;+            a = self.tree[a].up;+        }+        self.tree[1].weight += 1;+    }++    /// Decodes one symbol and counts it.+    fn read(&mut self, bits: &mut Bits) -> Option<usize> {+        let mut a = 1;+        // A tree over at most 512 symbols is never deeper than that.+        for _ in 0..512 {+            a = if bits.bit()? == 1 { self.tree[a].right } else { self.tree[a].left };+            let code = self.tree.get(a)?.code;+            if code >= 0 {+                self.update(a);+                return Some(code as usize);+            }+        }+        None+    }+}++/// One LZCOMP stream: an adaptive Huffman coded mix of literals and copies, optionally run-length+/// coded on top.+fn unpack(data: &[u8], version: u8) -> Option<Vec<u8>> {+    let mut bits = Bits { d: data, pos: 0 };+    let run_length = version != 1 && bits.bit()? == 1;+    let mut dist = Ahuff::new(8);+    let mut len = Ahuff::new(8);+    let out_len = bits.value(24)?;+    // Copy distances are written in groups of three bits; the group count a stream needs depends on+    // how long it is.+    let mut ranges = 1;+    while 1usize << (3 * ranges) < out_len {+        ranges += 1;+    }+    let dup2 = 256 + 8 * ranges;+    let mut sym = Ahuff::new(dup2 + 3);++    let mut buf = Vec::with_capacity(PRELOAD + out_len);+    for k in 0..32u8 {+        for j in 0..96u8 {+            buf.extend([k, j]);+        }+    }+    for j in 0..=255u8 {+        buf.extend([j; 4]);+    }+    buf.truncate(PRELOAD);+    while buf.len() < PRELOAD + out_len {+        let s = sym.read(&mut bits)?;+        if s < 256 {+            buf.push(s as u8);+        } else if s >= dup2 {+            // A byte copied from 2, 4 or 6 positions back.+            let v = *buf.get(buf.len().checked_sub(2 * (s - dup2 + 1))?)?;+            buf.push(v);+        } else {+            let first = s - 256;+            let groups = first / 8 + 1;+            let mut code = first % 8;+            let mut length = 0;+            loop {+                length = length << 2 | code & 3;+                if code & 4 == 0 {+                    break;+                }+                // No copy is longer than the stream, which also keeps the shift from overflowing.+                if length > out_len {+                    return None;+                }+                code = len.read(&mut bits)?;+            }+            length += 2;+            let mut distance = 0;+            for _ in 0..groups {+                distance = distance << 3 | dist.read(&mut bits)?;+            }+            distance += 1;+            if distance >= 512 {+                length += 1;+            }+            if buf.len() + length > PRELOAD + out_len {+                return None;+            }+            let from = buf.len().checked_sub(distance + length - 1)?;+            for at in from..from + length {+                buf.push(*buf.get(at)?);+            }+        }+    }+    let raw = &buf[PRELOAD..];+    if !run_length {+        return Some(raw.to_vec());+    }+    let (&escape, mut rest) = raw.split_first()?;+    let mut out = Vec::new();+    while let Some((&b, tail)) = rest.split_first() {+        rest = tail;+        if b != escape {+            out.push(b);+            continue;+        }+        let (&count, tail) = rest.split_first()?;+        rest = tail;+        if count == 0 {+            out.push(escape);+        } else {+            let (&v, tail) = rest.split_first()?;+            rest = tail;+            out.resize(out.len() + count as usize, v);+        }+        if out.len() > MAX_OUT {+            return None;+        }+    }+    Some(out)+}++/// One row of the table that turns a glyph point's flag byte into a coordinate step.+struct Triplet {+    bytes: usize,+    x_bits: u32,+    y_bits: u32,+    dx: i32,+    dy: i32,+    x_neg: bool,+    y_neg: bool,+}++/// The coordinate step a flag byte's low seven bits stand for.+fn triplet(index: u8) -> Triplet {+    let i = index as i32;+    let sign = |n: i32| (n & 1 == 0, n & 2 == 0);+    let (bytes, x_bits, y_bits, dx, dy, (x_neg, y_neg)) = match i {+        0..=9 => (2, 0, 8, 0, 256 * (i / 2), (false, i % 2 == 0)),+        10..=19 => (2, 8, 0, 256 * ((i - 10) / 2), 0, ((i - 10) % 2 == 0, false)),+        20..=83 => {+            let n = i - 20;+            (2, 4, 4, 1 + 16 * (n / 16), 1 + 16 * (n / 4 % 4), sign(n))+        }+        84..=119 => {+            let n = i - 84;+            (3, 8, 8, 1 + 256 * (n / 12), 1 + 256 * (n / 4 % 3), sign(n))+        }+        120..=123 => (4, 12, 12, 0, 0, sign(i)),+        _ => (5, 16, 16, 0, 0, sign(i)),+    };+    Triplet { bytes, x_bits, y_bits, dx, dy, x_neg, y_neg }+}++/// Pushes `values` the way a glyph program starts: `NPUSHB` or `NPUSHW`, 255 values at a time.+fn push_instructions(values: &[i16], out: &mut Vec<u8>) {+    for chunk in values.chunks(255) {+        if chunk.iter().all(|v| (0..=255).contains(v)) {+            out.extend([0x40, chunk.len() as u8]);+            out.extend(chunk.iter().map(|&v| v as u8));+        } else {+            out.extend([0x41, chunk.len() as u8]);+            out.extend(chunk.iter().flat_map(|v| v.to_be_bytes()));+        }+    }+}++/// The values a glyph's initial pushes set up, with `A,x,A,x,A` runs stored as hop codes.+fn push_data(c: &mut Cur, count: usize) -> Option<Vec<i16>> {+    let mut v: Vec<i16> = Vec::with_capacity(count);+    while v.len() < count {+        let hops = match c.d.get(c.p)? {+            251 => 1,+            252 => 2,+            _ => 0,+        };+        if hops == 0 {+            v.push(c.short255()?);+            continue;+        }+        c.p += 1;+        let a = *v.get(v.len().checked_sub(2)?)?;+        for _ in 0..hops {+            v.extend([a, c.short255()?]);+        }+        v.push(a);+    }+    (v.len() == count).then_some(v)+}++/// A glyph's program: the pushes, then `code_size` bytes of instructions.+fn program(glyf: &mut Cur, push: &mut Cur, code: &mut Cur) -> Option<Vec<u8>> {+    let (push_count, code_size) = (glyf.ushort255()?, glyf.ushort255()?);+    let mut out = Vec::new();+    push_instructions(&push_data(push, push_count as usize)?, &mut out);+    out.extend(code.take(code_size as usize)?);+    Some(out)+}++/// One glyph in TrueType form. Simple glyphs are written with 16-bit coordinate steps.+fn glyph(glyf: &mut Cur, push: &mut Cur, code: &mut Cur) -> Option<Vec<u8>> {+    let mut out = Vec::new();+    let mut n = glyf.i16()?;+    if n == 0 {+        return Some(out);+    }+    if n == -1 {+        out.extend(n.to_be_bytes());+        out.extend(glyf.take(8)?);+        loop {+            let flags = glyf.u16()?;+            out.extend(flags.to_be_bytes());+            let args = if flags & 1 != 0 { 4 } else { 2 };+            let scale = if flags & 0x08 != 0 {+                2+            } else if flags & 0x40 != 0 {+                4+            } else if flags & 0x80 != 0 {+                8+            } else {+                0+            };+            out.extend(glyf.take(2 + args + scale)?);+            if flags & 0x20 == 0 {+                if flags & 0x100 != 0 {+                    let p = program(glyf, push, code)?;+                    out.extend((p.len() as u16).to_be_bytes());+                    out.extend(p);+                }+                return Some(out);+            }+        }+    }+    let mut bbox = None;+    if n == 0x7FFF {+        n = glyf.i16()?;+        bbox = Some(glyf.take(8)?);+    }+    if n < 0 {+        return None;+    }+    let mut ends: Vec<u16> = Vec::with_capacity(n as usize);+    for _ in 0..n {+        let v = glyf.ushort255()?;+        ends.push(ends.last().map_or(Some(v), |e| e.checked_add(v))?);+    }+    let points = *ends.last()? as usize + 1;+    let flags = glyf.take(points)?;+    let (mut x, mut y) = (0i32, 0i32);+    let mut steps = Vec::with_capacity(points);+    let mut min = (i32::MAX, i32::MAX);+    let mut max = (i32::MIN, i32::MIN);+    for &f in flags {+        let t = triplet(f & 0x7F);+        let mut data = 0u32;+        for &b in glyf.take(t.bytes - 1)? {+            data = data << 8 | b as u32;+        }+        let shift = (t.bytes as u32 - 1) * 8;+        let step = |bits: u32, at: u32, delta: i32, neg: bool| {+            let v = (data >> (shift - at - bits)) as i32 & ((1 << bits) - 1);+            if neg { -(v + delta) } else { v + delta }+        };+        let (dx, dy) = (step(t.x_bits, 0, t.dx, t.x_neg), step(t.y_bits, t.x_bits, t.dy, t.y_neg));+        x += dx;+        y += dy;+        (min, max) = ((min.0.min(x), min.1.min(y)), (max.0.max(x), max.1.max(y)));+        steps.push((dx as i16, dy as i16));+    }+    let program = program(glyf, push, code)?;+    out.extend(n.to_be_bytes());+    match bbox {+        Some(b) => out.extend(b),+        None => out.extend([min.0, min.1, max.0, max.1].iter().flat_map(|&v| (v as i16).to_be_bytes())),+    }+    out.extend(ends.iter().flat_map(|e| e.to_be_bytes()));+    out.extend((program.len() as u16).to_be_bytes());+    out.extend(program);+    out.extend(flags.iter().map(|&f| (f >> 7) ^ 1));+    out.extend(steps.iter().flat_map(|s| s.0.to_be_bytes()));+    out.extend(steps.iter().flat_map(|s| s.1.to_be_bytes()));+    Some(out)+}++/// The `cvt ` table: a count, then each value as the step from the one before.+fn cvt(d: &[u8]) -> Option<Vec<u8>> {+    let mut c = Cur::new(d);+    let mut v = 0i16;+    let mut out = Vec::new();+    for _ in 0..c.u16()? {+        let code = c.u8()?;+        let step = match code {+            0..=237 => code as i16,+            239..=247 => -(c.u8()? as i16 + (code as i16 - 239) * 238),+            248..=255 => c.u8()? as i16 + (code as i16 - 247) * 238,+            238 => c.i16()?,+        };+        v = v.wrapping_add(step);+        out.extend(v.to_be_bytes());+    }+    Some(out)+}++/// The sum of the table's big-endian 32-bit words, as the table directory holds it.+fn checksum(d: &[u8]) -> u32 {+    d.chunks(4).fold(0u32, |s, c| {+        let mut w = [0; 4];+        w[..c.len()].copy_from_slice(c);+        s.wrapping_add(u32::from_be_bytes(w))+    })+}++/// The TrueType font from the three decompressed streams: the tables (CTF), the glyphs' push data+/// and their instructions.+fn rebuild(tables: &[u8], push: &[u8], code: &[u8]) -> Option<Vec<u8>> {+    let mut c = Cur::new(tables);+    let version = c.take(4)?;+    let count = c.u16()? as usize;+    c.take(6)?;+    let mut dir = Vec::with_capacity(count);+    for _ in 0..count {+        let tag: [u8; 4] = c.take(4)?.try_into().ok()?;+        c.take(4)?;+        let (off, len) = (u32::from_be_bytes(c.take(4)?.try_into().ok()?) as usize, u32::from_be_bytes(c.take(4)?.try_into().ok()?) as usize);+        dir.push((tag, tables.get(off..off.checked_add(len)?)?));+    }+    let table = |t: &[u8; 4]| dir.iter().find(|d| &d.0 == t).map(|d| d.1);+    let num_glyphs = u16::from_be_bytes(table(b"maxp")?.get(4..6)?.try_into().ok()?) as usize;++    let (mut g, mut p, mut k) = (Cur::new(table(b"glyf")?), Cur::new(push), Cur::new(code));+    let (mut glyf, mut loca) = (Vec::new(), Vec::with_capacity(4 * (num_glyphs + 1)));+    for _ in 0..num_glyphs {+        loca.extend((glyf.len() as u32).to_be_bytes());+        glyf.extend(glyph(&mut g, &mut p, &mut k)?);+        glyf.resize(glyf.len().next_multiple_of(4), 0);+        if glyf.len() > MAX_OUT {+            return None;+        }+    }+    loca.extend((glyf.len() as u32).to_be_bytes());++    let mut out_tables: Vec<([u8; 4], Vec<u8>)> = Vec::with_capacity(count);+    // Directory entries may share bytes, so the font is bounded as a whole, not table by table.+    let mut size = 12 + 16 * count;+    let cvt_table = table(b"cvt ").map(cvt).unwrap_or_else(|| Some(vec![]))?;+    for (tag, data) in &dir {+        let len = match tag {+            b"loca" => loca.len(),+            b"glyf" => glyf.len(),+            b"cvt " => cvt_table.len(),+            _ => data.len(),+        };+        size += len.next_multiple_of(4);+        if size > MAX_OUT {+            return None;+        }+        let data = match tag {+            b"loca" => loca.clone(),+            b"glyf" => glyf.clone(),+            b"cvt " => cvt_table.clone(),+            // shortcut: the device-metric tables are dropped, nothing here reads them.+            b"hdmx" | b"VDMX" => continue,+            b"head" => {+                let mut h = data.to_vec();+                // The offsets above are 32-bit.+                h.get_mut(50..52)?.copy_from_slice(&1u16.to_be_bytes());+                h+            }+            _ => data.to_vec(),+        };+        out_tables.push((*tag, data));+    }+    let n = out_tables.len();+    let entry_selector = n.max(1).ilog2() as usize;+    let search_range = 16 << entry_selector;+    let mut out = Vec::new();+    out.extend(version);+    out.extend((n as u16).to_be_bytes());+    out.extend((search_range as u16).to_be_bytes());+    out.extend((entry_selector as u16).to_be_bytes());+    out.extend(((16 * n).saturating_sub(search_range) as u16).to_be_bytes());+    let mut at = 12 + 16 * n;+    for (tag, data) in &out_tables {+        out.extend(tag);+        out.extend(checksum(data).to_be_bytes());+        out.extend((at as u32).to_be_bytes());+        out.extend((data.len() as u32).to_be_bytes());+        at += data.len().next_multiple_of(4);+    }+    for (_, data) in &out_tables {+        out.extend(data);+        out.resize(out.len().next_multiple_of(4), 0);+    }+    Some(out)+}++#[cfg(test)]+mod tests {+    use super::*;+    use skrifa::MetadataProvider;+    use skrifa::instance::{LocationRef, Size};+    use skrifa::outline::{DrawSettings, OutlinePen};+    use skrifa::raw::TableProvider;++    // ---- an MTX writer, enough to make test fonts from a TrueType one ----++    struct BitOut {+        bytes: Vec<u8>,+        n: usize,+    }++    impl BitOut {+        /// Appends one bit.+        fn bit(&mut self, b: usize) {+            if self.n.is_multiple_of(8) {+                self.bytes.push(0);+            }+            let last = self.bytes.len() - 1;+            self.bytes[last] |= (b as u8) << (7 - self.n % 8);+            self.n += 1;+        }+        /// Appends the low `bits` bits of `v`.+        fn value(&mut self, v: usize, bits: u32) {+            (0..bits).rev().for_each(|i| self.bit(v >> i & 1));+        }+    }++    impl Ahuff {+        /// Appends the code for `sym` and counts it.+        fn write(&mut self, sym: usize, out: &mut BitOut) {+            let leaf = self.leaf[sym];+            let (mut a, mut path) = (leaf, vec![]);+            while a != 1 {+                let up = self.tree[a].up;+                path.push((self.tree[up].right == a) as usize);+                a = up;+            }+            path.iter().rev().for_each(|&b| out.bit(b));+            self.update(leaf);+        }+    }++    /// The window an LZCOMP stream starts out holding.+    fn preload() -> Vec<u8> {+        let mut w = Vec::new();+        for k in 0..32u8 {+            for j in 0..96u8 {+                w.extend([k, j]);+            }+        }+        for j in 0..=255u8 {+            w.extend([j; 4]);+        }+        w+    }++    /// LZCOMP with literals, DUP2 and copies found by a search of the last kilobyte, run-length+    /// coded when `escape` is given.+    fn pack(data: &[u8], escape: Option<u8>) -> Vec<u8> {+        let data = match escape {+            None => data.to_vec(),+            Some(e) => {+                let mut v = vec![e];+                let mut i = 0;+                while i < data.len() {+                    let run = data[i..].iter().take(255).take_while(|&&b| b == data[i]).count();+                    if run > 3 {+                        v.extend([e, run as u8, data[i]]);+                        i += run;+                    } else {+                        v.extend(if data[i] == e { vec![e, 0] } else { vec![data[i]] });+                        i += 1;+                    }+                }+                v+            }+        };+        let mut out = BitOut { bytes: vec![], n: 0 };+        out.bit(escape.is_some() as usize);+        let (mut dist, mut len) = (Ahuff::new(8), Ahuff::new(8));+        out.value(data.len(), 24);+        let mut ranges = 1;+        while 1usize << (3 * ranges) < data.len() {+            ranges += 1;+        }+        let dup2 = 256 + 8 * ranges;+        let mut sym = Ahuff::new(dup2 + 3);+        let mut hist = preload();+        let mut i = 0;+        while i < data.len() {+            let at = hist.len();+            let mut best = (0, 0);+            for s in at.saturating_sub(1200)..at {+                let l = (0..(at - s).min(data.len() - i)).take_while(|&k| hist[s + k] == data[i + k]).count();+                if l > best.0 {+                    best = (l, s);+                }+            }+            let (l, s) = best;+            let distance = (at - s + 1).saturating_sub(l);+            let v = l.saturating_sub(if distance >= 512 { 3 } else { 2 });+            let n = (1..=ranges).find(|n| distance.saturating_sub(1) >> (3 * n) == 0);+            if let (true, Some(n)) = (l >= 3, n) {+                let mut groups = vec![v & 3];+                let mut r = v >> 2;+                while r > 0 {+                    groups.push(r & 3);+                    r >>= 2;+                }+                groups.reverse();+                let code = |k: usize| groups[k] | if k + 1 < groups.len() { 4 } else { 0 };+                sym.write(256 + (n - 1) * 8 + code(0), &mut out);+                for k in 1..groups.len() {+                    len.write(code(k), &mut out);+                }+                for g in (0..n).rev() {+                    dist.write((distance - 1) >> (3 * g) & 7, &mut out);+                }+                hist.extend(&data[i..i + l]);+                i += l;+                continue;+            }+            if hist.len() >= 2 && data[i] == hist[hist.len() - 2] {+                sym.write(dup2, &mut out);+            } else {+                sym.write(data[i] as usize, &mut out);+            }+            hist.push(data[i]);+            i += 1;+        }+        out.bytes+    }++    // ---- TrueType to CTF ----++    /// A big-endian 16-bit value at `at`.+    fn be16(d: &[u8], at: usize) -> usize {+        u16::from_be_bytes([d[at], d[at + 1]]) as usize+    }+    /// A big-endian 32-bit value at `at`.+    fn be32(d: &[u8], at: usize) -> usize {+        u32::from_be_bytes([d[at], d[at + 1], d[at + 2], d[at + 3]]) as usize+    }++    /// The font's tables by tag.+    fn tables(font: &[u8]) -> Vec<([u8; 4], &[u8])> {+        (0..be16(font, 4))+            .map(|i| {+                let e = 12 + 16 * i;+                (font[e..e + 4].try_into().unwrap(), &font[be32(font, e + 8)..be32(font, e + 8) + be32(font, e + 12)])+            })+            .collect()+    }++    /// Appends `v` as a `255USHORT`.+    fn put255ushort(out: &mut Vec<u8>, v: usize) {+        match v {+            0..=252 => out.push(v as u8),+            253..=505 => out.extend([255, (v - 253) as u8]),+            506..=761 => out.extend([254, (v - 506) as u8]),+            _ => out.extend([253, (v >> 8) as u8, v as u8]),+        }+    }++    /// Appends `v` as a `255SHORT`.+    fn put255short(out: &mut Vec<u8>, v: i16) {+        let m = v.unsigned_abs() as usize;+        if m >= 756 {+            out.extend([253, (v >> 8) as u8, v as u8]);+            return;+        }+        if v < 0 {+            out.push(250);+        }+        match m {+            0..=249 => out.push(m as u8),+            250..=499 => out.extend([255, (m - 250) as u8]),+            _ => out.extend([254, (m - 500) as u8]),+        }+    }++    /// The values the initial pushes of `program` leave on the stack, and what follows them.+    fn split_pushes(program: &[u8]) -> (Vec<i16>, &[u8]) {+        let (mut values, mut i) = (vec![], 0);+        while let Some(&op) = program.get(i) {+            let (n, words) = match op {+                0x40 | 0x41 => (program[i + 1] as usize, op == 0x41),+                0xB0..=0xB7 => ((op - 0xB0) as usize + 1, false),+                0xB8..=0xBF => ((op - 0xB8) as usize + 1, true),+                _ => break,+            };+            i += if op < 0x42 { 2 } else { 1 };+            for _ in 0..n {+                if words {+                    values.push(i16::from_be_bytes([program[i], program[i + 1]]));+                    i += 2;+                } else {+                    values.push(program[i] as i16);+                    i += 1;+                }+            }+        }+        (values, &program[i..])+    }++    /// Writes a glyph's `pushCount` and `codeSize` to `glyf`, its pushes (with hop codes where+    /// they fit) to `push` and its remaining instructions to `code`.+    fn put_program(program: &[u8], glyf: &mut Vec<u8>, push: &mut Vec<u8>, code: &mut Vec<u8>) {+        let (v, rest) = split_pushes(program);+        put255ushort(glyf, v.len());+        put255ushort(glyf, rest.len());+        let mut i = 0;+        while i < v.len() {+            let a = if i >= 2 { v[i - 2] } else { i16::MIN };+            if i >= 2 && i + 4 < v.len() && v[i] == a && v[i + 2] == a && v[i + 4] == a {+                push.push(252);+                put255short(push, v[i + 1]);+                put255short(push, v[i + 3]);+                i += 5;+            } else if i >= 2 && i + 2 < v.len() && v[i] == a && v[i + 2] == a {+                push.push(251);+                put255short(push, v[i + 1]);+                i += 3;+            } else {+                put255short(push, v[i]);+                i += 1;+            }+        }+        code.extend(rest);+    }++    /// The step from the previous point as the flag byte and the bytes that follow it.+    fn put_triplet(flag: &mut Vec<u8>, data: &mut Vec<u8>, dx: i32, dy: i32, on: bool) {+        let best = (0..128u8)+            .filter_map(|i| {+                let t = triplet(i);+                let fits = |d: i32, bits: u32, base: i32, neg: bool| {+                    if bits == 0 {+                        return d == 0;+                    }+                    let m = d.abs() - base;+                    (d == 0 || (d < 0) == neg) && (0..1 << bits).contains(&m)+                };+                (fits(dx, t.x_bits, t.dx, t.x_neg) && fits(dy, t.y_bits, t.dy, t.y_neg)).then_some((t.bytes, i))+            })+            .min()+            .unwrap();+        let t = triplet(best.1);+        let shift = (t.bytes as u32 - 1) * 8;+        let mut v = 0u32;+        if t.x_bits > 0 {+            v |= ((dx.abs() - t.dx) as u32) << (shift - t.x_bits);+        }+        if t.y_bits > 0 {+            v |= ((dy.abs() - t.dy) as u32) << (shift - t.x_bits - t.y_bits);+        }+        flag.push(best.1 | if on { 0 } else { 0x80 });+        data.extend(&v.to_be_bytes()[4 - (t.bytes - 1)..]);+    }++    /// A simple glyph's points: absolute coordinates and whether each is on the curve.+    fn points(g: &[u8], n: usize) -> (Vec<usize>, Vec<(i32, i32, bool)>, &[u8]) {+        let ends: Vec<usize> = (0..n).map(|i| be16(g, 10 + 2 * i)).collect();+        let count = ends.last().map_or(0, |e| e + 1);+        let ilen = be16(g, 10 + 2 * n);+        let instr = &g[12 + 2 * n..12 + 2 * n + ilen];+        let mut at = 12 + 2 * n + ilen;+        let mut flags = vec![];+        while flags.len() < count {+            let f = g[at];+            at += 1;+            let reps = if f & 8 != 0 {+                at += 1;+                g[at - 1] as usize+            } else {+                0+            };+            flags.extend(std::iter::repeat_n(f, 1 + reps));+        }+        let mut coord = |short: u8, same: u8| {+            let mut v = 0i32;+            flags+                .iter()+                .map(|&f| {+                    if f & short != 0 {+                        let d = g[at] as i32;+                        at += 1;+                        v += if f & same != 0 { d } else { -d };+                    } else if f & same == 0 {+                        v += i16::from_be_bytes([g[at], g[at + 1]]) as i32;+                        at += 2;+                    }+                    v+                })+                .collect::<Vec<_>>()+        };+        let xs = coord(2, 0x10);+        let ys = coord(4, 0x20);+        (ends, (0..count).map(|i| (xs[i], ys[i], flags[i] & 1 != 0)).collect(), instr)+    }++    /// The font as an MTX stream: TrueType glyphs in CTF form, `cvt ` compressed, the rest as is.+    fn to_mtx(font: &[u8], escape: Option<u8>) -> Vec<u8> {+        let t = tables(font);+        let get = |tag: &[u8; 4]| t.iter().find(|x| &x.0 == tag).unwrap().1;+        let loca_long = be16(get(b"head"), 50) == 1;+        let glyphs = be16(get(b"maxp"), 4);+        let at = |i: usize| if loca_long { be32(get(b"loca"), 4 * i) } else { 2 * be16(get(b"loca"), 2 * i) };+        let (mut glyf, mut push, mut code) = (vec![], vec![], vec![]);+        for i in 0..glyphs {+            let g = &get(b"glyf")[at(i)..at(i + 1)];+            if g.is_empty() {+                glyf.extend([0, 0]);+                continue;+            }+            let n = be16(g, 0) as i16;+            if n < 0 {+                glyf.extend(&g[..10]);+                let mut p = 10;+                loop {+                    let f = be16(g, p);+                    let len = 4+                        + if f & 1 != 0 { 4 } else { 2 }+                        + if f & 8 != 0 {+                            2+                        } else if f & 0x40 != 0 {+                            4+                        } else if f & 0x80 != 0 {+                            8+                        } else {+                            0+                        };+                    glyf.extend(&g[p..p + len]);+                    p += len;+                    if f & 0x20 == 0 {+                        if f & 0x100 != 0 {+                            let l = be16(g, p);+                            put_program(&g[p + 2..p + 2 + l], &mut glyf, &mut push, &mut code);+                        }+                        break;+                    }+                }+                continue;+            }+            let (ends, pts, instr) = points(g, n as usize);+            // Keep the box when it is not the points' own, as a font may.+            let (xs, ys): (Vec<i32>, Vec<i32>) = pts.iter().map(|p| (p.0, p.1)).unzip();+            let own = [xs.iter().min(), ys.iter().min(), xs.iter().max(), ys.iter().max()].map(|v| *v.unwrap() as i16 as usize);+            let stored = [be16(g, 2), be16(g, 4), be16(g, 6), be16(g, 8)].map(|v| v & 0xFFFF);+            if own.map(|v| v & 0xFFFF) != stored {+                glyf.extend(0x7FFFu16.to_be_bytes());+                glyf.extend(&g[..10]);+            } else {+                glyf.extend(&g[..2]);+            }+            for (k, e) in ends.iter().enumerate() {+                put255ushort(&mut glyf, if k == 0 { *e } else { e - ends[k - 1] });+            }+            let (mut flags, mut data) = (vec![], vec![]);+            let (mut px, mut py) = (0, 0);+            for &(x, y, on) in &pts {+                put_triplet(&mut flags, &mut data, x - px, y - py, on);+                (px, py) = (x, y);+            }+            glyf.extend(flags);+            glyf.extend(data);+            put_program(instr, &mut glyf, &mut push, &mut code);+        }+        // cvt: each value as a step from the last.+        let mut cvt = vec![];+        let raw = get(b"cvt ");+        cvt.extend(((raw.len() / 2) as u16).to_be_bytes());+        let mut last = 0i16;+        for c in raw.chunks(2) {+            let v = i16::from_be_bytes([c[0], c[1]]);+            let d = v.wrapping_sub(last);+            last = v;+            let m = d.unsigned_abs() as usize;+            if d == i16::MIN || m / 238 > 8 {+                cvt.extend([238, (d >> 8) as u8, d as u8]);+            } else if d < 0 {+                cvt.extend([239 + (m / 238) as u8, (m % 238) as u8]);+            } else if m >= 238 {+                cvt.extend([247 + (m / 238) as u8, (m % 238) as u8]);+            } else {+                cvt.push(m as u8);+            }+        }+        let kept: Vec<_> = t.iter().filter(|x| !matches!(&x.0, b"hdmx" | b"VDMX")).collect();+        let mut ctf = vec![];+        ctf.extend(&font[..4]);+        ctf.extend((kept.len() as u16).to_be_bytes());+        ctf.extend([0; 6]);+        let mut off = 12 + 16 * kept.len();+        let mut body: Vec<u8> = vec![];+        for (tag, data) in &kept {+            let data: &[u8] = match tag {+                b"glyf" => &glyf,+                b"cvt " => &cvt,+                b"loca" => &[],+                _ => data,+            };+            ctf.extend(tag);+            ctf.extend([0; 4]);+            ctf.extend(((if data.is_empty() { 0 } else { off }) as u32).to_be_bytes());+            ctf.extend((data.len() as u32).to_be_bytes());+            body.extend(data);+            off += data.len();+        }+        ctf.extend(body);+        let (a, b, c) = (pack(&ctf, escape), pack(&push, escape), pack(&code, escape));+        let mut out = vec![3, 0, 0, 0];+        out.extend(&((10 + a.len()) as u32).to_be_bytes()[1..]);+        out.extend(&((10 + a.len() + b.len()) as u32).to_be_bytes()[1..]);+        out.extend([a, b, c].concat());+        out+    }++    /// An Embedded OpenType header around `font`, with the given flags.+    fn eot(font: &[u8], flags: u32) -> Vec<u8> {+        let mut h = vec![0u8; 82];+        h[0..4].copy_from_slice(&((82 + font.len()) as u32).to_le_bytes());+        h[4..8].copy_from_slice(&(font.len() as u32).to_le_bytes());+        h[8..12].copy_from_slice(&0x0002_0002u32.to_le_bytes());+        h[12..16].copy_from_slice(&flags.to_le_bytes());+        h[34..36].copy_from_slice(&0x504Cu16.to_le_bytes());+        [h, font.to_vec()].concat()+    }++    #[derive(Default)]+    struct Path(String);++    impl OutlinePen for Path {+        fn move_to(&mut self, x: f32, y: f32) {+            self.0 += &format!("M{x},{y}");+        }+        fn line_to(&mut self, x: f32, y: f32) {+            self.0 += &format!("L{x},{y}");+        }+        fn quad_to(&mut self, a: f32, b: f32, x: f32, y: f32) {+            self.0 += &format!("Q{a},{b},{x},{y}");+        }+        fn curve_to(&mut self, a: f32, b: f32, c: f32, d: f32, x: f32, y: f32) {+            self.0 += &format!("C{a},{b},{c},{d},{x},{y}");+        }+        fn close(&mut self) {+            self.0 += "Z";+        }+    }++    /// Every glyph's outline as a string, in glyph order.+    fn outlines(font: &[u8]) -> Vec<String> {+        let f = skrifa::FontRef::new(font).unwrap();+        let glyphs = f.outline_glyphs();+        (0..f.maxp().unwrap().num_glyphs())+            .map(|g| {+                let mut p = Path::default();+                if let Some(o) = glyphs.get(skrifa::GlyphId::new(g as u32)) {+                    o.draw(DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), &mut p).unwrap();+                }+                p.0+            })+            .collect()+    }++    /// Each glyph's program as its pushed values and the instructions after them.+    fn programs(font: &[u8]) -> Vec<(Vec<i16>, Vec<u8>)> {+        let t = tables(font);+        let get = |tag: &[u8; 4]| t.iter().find(|x| &x.0 == tag).unwrap().1;+        let long = be16(get(b"head"), 50) == 1;+        let at = |i: usize| if long { be32(get(b"loca"), 4 * i) } else { 2 * be16(get(b"loca"), 2 * i) };+        (0..be16(get(b"maxp"), 4))+            .filter_map(|i| {+                let g = &get(b"glyf")[at(i)..at(i + 1)];+                (!g.is_empty() && (be16(g, 0) as i16) > 0).then(|| {+                    let (_, _, instr) = points(g, be16(g, 0));+                    let (v, rest) = split_pushes(instr);+                    (v, rest.to_vec())+                })+            })+            .collect()+    }++    const FONT: &[u8] = epaint_default_fonts::UBUNTU_LIGHT;++    /// A font through MTX and back keeps its outlines and glyph programs.+    #[test]+    fn a_compressed_embedded_font_decodes_to_the_same_glyphs() {+        let original = outlines(FONT);+        assert!(original.iter().filter(|o| !o.is_empty()).count() > 100);+        for (escape, flags) in [(None, 0x4), (Some(0), 0x4), (Some(0x41), 0x1000_0004)] {+            let mtx = to_mtx(FONT, escape);+            let part = eot(&if flags & 0x1000_0000 != 0 { mtx.iter().map(|b| b ^ 0x50).collect() } else { mtx }, flags);+            let font = crate::embedded_sfnt(&part).expect("decoded");+            assert!(outlines(&font) == original, "outlines differ (run-length escape {escape:?})");+            let (a, b) = (programs(&font), programs(FONT));+            if let Some(i) = (0..a.len()).find(|&i| a[i] != b[i]) {+                panic!("glyph program {i} differs: {:?} vs {:?}", a[i], b[i]);+            }+        }+    }++    /// Tables other than the translated ones come through unchanged.+    #[test]+    fn the_other_tables_survive() {+        let font = decompress(&to_mtx(FONT, None)).unwrap();+        let (a, b) = (tables(FONT), tables(&font));+        for (tag, data) in &a {+            let kept = b.iter().find(|x| &x.0 == tag);+            match tag {+                b"loca" | b"glyf" | b"head" => assert!(kept.is_some()),+                b"hdmx" | b"VDMX" => assert!(kept.is_none()),+                _ => assert!(kept.is_some_and(|k| k.1 == *data), "{}", String::from_utf8_lossy(tag)),+            }+        }+    }++    /// Hop codes repeat the value two back.+    #[test]+    fn push_data_expands_hop_codes() {+        // A,x1,A,x2,A and A,x1,A,x2,A,x3,A, after a lone 7.+        let mut c = Cur::new(&[7, 9, 251, 3, 8, 252, 3, 4, 250, 5]);+        assert_eq!(push_data(&mut c, 12).unwrap(), [7, 9, 7, 3, 7, 8, 7, 3, 7, 4, 7, -5]);+        assert!(Cur::new(&[251]).short255().is_some());+        // A hop code with nothing before it to repeat is invalid.+        assert!(push_data(&mut Cur::new(&[251, 1]), 3).is_none());+    }++    /// `255SHORT` and `255USHORT` read every form they are written in.+    #[test]+    fn short_numbers_use_every_form() {+        let mut out = vec![];+        let shorts = [0, 249, -249, 250, 499, 500, 755, -250, -755, 756, -756, 32767, -32768];+        shorts.iter().for_each(|&v| put255short(&mut out, v));+        let mut c = Cur::new(&out);+        assert!(shorts.iter().all(|&v| c.short255() == Some(v)));+        let mut out = vec![];+        let longs = [0, 252, 253, 505, 506, 761, 762, 65535];+        longs.iter().for_each(|&v| put255ushort(&mut out, v));+        let mut c = Cur::new(&out);+        assert!(longs.iter().all(|&v| c.ushort255() == Some(v as u16)));+    }++    /// `cvt` steps add up to the table's values.+    #[test]+    fn cvt_values_decode_as_running_sums() {+        // 5; then +1000 (two bytes); then -3000 (a word); then -238 (two bytes).+        let enc = [0, 4, 5, 251, 48, 238, 0xF4, 0x48, 240, 0];+        let v: Vec<i16> = cvt(&enc).unwrap().chunks(2).map(|c| i16::from_be_bytes([c[0], c[1]])).collect();+        assert_eq!(v, [5, 1005, -1995, -2233]);+    }++    /// Cut-off and corrupted data is refused.+    #[test]+    fn broken_input_is_refused_without_panicking() {+        let mtx = to_mtx(FONT, None);+        let part = eot(&mtx, 0x4);+        for len in (0..part.len()).step_by(997) {+            assert!(crate::embedded_sfnt(&part[..len]).is_none());+        }+        let mut bad = mtx.clone();+        for i in (10..bad.len()).step_by(211) {+            bad[i] ^= 0x5A;+            let _ = decompress(&bad);+        }+        assert!(decompress(&[]).is_none());+        assert!(decompress(&[9; 40]).is_none());+        assert!(decompress(&[3; 10]).is_none());+    }++    /// An MTX font whose directory lists one `len`-byte range `copies` times.+    fn shared_ranges(copies: usize, len: usize) -> Vec<u8> {+        let mut entries: Vec<([u8; 4], usize)> = vec![(*b"glyf", 0), (*b"head", 54), (*b"loca", 0), (*b"maxp", 32)];+        entries.extend((0..copies).map(|_| (*b"zz\0\0", len)));+        let mut ctf = vec![0, 1, 0, 0];+        ctf.extend((entries.len() as u16).to_be_bytes());+        ctf.extend([0; 6]);+        let body = 12 + 16 * entries.len();+        for (tag, l) in &entries {+            ctf.extend(tag);+            ctf.extend([0; 4]);+            ctf.extend(((if *tag == *b"zz\0\0" { body + 100 } else { body }) as u32).to_be_bytes());+            ctf.extend((*l as u32).to_be_bytes());+        }+        ctf.resize(body + 100 + len, 0);+        let (a, b) = (pack(&ctf, Some(7)), pack(&[], None));+        let mut out = vec![3, 0, 0, 0];+        out.extend(&((10 + a.len()) as u32).to_be_bytes()[1..]);+        out.extend(&((10 + a.len() + b.len()) as u32).to_be_bytes()[1..]);+        out.extend([a, b.clone(), b].concat());+        out+    }++    /// A `cvt ` table of 65535 one-byte values, which decodes to twice that.+    fn cvt_directory(copies: usize) -> Vec<u8> {+        let mut cvt = 65535u16.to_be_bytes().to_vec();+        cvt.resize(2 + 65535, 0);+        let mut entries = vec![(*b"glyf", 0, 0), (*b"head", 0, 54), (*b"loca", 0, 0), (*b"maxp", 0, 32)];+        entries.extend((0..copies).map(|_| (*b"cvt ", 1, cvt.len())));+        let mut ctf = vec![0, 1, 0, 0];+        ctf.extend((entries.len() as u16).to_be_bytes());+        ctf.extend([0; 6]);+        let body = 12 + 16 * entries.len();+        for (tag, at, l) in &entries {+            ctf.extend(tag);+            ctf.extend([0; 4]);+            ctf.extend(((body + at * 100) as u32).to_be_bytes());+            ctf.extend((*l as u32).to_be_bytes());+        }+        ctf.resize(body + 100, 0);+        ctf.extend(&cvt);+        let (a, b) = (pack(&ctf, Some(7)), pack(&[], None));+        let mut out = vec![3, 0, 0, 0];+        out.extend(&((10 + a.len()) as u32).to_be_bytes()[1..]);+        out.extend(&((10 + a.len() + b.len()) as u32).to_be_bytes()[1..]);+        out.extend([a, b.clone(), b].concat());+        out+    }++    /// Directory entries that share one range cannot add up to more than the size limit.+    #[test]+    fn the_rebuilt_font_is_bounded_as_a_whole() {+        assert!(decompress(&shared_ranges(3, 1 << 20)).is_some());+        assert!(decompress(&shared_ranges(70, 1 << 20)).is_none());+        // A table that grows when decoded is charged at its decoded size.+        assert!(decompress(&cvt_directory(3)).is_some());+        assert!(decompress(&cvt_directory(1000)).is_none());+    }+}diff --git a/crates/fonts/src/tests.rs b/crates/fonts/src/tests.rs--- a/crates/fonts/src/tests.rs+++ b/crates/fonts/src/tests.rs@@ -289,9 +289,9 @@ fn wrap_eot(font: &[u8], version: u32) -> Vec<u8> { #[test] fn embedded_open_type_yields_the_font() {     let font = epaint_default_fonts::UBUNTU_LIGHT;-    assert_eq!(embedded_sfnt(font), Some(font));-    assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0002_0001)), Some(font));-    assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0001_0000)), Some(font));+    assert_eq!(embedded_sfnt(font).as_deref(), Some(font));+    assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0002_0001)).as_deref(), Some(font));+    assert_eq!(embedded_sfnt(&wrap_eot(font, 0x0001_0000)).as_deref(), Some(font));     assert!(embedded_sfnt(b"not a font file").is_none());     assert!(embedded_sfnt(&wrap_eot(b"hello world!!", 0x0002_0002)).is_none());     let mut truncated = wrap_eot(font, 0x0002_0001);diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md--- a/docs/file-format-parity.md+++ b/docs/file-format-parity.md@@ -63,7 +63,7 @@ intermediate build that required inline `bytes` cannot read the new referenced r | WMV | yes (Windows) | embedded, not playable | | | Online video (YouTube embeds) | yes | no | Cloud feature | | Embedded OLE objects | yes | kept on round trip (opaque), not insertable or editable | `ShapeKind::Opaque` |-| Embedded fonts | yes | yes | `p:embeddedFontLst` read and written. TrueType, OpenType and Embedded OpenType faces are used to draw; MicroType Express subsets are kept and written back, not drawn |+| Embedded fonts | yes | yes | `p:embeddedFontLst` read and written. TrueType, OpenType, Embedded OpenType and MicroType Express faces are used to draw; the original part is written back | | 3-D models (`.glb`) | yes | kept | The model part round-trips (`p:contentPart`, including an `mc:AlternateContent` choice and its fallback picture). Not drawn as a mesh and not inserted from the ribbon |  ## PPTX coverage (PresentationML / DrawingML)@@ -99,7 +99,7 @@ the round trip even though DeckCraft doesn't model it. | Comments: legacy | yes | yes | | | Comments: modern threaded (`p188`) | yes | yes | Written next to the flattened legacy part; replies and resolved state round-trip (`threads_and_resolved_round_trip`) | | Unknown slide-level elements (`graphicFrame` OLE, extensions, `mc:AlternateContent`) | kept | kept | Rendered as a labelled placeholder with the preview picture when present |-| Embedded fonts | yes | yes | MicroType Express subsets kept, not drawn |+| Embedded fonts | yes | yes | MicroType Express subsets drawn and kept | | Tags (`p:custDataLst`) on the presentation, slides and shapes | yes | yes | Add-ins such as think-cell keep their identity; `tags_survive_round_trip` | | Custom XML data, sensitivity label (`docMetadata/LabelInfo.xml`), custom document properties | kept verbatim | yes | SharePoint metadata and the label survive a save; `custom_xml_label_and_custom_properties_survive_save` | | VBA | no | no | |diff --git a/docs/gaps.md b/docs/gaps.md--- a/docs/gaps.md+++ b/docs/gaps.md@@ -151,11 +151,6 @@ Detail in [file-format-parity.md](file-format-parity.md). - **Estimate:** 10–16 h (render the timeline frame by frame, which the GIF can then reuse for   transitions and builds; an H.264 encoder is the hard part, FilmCraft has one to copy). -### Embedded MicroType Express font subsets aren't drawn-- **Evidence:** `p:embeddedFontLst` is read and written. A raw TrueType/OpenType face, or one inside an Embedded OpenType wrapper, is loaded and used to draw. A MicroType Express subset (`application/x-fontdata` with neither form) is kept and written back, and the text still uses a stand-in.-- **Impact:** decks that embed a MicroType subset reflow until that face is installed.-- **Estimate:** 3–5 h.- ## Features  ### Font weights beyond Regular, Bold and Italic (#69)@@ -387,6 +382,7 @@ Detail in [localization-parity.md](localization-parity.md). | 2026-10-14 | minor | Community PR wave folded: one entry each for the soak tests (engine and UI), the PPTX corpus (seven owned corpus files), edit points / text warp / 3-D, proofing (Hunspell spelling, thesaurus, AutoCorrect), the smaller missing rows, dialogs (41 vs ~60; Print and Insert Object left) and pen/trackpad input; ranked rows 3, 4 and 10 de-duplicated (comment threads #70 and the pane timeline are done); the fixed command-palette entry (#79) removed | | 2026-10-13 | minor | EMF/WMF rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing. EMF+ records remain | | 2026-10-13 | minor | Embedded fonts are read, written and drawn (TrueType, OpenType, Embedded OpenType). The gap is now MicroType Express subsets, which are kept but not drawn |+| 2026-10-27 | minor | MicroType Express embedded fonts are decompressed and drawn; the gap is closed | | 2026-10-13 | minor | WordArt Transform gallery: Shape Format ▸ Text Effects lists the 40 presets and No Transform; `format.textWarp` applies one (adjust handles still open) | | 2026-10-13 | minor | Edit Points: smooth/straight/corner point types, Bézier handle dragging, right-click point menu, no selection box while editing | | 2026-10-13 | minor | Edit Points: convert to freeform, drag, add and delete vertices (`shape.editPoints`, `shape.movePoint`, `shape.addPoint`, `shape.deletePoint`); left: point types and control handles |diff --git a/docs/roadmap.md b/docs/roadmap.md--- a/docs/roadmap.md+++ b/docs/roadmap.md@@ -45,7 +45,7 @@ Beta means ~75% ready for real work and opening and saving PowerPoint's `.pptx` |---|---|---|---| | 1 | Stability: #60, #40/#59, soak and fuzz the editor | 10–20 | Stability 60 → 85% | | 2 | PPTX real-deck corpus and fixes | 20–35 | File formats 58 → 75% |-| 3 | PPTX round-trip losses (#70, #74, #77), `.potx`/`.ppsx`/`.pptm` content types, MicroType embedded-font subsets | 8–15 | File formats → 80% |+| 3 | PPTX round-trip losses (#70, #74, #77), `.potx`/`.ppsx`/`.pptm` content types | 8–15 | File formats → 80% | | 4 | EMF/WMF and SVG pictures | 8–14 | File formats, Pictures | | 5 | Vector PDF, printing | 10–16 | File formats, Files | | 6 | Localization infrastructure + zh-Hans, es, fr, de, ja (with IME) | 27–46 | Localization 8 → ~40% |@@ -121,6 +121,7 @@ plan above. | 2026-10-15 | trivial | PPTX trigger click steps survive save, including by-paragraph builds. Interactive-deck gaps remain media actions, audio triggers and click-box geometry; milestone scope and feature scores unchanged | | 2026-10-14 | trivial | M13 reliability: remote MCP validates IDs and envelopes, caps replies and rejects incomplete frames; #718 | | 2026-10-14 | minor | Community PR wave folded: one Review gate row, one Current focus item 2, one beta row each for items 9 and 10 (timeline and timer controls are in), one after-beta paragraph, one M9 and M10 row, one corpus row in the 2026-10-07 history |+| 2026-10-27 | trivial | Beta item 3: MicroType Express embedded-font subsets are drawn | | 2026-10-13 | trivial | Beta item 3: embedded fonts are in, except MicroType Express subsets | | 2026-10-13 | minor | M9: the WordArt Transform gallery applies a text warp; adjust handles stay open | | 2026-10-13 | trivial | Owned PPTX corpus check started; notes-master header and footer kept. Beta item 2 stays open for a larger corpus |