1#![forbid(unsafe_code)]
25
26#[cfg(feature = "bundled-faces")]
27pub mod bundled;
28pub mod cff;
29mod gvar;
30pub mod macos;
31pub mod native_route;
32pub mod outline;
33pub mod shaping;
34mod subset;
35pub mod text_run;
36pub use macos::{
37 DEFAULT_MAC_CONTEXT_BUDGET, MAX_MAC_RASTER_BYTES, MAX_MAC_RASTER_DIMENSION, MacBridgeDriver,
38 MacFontAdapter, MacFontAdapterConfig, MacFontAdapterError, RawCoreGraphicsRaster,
39 RawCoreTextGlyph, RawCoreTextLine, SimulatedMacBridge,
40};
41pub use native_route::{
42 FallbackFace, NativeShapingError, NativeShapingRequest, NativeShapingRoute, PlatformRunGlyph,
43 PlatformShapedOutput, ShapingRouteCapabilities, ShapingRouteKind, SimulatedFallbackRule,
44 SimulatedNativeRoute, assemble_platform_run,
45};
46pub use shaping::Direction;
47pub use subset::{EmbeddingFormat, Subset, SubsetError, SubsetErrorKind};
48pub use text_run::{
49 CaretAffinity, CaretPosition, FontId, FontOrigin, HitTestResult, OwnedTextRun, RunGlyph,
50 SelectionRect, TextCluster, TextRunContext, byte_to_utf16, utf16_to_byte,
51};
52
53#[must_use]
57pub fn variable_triangle_fixture() -> Vec<u8> {
58 gvar::variable_triangle_fixture()
59}
60
61const MAX_LAYOUT_GLYPHS: usize = 65_536;
68
69const MAX_COVERAGE_GLYPHS: usize = MAX_LAYOUT_GLYPHS;
71pub const MISSING_GLYPH_REMAP: u16 = u16::MAX;
76
77const MAX_VARIATION_AXES: usize = 64;
81const MAX_NAMED_INSTANCES: usize = 256;
84const MAX_AVAR_MAPS: usize = 64;
86type AvarAxisMaps = Vec<Option<Vec<(f32, f32)>>>;
88
89#[derive(Debug, Clone)]
90struct Cmap4Segment {
91 start: u16,
92 end: u16,
93 id_delta: u16,
94 id_range_offset: u16,
95 id_range_offset_pos: usize,
96}
97
98#[derive(Debug, Clone)]
99struct Cmap4Cache {
100 segments: Vec<Cmap4Segment>,
101 sorted_by_end: bool,
102}
103
104#[derive(Debug, Clone)]
106pub struct Font {
107 data: Vec<u8>,
108 pub units_per_em: u16,
110 pub num_glyphs: u16,
112 pub ascent: i16,
114 pub descent: i16,
116 pub line_gap: i16,
118 num_h_metrics: u16,
119 hmtx_off: usize,
120 cmap_off: usize,
121 cmap_format: u16,
122 cmap4_cache: Option<Cmap4Cache>,
123 glyf: Option<(usize, usize)>,
126 loca_off: Option<usize>,
128 loca_long: bool,
130 kern0: Option<(usize, u16)>,
132 variation: Option<FontVariation>,
135 latin1_glyphs: [u16; 256],
136 latin1_advances_1000: [u32; 256],
137}
138
139#[derive(Debug, Clone, Copy, PartialEq)]
145pub struct VariationAxis {
146 pub tag: [u8; 4],
148 pub min: f32,
150 pub default: f32,
152 pub max: f32,
154 pub flags: u16,
156 pub name_id: u16,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq)]
162pub struct AxisBounds {
163 pub min: f32,
165 pub default: f32,
167 pub max: f32,
169}
170
171#[derive(Debug, Clone, PartialEq)]
173pub struct NamedInstance {
174 pub subfamily_name_id: u16,
176 pub flags: u16,
178 pub coordinates: Vec<f32>,
180 pub postscript_name_id: Option<u16>,
182}
183
184#[derive(Debug, Clone)]
185struct FontVariation {
186 axes: Vec<VariationAxis>,
187 instances: Vec<NamedInstance>,
188 avar: AvarAxisMaps,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum FontError {
196 BadMagic,
198 MissingTable(&'static str),
200 Truncated,
202 NoUnicodeCmap,
204}
205
206impl core::fmt::Display for FontError {
207 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208 match self {
209 Self::BadMagic => write!(f, "not a TrueType/OpenType font"),
210 Self::MissingTable(t) => write!(f, "missing required font table: {t}"),
211 Self::Truncated => write!(f, "font data is truncated"),
212 Self::NoUnicodeCmap => write!(f, "no usable Unicode cmap (format 4/12)"),
213 }
214 }
215}
216
217impl std::error::Error for FontError {}
218
219pub(crate) fn be_u16(d: &[u8], o: usize) -> Option<u16> {
220 let bytes = d.get(o..o.checked_add(2)?)?;
221 Some(u16::from_be_bytes([bytes[0], bytes[1]]))
222}
223pub(crate) fn be_i16(d: &[u8], o: usize) -> Option<i16> {
224 be_u16(d, o).map(|v| v as i16)
225}
226pub(crate) fn be_u32(d: &[u8], o: usize) -> Option<u32> {
227 let bytes = d.get(o..o.checked_add(4)?)?;
228 Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
229}
230
231fn be_i32(d: &[u8], o: usize) -> Option<i32> {
232 be_u32(d, o).map(|v| v as i32)
233}
234
235fn fixed_16_16(v: i32) -> f32 {
237 (f64::from(v) / 65536.0) as f32
238}
239
240fn f2dot14(v: i16) -> f32 {
242 f32::from(v) / 16384.0
243}
244
245pub(crate) fn off(base: usize, delta: usize) -> Option<usize> {
246 base.checked_add(delta)
247}
248
249pub(crate) fn off_mul(base: usize, index: usize, stride: usize) -> Option<usize> {
250 base.checked_add(index.checked_mul(stride)?)
251}
252
253fn be_u16_at(d: &[u8], base: usize, delta: usize) -> Option<u16> {
254 be_u16(d, off(base, delta)?)
255}
256
257fn be_u32_at(d: &[u8], base: usize, delta: usize) -> Option<u32> {
258 be_u32(d, off(base, delta)?)
259}
260
261fn bytes_at(d: &[u8], base: usize, len: usize) -> Option<&[u8]> {
262 d.get(base..off(base, len)?)
263}
264
265fn write_u16(d: &mut [u8], off: usize, v: u16) -> Option<()> {
267 let b = v.to_be_bytes();
268 let dst = d.get_mut(off..off.checked_add(2)?)?;
269 dst.copy_from_slice(&b);
270 Some(())
271}
272
273pub(crate) fn write_u32(d: &mut [u8], off: usize, v: u32) -> Option<()> {
275 let b = v.to_be_bytes();
276 let dst = d.get_mut(off..off.checked_add(4)?)?;
277 dst.copy_from_slice(&b);
278 Some(())
279}
280
281pub(crate) fn table_checksum(d: &[u8]) -> u32 {
282 let mut sum: u32 = 0;
283 let mut chunks16 = d.chunks_exact(16);
284 for c in &mut chunks16 {
285 let w0 = u32::from_be_bytes([c[0], c[1], c[2], c[3]]);
286 let w1 = u32::from_be_bytes([c[4], c[5], c[6], c[7]]);
287 let w2 = u32::from_be_bytes([c[8], c[9], c[10], c[11]]);
288 let w3 = u32::from_be_bytes([c[12], c[13], c[14], c[15]]);
289 sum = sum
290 .wrapping_add(w0)
291 .wrapping_add(w1)
292 .wrapping_add(w2)
293 .wrapping_add(w3);
294 }
295 let mut chunks4 = chunks16.remainder().chunks_exact(4);
296 for c in &mut chunks4 {
297 sum = sum.wrapping_add(u32::from_be_bytes([c[0], c[1], c[2], c[3]]));
298 }
299 let rem = chunks4.remainder();
300 if !rem.is_empty() {
301 let mut buf = [0u8; 4];
302 buf[..rem.len()].copy_from_slice(rem);
303 sum = sum.wrapping_add(u32::from_be_bytes(buf));
304 }
305 sum
306}
307
308fn find_table(d: &[u8], tag: &[u8; 4]) -> Option<usize> {
309 find_table_full(d, tag).map(|(off, _)| off)
310}
311
312pub(crate) fn find_table_full(d: &[u8], tag: &[u8; 4]) -> Option<(usize, usize)> {
314 let num_tables = be_u16(d, 4)? as usize;
315 for i in 0..num_tables {
316 let rec = off_mul(12, i, 16)?;
317 if bytes_at(d, rec, 4)? == tag {
318 return Some((
319 be_u32_at(d, rec, 8)? as usize,
320 be_u32_at(d, rec, 12)? as usize,
321 ));
322 }
323 }
324 None
325}
326
327fn find_kern0(d: &[u8]) -> Option<(usize, u16)> {
329 let (kern, kern_len) = find_table_full(d, b"kern")?;
330 let table_end = kern.checked_add(kern_len)?;
331 let version = be_u16(d, kern)?;
332 let n_tables = be_u16_at(d, kern, 2)? as usize;
333 if version != 0 {
334 return None;
335 }
336
337 let mut sub = off(kern, 4)?;
338 for _ in 0..n_tables {
339 if sub.checked_add(6)? > table_end {
340 return None;
341 }
342 let length = be_u16_at(d, sub, 2)? as usize;
343 let coverage = be_u16_at(d, sub, 4)?;
344 let format = coverage >> 8;
345 let horizontal = coverage & 0x0001 != 0;
346 let minimum = coverage & 0x0002 != 0;
347 let pairs = off(sub, 14)?;
348 if format == 0 && horizontal && !minimum && length >= 14 {
349 let sub_end = sub.checked_add(length)?;
350 if sub_end > table_end {
351 return None;
352 }
353 let n_pairs = be_u16_at(d, sub, 6)?;
354 let bytes_needed = (n_pairs as usize).checked_mul(6)?;
355 if pairs.checked_add(bytes_needed)? <= sub_end {
356 return Some((pairs, n_pairs));
357 }
358 return None;
359 }
360 if length == 0 {
361 return None;
362 }
363 sub = sub.checked_add(length)?;
364 }
365 None
366}
367
368fn parse_fvar(d: &[u8], table_off: usize, table_len: usize) -> Option<FontVariation> {
372 let table_end = table_off.checked_add(table_len)?;
373 if table_off.checked_add(16)? > table_end {
374 return None;
375 }
376 let major = be_u16(d, table_off)?;
377 if major != 1 {
378 return None;
379 }
380 let axes_array_offset = be_u16_at(d, table_off, 4)? as usize;
381 let axis_count = be_u16_at(d, table_off, 8)? as usize;
382 let axis_size = be_u16_at(d, table_off, 10)? as usize;
383 let instance_count = be_u16_at(d, table_off, 12)? as usize;
384 let instance_size = be_u16_at(d, table_off, 14)? as usize;
385 if axis_size < 20 {
386 return None;
387 }
388 let n_axes = axis_count.min(MAX_VARIATION_AXES);
389 let axes_off = off(table_off, axes_array_offset)?;
390 let axes_bytes = n_axes.checked_mul(axis_size)?;
391 if axes_off.checked_add(axes_bytes)? > table_end {
392 return None;
393 }
394
395 let mut axes = Vec::with_capacity(n_axes);
396 for i in 0..n_axes {
397 let rec = off_mul(axes_off, i, axis_size)?;
398 let tag_bytes = bytes_at(d, rec, 4)?;
399 let mut tag = [0u8; 4];
400 tag.copy_from_slice(tag_bytes);
401 let min = fixed_16_16(be_i32(d, off(rec, 4)?)?);
402 let default = fixed_16_16(be_i32(d, off(rec, 8)?)?);
403 let max = fixed_16_16(be_i32(d, off(rec, 12)?)?);
404 let flags = be_u16_at(d, rec, 16)?;
405 let name_id = be_u16_at(d, rec, 18)?;
406 axes.push(VariationAxis {
407 tag,
408 min,
409 default,
410 max,
411 flags,
412 name_id,
413 });
414 }
415
416 let n_inst = instance_count.min(MAX_NAMED_INSTANCES);
417 let coord_bytes = n_axes.checked_mul(4)?;
418 let min_inst_size = 4usize.checked_add(coord_bytes)?;
419 let mut instances = Vec::new();
420 if instance_size >= min_inst_size {
421 let inst_off = off(axes_off, axes_bytes)?;
422 let inst_bytes = n_inst.checked_mul(instance_size)?;
423 if inst_off.checked_add(inst_bytes)? <= table_end {
424 let has_ps_name = instance_size >= min_inst_size.saturating_add(2);
425 for i in 0..n_inst {
426 let rec = off_mul(inst_off, i, instance_size)?;
427 let subfamily_name_id = be_u16(d, rec)?;
428 let flags = be_u16_at(d, rec, 2)?;
429 let mut coordinates = Vec::with_capacity(n_axes);
430 let mut ok = true;
431 for a in 0..n_axes {
432 let Some(coord) = off(rec, 4)
433 .and_then(|base| off_mul(base, a, 4))
434 .and_then(|o| be_i32(d, o))
435 else {
436 ok = false;
437 break;
438 };
439 coordinates.push(fixed_16_16(coord));
440 }
441 if !ok {
442 continue;
443 }
444 let postscript_name_id = if has_ps_name {
445 be_u16_at(d, rec, min_inst_size)
446 } else {
447 None
448 };
449 instances.push(NamedInstance {
450 subfamily_name_id,
451 flags,
452 coordinates,
453 postscript_name_id,
454 });
455 }
456 }
457 }
458
459 let avar = vec![None; axes.len()];
460 Some(FontVariation {
461 axes,
462 instances,
463 avar,
464 })
465}
466
467fn parse_avar(d: &[u8], table_off: usize, table_len: usize, n_axes: usize) -> Option<AvarAxisMaps> {
470 let table_end = table_off.checked_add(table_len)?;
471 if table_off.checked_add(8)? > table_end {
472 return None;
473 }
474 let major = be_u16(d, table_off)?;
475 if major != 1 {
476 return None;
477 }
478 let axis_count = be_u16_at(d, table_off, 6)? as usize;
479 if axis_count != n_axes {
480 return None;
481 }
482 let mut maps = Vec::with_capacity(n_axes);
483 let mut cursor = off(table_off, 8)?;
484 for _ in 0..n_axes {
485 if cursor.checked_add(2)? > table_end {
486 return None;
487 }
488 let count = be_u16(d, cursor)? as usize;
489 cursor = off(cursor, 2)?;
490 let n = count.min(MAX_AVAR_MAPS);
491 let bytes = n.checked_mul(4)?;
492 if cursor.checked_add(bytes)? > table_end {
493 return None;
494 }
495 let mut segs = Vec::with_capacity(n);
496 for i in 0..n {
497 let rec = off_mul(cursor, i, 4)?;
498 let from = f2dot14(be_i16(d, rec)?);
499 let to = f2dot14(be_i16_at(d, rec, 2)?);
500 segs.push((from, to));
501 }
502 let claimed = count.checked_mul(4)?;
504 cursor = off(cursor, claimed)?;
505 if segs.len() < 2 {
506 maps.push(None);
507 continue;
508 }
509 let sorted = segs.windows(2).all(|w| w[0].0 <= w[1].0);
512 maps.push(if sorted { Some(segs) } else { None });
513 }
514 Some(maps)
515}
516
517fn be_i16_at(d: &[u8], base: usize, delta: usize) -> Option<i16> {
518 be_i16(d, off(base, delta)?)
519}
520
521fn avar_map(maps: &[(f32, f32)], x: f32) -> f32 {
523 let Some(first) = maps.first() else {
524 return x;
525 };
526 if x <= first.0 {
527 return first.1;
528 }
529 let Some(last) = maps.last() else {
530 return x;
531 };
532 if x >= last.0 {
533 return last.1;
534 }
535 for pair in maps.windows(2) {
536 let (from0, to0) = pair[0];
537 let (from1, to1) = pair[1];
538 if x <= from1 {
539 let span = from1 - from0;
540 if span == 0.0 {
541 return to0;
542 }
543 let t = (x - from0) / span;
544 return to0 + t * (to1 - to0);
545 }
546 }
547 last.1
548}
549
550fn normalize_user(user: f32, axis: &VariationAxis) -> f32 {
554 let lo = axis.min.min(axis.max);
555 let hi = axis.min.max(axis.max);
556 let user = if user < lo {
557 lo
558 } else if user > hi {
559 hi
560 } else {
561 user
562 };
563 let default = if axis.default < lo {
564 lo
565 } else if axis.default > hi {
566 hi
567 } else {
568 axis.default
569 };
570 if user < default {
571 let span = default - lo;
572 if span == 0.0 {
573 0.0
574 } else {
575 (user - default) / span
576 }
577 } else if user > default {
578 let span = hi - default;
579 if span == 0.0 {
580 0.0
581 } else {
582 (user - default) / span
583 }
584 } else {
585 0.0
586 }
587}
588
589impl Font {
590 pub fn parse(data: Vec<u8>) -> Result<Self, FontError> {
596 let d = data.as_slice();
597 let magic = be_u32(d, 0).ok_or(FontError::Truncated)?;
598 if magic != 0x0001_0000 && magic != 0x7472_7565 && magic != 0x4F54_544F {
600 return Err(FontError::BadMagic);
601 }
602
603 let head = find_table(d, b"head").ok_or(FontError::MissingTable("head"))?;
604 let maxp = find_table(d, b"maxp").ok_or(FontError::MissingTable("maxp"))?;
605 let hhea = find_table(d, b"hhea").ok_or(FontError::MissingTable("hhea"))?;
606 let hmtx = find_table(d, b"hmtx").ok_or(FontError::MissingTable("hmtx"))?;
607 let cmap = find_table(d, b"cmap").ok_or(FontError::MissingTable("cmap"))?;
608
609 let units_per_em =
610 be_u16(d, off(head, 18).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
611 let num_glyphs =
612 be_u16(d, off(maxp, 4).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
613 let ascent =
614 be_i16(d, off(hhea, 4).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
615 let descent =
616 be_i16(d, off(hhea, 6).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
617 let line_gap =
618 be_i16(d, off(hhea, 8).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
619 let num_h_metrics =
620 be_u16(d, off(hhea, 34).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
621
622 let (cmap_off, cmap_format) = select_cmap(d, cmap).ok_or(FontError::NoUnicodeCmap)?;
623 let cmap4_cache = if cmap_format == 4 {
624 parse_cmap4_cache(d, cmap_off)
625 } else {
626 None
627 };
628
629 let loca_long = off(head, 50)
632 .and_then(|offset| be_i16(d, offset))
633 .unwrap_or(0)
634 != 0;
635 let loca_off = find_table(d, b"loca");
636 let glyf = find_table_full(d, b"glyf");
637 let kern0 = find_kern0(d);
638 let mut variation =
639 find_table_full(d, b"fvar").and_then(|(off, len)| parse_fvar(d, off, len));
640 if let Some(var) = variation.as_mut() {
641 if let Some(avar) = find_table_full(d, b"avar")
642 .and_then(|(off, len)| parse_avar(d, off, len, var.axes.len()))
643 {
644 var.avar = avar;
645 }
646 }
647
648 let mut font = Self {
649 data,
650 units_per_em,
651 num_glyphs,
652 ascent,
653 descent,
654 line_gap,
655 num_h_metrics,
656 hmtx_off: hmtx,
657 cmap_off,
658 cmap_format,
659 cmap4_cache,
660 glyf,
661 loca_off,
662 loca_long,
663 kern0,
664 variation,
665 latin1_glyphs: [0; 256],
666 latin1_advances_1000: [0; 256],
667 };
668 for b in 0..256 {
669 let gid = match font.cmap_format {
670 4 => font.cmap4_lookup(b as u32).unwrap_or(0),
671 12 => font.cmap12_lookup(b as u32).unwrap_or(0),
672 _ => 0,
673 };
674 font.latin1_glyphs[b] = gid;
675 }
676 if font.units_per_em > 0 {
677 for b in 0..256 {
678 let gid = font.latin1_glyphs[b];
679 let aw = font.advance_width(gid) as u32;
680 font.latin1_advances_1000[b] = aw * 1000 / font.units_per_em as u32;
681 }
682 }
683 Ok(font)
684 }
685
686 #[must_use]
688 pub fn has_glyf_outlines(&self) -> bool {
689 self.glyf.is_some() && self.loca_off.is_some()
690 }
691
692 #[must_use]
694 pub fn axes(&self) -> &[VariationAxis] {
695 self.variation
696 .as_ref()
697 .map(|v| v.axes.as_slice())
698 .unwrap_or(&[])
699 }
700
701 #[must_use]
703 pub fn named_instances(&self) -> &[NamedInstance] {
704 self.variation
705 .as_ref()
706 .map(|v| v.instances.as_slice())
707 .unwrap_or(&[])
708 }
709
710 #[must_use]
712 pub fn instance_bounds(&self, tag: [u8; 4]) -> Option<AxisBounds> {
713 self.axes()
714 .iter()
715 .find(|a| a.tag == tag)
716 .map(|a| AxisBounds {
717 min: a.min,
718 default: a.default,
719 max: a.max,
720 })
721 }
722
723 #[must_use]
729 pub fn normalized_axis(&self, tag: [u8; 4], user: f32) -> Option<f32> {
730 let var = self.variation.as_ref()?;
731 let (idx, axis) = var.axes.iter().enumerate().find(|(_, a)| a.tag == tag)?;
732 let mut n = normalize_user(user, axis);
733 if let Some(maps) = var.avar.get(idx).and_then(|m| m.as_ref()) {
734 n = avar_map(maps, n);
735 }
736 Some(n)
737 }
738
739 #[must_use]
748 pub fn instance(&self, weight: f32) -> Option<Font> {
749 crate::gvar::instance_font(self, weight)
750 }
751
752 #[must_use]
755 pub fn as_sfnt(&self) -> &[u8] {
756 &self.data
757 }
758
759 pub(crate) fn raw_bytes(&self) -> &[u8] {
760 self.as_sfnt()
761 }
762
763 fn glyph_range(&self, gid: u16) -> Option<(usize, usize)> {
767 let loca = self.loca_off?;
768 let (glyf_off, glyf_len) = self.glyf?;
769 let i = gid as usize;
770 let (start, end) = if self.loca_long {
771 (
772 be_u32(&self.data, off_mul(loca, i, 4)?)? as usize,
773 be_u32(&self.data, off_mul(loca, i.checked_add(1)?, 4)?)? as usize,
774 )
775 } else {
776 (
778 be_u16(&self.data, off_mul(loca, i, 2)?)? as usize * 2,
779 be_u16(&self.data, off_mul(loca, i.checked_add(1)?, 2)?)? as usize * 2,
780 )
781 };
782 if end < start || end > glyf_len {
783 return None;
784 }
785 Some((off(glyf_off, start)?, off(glyf_off, end)?))
786 }
787
788 #[must_use]
791 pub fn glyph_data(&self, gid: u16) -> Option<&[u8]> {
792 let (s, e) = self.glyph_range(gid)?;
793 self.data.get(s..e)
794 }
795
796 #[must_use]
799 pub fn glyph_bbox(&self, gid: u16) -> Option<[i16; 4]> {
800 let (s, e) = self.glyph_range(gid)?;
801 if e <= s {
802 return None; }
804 Some([
805 be_i16(&self.data, off(s, 2)?)?,
806 be_i16(&self.data, off(s, 4)?)?,
807 be_i16(&self.data, off(s, 6)?)?,
808 be_i16(&self.data, off(s, 8)?)?,
809 ])
810 }
811
812 #[must_use]
814 pub fn is_composite(&self, gid: u16) -> bool {
815 match self.glyph_range(gid) {
816 Some((s, e)) if e > s => be_i16(&self.data, s).is_some_and(|n| n < 0),
817 _ => false,
818 }
819 }
820
821 #[must_use]
824 pub fn glyph_components(&self, gid: u16) -> Vec<u16> {
825 const ARG_WORDS: u16 = 0x0001;
826 const WE_HAVE_SCALE: u16 = 0x0008;
827 const MORE: u16 = 0x0020;
828 const X_Y_SCALE: u16 = 0x0040;
829 const TWO_BY_TWO: u16 = 0x0080;
830
831 let mut out = Vec::new();
832 let Some((s, e)) = self.glyph_range(gid) else {
833 return out;
834 };
835 if e <= s || be_i16(&self.data, s).is_none_or(|n| n >= 0) {
836 return out;
837 }
838 let Some(mut p) = off(s, 10) else {
839 return out;
840 };
841 while let Some(component_record_end) = off(p, 4) {
842 if component_record_end > e {
843 break;
844 }
845 let Some(flags) = be_u16(&self.data, p) else {
846 break;
847 };
848 let Some(comp) = off(p, 2).and_then(|offset| be_u16(&self.data, offset)) else {
849 break;
850 };
851 let mut step = 4usize + if flags & ARG_WORDS != 0 { 4 } else { 2 };
852 step += if flags & WE_HAVE_SCALE != 0 {
853 2
854 } else if flags & X_Y_SCALE != 0 {
855 4
856 } else if flags & TWO_BY_TWO != 0 {
857 8
858 } else {
859 0
860 };
861 let Some(next) = off(p, step) else {
862 break;
863 };
864 if next > e {
865 break;
866 }
867 out.push(comp);
868 p = next;
869 if flags & MORE == 0 || p >= e {
870 break;
871 }
872 }
873 out
874 }
875
876 #[must_use]
879 pub fn advance_width(&self, gid: u16) -> u16 {
880 let last = self.num_h_metrics.saturating_sub(1);
881 let idx = gid.min(last) as usize;
882 off_mul(self.hmtx_off, idx, 4)
883 .and_then(|offset| be_u16(&self.data, offset))
884 .unwrap_or(0)
885 }
886
887 #[must_use]
890 pub fn left_side_bearing(&self, gid: u16) -> i16 {
891 if self.num_h_metrics == 0 {
892 return 0;
893 }
894 let gid = gid as usize;
895 let num_h_metrics = self.num_h_metrics as usize;
896 let offset = if gid < num_h_metrics {
897 off_mul(self.hmtx_off, gid, 4).and_then(|base| off(base, 2))
898 } else {
899 off_mul(self.hmtx_off, num_h_metrics, 4)
900 .and_then(|base| off_mul(base, gid - num_h_metrics, 2))
901 };
902 offset
903 .and_then(|offset| be_i16(&self.data, offset))
904 .unwrap_or(0)
905 }
906
907 #[must_use]
909 #[inline(always)]
910 pub fn glyph_index(&self, ch: char) -> u16 {
911 let cp = ch as u32;
912 if cp < 256 {
913 return self.latin1_glyphs[cp as usize];
914 }
915 match self.cmap_format {
916 4 => self.cmap4_lookup(cp).unwrap_or(0),
917 12 => self.cmap12_lookup(cp).unwrap_or(0),
918 _ => 0,
919 }
920 }
921
922 #[must_use]
927 #[inline(always)]
928 pub fn advance_1000(&self, ch: char) -> u32 {
929 let cp = ch as u32;
930 if cp < 256 {
931 return self.latin1_advances_1000[cp as usize];
932 }
933 if self.units_per_em == 0 {
934 return 0;
935 }
936 let aw = self.advance_width(self.glyph_index(ch)) as u32;
937 aw * 1000 / self.units_per_em as u32
938 }
939
940 #[must_use]
946 pub fn kerning_between_glyphs(&self, left: u16, right: u16) -> i16 {
947 let Some((pairs, n_pairs)) = self.kern0 else {
948 return 0;
949 };
950 let target = ((left as u32) << 16) | right as u32;
951 let mut lo = 0usize;
952 let mut hi = n_pairs as usize;
953 while lo < hi {
954 let mid = lo + (hi - lo) / 2;
955 let Some(rec) = off_mul(pairs, mid, 6) else {
956 return 0;
957 };
958 let Some(l) = be_u16(&self.data, rec) else {
959 return 0;
960 };
961 let Some(r) = off(rec, 2).and_then(|offset| be_u16(&self.data, offset)) else {
962 return 0;
963 };
964 let key = ((l as u32) << 16) | r as u32;
965 if key == target {
966 return off(rec, 4)
967 .and_then(|offset| be_i16(&self.data, offset))
968 .unwrap_or(0);
969 }
970 if key < target {
971 lo = mid + 1;
972 } else {
973 hi = mid;
974 }
975 }
976 0
977 }
978
979 #[must_use]
981 pub fn kerning(&self, left: char, right: char) -> i16 {
982 self.kerning_between_glyphs(self.glyph_index(left), self.glyph_index(right))
983 }
984
985 #[must_use]
987 pub fn kerning_1000(&self, left: char, right: char) -> i32 {
988 if self.units_per_em == 0 {
989 return 0;
990 }
991 self.kerning(left, right) as i32 * 1000 / self.units_per_em as i32
992 }
993
994 fn cmap4_lookup(&self, cp: u32) -> Option<u16> {
995 if cp > 0xFFFF {
996 return Some(0);
997 }
998 let c = cp as u16;
999 if let Some(cache) = &self.cmap4_cache {
1000 return self.cmap4_cached_lookup(c, cache);
1001 }
1002 self.cmap4_uncached_lookup(c)
1003 }
1004
1005 fn cmap4_cached_lookup(&self, c: u16, cache: &Cmap4Cache) -> Option<u16> {
1006 let segment = if cache.sorted_by_end {
1007 let idx = cache.segments.partition_point(|seg| seg.end < c);
1008 cache.segments.get(idx)
1009 } else {
1010 cache.segments.iter().find(|seg| c <= seg.end)
1011 }?;
1012
1013 if c < segment.start {
1014 return Some(0);
1015 }
1016 if segment.id_range_offset == 0 {
1017 return Some(c.wrapping_add(segment.id_delta));
1018 }
1019 let gi_addr = off(
1020 off(
1021 segment.id_range_offset_pos,
1022 segment.id_range_offset as usize,
1023 )?,
1024 2usize.checked_mul((c - segment.start) as usize)?,
1025 )?;
1026 let g = be_u16(&self.data, gi_addr)?;
1027 Some(if g == 0 {
1028 0
1029 } else {
1030 g.wrapping_add(segment.id_delta)
1031 })
1032 }
1033
1034 fn cmap4_uncached_lookup(&self, c: u16) -> Option<u16> {
1035 let d = &self.data;
1036 let base = self.cmap_off;
1037 let seg_x2 = be_u16(d, off(base, 6)?)? as usize;
1038 let seg_count = seg_x2 / 2;
1039 let end_codes = off(base, 14)?;
1040 let start_codes = off(off(end_codes, seg_x2)?, 2)?; let id_deltas = off(start_codes, seg_x2)?;
1042 let id_range_offsets = off(id_deltas, seg_x2)?;
1043 for i in 0..seg_count {
1044 let end = be_u16(d, off_mul(end_codes, i, 2)?)?;
1045 if c > end {
1046 continue;
1047 }
1048 let start = be_u16(d, off_mul(start_codes, i, 2)?)?;
1049 if c < start {
1050 return Some(0);
1051 }
1052 let id_delta = be_u16(d, off_mul(id_deltas, i, 2)?)?;
1053 let iro_pos = off_mul(id_range_offsets, i, 2)?;
1054 let id_range_offset = be_u16(d, iro_pos)?;
1055 if id_range_offset == 0 {
1056 return Some(c.wrapping_add(id_delta));
1057 }
1058 let gi_addr = off(
1059 off(iro_pos, id_range_offset as usize)?,
1060 2usize.checked_mul((c - start) as usize)?,
1061 )?;
1062 let g = be_u16(d, gi_addr)?;
1063 return Some(if g == 0 { 0 } else { g.wrapping_add(id_delta) });
1064 }
1065 Some(0)
1066 }
1067
1068 #[must_use]
1075 pub fn subset(&self, keep: &[char]) -> Option<Vec<u8>> {
1076 let seed: Vec<u16> = keep.iter().map(|&c| self.glyph_index(c)).collect();
1077 self.subset_core(&seed, keep, true, false)
1081 .ok()
1082 .map(|(bytes, _)| bytes)
1083 }
1084
1085 pub fn subset_glyphs(
1098 &self,
1099 glyphs: &[u16],
1100 cmap_chars: &[char],
1101 ) -> Option<(Vec<u8>, std::collections::BTreeMap<u16, u16>)> {
1102 let (bytes, lookup) = self.subset_glyphs_with_lookup(glyphs, cmap_chars)?;
1103 let mut new_of = std::collections::BTreeMap::new();
1104 for (old, new) in lookup.into_iter().enumerate() {
1105 if new != MISSING_GLYPH_REMAP {
1106 new_of.insert(u16::try_from(old).ok()?, new);
1107 }
1108 }
1109 Some((bytes, new_of))
1110 }
1111
1112 pub fn subset_glyphs_with_lookup(
1125 &self,
1126 glyphs: &[u16],
1127 cmap_chars: &[char],
1128 ) -> Option<(Vec<u8>, Vec<u16>)> {
1129 self.subset_core(glyphs, cmap_chars, false, false).ok()
1132 }
1133
1134 fn subset_core(
1135 &self,
1136 seed_glyphs: &[u16],
1137 cmap_chars: &[char],
1138 include_os2: bool,
1139 strict: bool,
1140 ) -> Result<(Vec<u8>, Vec<u16>), SubsetError> {
1141 let mut error = SubsetError::new(SubsetErrorKind::Malformed, *b"sfnt", None, None);
1142 if !self.has_glyf_outlines() {
1145 return Err(SubsetError::new(
1146 SubsetErrorKind::UnsupportedFormat,
1147 *b"sfnt",
1148 None,
1149 None,
1150 ));
1151 }
1152 let mut set: std::collections::BTreeSet<u16> = std::collections::BTreeSet::new();
1153 set.insert(0);
1154 for &gid in seed_glyphs {
1155 if strict && gid >= self.num_glyphs {
1156 return Err(SubsetError::new(
1157 SubsetErrorKind::InvalidGlyph,
1158 *b"maxp",
1159 Some(gid),
1160 None,
1161 ));
1162 }
1163 if gid != 0 && gid < self.num_glyphs {
1164 set.insert(gid);
1165 }
1166 }
1167 let mut worklist: Vec<u16> = set.iter().copied().collect();
1175 while let Some(gid) = worklist.pop() {
1176 if strict {
1177 self.validate_subset_glyph(gid)?;
1178 }
1179 if self.is_composite(gid) {
1180 for c in self.glyph_components(gid) {
1181 if c < self.num_glyphs && set.insert(c) {
1182 worklist.push(c);
1183 }
1184 }
1185 }
1186 }
1187 let old_gids: Vec<u16> = set.into_iter().collect(); let mut new_of_lookup = vec![MISSING_GLYPH_REMAP; usize::from(self.num_glyphs).max(1)];
1193 for (i, &g) in old_gids.iter().enumerate() {
1194 let new_gid = u16::try_from(i).ok().ok_or(error)?;
1195 *new_of_lookup.get_mut(usize::from(g)).ok_or(error)? = new_gid;
1196 }
1197 let n = old_gids.len();
1198 let n_u16 = u16::try_from(n).ok().ok_or(error)?;
1199
1200 let mut glyf_bytes: Vec<u8> = Vec::with_capacity(n.saturating_mul(64));
1202 let mut loca_bytes: Vec<u8> =
1203 Vec::with_capacity(n.checked_add(1).ok_or(error)?.checked_mul(4).ok_or(error)?);
1204 for &old in &old_gids {
1205 error = SubsetError::new(SubsetErrorKind::Malformed, *b"glyf", Some(old), None);
1206 let offset = u32::try_from(glyf_bytes.len()).ok().ok_or(error)?;
1207 loca_bytes.extend_from_slice(&offset.to_be_bytes());
1208 let gb = self.subset_glyph_bytes(old, &new_of_lookup).ok_or(error)?;
1209 glyf_bytes.extend_from_slice(&gb);
1210 let rem = glyf_bytes.len() % 4;
1213 if rem != 0 {
1214 glyf_bytes.resize(glyf_bytes.len() + (4 - rem), 0);
1215 }
1216 }
1217 let final_offset = u32::try_from(glyf_bytes.len()).ok().ok_or(error)?;
1218 loca_bytes.extend_from_slice(&final_offset.to_be_bytes());
1219
1220 error = SubsetError::new(SubsetErrorKind::Malformed, *b"maxp", None, None);
1223 let (maxp_off, maxp_len) = find_table_full(&self.data, b"maxp").ok_or(error)?;
1224 let mut maxp = self
1225 .data
1226 .get(maxp_off..off(maxp_off, maxp_len).ok_or(error)?)
1227 .ok_or(error)?
1228 .to_vec();
1229 write_u16(&mut maxp, 4, n_u16).ok_or(error)?;
1230
1231 error = SubsetError::new(SubsetErrorKind::Malformed, *b"hhea", None, None);
1233 let (hhea_off, hhea_len) = find_table_full(&self.data, b"hhea").ok_or(error)?;
1234 let mut hhea = self
1235 .data
1236 .get(hhea_off..off(hhea_off, hhea_len).ok_or(error)?)
1237 .ok_or(error)?
1238 .to_vec();
1239 write_u16(&mut hhea, 34, n_u16).ok_or(error)?;
1240
1241 let mut hmtx: Vec<u8> = Vec::with_capacity(n.checked_mul(4).ok_or(error)?);
1243 for &old in &old_gids {
1244 let [a0, a1] = self.advance_width(old).to_be_bytes();
1245 let [l0, l1] = self.left_side_bearing(old).to_be_bytes();
1246 hmtx.extend_from_slice(&[a0, a1, l0, l1]);
1247 }
1248
1249 error = SubsetError::new(SubsetErrorKind::Malformed, *b"head", None, None);
1251 let (head_off, head_len) = find_table_full(&self.data, b"head").ok_or(error)?;
1252 let mut head = self
1253 .data
1254 .get(head_off..off(head_off, head_len).ok_or(error)?)
1255 .ok_or(error)?
1256 .to_vec();
1257 write_u32(&mut head, 8, 0).ok_or(error)?;
1258 write_u16(&mut head, 50, 1).ok_or(error)?; error = SubsetError::new(SubsetErrorKind::Capacity, *b"cmap", None, None);
1266 let cmap = if self.subset_reaches_supplementary_plane(cmap_chars, &new_of_lookup) {
1267 self.build_cmap12(cmap_chars, &new_of_lookup).ok_or(error)?
1268 } else {
1269 self.build_cmap4(cmap_chars, &new_of_lookup).ok_or(error)?
1270 };
1271
1272 let mut name: Vec<u8> = Vec::with_capacity(6);
1274 name.extend_from_slice(&0u16.to_be_bytes());
1275 name.extend_from_slice(&0u16.to_be_bytes());
1276 name.extend_from_slice(&6u16.to_be_bytes());
1277
1278 let mut post: Vec<u8> = Vec::with_capacity(32);
1280 post.extend_from_slice(&0x0003_0000u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u16.to_be_bytes()); post.extend_from_slice(&0u16.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); let os2: Option<Vec<u8>> = if include_os2 {
1298 find_table_full(&self.data, b"OS/2")
1299 .and_then(|(o, l)| Some(self.data.get(o..off(o, l)?)?.to_vec()))
1300 } else {
1301 None
1302 };
1303
1304 let mut tables: Vec<(&[u8; 4], Vec<u8>)> = vec![
1306 (b"head", head),
1307 (b"hhea", hhea),
1308 (b"maxp", maxp),
1309 (b"hmtx", hmtx),
1310 (b"loca", loca_bytes),
1311 (b"glyf", glyf_bytes),
1312 (b"cmap", cmap),
1313 (b"name", name),
1314 (b"post", post),
1315 ];
1316 if let Some(os2) = os2 {
1317 tables.push((b"OS/2", os2));
1318 }
1319 tables.sort_by(|a, b| a.0.cmp(b.0)); error = SubsetError::new(SubsetErrorKind::Capacity, *b"sfnt", None, None);
1322 let num_tables = tables.len();
1323 let mut pw: usize = 1;
1325 let mut es: u16 = 0;
1326 while pw * 2 <= num_tables {
1327 pw *= 2;
1328 es += 1;
1329 }
1330 let search_range = (pw as u16).wrapping_mul(16);
1331 let entry_selector = es;
1332 let range_shift = (num_tables as u16)
1333 .wrapping_mul(16)
1334 .wrapping_sub(search_range);
1335
1336 let dir_size = 12 + num_tables * 16;
1337 let mut body: Vec<u8> = Vec::new();
1338 let mut records: Vec<([u8; 4], u32, u32, u32)> = Vec::with_capacity(num_tables);
1340 let mut head_offset: usize = 0;
1341 for (tag, bytes) in &tables {
1342 while (dir_size + body.len()) % 4 != 0 {
1344 body.push(0);
1345 }
1346 let table_offset = dir_size + body.len();
1347 if *tag == b"head" {
1348 head_offset = table_offset;
1349 }
1350 let checksum = table_checksum(bytes);
1351 records.push((
1352 **tag,
1353 checksum,
1354 u32::try_from(table_offset).ok().ok_or(error)?,
1355 u32::try_from(bytes.len()).ok().ok_or(error)?,
1356 ));
1357 body.extend_from_slice(bytes);
1358 }
1359 while body.len() % 4 != 0 {
1360 body.push(0);
1361 }
1362
1363 let mut out: Vec<u8> = Vec::with_capacity(dir_size + body.len());
1364 out.extend_from_slice(&0x0001_0000u32.to_be_bytes()); out.extend_from_slice(&(num_tables as u16).to_be_bytes());
1366 out.extend_from_slice(&search_range.to_be_bytes());
1367 out.extend_from_slice(&entry_selector.to_be_bytes());
1368 out.extend_from_slice(&range_shift.to_be_bytes());
1369 for (tag, checksum, toff, tlen) in &records {
1370 out.extend_from_slice(tag);
1371 out.extend_from_slice(&checksum.to_be_bytes());
1372 out.extend_from_slice(&toff.to_be_bytes());
1373 out.extend_from_slice(&tlen.to_be_bytes());
1374 }
1375 out.extend_from_slice(&body);
1376
1377 let file_checksum = table_checksum(&out);
1379 let adj = 0xB1B0_AFBAu32.wrapping_sub(file_checksum);
1380 write_u32(&mut out, off(head_offset, 8).ok_or(error)?, adj).ok_or(error)?;
1381
1382 Ok((out, new_of_lookup))
1383 }
1384
1385 fn subset_glyph_bytes(&self, old: u16, new_of: &[u16]) -> Option<Vec<u8>> {
1390 const ARG_WORDS: u16 = 0x0001;
1391 const WE_HAVE_SCALE: u16 = 0x0008;
1392 const MORE: u16 = 0x0020;
1393 const X_Y_SCALE: u16 = 0x0040;
1394 const TWO_BY_TWO: u16 = 0x0080;
1395 const WE_HAVE_INSTRUCTIONS: u16 = 0x0100;
1396
1397 let data = self.glyph_data(old).unwrap_or(&[]);
1398 if data.is_empty() {
1399 return Some(Vec::new());
1400 }
1401 let num_contours = be_i16(data, 0)?;
1402 if num_contours >= 0 {
1403 return strip_simple_glyph_instructions(data, num_contours as usize);
1404 }
1405 let mut out = data.to_vec();
1407 let mut p = 10usize; let mut instruction_flags_positions = Vec::new();
1409 let mut dangling_more = false;
1410 loop {
1411 let last_flags_pos = p;
1412 let flags = be_u16(&out, p)?;
1413
1414 if flags & WE_HAVE_INSTRUCTIONS != 0 {
1415 instruction_flags_positions.push(p);
1416 }
1417 let comp_old = be_u16_at(&out, p, 2)?;
1418 let comp_new = remapped_gid(new_of, comp_old).unwrap_or(0);
1424 let nb = comp_new.to_be_bytes();
1425 *out.get_mut(off(p, 2)?)? = nb[0];
1426 *out.get_mut(off(p, 3)?)? = nb[1];
1427 p = off(p, 4)?;
1428 p = off(p, if flags & ARG_WORDS != 0 { 4 } else { 2 })?;
1429 if flags & WE_HAVE_SCALE != 0 {
1430 p = off(p, 2)?;
1431 } else if flags & X_Y_SCALE != 0 {
1432 p = off(p, 4)?;
1433 } else if flags & TWO_BY_TWO != 0 {
1434 p = off(p, 8)?;
1435 }
1436 if flags & MORE == 0 {
1437 break;
1438 }
1439
1440 if off(p, 4).is_none_or(|end| end > out.len()) {
1449 write_u16(
1450 &mut out,
1451 last_flags_pos,
1452 flags & !(MORE | WE_HAVE_INSTRUCTIONS),
1453 )?;
1454 dangling_more = true;
1455 break;
1456 }
1457 }
1458 if dangling_more {
1459 for flags_pos in &instruction_flags_positions {
1460 let flags = be_u16(&out, *flags_pos)?;
1461 write_u16(&mut out, *flags_pos, flags & !WE_HAVE_INSTRUCTIONS)?;
1462 }
1463 } else if !instruction_flags_positions.is_empty() {
1464 for flags_pos in instruction_flags_positions {
1465 let flags = be_u16(&out, flags_pos)?;
1466 write_u16(&mut out, flags_pos, flags & !WE_HAVE_INSTRUCTIONS)?;
1467 }
1468 let instruction_len = be_u16(&out, p)? as usize;
1469 let instruction_start = off(p, 2)?;
1470 let instruction_end = off(instruction_start, instruction_len)?;
1471 if instruction_end > out.len() {
1472 return None;
1473 }
1474 out.drain(p..instruction_end);
1475 }
1476 Some(out)
1477 }
1478
1479 fn build_cmap4(&self, keep: &[char], new_of: &[u16]) -> Option<Vec<u8>> {
1483 let mut codes: std::collections::BTreeMap<u16, u16> = std::collections::BTreeMap::new();
1485 for &ch in keep {
1486 let cp = ch as u32;
1487 if cp >= 0xFFFF {
1488 continue;
1489 }
1490 let old = self.glyph_index(ch);
1491 let Some(ng) = remapped_gid(new_of, old) else {
1495 continue;
1496 };
1497 codes.insert(cp as u16, ng);
1498 }
1499 let entries: Vec<(u16, u16)> = codes.into_iter().collect();
1500 let seg_count = entries.len().checked_add(1)?; let sub_len = 16usize.checked_add(seg_count.checked_mul(8)?)?;
1502 let sub_len_u16 = u16::try_from(sub_len).ok()?;
1503 let seg_count_x2 = u16::try_from(seg_count.checked_mul(2)?).ok()?;
1504
1505 let mut pw: usize = 1;
1506 let mut es: u16 = 0;
1507 while pw * 2 <= seg_count {
1508 pw *= 2;
1509 es += 1;
1510 }
1511 let search_range = u16::try_from(pw.checked_mul(2)?).ok()?;
1512 let entry_selector = es;
1513 let range_shift = seg_count_x2.checked_sub(search_range)?;
1514
1515 let mut sub: Vec<u8> = Vec::with_capacity(sub_len);
1516 sub.extend_from_slice(&4u16.to_be_bytes()); sub.extend_from_slice(&sub_len_u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); sub.extend_from_slice(&seg_count_x2.to_be_bytes()); sub.extend_from_slice(&search_range.to_be_bytes());
1521 sub.extend_from_slice(&entry_selector.to_be_bytes());
1522 sub.extend_from_slice(&range_shift.to_be_bytes());
1523 for &(code, _) in &entries {
1525 sub.extend_from_slice(&code.to_be_bytes());
1526 }
1527 sub.extend_from_slice(&0xFFFFu16.to_be_bytes());
1528 sub.extend_from_slice(&0u16.to_be_bytes());
1530 for &(code, _) in &entries {
1532 sub.extend_from_slice(&code.to_be_bytes());
1533 }
1534 sub.extend_from_slice(&0xFFFFu16.to_be_bytes());
1535 for &(code, ng) in &entries {
1537 sub.extend_from_slice(&ng.wrapping_sub(code).to_be_bytes());
1538 }
1539 sub.extend_from_slice(&1u16.to_be_bytes());
1541 for _ in &entries {
1543 sub.extend_from_slice(&0u16.to_be_bytes());
1544 }
1545 sub.extend_from_slice(&0u16.to_be_bytes());
1546
1547 let mut cmap: Vec<u8> = Vec::with_capacity(12 + sub.len());
1548 cmap.extend_from_slice(&0u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&3u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&12u32.to_be_bytes()); cmap.extend_from_slice(&sub);
1554 Some(cmap)
1555 }
1556
1557 fn subset_reaches_supplementary_plane(&self, keep: &[char], new_of: &[u16]) -> bool {
1560 keep.iter().any(|&ch| {
1561 (ch as u32) >= 0x1_0000 && remapped_gid(new_of, self.glyph_index(ch)).is_some()
1562 })
1563 }
1564
1565 fn build_cmap12(&self, keep: &[char], new_of: &[u16]) -> Option<Vec<u8>> {
1572 let mut codes: std::collections::BTreeMap<u32, u16> = std::collections::BTreeMap::new();
1574 for &ch in keep {
1575 let old = self.glyph_index(ch);
1576 let Some(ng) = remapped_gid(new_of, old) else {
1577 continue;
1578 };
1579 codes.insert(u32::from(ch), ng);
1580 }
1581 struct Group {
1582 start_cp: u32,
1583 end_cp: u32,
1584 start_gid: u16,
1585 }
1586 let mut groups: Vec<Group> = Vec::with_capacity(codes.len());
1587 for (&cp, &ng) in &codes {
1588 match groups.last_mut() {
1589 Some(g)
1590 if g.end_cp.checked_add(1) == Some(cp)
1591 && u64::from(g.start_gid) + (g.end_cp - g.start_cp) as u64 + 1
1592 == u64::from(ng) =>
1593 {
1594 g.end_cp = cp;
1595 }
1596 _ => groups.push(Group {
1597 start_cp: cp,
1598 end_cp: cp,
1599 start_gid: ng,
1600 }),
1601 }
1602 }
1603
1604 let sub_len = 16usize.checked_add(groups.len().checked_mul(12)?)?;
1605 if sub_len > u32::MAX as usize {
1606 return None;
1607 }
1608 let mut sub: Vec<u8> = Vec::with_capacity(sub_len);
1609 sub.extend_from_slice(&12u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); sub.extend_from_slice(&(sub_len as u32).to_be_bytes()); sub.extend_from_slice(&0u32.to_be_bytes()); sub.extend_from_slice(&(groups.len() as u32).to_be_bytes()); for g in &groups {
1615 sub.extend_from_slice(&g.start_cp.to_be_bytes());
1616 sub.extend_from_slice(&g.end_cp.to_be_bytes());
1617 sub.extend_from_slice(&u32::from(g.start_gid).to_be_bytes());
1618 }
1619
1620 let mut cmap: Vec<u8> = Vec::with_capacity(12 + sub.len());
1621 cmap.extend_from_slice(&0u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&3u16.to_be_bytes()); cmap.extend_from_slice(&10u16.to_be_bytes()); cmap.extend_from_slice(&12u32.to_be_bytes()); cmap.extend_from_slice(&sub);
1627 Some(cmap)
1628 }
1629
1630 fn cmap12_lookup(&self, cp: u32) -> Option<u16> {
1631 let d = &self.data;
1632 let base = self.cmap_off;
1633 let num_groups = be_u32(d, off(base, 12)?)? as usize;
1634 for i in 0..num_groups {
1635 let g = off_mul(off(base, 16)?, i, 12)?;
1636 let start = be_u32(d, g)?;
1637 let end = be_u32(d, off(g, 4)?)?;
1638 if cp >= start && cp <= end {
1639 let start_gid = be_u32(d, off(g, 8)?)?;
1640 let gid = start_gid.checked_add(cp - start)?;
1641 return Some((gid & 0xFFFF) as u16);
1642 }
1643 }
1644 Some(0)
1645 }
1646}
1647
1648fn remapped_gid(new_of: &[u16], old: u16) -> Option<u16> {
1649 match new_of.get(usize::from(old)).copied()? {
1650 MISSING_GLYPH_REMAP => None,
1651 gid => Some(gid),
1652 }
1653}
1654
1655fn strip_simple_glyph_instructions(data: &[u8], contour_count: usize) -> Option<Vec<u8>> {
1656 let instruction_len_offset = off(10, contour_count.checked_mul(2)?)?;
1657 let instruction_len = be_u16(data, instruction_len_offset)? as usize;
1658 let instruction_start = off(instruction_len_offset, 2)?;
1659 let instruction_end = off(instruction_start, instruction_len)?;
1660 if instruction_end > data.len() {
1661 return None;
1662 }
1663
1664 let mut out = Vec::with_capacity(data.len().saturating_sub(instruction_len));
1665 out.extend_from_slice(data.get(..instruction_len_offset)?);
1666 out.extend_from_slice(&0u16.to_be_bytes());
1667 out.extend_from_slice(data.get(instruction_end..)?);
1668 Some(out)
1669}
1670
1671fn parse_cmap4_cache(d: &[u8], base: usize) -> Option<Cmap4Cache> {
1672 let seg_x2 = be_u16(d, off(base, 6)?)? as usize;
1673 let seg_count = seg_x2 / 2;
1674 let end_codes = off(base, 14)?;
1675 let start_codes = off(off(end_codes, seg_x2)?, 2)?;
1676 let id_deltas = off(start_codes, seg_x2)?;
1677 let id_range_offsets = off(id_deltas, seg_x2)?;
1678
1679 let mut segments = Vec::with_capacity(seg_count);
1680 let mut sorted_by_end = true;
1681 let mut prev_end: Option<u16> = None;
1682 for i in 0..seg_count {
1683 let end = be_u16(d, off_mul(end_codes, i, 2)?)?;
1684 let start = be_u16(d, off_mul(start_codes, i, 2)?)?;
1685 let id_delta = be_u16(d, off_mul(id_deltas, i, 2)?)?;
1686 let id_range_offset_pos = off_mul(id_range_offsets, i, 2)?;
1687 let id_range_offset = be_u16(d, id_range_offset_pos)?;
1688 if prev_end.is_some_and(|prev| end < prev) {
1689 sorted_by_end = false;
1690 }
1691 prev_end = Some(end);
1692 segments.push(Cmap4Segment {
1693 start,
1694 end,
1695 id_delta,
1696 id_range_offset,
1697 id_range_offset_pos,
1698 });
1699 }
1700
1701 Some(Cmap4Cache {
1702 segments,
1703 sorted_by_end,
1704 })
1705}
1706
1707fn select_cmap(d: &[u8], cmap: usize) -> Option<(usize, u16)> {
1711 let num = be_u16(d, off(cmap, 2)?)? as usize;
1712 let mut best: Option<(usize, u16, u8)> = None; for i in 0..num {
1714 let rec = off_mul(off(cmap, 4)?, i, 8)?;
1715 let platform = be_u16(d, rec)?;
1716 let encoding = be_u16(d, off(rec, 2)?)?;
1717 let sub = off(cmap, be_u32(d, off(rec, 4)?)? as usize)?;
1718 let format = be_u16(d, sub)?;
1719 let unicode = matches!((platform, encoding), (0, _) | (3, 1) | (3, 10));
1720 if !unicode {
1721 continue;
1722 }
1723 let rank = match format {
1724 12 => 3,
1725 4 => {
1726 if (platform, encoding) == (3, 1) || platform == 0 {
1727 2
1728 } else {
1729 1
1730 }
1731 }
1732 _ => continue,
1733 };
1734 if best.is_none_or(|(_, _, r)| rank > r) {
1735 best = Some((sub, format, rank));
1736 }
1737 }
1738 best.map(|(off, fmt, _)| (off, fmt))
1739}
1740
1741#[derive(Default)]
1753struct PairKeyHasher(u64);
1754
1755impl std::hash::Hasher for PairKeyHasher {
1756 fn write_u32(&mut self, v: u32) {
1757 self.0 = u64::from(v).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1758 }
1759
1760 fn write(&mut self, _bytes: &[u8]) {
1761 }
1763
1764 fn finish(&self) -> u64 {
1765 let mut x = self.0;
1766 x ^= x >> 29;
1767 x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
1768 x ^= x >> 32;
1769 x
1770 }
1771}
1772
1773type PairMap = std::collections::HashMap<u32, i16, std::hash::BuildHasherDefault<PairKeyHasher>>;
1774
1775fn pair_key(left: u16, right: u16) -> u32 {
1777 (u32::from(left) << 16) | u32::from(right)
1778}
1779
1780#[derive(Clone, Debug)]
1782enum ClassDef {
1783 Format1 { start: u16, classes: Vec<u16> },
1785 Format2 {
1789 ranges: Vec<(u16, u16, u16)>,
1790 dense: bool,
1791 },
1792}
1793
1794impl ClassDef {
1795 fn class(&self, g: u16) -> u16 {
1797 match self {
1798 ClassDef::Format1 { start, classes } => {
1799 if g >= *start {
1800 let i = (g - *start) as usize;
1801 if i < classes.len() {
1802 return classes[i];
1803 }
1804 }
1805 0
1806 }
1807 ClassDef::Format2 { ranges, dense } => {
1808 if *dense {
1809 let idx = ranges.partition_point(|&(s, _, _)| s <= g);
1812 if idx > 0 {
1813 let (_, e, c) = ranges[idx - 1];
1814 if g <= e {
1815 return c;
1816 }
1817 }
1818 0
1819 } else {
1820 for &(s, e, c) in ranges {
1821 if g >= s && g <= e {
1822 return c;
1823 }
1824 }
1825 0
1826 }
1827 }
1828 }
1829 }
1830}
1831
1832#[derive(Clone, Debug)]
1835enum KernSubtable {
1836 Format1 { pairs: PairMap },
1838 Format2 {
1840 coverage: Vec<u16>,
1842 class1: ClassDef,
1843 class2: ClassDef,
1844 class1_count: u16,
1847 class2_count: u16,
1848 matrix: Vec<i16>,
1851 },
1852}
1853
1854impl KernSubtable {
1855 fn lookup(&self, left: u16, right: u16) -> Option<i16> {
1861 match self {
1862 KernSubtable::Format1 { pairs } => pairs.get(&pair_key(left, right)).copied(),
1863 KernSubtable::Format2 {
1864 coverage,
1865 class1,
1866 class2,
1867 class1_count,
1868 class2_count,
1869 matrix,
1870 } => {
1871 if coverage.binary_search(&left).is_err() {
1873 return None;
1874 }
1875 let c1 = class1.class(left) as usize;
1876 let c2 = class2.class(right) as usize;
1877 let c1_count = *class1_count as usize;
1878 let c2_count = *class2_count as usize;
1879 if c1 >= c1_count || c2 >= c2_count {
1881 return Some(0);
1882 }
1883 if matrix.is_empty() {
1885 return Some(0);
1886 }
1887 let idx = c1.checked_mul(c2_count)?.checked_add(c2)?;
1888 Some(matrix.get(idx).copied().unwrap_or(0))
1891 }
1892 }
1893 }
1894}
1895
1896#[derive(Clone, Debug, Default)]
1902pub struct Kerning {
1903 subtables: Vec<KernSubtable>,
1904}
1905
1906impl Kerning {
1907 #[must_use]
1911 pub fn pair(&self, left: u16, right: u16) -> i16 {
1912 for st in &self.subtables {
1913 if let Some(v) = st.lookup(left, right) {
1914 return v;
1915 }
1916 }
1917 0
1918 }
1919
1920 pub fn for_each_ascii_pair(
1937 &self,
1938 glyph_of: impl Fn(u8) -> u16,
1939 mut emit: impl FnMut(u8, u8, i16),
1940 ) {
1941 let glyphs: [u16; 128] = std::array::from_fn(|b| glyph_of(b as u8));
1942 let mut by_glyph: Vec<(u16, u8)> = glyphs
1945 .iter()
1946 .enumerate()
1947 .map(|(b, &g)| (g, b as u8))
1948 .collect();
1949 by_glyph.sort_unstable();
1950
1951 let mut defined = [[false; 128]; 128];
1954
1955 for st in &self.subtables {
1956 match st {
1957 KernSubtable::Format1 { pairs } => {
1958 for (&key, &v) in pairs {
1959 let left = (key >> 16) as u16;
1960 let right = (key & 0xFFFF) as u16;
1961 let ls = by_glyph.partition_point(|&(g, _)| g < left);
1962 let le = by_glyph.partition_point(|&(g, _)| g <= left);
1963 let rs = by_glyph.partition_point(|&(g, _)| g < right);
1964 let re = by_glyph.partition_point(|&(g, _)| g <= right);
1965 for &(_, l) in &by_glyph[ls..le] {
1966 for &(_, r) in &by_glyph[rs..re] {
1967 let row = &mut defined[usize::from(l)];
1968 if row[usize::from(r)] {
1969 continue;
1970 }
1971 row[usize::from(r)] = true;
1972 if v != 0 {
1973 emit(l, r, v);
1974 }
1975 }
1976 }
1977 }
1978 }
1979 KernSubtable::Format2 {
1980 coverage,
1981 class1,
1982 class2,
1983 class1_count,
1984 class2_count,
1985 matrix,
1986 } => {
1987 let c2_of: [u16; 128] = std::array::from_fn(|b| class2.class(glyphs[b]));
1991 let c1_count = usize::from(*class1_count);
1992 let c2_count = usize::from(*class2_count);
1993 for l in 0..128u8 {
1994 if coverage.binary_search(&glyphs[usize::from(l)]).is_err() {
1995 continue;
1996 }
1997 let c1 = usize::from(class1.class(glyphs[usize::from(l)]));
1998 for r in 0..128u8 {
1999 let row = &mut defined[usize::from(l)];
2000 if row[usize::from(r)] {
2001 continue;
2002 }
2003 let value = if c1 >= c1_count
2008 || usize::from(c2_of[usize::from(r)]) >= c2_count
2009 || matrix.is_empty()
2010 {
2011 Some(0)
2012 } else {
2013 c1.checked_mul(c2_count)
2014 .and_then(|m| m.checked_add(usize::from(c2_of[usize::from(r)])))
2015 .map(|idx| matrix.get(idx).copied().unwrap_or(0))
2016 };
2017 if let Some(v) = value {
2018 row[usize::from(r)] = true;
2019 if v != 0 {
2020 emit(l, r, v);
2021 }
2022 }
2023 }
2024 }
2025 }
2026 }
2027 }
2028 }
2029}
2030
2031fn value_record_size(value_format: u16) -> usize {
2033 value_format.count_ones() as usize * 2
2034}
2035
2036fn value_record_x_advance(d: &[u8], off: usize, value_format: u16) -> Option<i16> {
2042 const X_ADVANCE: u16 = 0x0004;
2043 if value_format & X_ADVANCE == 0 {
2044 return Some(0);
2045 }
2046 let skip = (value_format & 0x0003).count_ones() as usize * 2;
2047 be_i16(d, off.checked_add(skip)?)
2048}
2049
2050fn parse_coverage_glyphs(d: &[u8], cov: usize) -> Option<Vec<u16>> {
2053 let format = be_u16(d, cov)?;
2054 match format {
2055 1 => {
2056 let count = be_u16_at(d, cov, 2)? as usize;
2057 let mut v = Vec::with_capacity(count.min(d.len() / 2 + 1));
2058 for i in 0..count {
2059 v.push(be_u16(d, off_mul(off(cov, 4)?, i, 2)?)?);
2060 }
2061 Some(v)
2062 }
2063 2 => {
2064 let range_count = be_u16_at(d, cov, 2)? as usize;
2065 let mut by_index: std::collections::BTreeMap<u32, u16> =
2068 std::collections::BTreeMap::new();
2069 let mut total: usize = 0;
2075 for i in 0..range_count {
2076 let rec = off_mul(off(cov, 4)?, i, 6)?;
2077 let start = be_u16(d, rec)? as u32;
2078 let end = be_u16_at(d, rec, 2)? as u32;
2079 let start_idx = be_u16_at(d, rec, 4)? as u32;
2080 if end < start {
2081 continue;
2082 }
2083 total = total.checked_add((end - start + 1) as usize)?;
2084 if total > MAX_COVERAGE_GLYPHS {
2085 return None;
2086 }
2087 let mut g = start;
2088 let mut idx = start_idx;
2089 while g <= end {
2090 by_index.insert(idx, g as u16);
2091 g += 1;
2092 idx += 1;
2093 }
2094 }
2095 Some(by_index.into_values().collect())
2096 }
2097 _ => None,
2098 }
2099}
2100
2101fn parse_class_def(d: &[u8], cd: usize) -> Option<ClassDef> {
2103 let format = be_u16(d, cd)?;
2104 match format {
2105 1 => {
2106 let start = be_u16_at(d, cd, 2)?;
2107 let count = be_u16_at(d, cd, 4)? as usize;
2108 let mut classes = Vec::with_capacity(count.min(d.len() / 2 + 1));
2109 for i in 0..count {
2110 classes.push(be_u16(d, off_mul(off(cd, 6)?, i, 2)?)?);
2111 }
2112 Some(ClassDef::Format1 { start, classes })
2113 }
2114 2 => {
2115 let range_count = be_u16_at(d, cd, 2)? as usize;
2116 let mut ranges = Vec::with_capacity(range_count.min(d.len() / 6 + 1));
2117 for i in 0..range_count {
2118 let rec = off_mul(off(cd, 4)?, i, 6)?;
2119 let s = be_u16(d, rec)?;
2120 let e = be_u16_at(d, rec, 2)?;
2121 let c = be_u16_at(d, rec, 4)?;
2122 ranges.push((s, e, c));
2123 }
2124 ranges.sort_by_key(|&(s, _, _)| s);
2130 let dense = ranges
2131 .windows(2)
2132 .all(|w| w[0].1 < w[1].0 || (w[0].1 == w[1].0 && w[0].2 == w[1].2));
2133 Some(ClassDef::Format2 { ranges, dense })
2134 }
2135 _ => None,
2136 }
2137}
2138
2139fn parse_pair_subtable(d: &[u8], sub: usize) -> Option<KernSubtable> {
2141 let pos_format = be_u16(d, sub)?;
2142 match pos_format {
2143 1 => parse_pair_format1(d, sub),
2144 2 => parse_pair_format2(d, sub),
2145 _ => None,
2146 }
2147}
2148
2149fn parse_pair_format1(d: &[u8], sub: usize) -> Option<KernSubtable> {
2151 let cov_off = be_u16_at(d, sub, 2)? as usize;
2152 let vf1 = be_u16_at(d, sub, 4)?;
2153 let vf2 = be_u16_at(d, sub, 6)?;
2154 let pair_set_count = be_u16_at(d, sub, 8)? as usize;
2155
2156 let rec1_size = value_record_size(vf1);
2157 let rec2_size = value_record_size(vf2);
2158 let pair_rec_size = off(2, off(rec1_size, rec2_size)?)?;
2160
2161 let coverage = parse_coverage_glyphs(d, off(sub, cov_off)?)?;
2162
2163 let mut pairs: PairMap = std::collections::HashMap::default();
2164
2165 let mut work: usize = 0;
2169 for i in 0..pair_set_count {
2170 work += 1;
2171 if work > MAX_LAYOUT_GLYPHS {
2172 break;
2173 }
2174 let Some(left_glyph) = coverage.get(i).copied() else {
2176 continue;
2177 };
2178 let Some(ps_off) = off_mul(off(sub, 10)?, i, 2).and_then(|slot| be_u16(d, slot)) else {
2179 continue;
2180 };
2181 let Some(ps) = off(sub, ps_off as usize) else {
2182 continue;
2183 };
2184 let Some(pair_value_count) = be_u16(d, ps) else {
2185 continue;
2186 };
2187 let Some(mut p) = off(ps, 2) else {
2188 continue;
2189 };
2190 for _ in 0..pair_value_count {
2191 work += 1;
2192 if work > MAX_LAYOUT_GLYPHS {
2193 break;
2194 }
2195 let Some(second) = be_u16(d, p) else {
2196 break;
2197 };
2198 let x_adv = off(p, 2)
2199 .and_then(|value_off| value_record_x_advance(d, value_off, vf1))
2200 .unwrap_or(0);
2201 pairs.entry(pair_key(left_glyph, second)).or_insert(x_adv);
2203 let Some(np) = p.checked_add(pair_rec_size) else {
2204 break;
2205 };
2206 p = np;
2207 }
2208 }
2209
2210 Some(KernSubtable::Format1 { pairs })
2211}
2212
2213fn parse_pair_format2(d: &[u8], sub: usize) -> Option<KernSubtable> {
2215 let cov_off = be_u16_at(d, sub, 2)? as usize;
2216 let vf1 = be_u16_at(d, sub, 4)?;
2217 let vf2 = be_u16_at(d, sub, 6)?;
2218 let class_def1_off = be_u16_at(d, sub, 8)? as usize;
2219 let class_def2_off = be_u16_at(d, sub, 10)? as usize;
2220 let class1_count = be_u16_at(d, sub, 12)? as usize;
2221 let class2_count = be_u16_at(d, sub, 14)? as usize;
2222
2223 let rec1_size = value_record_size(vf1);
2224 let rec2_size = value_record_size(vf2);
2225 let class_rec_size = off(rec1_size, rec2_size)?;
2226
2227 let matrix_base = off(sub, 16)?;
2229 let cell_count = class1_count.checked_mul(class2_count)?;
2230
2231 let matrix: Vec<i16> = if class_rec_size == 0 {
2234 Vec::new()
2236 } else {
2237 let needed = cell_count.checked_mul(class_rec_size)?;
2238 let end = matrix_base.checked_add(needed)?;
2239 if end > d.len() {
2240 return None;
2242 }
2243 let mut m = Vec::with_capacity(cell_count);
2244 for idx in 0..cell_count {
2245 let cell = off_mul(matrix_base, idx, class_rec_size)?;
2246 let x_adv = value_record_x_advance(d, cell, vf1).unwrap_or(0);
2248 m.push(x_adv);
2249 }
2250 m
2251 };
2252
2253 let mut coverage = parse_coverage_glyphs(d, off(sub, cov_off)?)?;
2254 coverage.sort_unstable();
2255
2256 let class1 = parse_class_def(d, off(sub, class_def1_off)?)?;
2257 let class2 = parse_class_def(d, off(sub, class_def2_off)?)?;
2258
2259 Some(KernSubtable::Format2 {
2260 coverage,
2261 class1,
2262 class2,
2263 class1_count: class1_count as u16,
2264 class2_count: class2_count as u16,
2265 matrix,
2266 })
2267}
2268
2269fn resolve_extension(d: &[u8], sub: usize) -> Option<(u16, usize)> {
2272 let pos_format = be_u16(d, sub)?;
2273 if pos_format != 1 {
2274 return None;
2275 }
2276 let ext_type = be_u16_at(d, sub, 2)?;
2277 let ext_off = be_u32_at(d, sub, 4)? as usize;
2278 Some((ext_type, sub.checked_add(ext_off)?))
2279}
2280
2281impl Font {
2282 #[must_use]
2287 pub fn gpos_kerning(&self) -> Kerning {
2288 self.parse_gpos_kerning().unwrap_or_default()
2289 }
2290
2291 fn parse_gpos_kerning(&self) -> Option<Kerning> {
2292 let d = &self.data;
2293 let (gpos, _gpos_len) = find_table_full(d, b"GPOS")?;
2294
2295 let feature_list_off = be_u16_at(d, gpos, 6)? as usize;
2297 let lookup_list_off = be_u16_at(d, gpos, 8)? as usize;
2298 let feature_list = off(gpos, feature_list_off)?;
2299 let lookup_list = off(gpos, lookup_list_off)?;
2300
2301 let feature_count = be_u16(d, feature_list)? as usize;
2303 let mut lookup_indices: Vec<u16> = Vec::new();
2304 for i in 0..feature_count {
2305 let rec = off_mul(off(feature_list, 2)?, i, 6)?;
2307 let Some(tag) = bytes_at(d, rec, 4) else {
2308 break;
2309 };
2310 if tag != b"kern" {
2311 continue;
2312 }
2313 let Some(feat_off) = be_u16_at(d, rec, 4) else {
2314 continue;
2315 };
2316 let Some(feat) = off(feature_list, feat_off as usize) else {
2317 continue;
2318 };
2319 let Some(lookup_index_count) = be_u16_at(d, feat, 2) else {
2321 continue;
2322 };
2323 for j in 0..lookup_index_count as usize {
2324 if let Some(idx) = off_mul(off(feat, 4)?, j, 2).and_then(|slot| be_u16(d, slot)) {
2325 if !lookup_indices.contains(&idx) {
2326 lookup_indices.push(idx);
2327 }
2328 }
2329 }
2330 }
2331
2332 let lookup_count = be_u16(d, lookup_list)? as usize;
2334 let mut subtables: Vec<KernSubtable> = Vec::new();
2335
2336 for &li in &lookup_indices {
2337 let li = li as usize;
2338 if li >= lookup_count {
2339 continue;
2340 }
2341 let Some(lookup_off) =
2342 off_mul(off(lookup_list, 2)?, li, 2).and_then(|slot| be_u16(d, slot))
2343 else {
2344 continue;
2345 };
2346 let Some(lookup) = off(lookup_list, lookup_off as usize) else {
2347 continue;
2348 };
2349 let Some(lookup_type) = be_u16(d, lookup) else {
2351 continue;
2352 };
2353 let Some(sub_count) = be_u16_at(d, lookup, 4) else {
2354 continue;
2355 };
2356
2357 for s in 0..sub_count as usize {
2358 let Some(sub_off) = off_mul(off(lookup, 6)?, s, 2).and_then(|slot| be_u16(d, slot))
2359 else {
2360 continue;
2361 };
2362 let Some(sub) = off(lookup, sub_off as usize) else {
2363 continue;
2364 };
2365
2366 match lookup_type {
2367 2 => {
2368 if let Some(st) = parse_pair_subtable(d, sub) {
2369 subtables.push(st);
2370 }
2371 }
2372 9 => {
2373 if let Some((ext_type, real_sub)) = resolve_extension(d, sub) {
2375 if ext_type == 2 {
2376 if let Some(st) = parse_pair_subtable(d, real_sub) {
2377 subtables.push(st);
2378 }
2379 }
2380 }
2381 }
2382 _ => {}
2383 }
2384 }
2385 }
2386
2387 Some(Kerning { subtables })
2388 }
2389}
2390
2391#[derive(Clone, Debug)]
2399struct LigRule {
2400 components: Vec<u16>,
2401 ligature: u16,
2402}
2403
2404#[derive(Clone, Debug, Default)]
2407pub struct Ligatures {
2408 rules: std::collections::BTreeMap<u16, Vec<LigRule>>,
2410}
2411
2412impl Ligatures {
2413 #[must_use]
2415 pub fn is_empty(&self) -> bool {
2416 self.rules.is_empty()
2417 }
2418
2419 pub fn rule_start_glyphs(&self) -> impl Iterator<Item = &u16> {
2424 self.rules.keys()
2425 }
2426
2427 #[must_use]
2432 pub fn max_rule_len(&self) -> usize {
2433 self.rules
2434 .values()
2435 .flat_map(|rules| rules.iter().map(|r| r.components.len() + 1))
2436 .max()
2437 .unwrap_or(1)
2438 }
2439
2440 #[must_use]
2444 pub fn substitute(&self, gids: &[u16]) -> Vec<u16> {
2445 self.substitute_with_spans(gids)
2446 .into_iter()
2447 .map(|(g, _)| g)
2448 .collect()
2449 }
2450
2451 #[must_use]
2456 pub fn substitute_with_spans(&self, gids: &[u16]) -> Vec<(u16, usize)> {
2457 let mut out = Vec::with_capacity(gids.len());
2458 self.substitute_with_spans_into(gids, &mut out);
2459 out
2460 }
2461
2462 pub fn substitute_with_spans_into(&self, gids: &[u16], out: &mut Vec<(u16, usize)>) {
2467 out.clear();
2468 out.reserve(gids.len());
2469 let mut i = 0;
2470 while i < gids.len() {
2471 let mut applied = false;
2472 if let Some(rules) = self.rules.get(&gids[i]) {
2473 for r in rules {
2474 let n = r.components.len();
2475 if i + 1 + n <= gids.len() && gids[i + 1..i + 1 + n] == r.components[..] {
2476 out.push((r.ligature, n + 1));
2477 i += n + 1;
2478 applied = true;
2479 break;
2480 }
2481 }
2482 }
2483 if !applied {
2484 out.push((gids[i], 1));
2485 i += 1;
2486 }
2487 }
2488 }
2489}
2490
2491impl Font {
2492 #[must_use]
2497 pub fn gsub_ligatures(&self) -> Ligatures {
2498 self.parse_gsub_ligatures().unwrap_or_default()
2499 }
2500
2501 fn parse_gsub_ligatures(&self) -> Option<Ligatures> {
2502 let d = &self.data;
2503 let (gsub, _) = find_table_full(d, b"GSUB")?;
2504 let feature_list = off(gsub, be_u16_at(d, gsub, 6)? as usize)?;
2505 let lookup_list = off(gsub, be_u16_at(d, gsub, 8)? as usize)?;
2506
2507 let feature_count = be_u16(d, feature_list)? as usize;
2509 let mut lookup_indices: Vec<u16> = Vec::new();
2510 for i in 0..feature_count {
2511 let rec = off_mul(off(feature_list, 2)?, i, 6)?;
2512 let Some(tag) = bytes_at(d, rec, 4) else {
2513 break;
2514 };
2515 if tag != b"liga" {
2516 continue;
2517 }
2518 let Some(feat_off) = be_u16_at(d, rec, 4) else {
2519 continue;
2520 };
2521 let Some(feat) = off(feature_list, feat_off as usize) else {
2522 continue;
2523 };
2524 let Some(n) = be_u16_at(d, feat, 2) else {
2525 continue;
2526 };
2527 for j in 0..n as usize {
2528 if let Some(idx) = off_mul(off(feat, 4)?, j, 2).and_then(|slot| be_u16(d, slot)) {
2529 if !lookup_indices.contains(&idx) {
2530 lookup_indices.push(idx);
2531 }
2532 }
2533 }
2534 }
2535
2536 let lookup_count = be_u16(d, lookup_list)? as usize;
2537 let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
2538 std::collections::BTreeMap::new();
2539 for &li in &lookup_indices {
2540 let li = li as usize;
2541 if li >= lookup_count {
2542 continue;
2543 }
2544 let Some(lookup_off) =
2545 off_mul(off(lookup_list, 2)?, li, 2).and_then(|slot| be_u16(d, slot))
2546 else {
2547 continue;
2548 };
2549 let Some(lookup) = off(lookup_list, lookup_off as usize) else {
2550 continue;
2551 };
2552 let Some(lookup_type) = be_u16(d, lookup) else {
2553 continue;
2554 };
2555 let Some(sub_count) = be_u16_at(d, lookup, 4) else {
2556 continue;
2557 };
2558 for s in 0..sub_count as usize {
2559 let Some(sub_off) = off_mul(off(lookup, 6)?, s, 2).and_then(|slot| be_u16(d, slot))
2560 else {
2561 continue;
2562 };
2563 let Some(sub) = off(lookup, sub_off as usize) else {
2564 continue;
2565 };
2566 match lookup_type {
2567 4 => parse_ligature_subst(d, sub, &mut rules),
2568 7 => {
2570 if let Some((ext_type, real)) = resolve_extension(d, sub) {
2571 if ext_type == 4 {
2572 parse_ligature_subst(d, real, &mut rules);
2573 }
2574 }
2575 }
2576 _ => {}
2577 }
2578 }
2579 }
2580 for v in rules.values_mut() {
2582 v.sort_by_key(|r| std::cmp::Reverse(r.components.len()));
2583 }
2584 Some(Ligatures { rules })
2585 }
2586}
2587
2588fn parse_ligature_subst(
2590 d: &[u8],
2591 sub: usize,
2592 rules: &mut std::collections::BTreeMap<u16, Vec<LigRule>>,
2593) {
2594 let Some(format) = be_u16(d, sub) else {
2595 return;
2596 };
2597 if format != 1 {
2598 return;
2599 }
2600 let Some(cov_off) = be_u16_at(d, sub, 2) else {
2601 return;
2602 };
2603 let Some(set_count) = be_u16_at(d, sub, 4) else {
2604 return;
2605 };
2606 let Some(coverage) = off(sub, cov_off as usize).and_then(|cov| parse_coverage_glyphs(d, cov))
2607 else {
2608 return;
2609 };
2610 let Some(set_offsets) = off(sub, 6) else {
2611 return;
2612 };
2613 let mut work: usize = 0;
2618 for i in 0..set_count as usize {
2619 work += 1;
2620 if work > MAX_LAYOUT_GLYPHS {
2621 return;
2622 }
2623 let Some(first) = coverage.get(i).copied() else {
2625 continue;
2626 };
2627 let Some(set_off) = off_mul(set_offsets, i, 2).and_then(|slot| be_u16(d, slot)) else {
2628 continue;
2629 };
2630 let Some(lig_set) = off(sub, set_off as usize) else {
2631 continue;
2632 };
2633 let Some(lig_count) = be_u16(d, lig_set) else {
2634 continue;
2635 };
2636 let Some(lig_offsets) = off(lig_set, 2) else {
2637 continue;
2638 };
2639 for j in 0..lig_count as usize {
2640 work += 1;
2641 if work > MAX_LAYOUT_GLYPHS {
2642 return;
2643 }
2644 let Some(lig_off) = off_mul(lig_offsets, j, 2).and_then(|slot| be_u16(d, slot)) else {
2645 continue;
2646 };
2647 let Some(lig) = off(lig_set, lig_off as usize) else {
2648 continue;
2649 };
2650 let Some(lig_glyph) = be_u16(d, lig) else {
2651 continue;
2652 };
2653 let Some(comp_count) = be_u16_at(d, lig, 2) else {
2654 continue;
2655 };
2656 if comp_count == 0 {
2657 continue;
2658 }
2659 let mut components = Vec::with_capacity(comp_count as usize - 1);
2661 let mut ok = true;
2662 let Some(component_base) = off(lig, 4) else {
2663 continue;
2664 };
2665 for k in 0..(comp_count as usize - 1) {
2666 match off_mul(component_base, k, 2).and_then(|slot| be_u16(d, slot)) {
2667 Some(g) => components.push(g),
2668 None => {
2669 ok = false;
2670 break;
2671 }
2672 }
2673 }
2674 if ok {
2675 rules.entry(first).or_default().push(LigRule {
2676 components,
2677 ligature: lig_glyph,
2678 });
2679 }
2680 }
2681 }
2682}
2683
2684#[cfg(test)]
2685#[cfg_attr(coverage_nightly, coverage(off))]
2686#[allow(clippy::indexing_slicing, clippy::unwrap_used)]
2687mod dos_tests {
2688 use super::{MAX_COVERAGE_GLYPHS, parse_coverage_glyphs};
2689
2690 fn be(v: u16) -> [u8; 2] {
2691 v.to_be_bytes()
2692 }
2693
2694 #[test]
2695 fn coverage_format2_valid_range_expands() {
2696 let mut d = Vec::new();
2698 d.extend_from_slice(&be(2));
2699 d.extend_from_slice(&be(1));
2700 d.extend_from_slice(&be(10)); d.extend_from_slice(&be(20)); d.extend_from_slice(&be(0)); let got = parse_coverage_glyphs(&d, 0).unwrap();
2704 assert_eq!(got, (10u16..=20).collect::<Vec<_>>());
2705 }
2706
2707 #[test]
2708 fn coverage_format2_overclaiming_table_is_rejected_not_expanded() {
2709 let mut d = Vec::new();
2712 d.extend_from_slice(&be(2));
2713 d.extend_from_slice(&be(2)); for _ in 0..2 {
2715 d.extend_from_slice(&be(0)); d.extend_from_slice(&be(0xFFFF)); d.extend_from_slice(&be(0)); }
2719 assert!(parse_coverage_glyphs(&d, 0).is_none());
2720 assert_eq!(MAX_COVERAGE_GLYPHS, 65_536);
2722 }
2723}
2724
2725#[cfg(test)]
2726#[cfg_attr(coverage_nightly, coverage(off))]
2727#[allow(clippy::unwrap_used, clippy::expect_used)]
2728mod subset_degradation_tests {
2729 use super::{
2730 Font, MISSING_GLYPH_REMAP, be_i16, be_u16, find_table_full, strip_simple_glyph_instructions,
2731 };
2732
2733 fn cm_regular() -> Font {
2735 let bytes = std::fs::read(concat!(
2736 env!("CARGO_MANIFEST_DIR"),
2737 "/fonts/computer-modern/cmunrm.ttf"
2738 ))
2739 .expect("read bundled font");
2740 Font::parse(bytes).expect("parse bundled font")
2741 }
2742
2743 fn all_faces() -> Vec<Font> {
2744 let base = env!("CARGO_MANIFEST_DIR");
2745 [
2746 "/fonts/computer-modern/cmunrm.ttf",
2747 "/fonts/computer-modern/cmunbx.ttf",
2748 "/fonts/computer-modern/cmunti.ttf",
2749 "/fonts/computer-modern/cmunbi.ttf",
2750 "/fonts/computer-modern/cmuntt.ttf",
2751 "/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
2752 "/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf",
2753 "/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf",
2754 "/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf",
2755 ]
2756 .iter()
2757 .filter_map(|p| Font::parse(std::fs::read(format!("{base}{p}")).ok()?).ok())
2758 .collect()
2759 }
2760
2761 fn test_remap(font: &Font, pairs: &[(u16, u16)]) -> Vec<u16> {
2762 let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs).max(1)];
2763 for &(old, new) in pairs {
2764 if let Some(slot) = new_of.get_mut(usize::from(old)) {
2765 *slot = new;
2766 }
2767 }
2768 new_of
2769 }
2770
2771 #[test]
2777 fn subset_glyphs_with_lookup_matches_btreemap_remap() {
2778 fn assert_agree(font: &Font, glyphs: &[u16], cmap_chars: &[char]) {
2779 let (map_bytes, remap) = font
2780 .subset_glyphs(glyphs, cmap_chars)
2781 .expect("map-path subset");
2782 let (dense_bytes, lookup) = font
2783 .subset_glyphs_with_lookup(glyphs, cmap_chars)
2784 .expect("dense-path subset");
2785 assert_eq!(map_bytes, dense_bytes, "font bytes must be identical");
2786 assert_eq!(
2787 lookup.len(),
2788 usize::from(font.num_glyphs).max(1),
2789 "dense lookup covers every source glyph"
2790 );
2791 let mut mapped = 0usize;
2792 for (old, new) in lookup.iter().enumerate() {
2793 if *new == MISSING_GLYPH_REMAP {
2794 assert!(
2795 !remap.contains_key(&(old as u16)),
2796 "dense sentinel at {old} must be absent from the map"
2797 );
2798 } else {
2799 mapped += 1;
2800 assert_eq!(
2801 remap.get(&(old as u16)).copied(),
2802 Some(*new),
2803 "dense entry {old} -> {new} must match the map"
2804 );
2805 }
2806 }
2807 assert_eq!(
2808 remap.len(),
2809 mapped,
2810 "map and dense table cover the same glyphs"
2811 );
2812 let dense_pairs: Vec<(u16, u16)> = lookup
2816 .iter()
2817 .enumerate()
2818 .filter(|&(_, &v)| v != MISSING_GLYPH_REMAP)
2819 .map(|(old, &v)| (old as u16, v))
2820 .collect();
2821 let map_pairs: Vec<(u16, u16)> = remap.iter().map(|(&k, &v)| (k, v)).collect();
2822 assert_eq!(dense_pairs, map_pairs);
2823 }
2824
2825 for font in all_faces() {
2826 let a = font.glyph_index('A');
2827 let b = font.glyph_index('B');
2828 let q = font.glyph_index('Q');
2829 let composite =
2832 (0..font.num_glyphs).find(|&g| font.is_composite(g) && g != a && g != b && g != q);
2833 assert_agree(&font, &[], &[]);
2835 assert_agree(&font, &[], &['A', '\u{1D49C}']);
2837 assert_agree(&font, &[0], &[]);
2839 assert_agree(&font, &[a, b, q], &['A', 'B', 'Q']);
2841 let over = font.num_glyphs.saturating_add(3);
2844 assert_agree(
2845 &font,
2846 &[over, q, 0, u16::MAX, b, a, a, over],
2847 &['A', 'B', 'Q'],
2848 );
2849 if let Some(comp) = composite {
2850 assert_agree(&font, &[comp], &[]);
2851 let (bytes, _) = font.subset_glyphs(&[comp], &[]).expect("composite subset");
2852 let sub = Font::parse(bytes).expect("composite subset re-parses");
2853 assert!(
2854 sub.num_glyphs > 2,
2855 "closure must have pulled components beyond .notdef + the composite"
2856 );
2857 }
2858 }
2859 }
2860
2861 fn simple_instruction_len(data: &[u8]) -> Option<usize> {
2862 let contours = be_i16(data, 0)?;
2863 if contours < 0 {
2864 return None;
2865 }
2866 let instruction_len_offset = 10usize.checked_add((contours as usize).checked_mul(2)?)?;
2867 be_u16(data, instruction_len_offset).map(usize::from)
2868 }
2869
2870 #[test]
2871 fn simple_glyph_instruction_stripper_zeroes_length_and_removes_bytes() {
2872 let mut glyph = Vec::new();
2873 glyph.extend_from_slice(&1i16.to_be_bytes()); glyph.extend_from_slice(&[0u8; 8]); glyph.extend_from_slice(&0u16.to_be_bytes()); glyph.extend_from_slice(&3u16.to_be_bytes()); glyph.extend_from_slice(&[0xAA, 0xBB, 0xCC]); glyph.extend_from_slice(&[0x11, 0x22, 0x33]); let stripped = strip_simple_glyph_instructions(&glyph, 1).expect("valid simple glyph");
2881 assert_eq!(simple_instruction_len(&stripped), Some(0));
2882 assert_eq!(stripped.len(), glyph.len() - 3);
2883 assert_eq!(&stripped[stripped.len() - 3..], &[0x11, 0x22, 0x33]);
2884 }
2885
2886 #[test]
2887 fn subset_glyph_bytes_strips_simple_instructions_when_present() {
2888 let Some((font, gid, original_len)) = all_faces().into_iter().find_map(|font| {
2889 (1..font.num_glyphs).find_map(|gid| {
2890 let data = font.glyph_data(gid)?;
2891 let len = simple_instruction_len(data)?;
2892 (len > 0).then_some((font.clone(), gid, len))
2893 })
2894 }) else {
2895 eprintln!("skipping: bundled fonts have no hinted simple glyphs");
2896 return;
2897 };
2898
2899 let new_of = test_remap(&font, &[(0u16, 0u16), (gid, 1u16)]);
2900
2901 let stripped = font
2902 .subset_glyph_bytes(gid, &new_of)
2903 .expect("hinted simple glyph should subset");
2904 assert_eq!(simple_instruction_len(&stripped), Some(0));
2905 assert_eq!(
2906 stripped.len(),
2907 font.glyph_data(gid).expect("original glyph").len() - original_len
2908 );
2909 }
2910
2911 #[test]
2912 fn subset_hmtx_preserves_true_left_side_bearings() {
2913 let (font, ch, old_gid, old_lsb) = all_faces()
2914 .into_iter()
2915 .find_map(|font| {
2916 (33u8..=126).find_map(|byte| {
2917 let ch = char::from(byte);
2918 let gid = font.glyph_index(ch);
2919 let lsb = font.left_side_bearing(gid);
2920 (gid != 0 && lsb != 0).then_some((font.clone(), ch, gid, lsb))
2921 })
2922 })
2923 .expect("at least one bundled printable glyph has a nonzero lsb");
2924
2925 let (bytes, remap) = font
2926 .subset_glyphs(&[old_gid], &[ch])
2927 .expect("subset with nonzero-lsb glyph");
2928 let subset = Font::parse(bytes).expect("subset re-parses");
2929 let new_gid = remap[&old_gid];
2930 assert_eq!(subset.left_side_bearing(new_gid), old_lsb);
2931 }
2932
2933 #[test]
2934 fn html_subset_carries_verbatim_os2_while_pdf_subset_stays_lean() {
2935 for font in all_faces() {
2941 let (src_off, src_len) = find_table_full(&font.data, b"OS/2")
2942 .expect("every bundled face carries an OS/2 table");
2943 let src_os2 = font.data[src_off..src_off + src_len].to_vec();
2944
2945 let html_bytes = font.subset(&['A', 'b']).expect("html subset");
2946 let html_font = Font::parse(html_bytes).expect("html subset re-parses");
2947 let (o, l) = find_table_full(&html_font.data, b"OS/2")
2948 .expect("html subset must keep OS/2 for browser sanitizers");
2949 assert_eq!(
2950 &html_font.data[o..o + l],
2951 &src_os2[..],
2952 "OS/2 must be copied verbatim"
2953 );
2954
2955 let gid = font.glyph_index('A');
2956 assert_ne!(gid, 0, "bundled faces must map 'A'");
2957 let (pdf_bytes, _) = font.subset_glyphs(&[gid], &['A']).expect("pdf subset");
2958 assert!(
2959 find_table_full(&pdf_bytes, b"OS/2").is_none(),
2960 "pdf subset must not grow an OS/2 table (golden bytes)"
2961 );
2962 assert!(Font::parse(pdf_bytes).is_ok());
2963 }
2964 }
2965
2966 #[test]
2967 fn subset_skips_cmap_char_whose_glyph_is_absent_from_the_set() {
2968 let font = cm_regular();
2972 let g_b = font.glyph_index('B');
2973 assert_ne!(g_b, 0, "test font must map 'B'");
2974 let out = font.subset_glyphs(&[g_b], &['A', 'B']);
2976 let (bytes, _) = out.expect("un-subsettable cmap char must be skipped, not abort");
2977 assert!(Font::parse(bytes).is_ok());
2979 }
2980
2981 #[test]
2982 fn simple_instruction_len_rejects_composite_data() {
2983 assert_eq!(simple_instruction_len(&(-1i16).to_be_bytes()), None);
2986 }
2987
2988 #[test]
2989 fn subset_glyph_bytes_substitutes_notdef_for_a_missing_component() {
2990 let (font, comp) = all_faces()
2993 .into_iter()
2994 .find_map(|f| {
2995 (1..f.num_glyphs)
2996 .find(|&g| f.is_composite(g))
2997 .map(|g| (f, g))
2998 })
2999 .expect("at least one bundled face has a composite glyph");
3000 let new_of = test_remap(&font, &[(0u16, 0u16), (comp, 1u16)]);
3003 let bytes = font
3004 .subset_glyph_bytes(comp, &new_of)
3005 .expect("missing component must be substituted, not abort");
3006 assert!(!bytes.is_empty(), "a composite glyph is non-empty");
3007 }
3008}
3009
3010#[cfg(test)]
3011#[cfg_attr(coverage_nightly, coverage(off))]
3012#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
3013mod synthetic_font_tests {
3014 use super::*;
3015
3016 fn push16(out: &mut Vec<u8>, v: u16) {
3019 out.extend_from_slice(&v.to_be_bytes());
3020 }
3021
3022 fn push_i16(out: &mut Vec<u8>, v: i16) {
3023 out.extend_from_slice(&v.to_be_bytes());
3024 }
3025
3026 fn push32(out: &mut Vec<u8>, v: u32) {
3027 out.extend_from_slice(&v.to_be_bytes());
3028 }
3029
3030 fn sfnt(magic: u32, tables: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
3035 let mut out = Vec::new();
3036 push32(&mut out, magic);
3037 push16(&mut out, u16::try_from(tables.len()).unwrap());
3038 out.extend_from_slice(&[0u8; 6]); let mut offset = 12 + tables.len() * 16;
3040 let mut body = Vec::new();
3041 for (tag, bytes) in tables {
3042 out.extend_from_slice(&tag[..]);
3043 push32(&mut out, 0); push32(&mut out, u32::try_from(offset).unwrap());
3045 push32(&mut out, u32::try_from(bytes.len()).unwrap());
3046 offset += bytes.len();
3047 body.extend_from_slice(bytes);
3048 }
3049 out.extend_from_slice(&body);
3050 out
3051 }
3052
3053 fn head_table(upem: u16, loca_long: bool) -> Vec<u8> {
3054 let mut t = vec![0u8; 54];
3055 t[18..20].copy_from_slice(&upem.to_be_bytes());
3056 t[50..52].copy_from_slice(&u16::from(loca_long).to_be_bytes());
3057 t
3058 }
3059
3060 fn maxp_table(num_glyphs: u16) -> Vec<u8> {
3061 let mut t = vec![0u8; 6];
3062 t[4..6].copy_from_slice(&num_glyphs.to_be_bytes());
3063 t
3064 }
3065
3066 fn hhea_table(num_h_metrics: u16) -> Vec<u8> {
3067 let mut t = vec![0u8; 36];
3068 t[4..6].copy_from_slice(&700i16.to_be_bytes());
3069 t[6..8].copy_from_slice(&(-200i16).to_be_bytes());
3070 t[8..10].copy_from_slice(&50i16.to_be_bytes());
3071 t[34..36].copy_from_slice(&num_h_metrics.to_be_bytes());
3072 t
3073 }
3074
3075 fn hmtx_long(metrics: &[(u16, i16)]) -> Vec<u8> {
3076 let mut t = Vec::new();
3077 for &(aw, lsb) in metrics {
3078 push16(&mut t, aw);
3079 push_i16(&mut t, lsb);
3080 }
3081 t
3082 }
3083
3084 fn cmap4_table(segs: &[(u16, u16, u16, u16)], glyph_id_array: &[u8]) -> Vec<u8> {
3088 let seg_count = segs.len();
3089 let mut t = Vec::new();
3090 push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 3); push16(&mut t, 1); push32(&mut t, 12); push16(&mut t, 4); push16(
3097 &mut t,
3098 u16::try_from(16 + seg_count * 8 + glyph_id_array.len()).unwrap(),
3099 );
3100 push16(&mut t, 0); push16(&mut t, u16::try_from(seg_count * 2).unwrap()); push16(&mut t, 0); push16(&mut t, 0); push16(&mut t, 0); for &(end, _, _, _) in segs {
3106 push16(&mut t, end);
3107 }
3108 push16(&mut t, 0); for &(_, start, _, _) in segs {
3110 push16(&mut t, start);
3111 }
3112 for &(_, _, delta, _) in segs {
3113 push16(&mut t, delta);
3114 }
3115 for &(_, _, _, iro) in segs {
3116 push16(&mut t, iro);
3117 }
3118 t.extend_from_slice(glyph_id_array);
3119 t
3120 }
3121
3122 fn cmap4_simple(map: &[(u16, u16)]) -> Vec<u8> {
3125 let mut segs: Vec<(u16, u16, u16, u16)> = map
3126 .iter()
3127 .map(|&(code, gid)| (code, code, gid.wrapping_sub(code), 0))
3128 .collect();
3129 segs.push((0xFFFF, 0xFFFF, 1, 0));
3130 cmap4_table(&segs, &[])
3131 }
3132
3133 fn cmap12_table(groups: &[(u32, u32, u32)]) -> Vec<u8> {
3135 let mut t = Vec::new();
3136 push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 3); push16(&mut t, 10); push32(&mut t, 12); push16(&mut t, 12); push16(&mut t, 0); push32(&mut t, u32::try_from(16 + groups.len() * 12).unwrap());
3144 push32(&mut t, 0); push32(&mut t, u32::try_from(groups.len()).unwrap());
3146 for &(start, end, gid) in groups {
3147 push32(&mut t, start);
3148 push32(&mut t, end);
3149 push32(&mut t, gid);
3150 }
3151 t
3152 }
3153
3154 fn base_tables(
3155 num_glyphs: u16,
3156 num_h_metrics: u16,
3157 upem: u16,
3158 hmtx: Vec<u8>,
3159 cmap: Vec<u8>,
3160 ) -> Vec<(&'static [u8; 4], Vec<u8>)> {
3161 vec![
3162 (b"head", head_table(upem, false)),
3163 (b"maxp", maxp_table(num_glyphs)),
3164 (b"hhea", hhea_table(num_h_metrics)),
3165 (b"hmtx", hmtx),
3166 (b"cmap", cmap),
3167 ]
3168 }
3169
3170 fn parse(tables: &[(&[u8; 4], Vec<u8>)]) -> Font {
3171 Font::parse(sfnt(0x0001_0000, tables)).expect("synthetic font parses")
3172 }
3173
3174 const ARGW: u16 = 0x0001; const WHS: u16 = 0x0008; const MORE: u16 = 0x0020; const XYS: u16 = 0x0040; const TWO: u16 = 0x0080; const INSTR: u16 = 0x0100; fn composite_glyph(bbox: [i16; 4], records: &[(u16, u16, &[u8])], trailer: &[u8]) -> Vec<u8> {
3186 let mut g = Vec::new();
3187 push_i16(&mut g, -1);
3188 for v in bbox {
3189 push_i16(&mut g, v);
3190 }
3191 for &(flags, gid, payload) in records {
3192 push16(&mut g, flags);
3193 push16(&mut g, gid);
3194 g.extend_from_slice(payload);
3195 }
3196 g.extend_from_slice(trailer);
3197 g
3198 }
3199
3200 fn simple_glyph16() -> Vec<u8> {
3202 let mut g = Vec::new();
3203 push_i16(&mut g, 1); g.extend_from_slice(&[0u8; 8]); push16(&mut g, 0); push16(&mut g, 0); g.extend_from_slice(&[0x01, 0x00]); g
3209 }
3210
3211 fn zoo_font() -> Font {
3218 let glyphs: Vec<Vec<u8>> = vec![
3219 Vec::new(),
3220 1i16.to_be_bytes().to_vec(),
3221 composite_glyph([1, 2, 3, 4], &[(WHS, 5, &[0, 0, 0x40, 0])], &[]),
3222 composite_glyph([0; 4], &[(XYS, 5, &[0, 0, 0x40, 0, 0x40, 0])], &[]),
3223 composite_glyph(
3224 [0; 4],
3225 &[(TWO, 5, &[0, 0, 0x40, 0, 0, 0, 0, 0, 0x40, 0])],
3226 &[],
3227 ),
3228 simple_glyph16(),
3229 composite_glyph(
3230 [0; 4],
3231 &[(ARGW | MORE, 2, &[0, 0, 0, 0]), (0, 3, &[0, 0])],
3232 &[],
3233 ),
3234 composite_glyph([0; 4], &[(MORE, 5, &[0, 0])], &[]),
3235 composite_glyph([0; 4], &[(INSTR, 5, &[0, 0])], &[0xFF, 0xFF]),
3236 composite_glyph([0; 4], &[(0, 900, &[0, 0])], &[]),
3237 composite_glyph([0; 4], &[(MORE, 5, &[0, 0])], &[0, 0]),
3238 composite_glyph([0; 4], &[(TWO, 5, &[0, 0, 0x40, 0])], &[]),
3239 composite_glyph([0; 4], &[(INSTR, 5, &[0, 0])], &[0x00, 0x02, 0xAA, 0xBB]),
3240 ];
3241 let mut glyf = Vec::new();
3242 let mut loca = Vec::new();
3243 push16(&mut loca, 0);
3244 for g in &glyphs {
3245 glyf.extend_from_slice(g);
3246 push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
3247 }
3248 let metrics: Vec<(u16, i16)> = (0..13u16).map(|g| (500 + g, g as i16)).collect();
3249 let mut tables = base_tables(13, 13, 1000, hmtx_long(&metrics), cmap4_simple(&[]));
3250 tables.push((b"loca", loca));
3251 tables.push((b"glyf", glyf));
3252 parse(&tables)
3253 }
3254
3255 fn truncated_composite_font(keep: usize) -> Font {
3258 let glyph = composite_glyph([0; 4], &[(0, 5, &[0, 0])], &[]);
3259 assert_eq!(glyph.len(), 16);
3260 let mut loca = Vec::new();
3261 push16(&mut loca, 0);
3262 push16(&mut loca, 8);
3263 let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
3264 tables.push((b"loca", loca));
3265 tables.push((b"glyf", glyph));
3266 let mut bytes = sfnt(0x0001_0000, &tables);
3267 bytes.truncate(bytes.len() - (16 - keep));
3268 Font::parse(bytes).expect("glyf payload is lazily read")
3269 }
3270
3271 fn kern0_table(pairs: &[(u16, u16, i16)]) -> Vec<u8> {
3273 let mut t = Vec::new();
3274 push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, u16::try_from(14 + pairs.len() * 6).unwrap()); push16(&mut t, 0x0001); push16(&mut t, u16::try_from(pairs.len()).unwrap()); t.extend_from_slice(&[0u8; 6]); for &(l, r, v) in pairs {
3282 push16(&mut t, l);
3283 push16(&mut t, r);
3284 push_i16(&mut t, v);
3285 }
3286 t
3287 }
3288
3289 fn gpos_table(ext_format: u16, ext_type: u16, lookup_index: u16, pos_format: u16) -> Vec<u8> {
3292 let mut g = Vec::new();
3293 push32(&mut g, 0x0001_0000); push16(&mut g, 0); push16(&mut g, 10); push16(&mut g, 24); push16(&mut g, 1); g.extend_from_slice(b"kern");
3300 push16(&mut g, 8); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, lookup_index);
3305 push16(&mut g, 1); push16(&mut g, 4); push16(&mut g, 9); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, 8); push16(&mut g, ext_format);
3315 push16(&mut g, ext_type);
3316 push32(&mut g, 8); push16(&mut g, pos_format);
3319 push16(&mut g, 18); push16(&mut g, 0x0004); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, 12); push16(&mut g, 1); push16(&mut g, 6); push_i16(&mut g, -40); push16(&mut g, 1);
3330 push16(&mut g, 1);
3331 push16(&mut g, 5);
3332 assert_eq!(g.len(), 68);
3333 g
3334 }
3335
3336 fn gpos_kerning_of(table: Vec<u8>) -> Kerning {
3339 let mut tables = base_tables(
3340 2,
3341 2,
3342 1000,
3343 hmtx_long(&[(600, 0), (600, 0)]),
3344 cmap4_simple(&[]),
3345 );
3346 tables.push((b"GPOS", table));
3347 parse(&tables).gpos_kerning()
3348 }
3349
3350 fn gpos_font(ext_format: u16, ext_type: u16, lookup_index: u16, pos_format: u16) -> Kerning {
3351 gpos_kerning_of(gpos_table(ext_format, ext_type, lookup_index, pos_format))
3352 }
3353
3354 fn gsub_table(ext_format: u16, ext_type: u16, lookup_index: u16) -> Vec<u8> {
3357 let mut g = Vec::new();
3358 push32(&mut g, 0x0001_0000);
3359 push16(&mut g, 0); push16(&mut g, 10); push16(&mut g, 24); push16(&mut g, 1);
3364 g.extend_from_slice(b"liga");
3365 push16(&mut g, 8); push16(&mut g, 0);
3368 push16(&mut g, 1);
3369 push16(&mut g, lookup_index);
3370 push16(&mut g, 1);
3372 push16(&mut g, 4); push16(&mut g, 7);
3375 push16(&mut g, 0);
3376 push16(&mut g, 1);
3377 push16(&mut g, 8); push16(&mut g, ext_format);
3380 push16(&mut g, ext_type);
3381 push32(&mut g, 8); push16(&mut g, 1); push16(&mut g, 28); push16(&mut g, 1); push16(&mut g, 8); push16(&mut g, 2); push16(&mut g, 6); push16(&mut g, 14); push16(&mut g, 99);
3393 push16(&mut g, 3);
3394 push16(&mut g, 11);
3395 push16(&mut g, 12);
3396 push16(&mut g, 77);
3398 push16(&mut g, 2);
3399 push16(&mut g, 11);
3400 push16(&mut g, 1);
3402 push16(&mut g, 1);
3403 push16(&mut g, 10);
3404 assert_eq!(g.len(), 78);
3405 g
3406 }
3407
3408 fn gsub_ligatures_of(table: Vec<u8>) -> Ligatures {
3410 let mut tables = base_tables(
3411 2,
3412 2,
3413 1000,
3414 hmtx_long(&[(600, 0), (600, 0)]),
3415 cmap4_simple(&[]),
3416 );
3417 tables.push((b"GSUB", table));
3418 parse(&tables).gsub_ligatures()
3419 }
3420
3421 fn gsub_font(ext_format: u16, ext_type: u16, lookup_index: u16) -> Ligatures {
3422 gsub_ligatures_of(gsub_table(ext_format, ext_type, lookup_index))
3423 }
3424
3425 #[test]
3428 fn parse_error_variants_and_display_messages() {
3429 assert_eq!(Font::parse(Vec::new()).err(), Some(FontError::Truncated));
3430 assert_eq!(
3431 Font::parse(vec![0x00, 0x02, 0x00, 0x00]).err(),
3432 Some(FontError::BadMagic)
3433 );
3434
3435 let mut tables: Vec<(&[u8; 4], Vec<u8>)> = Vec::new();
3437 let steps: [(&'static [u8; 4], &'static str, Vec<u8>); 4] = [
3438 (b"head", "head", head_table(1000, false)),
3439 (b"maxp", "maxp", maxp_table(1)),
3440 (b"hhea", "hhea", hhea_table(1)),
3441 (b"hmtx", "hmtx", hmtx_long(&[(500, 0)])),
3442 ];
3443 for (tag, name, table) in steps {
3444 assert_eq!(
3445 Font::parse(sfnt(0x0001_0000, &tables)).err(),
3446 Some(FontError::MissingTable(name))
3447 );
3448 tables.push((tag, table));
3449 }
3450 assert_eq!(
3451 Font::parse(sfnt(0x0001_0000, &tables)).err(),
3452 Some(FontError::MissingTable("cmap"))
3453 );
3454
3455 let mut bad_cmap = Vec::new();
3458 push16(&mut bad_cmap, 0);
3459 push16(&mut bad_cmap, 2);
3460 push16(&mut bad_cmap, 1); push16(&mut bad_cmap, 0);
3462 push32(&mut bad_cmap, 20);
3463 push16(&mut bad_cmap, 3); push16(&mut bad_cmap, 1);
3465 push32(&mut bad_cmap, 20);
3466 push16(&mut bad_cmap, 6); tables.push((b"cmap", bad_cmap));
3468 assert_eq!(
3469 Font::parse(sfnt(0x0001_0000, &tables)).err(),
3470 Some(FontError::NoUnicodeCmap)
3471 );
3472
3473 let short_head: Vec<(&[u8; 4], Vec<u8>)> = vec![
3475 (b"maxp", maxp_table(1)),
3476 (b"hhea", hhea_table(1)),
3477 (b"hmtx", hmtx_long(&[(500, 0)])),
3478 (b"cmap", cmap4_simple(&[])),
3479 (b"head", vec![0u8; 10]),
3480 ];
3481 assert_eq!(
3482 Font::parse(sfnt(0x0001_0000, &short_head)).err(),
3483 Some(FontError::Truncated)
3484 );
3485
3486 assert_eq!(
3487 FontError::BadMagic.to_string(),
3488 "not a TrueType/OpenType font"
3489 );
3490 assert_eq!(
3491 FontError::MissingTable("hhea").to_string(),
3492 "missing required font table: hhea"
3493 );
3494 assert_eq!(FontError::Truncated.to_string(), "font data is truncated");
3495 assert_eq!(
3496 FontError::NoUnicodeCmap.to_string(),
3497 "no usable Unicode cmap (format 4/12)"
3498 );
3499 }
3500
3501 #[test]
3502 fn parse_accepts_true_and_otto_magics() {
3503 let tables = base_tables(
3504 3,
3505 3,
3506 2048,
3507 hmtx_long(&[(500, 1), (510, 2), (520, 3)]),
3508 cmap4_simple(&[(0x41, 1)]),
3509 );
3510 let t = Font::parse(sfnt(0x7472_7565, &tables)).expect("'true' magic parses");
3511 assert_eq!(t.units_per_em, 2048);
3512 assert_eq!(t.num_glyphs, 3);
3513 assert_eq!(t.ascent, 700);
3514 assert_eq!(t.descent, -200);
3515 assert_eq!(t.line_gap, 50);
3516 assert_eq!(t.glyph_index('A'), 1);
3517
3518 let o = Font::parse(sfnt(0x4F54_544F, &tables)).expect("'OTTO' magic parses");
3520 assert!(!o.has_glyf_outlines());
3521 assert_eq!(o.subset(&['A']), None);
3522 assert_eq!(o.glyph_bbox(1), None);
3523 assert_eq!(o.glyph_data(1), None);
3524 assert!(!o.is_composite(1));
3525 assert!(o.glyph_components(1).is_empty());
3526 }
3527
3528 #[test]
3531 fn left_side_bearing_reads_trailing_run_and_zero_metrics() {
3532 let mut hmtx = hmtx_long(&[(500, 50)]);
3535 push_i16(&mut hmtx, -7);
3536 push_i16(&mut hmtx, 33);
3537 let font = parse(&base_tables(3, 1, 1000, hmtx, cmap4_simple(&[])));
3538 assert_eq!(font.left_side_bearing(0), 50);
3539 assert_eq!(font.left_side_bearing(1), -7);
3540 assert_eq!(font.left_side_bearing(2), 33);
3541 assert_eq!(font.advance_width(0), 500);
3542 assert_eq!(font.advance_width(2), 500);
3543
3544 let font0 = parse(&base_tables(1, 0, 1000, Vec::new(), cmap4_simple(&[])));
3546 assert_eq!(font0.left_side_bearing(0), 0);
3547 }
3548
3549 #[test]
3552 fn legacy_kern_pair_and_char_kerning() {
3553 let pairs = [(1u16, 2u16, -30i16), (1, 3, 15), (4, 1, 7)];
3554 let metrics: Vec<(u16, i16)> = (0..8u16).map(|g| (600 + g, 0)).collect();
3555 let mut tables = base_tables(
3556 8,
3557 8,
3558 1000,
3559 hmtx_long(&metrics),
3560 cmap4_simple(&[(0x41, 1), (0x56, 2)]),
3561 );
3562 tables.push((b"kern", kern0_table(&pairs)));
3563 let font = parse(&tables);
3564 assert_eq!(font.kerning_between_glyphs(1, 2), -30);
3565 assert_eq!(font.kerning_between_glyphs(1, 3), 15);
3566 assert_eq!(font.kerning_between_glyphs(4, 1), 7);
3567 assert_eq!(font.kerning_between_glyphs(2, 1), 0);
3568 assert_eq!(font.kerning_between_glyphs(1, 4), 0);
3569 assert_eq!(font.kerning('A', 'V'), -30);
3570 assert_eq!(font.kerning_1000('A', 'V'), -30); assert_eq!(font.advance_1000('A'), 601);
3572
3573 let mut zero = base_tables(
3575 8,
3576 8,
3577 0,
3578 hmtx_long(&metrics),
3579 cmap4_simple(&[(0x41, 1), (0x56, 2)]),
3580 );
3581 zero.push((b"kern", kern0_table(&pairs)));
3582 let z = parse(&zero);
3583 assert_eq!(z.units_per_em, 0);
3584 assert_eq!(z.advance_1000('A'), 0);
3585 assert_eq!(z.kerning_1000('A', 'V'), 0);
3586 }
3587
3588 #[test]
3589 fn legacy_kern_skips_short_vertical_minimum_and_format2_subtables() {
3590 let mut k = Vec::new();
3591 push16(&mut k, 0); push16(&mut k, 5); push16(&mut k, 0);
3595 push16(&mut k, 10);
3596 push16(&mut k, 0x0001);
3597 k.extend_from_slice(&[0u8; 4]);
3598 push16(&mut k, 0);
3600 push16(&mut k, 14);
3601 push16(&mut k, 0x0000);
3602 k.extend_from_slice(&[0u8; 8]);
3603 push16(&mut k, 0);
3605 push16(&mut k, 14);
3606 push16(&mut k, 0x0003);
3607 k.extend_from_slice(&[0u8; 8]);
3608 push16(&mut k, 0);
3610 push16(&mut k, 14);
3611 push16(&mut k, 0x0201);
3612 k.extend_from_slice(&[0u8; 8]);
3613 push16(&mut k, 0);
3615 push16(&mut k, 20);
3616 push16(&mut k, 0x0001);
3617 push16(&mut k, 1); k.extend_from_slice(&[0u8; 6]);
3619 push16(&mut k, 3);
3620 push16(&mut k, 4);
3621 push_i16(&mut k, -11);
3622
3623 let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
3624 tables.push((b"kern", k));
3625 let font = parse(&tables);
3626 assert_eq!(font.kerning_between_glyphs(3, 4), -11);
3627 assert_eq!(font.kerning_between_glyphs(3, 5), 0);
3628 }
3629
3630 #[test]
3631 fn legacy_kern_rejects_malformed_table_headers() {
3632 fn kern_font(kern: Vec<u8>) -> Font {
3633 let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
3634 tables.push((b"kern", kern));
3635 parse(&tables)
3636 }
3637 let mut v1 = kern0_table(&[(1, 2, -30)]);
3639 v1[0..2].copy_from_slice(&1u16.to_be_bytes());
3640 assert_eq!(kern_font(v1).kerning_between_glyphs(1, 2), 0);
3641
3642 let mut zero_len = Vec::new();
3644 push16(&mut zero_len, 0);
3645 push16(&mut zero_len, 2);
3646 push16(&mut zero_len, 0); push16(&mut zero_len, 0); push16(&mut zero_len, 0x0000); zero_len.extend_from_slice(&[0u8; 8]);
3650 assert_eq!(kern_font(zero_len).kerning_between_glyphs(1, 2), 0);
3651
3652 let mut walk_off = Vec::new();
3654 push16(&mut walk_off, 0);
3655 push16(&mut walk_off, 2);
3656 push16(&mut walk_off, 0);
3657 push16(&mut walk_off, 14);
3658 push16(&mut walk_off, 0x0000); walk_off.extend_from_slice(&[0u8; 8]);
3660 assert_eq!(kern_font(walk_off).kerning_between_glyphs(1, 2), 0);
3661
3662 let mut overlong = Vec::new();
3664 push16(&mut overlong, 0);
3665 push16(&mut overlong, 1);
3666 push16(&mut overlong, 0);
3667 push16(&mut overlong, 200); push16(&mut overlong, 0x0001);
3669 overlong.extend_from_slice(&[0u8; 8]);
3670 assert_eq!(kern_font(overlong).kerning_between_glyphs(1, 2), 0);
3671
3672 let mut hungry = Vec::new();
3674 push16(&mut hungry, 0);
3675 push16(&mut hungry, 1);
3676 push16(&mut hungry, 0);
3677 push16(&mut hungry, 20); push16(&mut hungry, 0x0001);
3679 push16(&mut hungry, 3); hungry.extend_from_slice(&[0u8; 12]);
3681 assert_eq!(kern_font(hungry).kerning_between_glyphs(1, 2), 0);
3682
3683 let mut vertical_only = Vec::new();
3685 push16(&mut vertical_only, 0);
3686 push16(&mut vertical_only, 1);
3687 push16(&mut vertical_only, 0);
3688 push16(&mut vertical_only, 14);
3689 push16(&mut vertical_only, 0x0000);
3690 vertical_only.extend_from_slice(&[0u8; 8]);
3691 assert_eq!(kern_font(vertical_only).kerning_between_glyphs(1, 2), 0);
3692 }
3693
3694 #[test]
3695 fn legacy_kern_truncated_pair_records_kern_to_zero() {
3696 let pairs = [(1u16, 2u16, -30i16), (1, 3, 15), (4, 1, 7), (5, 5, 9)];
3701 for chop in [10usize, 16] {
3702 let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
3703 tables.push((b"kern", kern0_table(&pairs)));
3704 let mut bytes = sfnt(0x0001_0000, &tables);
3705 bytes.truncate(bytes.len() - chop);
3706 let font = Font::parse(bytes).expect("kern pair payload is lazily read");
3707 assert_eq!(font.kerning_between_glyphs(1, 2), 0, "chop={chop}");
3708 }
3709 }
3710
3711 #[test]
3714 fn glyph_range_rejects_inverted_and_overlong_loca_entries() {
3715 let mut loca = Vec::new();
3718 push16(&mut loca, 2);
3719 push16(&mut loca, 1);
3720 push16(&mut loca, 3);
3721 let mut tables = base_tables(
3722 2,
3723 2,
3724 1000,
3725 hmtx_long(&[(500, 0), (500, 0)]),
3726 cmap4_simple(&[]),
3727 );
3728 tables.push((b"loca", loca));
3729 tables.push((b"glyf", vec![0u8; 4]));
3730 let font = parse(&tables);
3731 assert!(font.has_glyf_outlines());
3732 assert_eq!(font.glyph_data(0), None);
3733 assert_eq!(font.glyph_data(1), None);
3734 assert_eq!(font.glyph_bbox(0), None);
3735 assert!(!font.is_composite(0));
3736 }
3737
3738 #[test]
3739 fn glyph_components_walk_all_transform_variants() {
3740 let font = zoo_font();
3741 assert!(font.glyph_components(0).is_empty()); assert!(font.glyph_components(1).is_empty()); assert_eq!(font.glyph_components(2), vec![5]); assert_eq!(font.glyph_components(3), vec![5]); assert_eq!(font.glyph_components(4), vec![5]); assert_eq!(font.glyph_components(6), vec![2, 3]); assert_eq!(font.glyph_components(7), vec![5]); assert_eq!(font.glyph_components(10), vec![5]); assert!(font.glyph_components(11).is_empty()); assert!(font.is_composite(2));
3751 assert!(!font.is_composite(1));
3752 assert!(!font.is_composite(0));
3753 assert_eq!(font.glyph_bbox(2), Some([1, 2, 3, 4]));
3754 assert_eq!(font.glyph_bbox(0), None);
3755 assert_eq!(font.glyph_data(0), Some(&[][..]));
3756 }
3757
3758 #[test]
3759 fn glyph_components_stop_at_truncated_component_records() {
3760 for keep in [1usize, 10, 12] {
3763 let font = truncated_composite_font(keep);
3764 assert!(font.glyph_components(0).is_empty(), "keep={keep}");
3765 assert_eq!(font.glyph_data(0), None, "keep={keep}");
3766 }
3767 }
3768
3769 #[test]
3772 fn subset_rewrites_component_ids_across_transform_variants() {
3773 let font = zoo_font();
3774 let (bytes, remap) = font.subset_glyphs(&[2, 3, 4], &[]).expect("subset");
3775 let remap: Vec<(u16, u16)> = remap.into_iter().collect();
3776 assert_eq!(remap, vec![(0, 0), (2, 1), (3, 2), (4, 3), (5, 4)]);
3777 let sub = Font::parse(bytes).expect("subset re-parses");
3778 assert_eq!(sub.num_glyphs, 5);
3779 assert_eq!(sub.glyph_components(1), vec![4]);
3780 assert_eq!(sub.glyph_components(2), vec![4]);
3781 assert_eq!(sub.glyph_components(3), vec![4]);
3782 assert_eq!(sub.glyph_bbox(1), Some([1, 2, 3, 4]));
3783 assert_eq!(sub.advance_width(1), 502);
3784 assert_eq!(sub.left_side_bearing(1), 2);
3785 assert_eq!(sub.advance_width(4), 505);
3786 assert_eq!(sub.left_side_bearing(4), 5);
3787 }
3788
3789 #[test]
3790 fn subset_closure_skips_component_ids_past_num_glyphs() {
3791 let font = zoo_font();
3792 let (bytes, remap) = font.subset_glyphs(&[9], &[]).expect("subset");
3793 assert_eq!(remap.get(&9).copied(), Some(1));
3794 assert_eq!(remap.len(), 2); let sub = Font::parse(bytes).expect("subset re-parses");
3796 assert_eq!(sub.num_glyphs, 2);
3797 assert_eq!(sub.glyph_components(1), vec![0]);
3799 }
3800
3801 #[test]
3802 fn subset_shares_a_component_between_two_composites() {
3803 let font = zoo_font();
3804 let (bytes, remap) = font.subset_glyphs(&[2, 4], &[]).expect("subset");
3805 let remap: Vec<(u16, u16)> = remap.into_iter().collect();
3806 assert_eq!(remap, vec![(0, 0), (2, 1), (4, 2), (5, 3)]);
3807 let sub = Font::parse(bytes).expect("subset re-parses");
3808 assert_eq!(sub.glyph_components(1), vec![3]);
3809 assert_eq!(sub.glyph_components(2), vec![3]);
3810 }
3811
3812 #[test]
3813 fn subset_tolerates_composite_whose_last_record_dangles_more() {
3814 let font = zoo_font();
3820 assert_eq!(font.glyph_components(7), vec![5]);
3821 let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
3822 new_of[0] = 0;
3823 new_of[5] = 1;
3824 new_of[7] = 2;
3825 let out = font
3826 .subset_glyph_bytes(7, &new_of)
3827 .expect("dangling MORE bit is tolerated and stripped");
3828 assert_eq!(be_u16(&out, 10), Some(0));
3830 assert!(font.subset_glyphs(&[7], &[]).is_some());
3831 }
3832
3833 #[test]
3834 fn subset_tolerates_composite_whose_dangling_record_claims_instructions() {
3835 let glyph = composite_glyph([0; 4], &[(0x0100 | 0x0020, 0, &[0, 0])], &[]);
3840 let mut glyf = Vec::new();
3841 let mut loca = Vec::new();
3842 push16(&mut loca, 0); push16(&mut loca, 0);
3844 glyf.extend_from_slice(&glyph);
3845 push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
3846 let mut tables = base_tables(
3847 2,
3848 1,
3849 1000,
3850 hmtx_long(&[(500, 0), (500, 1)]),
3851 cmap4_simple(&[]),
3852 );
3853 tables.push((b"loca", loca));
3854 tables.push((b"glyf", glyf));
3855 let font = parse(&tables);
3856
3857 let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
3858 new_of[0] = 0;
3859 new_of[1] = 1;
3860 let out = font
3861 .subset_glyph_bytes(1, &new_of)
3862 .expect("dangling INSTRUCTIONS|MORE record is tolerated");
3863 let flags = be_u16(&out, 10).expect("record flags readable");
3866 assert_eq!(
3867 flags & (0x0100 | 0x0020),
3868 0,
3869 "MORE and INSTRUCTIONS cleared"
3870 );
3871 }
3872
3873 #[test]
3874 fn subset_emits_format12_cmap_when_supplementary_plane_is_kept() {
3875 let groups: &[(u32, u32, u32)] =
3879 &[(u32::from('A'), u32::from('B'), 1), (0x1D49C, 0x1D49D, 3)];
3880 let mut tables = base_tables(
3881 5,
3882 1,
3883 1000,
3884 hmtx_long(&[(500, 0), (505, 1), (510, 2), (515, 3)]),
3885 cmap12_table(groups),
3886 );
3887
3888 let mut glyf = Vec::new();
3889 let mut loca = Vec::new();
3890 push16(&mut loca, 0);
3891 for _ in 0..4 {
3892 let g = simple_glyph16();
3893 glyf.extend_from_slice(&g);
3894 push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
3895 }
3896 while glyf.len() % 4 != 0 {
3897 glyf.push(0);
3898 }
3899 tables.push((b"loca", loca));
3900 tables.push((b"glyf", glyf));
3901 let font = parse(&tables);
3902 assert_ne!(font.glyph_index('A'), 0);
3903 assert_ne!(font.glyph_index('\u{1D49C}'), 0);
3904
3905 let subset = font
3906 .subset(&['A', 'B', '\u{1D49C}', '\u{1D49D}'])
3907 .expect("subset");
3908 let reparsed = Font::parse(subset).expect("subset re-parses");
3909 assert_ne!(reparsed.glyph_index('A'), 0, "BMP letter survives");
3910 assert_ne!(reparsed.glyph_index('B'), 0, "BMP letter survives");
3911 assert_ne!(
3912 reparsed.glyph_index('\u{1D49C}'),
3913 0,
3914 "script A must survive the subset"
3915 );
3916 assert_ne!(
3917 reparsed.glyph_index('\u{1D49D}'),
3918 0,
3919 "script B must survive the subset"
3920 );
3921 }
3922
3923 #[test]
3924 fn subset_strips_valid_composite_instructions_and_clears_the_flag() {
3925 let font = zoo_font();
3926 let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
3927 new_of[0] = 0;
3928 new_of[5] = 1;
3929 new_of[12] = 2;
3930 let out = font
3931 .subset_glyph_bytes(12, &new_of)
3932 .expect("valid instructions strip");
3933 assert_eq!(out.len(), 16); assert_eq!(be_u16(&out, 10), Some(0)); assert_eq!(be_u16(&out, 12), Some(1)); let (bytes, remap) = font.subset_glyphs(&[12], &[]).expect("subset");
3937 assert_eq!(remap.get(&12).copied(), Some(2));
3938 let sub = Font::parse(bytes).expect("subset re-parses");
3939 assert_eq!(sub.glyph_components(2), vec![1]);
3940 }
3941
3942 #[test]
3943 fn subset_cmap_skips_supplementary_plane_chars() {
3944 let font = zoo_font();
3945 let (bytes, _) = font.subset_glyphs(&[2], &['😀']).expect("subset");
3946 let sub = Font::parse(bytes).expect("subset re-parses");
3947 assert_eq!(sub.glyph_index('😀'), 0); }
3949
3950 #[test]
3951 fn subset_rejects_composite_with_overlong_instruction_claim() {
3952 let font = zoo_font();
3953 let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
3954 new_of[0] = 0;
3955 new_of[5] = 1;
3956 new_of[8] = 2;
3957 assert_eq!(font.subset_glyph_bytes(8, &new_of), None);
3958 assert!(font.subset_glyphs(&[8], &[]).is_none());
3959 }
3960
3961 #[test]
3962 fn strip_simple_glyph_instructions_rejects_overlong_length() {
3963 let mut glyph = Vec::new();
3964 push_i16(&mut glyph, 1);
3965 glyph.extend_from_slice(&[0u8; 8]); push16(&mut glyph, 0); push16(&mut glyph, 255); assert_eq!(strip_simple_glyph_instructions(&glyph, 1), None);
3969 }
3970
3971 #[test]
3974 fn cmap4_truncated_segment_arrays_fall_back_to_uncached_lookup() {
3975 let segs = [
3980 (0x5Au16, 0x41u16, 1u16.wrapping_sub(0x41), 0u16), (0x61, 0x61, 1, 6), (0x62, 0x62, 0, 2), (0x63, 0x63, 0, 0), (0x64, 0x64, 0, 7), (0x00FF, 0x00F0, 0, 0), ];
3987 let tables = base_tables(30, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_table(&segs, &[]));
3988 let mut bytes = sfnt(0x0001_0000, &tables);
3989 bytes.truncate(bytes.len() - 2); let font = Font::parse(bytes).expect("cmap payload is lazily read");
3991 assert!(font.cmap4_cache.is_none());
3992 assert_eq!(font.glyph_index('A'), 1);
3993 assert_eq!(font.glyph_index('Z'), 26);
3994 assert_eq!(font.glyph_index('@'), 0); assert_eq!(font.glyph_index('a'), 8); assert_eq!(font.glyph_index('b'), 0); assert_eq!(font.glyph_index('c'), 99); assert_eq!(font.glyph_index('d'), 0); assert_eq!(font.glyph_index('õ'), 0); assert_eq!(font.glyph_index('Ā'), 0); assert_eq!(font.glyph_index('😀'), 0); }
4003
4004 #[test]
4005 fn cmap4_cached_lookup_reads_glyph_id_array() {
4006 let segs = [(0x42u16, 0x41u16, 3u16, 4u16), (0xFFFF, 0xFFFF, 1, 0)];
4007 let mut array = Vec::new();
4008 push16(&mut array, 7);
4009 push16(&mut array, 0);
4010 let font = parse(&base_tables(
4011 20,
4012 1,
4013 1000,
4014 hmtx_long(&[(500, 0)]),
4015 cmap4_table(&segs, &array),
4016 ));
4017 let cache = font.cmap4_cache.as_ref().expect("valid table caches");
4018 assert!(cache.sorted_by_end);
4019 assert_eq!(font.glyph_index('A'), 10); assert_eq!(font.glyph_index('B'), 0); assert_eq!(font.glyph_index('C'), 0); }
4023
4024 #[test]
4025 fn cmap4_unsorted_segments_use_first_match_linear_scan() {
4026 let segs = [
4027 (0x61u16, 0x61u16, 2u16.wrapping_sub(0x61), 0u16),
4028 (0x5A, 0x41, 1u16.wrapping_sub(0x41), 0),
4029 (0xFFFF, 0xFFFF, 1, 0),
4030 ];
4031 let font = parse(&base_tables(
4032 30,
4033 1,
4034 1000,
4035 hmtx_long(&[(500, 0)]),
4036 cmap4_table(&segs, &[]),
4037 ));
4038 let cache = font
4039 .cmap4_cache
4040 .as_ref()
4041 .expect("caches even when unsorted");
4042 assert!(!cache.sorted_by_end);
4043 assert_eq!(font.glyph_index('a'), 2);
4044 assert_eq!(font.glyph_index('A'), 0);
4047 assert_eq!(font.glyph_index('p'), 0);
4048 }
4049
4050 #[test]
4051 fn cmap4_unsorted_lookup_misses_when_no_segment_covers_the_code() {
4052 let segs = [
4055 (0x61u16, 0x61u16, 2u16.wrapping_sub(0x61), 0u16),
4056 (0x5A, 0x41, 1u16.wrapping_sub(0x41), 0),
4057 ];
4058 let font = parse(&base_tables(
4059 30,
4060 1,
4061 1000,
4062 hmtx_long(&[(500, 0)]),
4063 cmap4_table(&segs, &[]),
4064 ));
4065 assert!(!font.cmap4_cache.as_ref().expect("caches").sorted_by_end);
4066 assert_eq!(font.glyph_index('a'), 2);
4067 assert_eq!(font.glyph_index('p'), 0); }
4069
4070 #[test]
4071 fn select_cmap_accepts_format4_under_a_non_bmp_encoding_record() {
4072 let mut cmap = cmap4_simple(&[(0x41, 1)]);
4075 cmap[6..8].copy_from_slice(&10u16.to_be_bytes()); let font = parse(&base_tables(5, 1, 1000, hmtx_long(&[(500, 0)]), cmap));
4077 assert_eq!(font.cmap_format, 4);
4078 assert_eq!(font.glyph_index('A'), 1);
4079 }
4080
4081 #[test]
4082 fn cmap12_groups_map_across_planes_and_truncate_gids() {
4083 let cmap = cmap12_table(&[
4084 (0x41, 0x5A, 100),
4085 (0x2000, 0x2000, 0x0001_2345),
4086 (0x1F600, 0x1F601, 7),
4087 ]);
4088 let font = parse(&base_tables(200, 1, 1000, hmtx_long(&[(500, 0)]), cmap));
4089 assert_eq!(font.cmap_format, 12);
4090 assert!(font.cmap4_cache.is_none());
4091 assert_eq!(font.glyph_index('A'), 100);
4092 assert_eq!(font.glyph_index('Z'), 125);
4093 assert_eq!(font.glyph_index('\u{2000}'), 0x2345); assert_eq!(font.glyph_index('😀'), 7);
4095 assert_eq!(font.glyph_index('😁'), 8);
4096 assert_eq!(font.glyph_index('0'), 0); }
4098
4099 #[test]
4102 fn gpos_extension_lookup_resolves_wrapped_pair_kerning() {
4103 let kern = gpos_font(1, 2, 0, 1);
4104 assert_eq!(kern.pair(5, 6), -40);
4105 assert_eq!(kern.pair(5, 7), 0);
4106 assert_eq!(kern.pair(6, 6), 0);
4107 }
4108
4109 #[test]
4110 fn gpos_skips_foreign_extensions_bad_formats_and_lookup_indices() {
4111 assert_eq!(gpos_font(1, 5, 0, 1).pair(5, 6), 0);
4113 assert_eq!(gpos_font(2, 2, 0, 1).pair(5, 6), 0);
4115 assert_eq!(gpos_font(1, 2, 0, 3).pair(5, 6), 0);
4117 assert_eq!(gpos_font(1, 2, 9, 1).pair(5, 6), 0);
4119 }
4120
4121 #[test]
4122 fn gpos_truncated_structures_yield_empty_kerning() {
4123 for end in [16usize, 20, 22, 26, 28, 32, 34] {
4128 let mut g = gpos_table(1, 2, 0, 1);
4129 g.truncate(end);
4130 assert_eq!(gpos_kerning_of(g).pair(5, 6), 0, "end={end}");
4131 }
4132 let mut over = gpos_table(1, 2, 0, 1);
4135 over[10..12].copy_from_slice(&12u16.to_be_bytes());
4136 assert_eq!(gpos_kerning_of(over).pair(5, 6), -40);
4137 let mut direct = gpos_table(1, 2, 0, 1);
4139 direct[28..30].copy_from_slice(&1u16.to_be_bytes());
4140 assert_eq!(gpos_kerning_of(direct).pair(5, 6), 0);
4141 }
4142
4143 #[test]
4144 fn resolve_extension_requires_format_1() {
4145 let mut d = Vec::new();
4146 push16(&mut d, 2);
4147 push16(&mut d, 2);
4148 push32(&mut d, 8);
4149 assert_eq!(resolve_extension(&d, 0), None);
4150 d[0..2].copy_from_slice(&1u16.to_be_bytes());
4151 assert_eq!(resolve_extension(&d, 0), Some((2, 8)));
4152 assert!(parse_pair_subtable(&[0, 3], 0).is_none());
4154 }
4155
4156 #[test]
4157 fn value_record_x_advance_field_extraction() {
4158 assert_eq!(value_record_x_advance(&[], 0, 0), Some(0));
4160 let rec = [0, 0, 0, 0, 0x12, 0x34];
4162 assert_eq!(value_record_x_advance(&rec, 0, 0x0007), Some(0x1234));
4163 assert_eq!(value_record_x_advance(&[0], 0, 0x0004), None);
4165 }
4166
4167 #[test]
4168 fn coverage_and_class_def_malformed_and_boundary_variants() {
4169 let mut cov = Vec::new();
4171 push16(&mut cov, 2);
4172 push16(&mut cov, 1);
4173 push16(&mut cov, 20); push16(&mut cov, 10); push16(&mut cov, 0);
4176 assert_eq!(parse_coverage_glyphs(&cov, 0), Some(Vec::new()));
4177 assert_eq!(parse_coverage_glyphs(&[0, 3, 0, 0], 0), None);
4179 assert!(parse_class_def(&[0, 3, 0, 0], 0).is_none());
4181
4182 let mut cd = Vec::new();
4184 push16(&mut cd, 1);
4185 push16(&mut cd, 5); push16(&mut cd, 2); push16(&mut cd, 7);
4188 push16(&mut cd, 9);
4189 let cd1 = parse_class_def(&cd, 0).expect("format 1 parses");
4190 assert_eq!(cd1.class(5), 7);
4191 assert_eq!(cd1.class(6), 9);
4192 assert_eq!(cd1.class(7), 0); assert_eq!(cd1.class(4), 0); let mut cd2b = Vec::new();
4197 push16(&mut cd2b, 2);
4198 push16(&mut cd2b, 1);
4199 push16(&mut cd2b, 10);
4200 push16(&mut cd2b, 20);
4201 push16(&mut cd2b, 3);
4202 let cd2 = parse_class_def(&cd2b, 0).expect("format 2 parses");
4203 assert_eq!(cd2.class(15), 3);
4204 assert_eq!(cd2.class(9), 0);
4205 assert_eq!(cd2.class(21), 0);
4206 }
4207
4208 #[test]
4209 fn kern_subtable_format2_guards_class_ranges_and_empty_matrix() {
4210 let st = KernSubtable::Format2 {
4211 coverage: vec![5, 9],
4212 class1: ClassDef::Format1 {
4213 start: 5,
4214 classes: vec![1, 0, 0, 0, 9],
4215 },
4216 class2: ClassDef::Format1 {
4217 start: 6,
4218 classes: vec![1, 7],
4219 },
4220 class1_count: 2,
4221 class2_count: 2,
4222 matrix: vec![0, 0, 0, -55],
4223 };
4224 assert_eq!(st.lookup(4, 6), None); assert_eq!(st.lookup(5, 6), Some(-55)); assert_eq!(st.lookup(9, 6), Some(0)); assert_eq!(st.lookup(5, 7), Some(0)); let empty = KernSubtable::Format2 {
4230 coverage: vec![5],
4231 class1: ClassDef::Format2 {
4232 ranges: Vec::new(),
4233 dense: true,
4234 },
4235 class2: ClassDef::Format2 {
4236 ranges: Vec::new(),
4237 dense: true,
4238 },
4239 class1_count: 1,
4240 class2_count: 1,
4241 matrix: Vec::new(),
4242 };
4243 assert_eq!(empty.lookup(5, 6), Some(0));
4244
4245 let kerning = Kerning {
4247 subtables: vec![empty, st],
4248 };
4249 assert_eq!(kerning.pair(5, 6), 0);
4250 assert_eq!(kerning.pair(4, 6), 0);
4251 }
4252
4253 #[test]
4254 fn for_each_ascii_pair_matches_brute_force_pair_on_bundled_faces() {
4255 let base = env!("CARGO_MANIFEST_DIR");
4260 let mut any_kerning_face = false;
4261 for path in [
4262 "/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
4263 "/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf",
4264 "/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf",
4265 "/fonts/computer-modern/cmunrm.ttf",
4266 "/fonts/computer-modern/cmuntt.ttf",
4267 "/fonts/noto-sans-math/NotoSansMathSymbols.ttf",
4268 ] {
4269 let Ok(bytes) = std::fs::read(format!("{base}{path}")) else {
4270 continue;
4271 };
4272 let Ok(font) = Font::parse(bytes) else {
4273 continue;
4274 };
4275 let kern = font.gpos_kerning();
4276 let glyphs: [u16; 128] = std::array::from_fn(|b| font.glyph_index(b as u8 as char));
4277 let nonzero_pairs = kern
4278 .subtables
4279 .iter()
4280 .map(|st| match st {
4281 KernSubtable::Format1 { pairs } => pairs.len(),
4282 KernSubtable::Format2 { coverage, .. } => coverage.len() * 128,
4283 })
4284 .sum::<usize>();
4285 if nonzero_pairs == 0 {
4286 continue;
4287 }
4288 any_kerning_face = true;
4289
4290 let mut enumerated = [0i16; 128 * 128];
4291 let mut emitted = 0usize;
4292 kern.for_each_ascii_pair(
4293 |b| glyphs[usize::from(b)],
4294 |l, r, v| {
4295 enumerated[usize::from(l) * 128 + usize::from(r)] = v;
4296 emitted += 1;
4297 },
4298 );
4299
4300 let mut brute = [0i16; 128 * 128];
4301 for l in 0..128usize {
4302 for r in 0..128usize {
4303 brute[l * 128 + r] = kern.pair(glyphs[l], glyphs[r]);
4304 }
4305 }
4306 assert_eq!(enumerated, brute, "enumeration != brute force for {path}");
4307 assert_eq!(
4308 emitted,
4309 brute.iter().filter(|&&v| v != 0).count(),
4310 "emission count != nonzero cells for {path}"
4311 );
4312 }
4313 assert!(
4314 any_kerning_face,
4315 "test is vacuous: no bundled face has GPOS kerning"
4316 );
4317 }
4318
4319 #[test]
4320 fn for_each_ascii_pair_first_match_duplicate_glyphs_and_zero_shadowing() {
4321 let glyph_of = |b: u8| -> u16 {
4324 match b {
4325 10..=12 => u16::from(b) - 5,
4326 13 | 14 => 9,
4327 other => u16::from(other),
4328 }
4329 };
4330
4331 let covered_all_rights = KernSubtable::Format2 {
4332 coverage: vec![5],
4333 class1: ClassDef::Format1 {
4334 start: 5,
4335 classes: vec![1],
4336 },
4337 class2: ClassDef::Format1 {
4338 start: 6,
4339 classes: vec![1, 1],
4340 },
4341 class1_count: 2,
4342 class2_count: 2,
4343 matrix: vec![0, 0, 30, -25],
4346 };
4347 let specific = KernSubtable::Format1 {
4348 pairs: PairMap::from_iter([(pair_key(5, 6), -99), (pair_key(9, 9), -12)]),
4349 };
4350 let out_of_range_class_zero = KernSubtable::Format2 {
4351 coverage: vec![9],
4352 class1: ClassDef::Format1 {
4353 start: 9,
4354 classes: vec![9], },
4356 class2: ClassDef::Format2 {
4357 ranges: Vec::new(),
4358 dense: true,
4359 },
4360 class1_count: 2,
4361 class2_count: 2,
4362 matrix: vec![0; 4],
4363 };
4364 let defined_zero = KernSubtable::Format1 {
4365 pairs: PairMap::from_iter([(pair_key(6, 6), 0)]),
4366 };
4367 let shadowed = KernSubtable::Format1 {
4368 pairs: PairMap::from_iter([(pair_key(6, 6), -77)]),
4369 };
4370
4371 let kerning = Kerning {
4372 subtables: vec![
4373 covered_all_rights,
4374 specific,
4375 out_of_range_class_zero,
4376 defined_zero,
4377 shadowed,
4378 ],
4379 };
4380
4381 let mut enumerated = [0i16; 128 * 128];
4382 kerning.for_each_ascii_pair(glyph_of, |l, r, v| {
4383 enumerated[usize::from(l) * 128 + usize::from(r)] = v;
4384 });
4385 let mut brute = [0i16; 128 * 128];
4386 for l in 0..128usize {
4387 for r in 0..128usize {
4388 brute[l * 128 + r] = kerning.pair(glyph_of(l as u8), glyph_of(r as u8));
4389 }
4390 }
4391 assert_eq!(enumerated, brute);
4392
4393 let cell = |l: u8, r: u8| enumerated[usize::from(l) * 128 + usize::from(r)];
4398 assert_eq!(cell(10, 11), -25); assert_eq!(cell(10, 12), -25); assert_eq!(cell(10, 13), 30); assert_eq!(cell(13, 13), -12); assert_eq!(cell(13, 14), -12);
4403 assert_eq!(cell(14, 13), -12);
4404 assert_eq!(cell(14, 14), -12);
4405 assert_eq!(cell(11, 11), 0); assert_eq!(cell(13, 11), 0); let mut calls = 0;
4410 Kerning::default().for_each_ascii_pair(glyph_of, |_, _, _| calls += 1);
4411 assert_eq!(calls, 0);
4412 }
4413
4414 #[test]
4415 fn pair_format1_skips_malformed_sets_and_truncated_records() {
4416 let mut d = Vec::new();
4419 push16(&mut d, 1); push16(&mut d, 20); push16(&mut d, 0x0004); push16(&mut d, 0); push16(&mut d, 2); push16(&mut d, 14); push16(&mut d, 14); push16(&mut d, 1); push16(&mut d, 6); push_i16(&mut d, -40);
4429 push16(&mut d, 1); push16(&mut d, 1);
4431 push16(&mut d, 5);
4432 let st = parse_pair_subtable(&d, 0).expect("format 1 parses");
4433 assert_eq!(st.lookup(5, 6), Some(-40));
4434 assert_eq!(st.lookup(5, 7), None);
4435
4436 let mut d2 = Vec::new();
4438 push16(&mut d2, 1);
4439 push16(&mut d2, 12); push16(&mut d2, 0x0004);
4441 push16(&mut d2, 0);
4442 push16(&mut d2, 1);
4443 push16(&mut d2, 0x4000); push16(&mut d2, 1);
4445 push16(&mut d2, 1);
4446 push16(&mut d2, 5);
4447 let st2 = parse_pair_subtable(&d2, 0).expect("parses to an empty set");
4448 assert_eq!(st2.lookup(5, 6), None);
4449
4450 let mut d3 = Vec::new();
4452 push16(&mut d3, 1);
4453 push16(&mut d3, 12); push16(&mut d3, 0x0004);
4455 push16(&mut d3, 0);
4456 push16(&mut d3, 1);
4457 push16(&mut d3, 18); push16(&mut d3, 1); push16(&mut d3, 1);
4460 push16(&mut d3, 5);
4461 push16(&mut d3, 2); push16(&mut d3, 6);
4463 push_i16(&mut d3, -40);
4464 let st3 = parse_pair_subtable(&d3, 0).expect("parses the readable record");
4465 assert_eq!(st3.lookup(5, 6), Some(-40));
4466 assert_eq!(st3.lookup(5, 0), None);
4467 }
4468
4469 #[test]
4470 fn pair_format1_work_cap_stops_aliased_pair_set_expansion() {
4471 for count in [65_535u16, 65_534] {
4474 let mut d = Vec::new();
4475 push16(&mut d, 1); push16(&mut d, 14); push16(&mut d, 0); push16(&mut d, 0); push16(&mut d, 2); push16(&mut d, 22); push16(&mut d, 22); push16(&mut d, 1); push16(&mut d, 2);
4484 push16(&mut d, 5);
4485 push16(&mut d, 6);
4486 push16(&mut d, count); d.resize(d.len() + usize::from(count) * 2, 0); let st = parse_pair_subtable(&d, 0).expect("parses under the work cap");
4489 assert_eq!(st.lookup(5, 0), Some(0), "count={count}");
4492 assert_eq!(st.lookup(6, 0), None, "count={count}");
4493 }
4494 }
4495
4496 #[test]
4497 fn pair_format2_empty_value_formats_and_oversized_matrix() {
4498 let mut d = Vec::new();
4501 push16(&mut d, 2); push16(&mut d, 16); push16(&mut d, 0); push16(&mut d, 0); push16(&mut d, 22); push16(&mut d, 22); push16(&mut d, 1); push16(&mut d, 1); push16(&mut d, 1); push16(&mut d, 1);
4511 push16(&mut d, 3);
4512 push16(&mut d, 1); push16(&mut d, 0);
4514 push16(&mut d, 0);
4515 let st = parse_pair_subtable(&d, 0).expect("empty-value format 2 parses");
4516 assert!(matches!(
4517 &st,
4518 KernSubtable::Format2 { matrix, .. } if matrix.is_empty()
4519 ));
4520 assert_eq!(st.lookup(3, 42), Some(0));
4521 assert_eq!(st.lookup(4, 42), None);
4522
4523 let mut big = Vec::new();
4525 push16(&mut big, 2);
4526 push16(&mut big, 16);
4527 push16(&mut big, 0x0004);
4528 push16(&mut big, 0);
4529 push16(&mut big, 22);
4530 push16(&mut big, 22);
4531 push16(&mut big, 0xFFFF);
4532 push16(&mut big, 0xFFFF);
4533 assert!(parse_pair_subtable(&big, 0).is_none());
4534 }
4535
4536 #[test]
4539 fn gsub_extension_lookup_parses_greedy_ligatures() {
4540 let ligs = gsub_font(1, 4, 0);
4541 assert!(!ligs.is_empty());
4542 assert!(Ligatures::default().is_empty());
4543 assert_eq!(ligs.substitute(&[10, 11, 12]), vec![99]);
4544 assert_eq!(ligs.substitute(&[10, 11, 7]), vec![77, 7]);
4545 assert_eq!(ligs.substitute(&[10, 7]), vec![10, 7]);
4546 assert_eq!(
4547 ligs.substitute_with_spans(&[10, 11, 12, 10, 11]),
4548 vec![(99, 3), (77, 2)]
4549 );
4550 }
4551
4552 #[test]
4553 fn substitute_with_spans_into_matches_allocating_variant() {
4554 let ligs = gsub_font(1, 4, 0);
4555 let corpora: [&[u16]; 7] = [
4556 &[],
4557 &[10, 11, 12, 10, 11],
4558 &[10, 11, 7],
4559 &[10, 7],
4560 &[99, 99, 99],
4561 &[10, 11, 12, 10, 11, 12, 10, 11],
4562 &[7, 8, 9, 10],
4563 ];
4564 let mut scratch = vec![(u16::MAX, usize::MAX); 4];
4567 for gids in corpora {
4568 let mut into = Vec::new();
4569 ligs.substitute_with_spans_into(gids, &mut into);
4570 assert_eq!(into, ligs.substitute_with_spans(gids));
4571 ligs.substitute_with_spans_into(gids, &mut scratch);
4572 assert_eq!(scratch, into);
4573 }
4574 }
4575
4576 #[test]
4577 fn max_rule_len_reports_longest_rule() {
4578 assert_eq!(gsub_font(1, 4, 0).max_rule_len(), 3);
4581 assert_eq!(Ligatures::default().max_rule_len(), 1);
4583 }
4584
4585 #[test]
4586 fn gsub_skips_foreign_extensions_and_bad_lookup_indices() {
4587 assert!(gsub_font(1, 2, 0).is_empty());
4589 assert!(gsub_font(2, 4, 0).is_empty());
4591 assert!(gsub_font(1, 4, 9).is_empty());
4593 }
4594
4595 #[test]
4596 fn gsub_truncated_structures_yield_no_ligatures() {
4597 for end in [16usize, 20, 22, 26, 28, 32, 34] {
4600 let mut g = gsub_table(1, 4, 0);
4601 g.truncate(end);
4602 assert!(gsub_ligatures_of(g).is_empty(), "end={end}");
4603 }
4604 let mut over = gsub_table(1, 4, 0);
4607 over[10..12].copy_from_slice(&12u16.to_be_bytes());
4608 assert_eq!(gsub_ligatures_of(over).substitute(&[10, 11]), vec![77]);
4609 let mut direct = gsub_table(1, 4, 0);
4611 direct[28..30].copy_from_slice(&1u16.to_be_bytes());
4612 assert!(gsub_ligatures_of(direct).is_empty());
4613 }
4614
4615 #[test]
4616 fn ligature_subst_skips_malformed_entries() {
4617 let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
4618 std::collections::BTreeMap::new();
4619
4620 parse_ligature_subst(&[0, 2, 0, 0], 0, &mut rules);
4622 assert!(rules.is_empty());
4623
4624 parse_ligature_subst(&[], 0, &mut rules); parse_ligature_subst(&[0, 1], 0, &mut rules); parse_ligature_subst(&[0, 1, 0, 8], 0, &mut rules); parse_ligature_subst(&[0, 1, 0, 6, 0, 1, 0, 3], 0, &mut rules); assert!(rules.is_empty());
4630
4631 let mut d = Vec::new();
4633 push16(&mut d, 1); push16(&mut d, 8); push16(&mut d, 1); push16(&mut d, 0x4000); push16(&mut d, 1); push16(&mut d, 1);
4639 push16(&mut d, 10);
4640 parse_ligature_subst(&d, 0, &mut rules);
4641 assert!(rules.is_empty());
4642
4643 let mut d2 = Vec::new();
4646 push16(&mut d2, 1); push16(&mut d2, 20); push16(&mut d2, 2); push16(&mut d2, 10); push16(&mut d2, 10); push16(&mut d2, 1); push16(&mut d2, 4); push16(&mut d2, 77); push16(&mut d2, 2); push16(&mut d2, 11); push16(&mut d2, 1); push16(&mut d2, 1);
4658 push16(&mut d2, 10);
4659 parse_ligature_subst(&d2, 0, &mut rules);
4660 assert_eq!(rules.len(), 1);
4661 assert_eq!(rules[&10].len(), 1);
4662 assert_eq!(rules[&10][0].components, vec![11]);
4663 assert_eq!(rules[&10][0].ligature, 77);
4664
4665 rules.clear();
4668 let mut d3 = Vec::new();
4669 push16(&mut d3, 1); push16(&mut d3, 8); push16(&mut d3, 1); push16(&mut d3, 14); push16(&mut d3, 1); push16(&mut d3, 1);
4675 push16(&mut d3, 10);
4676 push16(&mut d3, 4); push16(&mut d3, 10); push16(&mut d3, 20); push16(&mut d3, 0x4000); push16(&mut d3, 14); push16(&mut d3, 33); push16(&mut d3, 0); push16(&mut d3, 88); push16(&mut d3, 5); push16(&mut d3, 11);
4686 push16(&mut d3, 12);
4687 assert_eq!(d3.len(), 36);
4688 parse_ligature_subst(&d3, 0, &mut rules);
4689 assert!(rules.is_empty());
4690 }
4691
4692 #[test]
4693 fn ligature_subst_work_cap_stops_aliased_sets() {
4694 for lig_count in [65_535u16, 65_534] {
4698 let mut d = Vec::new();
4699 push16(&mut d, 1); push16(&mut d, 10); push16(&mut d, 2); push16(&mut d, 18); push16(&mut d, 18); push16(&mut d, 1); push16(&mut d, 2);
4706 push16(&mut d, 10);
4707 push16(&mut d, 11);
4708 push16(&mut d, lig_count); let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
4710 std::collections::BTreeMap::new();
4711 parse_ligature_subst(&d, 0, &mut rules);
4712 assert!(rules.is_empty(), "lig_count={lig_count}");
4713 }
4714 }
4715
4716 fn f32_to_fixed(v: f32) -> i32 {
4719 (f64::from(v) * 65536.0).round() as i32
4720 }
4721
4722 fn f32_to_f2dot14(v: f32) -> i16 {
4723 (v * 16384.0).round() as i16
4724 }
4725
4726 fn push_i32(out: &mut Vec<u8>, v: i32) {
4727 out.extend_from_slice(&v.to_be_bytes());
4728 }
4729
4730 fn fvar_table(
4733 axes: &[(&[u8; 4], f32, f32, f32, u16)],
4734 instances: &[(u16, &[f32])],
4735 with_ps_name: bool,
4736 ) -> Vec<u8> {
4737 let axis_count = u16::try_from(axes.len()).unwrap();
4738 let instance_count = u16::try_from(instances.len()).unwrap();
4739 let instance_size = 4 + 4 * axis_count + u16::from(with_ps_name) * 2;
4740 let mut t = Vec::new();
4741 push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, 16); push16(&mut t, 0); push16(&mut t, axis_count);
4746 push16(&mut t, 20); push16(&mut t, instance_count);
4748 push16(&mut t, instance_size);
4749 for &(tag, min, default, max, name_id) in axes {
4750 t.extend_from_slice(&tag[..]);
4751 push_i32(&mut t, f32_to_fixed(min));
4752 push_i32(&mut t, f32_to_fixed(default));
4753 push_i32(&mut t, f32_to_fixed(max));
4754 push16(&mut t, 0); push16(&mut t, name_id);
4756 }
4757 for &(name_id, coords) in instances {
4758 push16(&mut t, name_id);
4759 push16(&mut t, 0); for &c in coords {
4761 push_i32(&mut t, f32_to_fixed(c));
4762 }
4763 if with_ps_name {
4764 push16(&mut t, name_id.saturating_add(1000));
4765 }
4766 }
4767 t
4768 }
4769
4770 fn avar_table(maps: &[&[(f32, f32)]]) -> Vec<u8> {
4772 let mut t = Vec::new();
4773 push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, 0); push16(&mut t, u16::try_from(maps.len()).unwrap());
4777 for axis in maps {
4778 push16(&mut t, u16::try_from(axis.len()).unwrap());
4779 for &(from, to) in *axis {
4780 push_i16(&mut t, f32_to_f2dot14(from));
4781 push_i16(&mut t, f32_to_f2dot14(to));
4782 }
4783 }
4784 t
4785 }
4786
4787 fn variation_font(fvar: Vec<u8>, avar: Option<Vec<u8>>) -> Font {
4788 let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
4789 tables.push((b"fvar", fvar));
4790 if let Some(avar) = avar {
4791 tables.push((b"avar", avar));
4792 }
4793 parse(&tables)
4794 }
4795
4796 fn fmd_test_vf_bytes() -> Vec<u8> {
4799 let fvar = fvar_table(
4800 &[(b"wght", 100.0, 400.0, 900.0, 256)],
4801 &[(258, &[400.0]), (259, &[700.0])],
4802 true,
4803 );
4804 let avar = avar_table(&[&[(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)]]);
4805 let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
4806 tables.push((b"fvar", fvar));
4807 tables.push((b"avar", avar));
4808 sfnt(0x0001_0000, &tables)
4809 }
4810
4811 fn log_check(id: &str, subject: &str, ok: bool) {
4812 eprintln!(
4813 "check id={id} subject={subject} outcome={}",
4814 if ok { "PASS" } else { "FAIL" }
4815 );
4816 assert!(ok, "{id}: {subject}");
4817 }
4818
4819 #[test]
4820 fn fvar_axes_tags_and_named_instances() {
4821 let font = variation_font(
4822 fvar_table(
4823 &[
4824 (b"wght", 100.0, 400.0, 900.0, 256),
4825 (b"wdth", 75.0, 100.0, 125.0, 257),
4826 ],
4827 &[(258, &[400.0, 100.0]), (259, &[700.0, 100.0])],
4828 true,
4829 ),
4830 None,
4831 );
4832 let axes = font.axes();
4833 log_check("gk3v.1.axes.count", "two axes", axes.len() == 2);
4834 log_check(
4835 "gk3v.1.axes.wght",
4836 "first tag wght",
4837 axes[0].tag == *b"wght",
4838 );
4839 log_check(
4840 "gk3v.1.axes.wdth",
4841 "second tag wdth",
4842 axes[1].tag == *b"wdth",
4843 );
4844 let wght = font.instance_bounds(*b"wght").expect("wght present");
4845 log_check(
4846 "gk3v.1.bounds.wght",
4847 "wght 100/400/900",
4848 (wght.min - 100.0).abs() < 1e-4
4849 && (wght.default - 400.0).abs() < 1e-4
4850 && (wght.max - 900.0).abs() < 1e-4,
4851 );
4852 log_check(
4853 "gk3v.1.bounds.missing",
4854 "unknown tag is None",
4855 font.instance_bounds(*b"opsz").is_none(),
4856 );
4857 let inst = font.named_instances();
4858 log_check("gk3v.1.inst.count", "two named instances", inst.len() == 2);
4859 log_check(
4860 "gk3v.1.inst.regular",
4861 "Regular at wght=400",
4862 inst[0].subfamily_name_id == 258
4863 && (inst[0].coordinates[0] - 400.0).abs() < 1e-4
4864 && inst[0].postscript_name_id == Some(1258),
4865 );
4866 log_check(
4867 "gk3v.1.inst.bold",
4868 "Bold at wght=700",
4869 inst[1].subfamily_name_id == 259 && (inst[1].coordinates[0] - 700.0).abs() < 1e-4,
4870 );
4871 log_check(
4872 "gk3v.1.static",
4873 "static face has no axes",
4874 parse(&base_tables(
4875 1,
4876 1,
4877 1000,
4878 hmtx_long(&[(500, 0)]),
4879 cmap4_simple(&[]),
4880 ))
4881 .axes()
4882 .is_empty(),
4883 );
4884 }
4885
4886 #[test]
4887 fn avar_clamp_edges_map_to_endpoints() {
4888 let font = variation_font(
4890 fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false),
4891 Some(avar_table(&[&[
4892 (-1.0, -1.0),
4893 (0.0, 0.0),
4894 (0.5, 0.25),
4895 (1.0, 1.0),
4896 ]])),
4897 );
4898 let below = font.normalized_axis(*b"wght", 0.0);
4899 let at_min = font.normalized_axis(*b"wght", 100.0);
4900 let at_def = font.normalized_axis(*b"wght", 400.0);
4901 let at_max = font.normalized_axis(*b"wght", 900.0);
4902 let above = font.normalized_axis(*b"wght", 2000.0);
4903 log_check(
4904 "gk3v.1.avar.below",
4905 "below-min → -1",
4906 below.is_some_and(|v| (v + 1.0).abs() < 1e-4),
4907 );
4908 log_check(
4909 "gk3v.1.avar.min",
4910 "min → -1",
4911 at_min.is_some_and(|v| (v + 1.0).abs() < 1e-4),
4912 );
4913 log_check(
4914 "gk3v.1.avar.default",
4915 "default → 0",
4916 at_def.is_some_and(|v| v.abs() < 1e-4),
4917 );
4918 log_check(
4919 "gk3v.1.avar.max",
4920 "max → +1",
4921 at_max.is_some_and(|v| (v - 1.0).abs() < 1e-4),
4922 );
4923 log_check(
4924 "gk3v.1.avar.above",
4925 "above-max → +1",
4926 above.is_some_and(|v| (v - 1.0).abs() < 1e-4),
4927 );
4928 let mid = font.normalized_axis(*b"wght", 650.0);
4930 log_check(
4931 "gk3v.1.avar.mid",
4932 "650 → avar(0.5)=0.25",
4933 mid.is_some_and(|v| (v - 0.25).abs() < 1e-3),
4934 );
4935 }
4936
4937 #[test]
4938 fn fvar_avar_truncation_and_hostile_headers() {
4939 let good = fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false);
4940 let font = variation_font(good[..10].to_vec(), None);
4941 log_check(
4942 "gk3v.1.trunc.header",
4943 "truncated fvar header → no axes",
4944 font.axes().is_empty(),
4945 );
4946
4947 let mut bad_ver = good.clone();
4948 bad_ver[0..2].copy_from_slice(&2u16.to_be_bytes());
4949 log_check(
4950 "gk3v.1.trunc.version",
4951 "fvar major!=1 → no axes",
4952 variation_font(bad_ver, None).axes().is_empty(),
4953 );
4954
4955 let mut tiny_axis = good.clone();
4956 tiny_axis[10..12].copy_from_slice(&8u16.to_be_bytes());
4957 log_check(
4958 "gk3v.1.trunc.axisSize",
4959 "axisSize < 20 → no axes",
4960 variation_font(tiny_axis, None).axes().is_empty(),
4961 );
4962
4963 let mismatched = variation_font(
4965 fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false),
4966 Some(avar_table(&[
4967 &[(-1.0, -1.0), (1.0, 1.0)],
4968 &[(-1.0, -1.0), (1.0, 1.0)],
4969 ])),
4970 );
4971 log_check(
4972 "gk3v.1.avar.mismatch",
4973 "avar axisCount mismatch → identity",
4974 mismatched.axes().len() == 1
4975 && mismatched
4976 .normalized_axis(*b"wght", 100.0)
4977 .is_some_and(|v| (v + 1.0).abs() < 1e-4),
4978 );
4979 }
4980
4981 #[test]
4982 fn fvar_avar_lcg_mutation_never_panics() {
4983 let base = fmd_test_vf_bytes();
4984 let mut state = 0xC0FF_EE00u64;
4985 let mut lcg = move || {
4986 state = state
4987 .wrapping_mul(6_364_136_223_846_793_005)
4988 .wrapping_add(1);
4989 (state >> 33) as usize
4990 };
4991 for round in 0..128 {
4992 let mut mutated = base.clone();
4993 for _ in 0..8 {
4994 let pos = lcg() % mutated.len();
4995 let bit = 1u8 << (lcg() % 8);
4996 mutated[pos] ^= bit;
4997 }
4998 let outcome = std::panic::catch_unwind(move || {
4999 if let Ok(font) = Font::parse(mutated) {
5000 let _ = font.axes();
5001 let _ = font.named_instances();
5002 let _ = font.instance_bounds(*b"wght");
5003 let _ = font.normalized_axis(*b"wght", 0.0);
5004 let _ = font.normalized_axis(*b"wght", 400.0);
5005 let _ = font.normalized_axis(*b"wght", 9999.0);
5006 }
5007 });
5008 log_check(
5009 "gk3v.1.lcg",
5010 &format!("round {round} no panic"),
5011 outcome.is_ok(),
5012 );
5013 }
5014 for cut in (0..base.len()).step_by(7) {
5015 let truncated = base[..cut].to_vec();
5016 let outcome = std::panic::catch_unwind(move || {
5017 if let Ok(font) = Font::parse(truncated) {
5018 let _ = font.axes();
5019 let _ = font.normalized_axis(*b"wght", 100.0);
5020 }
5021 });
5022 log_check(
5023 "gk3v.1.trunc.sweep",
5024 &format!("cut {cut} no panic"),
5025 outcome.is_ok(),
5026 );
5027 }
5028 }
5029
5030 #[test]
5031 fn fmd_test_vf_fixture_round_trip() {
5032 let bytes = fmd_test_vf_bytes();
5033 let committed = include_bytes!("../fonts/test-variable/FmdTestVF.ttf");
5034 log_check(
5035 "gk3v.1.fixture.bytes",
5036 "committed TTF matches generator",
5037 bytes.as_slice() == committed,
5038 );
5039 let font = Font::parse(bytes).expect("test VF parses");
5040 log_check(
5041 "gk3v.1.fixture.axes",
5042 "committed-shape VF has wght",
5043 font.axes().len() == 1 && font.axes()[0].tag == *b"wght",
5044 );
5045 log_check(
5046 "gk3v.1.fixture.inst",
5047 "Regular + Bold instances",
5048 font.named_instances().len() == 2,
5049 );
5050 if std::env::var("FMD_DUMP_TEST_VF").ok().as_deref() == Some("1") {
5051 std::fs::write("/tmp/FmdTestVF.ttf", fmd_test_vf_bytes()).unwrap();
5052 eprintln!("check id=gk3v.1.dump subject=/tmp/FmdTestVF.ttf outcome=PASS");
5053 }
5054 }
5055}