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
@@ -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 |@@ -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()); } } }@@ -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 thenew file mode 100644@@ -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());+ }+}@@ -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);@@ -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 | |@@ -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 |@@ -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 |