1use std::{collections::HashSet, hash::Hash};
4
5pub use read_fonts::tables::layout::LookupFlag;
6use read_fonts::FontRead;
7
8pub mod builders;
9#[cfg(test)]
10mod extended;
11#[cfg(test)]
12mod lookup_variations;
13#[cfg(test)]
14mod spec_tests;
15
16include!("../../generated/generated_layout.rs");
17
18impl FeatureVariations {
19 fn compute_version(&self) -> MajorMinor {
20 if self.lookup_variation_records.is_some() {
21 MajorMinor::VERSION_1_1
22 } else {
23 MajorMinor::VERSION_1_0
24 }
25 }
26
27 fn validate_lookup_variations(&self, ctx: &mut ValidationCtx) {
28 if let Some(records) = &self.lookup_variation_records {
29 if records
30 .windows(2)
31 .any(|pair| pair[0].feature_index >= pair[1].feature_index)
32 {
33 ctx.report("lookup variation records must have increasing, unique feature indices");
34 }
35 }
36 }
37}
38
39macro_rules! lookup_type {
41 (gpos, $ty:ty, $val:expr) => {
42 impl LookupSubtable for $ty {
43 const TYPE: LookupType = LookupType::Gpos($val);
44 }
45 };
46
47 (gsub, $ty:ty, $val:expr) => {
48 impl LookupSubtable for $ty {
49 const TYPE: LookupType = LookupType::Gsub($val);
50 }
51 };
52}
53
54macro_rules! original_lookup {
57 ($group:ident, $variant:ident, $old:ty, $new:ty) => {
58 impl From<Lookup<$old>> for $group {
59 fn from(lookup: Lookup<$old>) -> Self {
60 Self::$variant(Lookup {
61 lookup_flag: lookup.lookup_flag,
62 subtables: lookup
63 .subtables
64 .into_iter()
65 .map(|subtable| OffsetMarker::new(<$new>::from(subtable.into_inner())))
66 .collect(),
67 mark_filtering_set: lookup.mark_filtering_set,
68 })
69 }
70 }
71 };
72}
73
74pub(crate) use original_lookup;
75
76macro_rules! table_newtype {
82 ($name:ident, $inner:ident, $read_type:path) => {
83 #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
90 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91 pub struct $name($inner);
92
93 impl $name {
94 pub fn as_inner(&self) -> &$inner {
96 &self.0
97 }
98 }
99
100 impl std::ops::Deref for $name {
101 type Target = $inner;
102 fn deref(&self) -> &Self::Target {
103 &self.0
104 }
105 }
106
107 impl std::ops::DerefMut for $name {
108 fn deref_mut(&mut self) -> &mut Self::Target {
109 &mut self.0
110 }
111 }
112
113 impl FontWrite for $name {
114 fn write_into(&self, writer: &mut TableWriter) {
115 self.0.write_into(writer)
116 }
117
118 fn table_type(&self) -> crate::table_type::TableType {
119 self.0.table_type()
120 }
121 }
122
123 impl Validate for $name {
124 fn validate_impl(&self, ctx: &mut ValidationCtx) {
125 self.0.validate_impl(ctx)
126 }
127 }
128
129 impl<'a> FromObjRef<$read_type> for $name {
130 fn from_obj_ref(obj: &$read_type, _data: FontData) -> Self {
131 Self(FromObjRef::from_obj_ref(obj, _data))
132 }
133 }
134
135 impl<'a> FromTableRef<$read_type> for $name {}
136
137 impl From<$inner> for $name {
138 fn from(src: $inner) -> $name {
139 $name(src)
140 }
141 }
142 };
143}
144
145pub(crate) use lookup_type;
146pub(crate) use table_newtype;
147
148impl FontWrite for LookupFlag {
149 fn write_into(&self, writer: &mut TableWriter) {
150 self.to_bits().write_into(writer)
151 }
152}
153
154impl<T: LookupSubtable + FontWrite> FontWrite for Lookup<T> {
155 fn write_into(&self, writer: &mut TableWriter) {
156 T::TYPE.write_into(writer);
157 self.lookup_flag.write_into(writer);
158 u16::try_from(self.subtables.len())
159 .unwrap()
160 .write_into(writer);
161 self.subtables.write_into(writer);
162 self.mark_filtering_set.write_into(writer);
163 }
164
165 fn table_type(&self) -> crate::table_type::TableType {
166 T::TYPE.into()
167 }
168}
169
170impl Lookup<SequenceContext> {
171 pub fn into_concrete<T: From<SequenceContext>>(self) -> Lookup<T> {
173 let Lookup {
174 lookup_flag,
175 subtables,
176 mark_filtering_set,
177 } = self;
178 let subtables = subtables
179 .into_iter()
180 .map(|offset| OffsetMarker::new(offset.into_inner().into()))
181 .collect();
182 Lookup {
183 lookup_flag,
184 subtables,
185 mark_filtering_set,
186 }
187 }
188}
189
190impl Lookup<ChainedSequenceContext> {
191 pub fn into_concrete<T: From<ChainedSequenceContext>>(self) -> Lookup<T> {
193 let Lookup {
194 lookup_flag,
195 subtables,
196 mark_filtering_set,
197 } = self;
198 let subtables = subtables
199 .into_iter()
200 .map(|offset| OffsetMarker::new(offset.into_inner().into()))
201 .collect();
202 Lookup {
203 lookup_flag,
204 subtables,
205 mark_filtering_set,
206 }
207 }
208}
209
210pub trait LookupSubtable {
215 const TYPE: LookupType;
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum LookupType {
222 Gpos(u16),
223 Gsub(u16),
224}
225
226impl LookupType {
227 pub(crate) const GSUB_EXT_TYPE: u16 = 7;
228 pub(crate) const GPOS_EXT_TYPE: u16 = 9;
229 pub(crate) const PAIR_POS: u16 = 2;
230 pub(crate) const MARK_TO_BASE: u16 = 4;
231
232 pub(crate) fn to_raw(self) -> u16 {
233 match self {
234 LookupType::Gpos(val) => val,
235 LookupType::Gsub(val) => val,
236 }
237 }
238
239 pub(crate) fn promote(self) -> Self {
240 match self {
241 LookupType::Gpos(Self::GPOS_EXT_TYPE) | LookupType::Gsub(Self::GSUB_EXT_TYPE) => {
242 panic!("should never be promoting an extension subtable")
243 }
244 LookupType::Gpos(_) => LookupType::Gpos(Self::GPOS_EXT_TYPE),
245 LookupType::Gsub(_) => LookupType::Gsub(Self::GSUB_EXT_TYPE),
246 }
247 }
248}
249
250impl FontWrite for LookupType {
251 fn write_into(&self, writer: &mut TableWriter) {
252 self.to_raw().write_into(writer)
253 }
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub enum FeatureParams {
259 StylisticSet(StylisticSetParams),
260 Size(SizeParams),
261 CharacterVariant(CharacterVariantParams),
262}
263
264impl FontWrite for FeatureParams {
265 fn write_into(&self, writer: &mut TableWriter) {
266 match self {
267 FeatureParams::StylisticSet(table) => table.write_into(writer),
268 FeatureParams::Size(table) => table.write_into(writer),
269 FeatureParams::CharacterVariant(table) => table.write_into(writer),
270 }
271 }
272}
273
274impl Validate for FeatureParams {
275 fn validate_impl(&self, ctx: &mut ValidationCtx) {
276 match self {
277 Self::StylisticSet(table) => table.validate_impl(ctx),
278 Self::Size(table) => table.validate_impl(ctx),
279 Self::CharacterVariant(table) => table.validate_impl(ctx),
280 }
281 }
282}
283
284impl FromObjRef<read_fonts::tables::layout::FeatureParams<'_>> for FeatureParams {
285 fn from_obj_ref(from: &read_fonts::tables::layout::FeatureParams, data: FontData) -> Self {
286 use read_fonts::tables::layout::FeatureParams as FromType;
287 match from {
288 FromType::Size(thing) => Self::Size(SizeParams::from_obj_ref(thing, data)),
289 FromType::StylisticSet(thing) => {
290 Self::StylisticSet(FromObjRef::from_obj_ref(thing, data))
291 }
292 FromType::CharacterVariant(thing) => {
293 Self::CharacterVariant(FromObjRef::from_obj_ref(thing, data))
294 }
295 }
296 }
297}
298
299impl FromTableRef<read_fonts::tables::layout::FeatureParams<'_>> for FeatureParams {}
300
301impl ClassDefFormat1 {
302 fn iter(&self) -> impl Iterator<Item = (GlyphId16, u16)> + '_ {
303 self.class_value_array.iter().enumerate().map(|(i, cls)| {
304 (
305 GlyphId16::new(self.start_glyph_id.to_u16().saturating_add(i as u16)),
306 *cls,
307 )
308 })
309 }
310}
311
312impl ClassRangeRecord {
313 fn validate_glyph_range(&self, ctx: &mut ValidationCtx) {
314 if self.start_glyph_id > self.end_glyph_id {
315 ctx.report(format!(
316 "start_glyph_id {} larger than end_glyph_id {}",
317 self.start_glyph_id, self.end_glyph_id
318 ));
319 }
320 }
321}
322
323impl ClassDefFormat2 {
324 fn iter(&self) -> impl Iterator<Item = (GlyphId16, u16)> + '_ {
325 self.class_range_records.iter().flat_map(|rcd| {
326 (rcd.start_glyph_id.to_u16()..=rcd.end_glyph_id.to_u16())
327 .map(|gid| (GlyphId16::new(gid), rcd.class))
328 })
329 }
330}
331
332impl ClassDef {
333 pub fn iter(&self) -> impl Iterator<Item = (GlyphId, u32)> + '_ {
334 let (one, two, three, four) = match self {
335 Self::Format1(t) => (
336 Some(t.iter().map(|(g, c)| (g.into(), u32::from(c)))),
337 None,
338 None,
339 None,
340 ),
341 Self::Format2(t) => (
342 None,
343 Some(t.iter().map(|(g, c)| (g.into(), u32::from(c)))),
344 None,
345 None,
346 ),
347 Self::Format3(t) => (
348 None,
349 None,
350 Some(
351 t.class_value_array
352 .iter()
353 .enumerate()
354 .filter_map(move |(i, c)| {
355 let g = t.start_glyph_id.to_u32().checked_add(i as u32)?;
356 (g <= Uint24::MAX.to_u32()).then_some((GlyphId::new(g), c.to_u32()))
357 }),
358 ),
359 None,
360 ),
361 Self::Format4(t) => (
362 None,
363 None,
364 None,
365 Some(t.class_range_records.iter().flat_map(|r| {
366 (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32())
367 .map(move |g| (GlyphId::new(g), u32::from(r.class)))
368 })),
369 ),
370 };
371 one.into_iter()
372 .flatten()
373 .chain(two.into_iter().flatten())
374 .chain(three.into_iter().flatten())
375 .chain(four.into_iter().flatten())
376 }
377
378 pub fn get(&self, glyph: impl Into<GlyphId>) -> u32 {
380 self.get_raw(glyph.into()).unwrap_or(0)
381 }
382
383 fn get_raw(&self, glyph: impl Into<GlyphId>) -> Option<u32> {
384 let glyph = glyph.into().to_u32();
385 match self {
386 Self::Format1(t) => glyph
387 .checked_sub(t.start_glyph_id.to_u32())
388 .and_then(|i| t.class_value_array.get(i as usize))
389 .map(|c| u32::from(*c)),
390 Self::Format2(t) => t
391 .class_range_records
392 .iter()
393 .find(|r| (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).contains(&glyph))
394 .map(|r| u32::from(r.class)),
395 Self::Format3(t) => glyph
396 .checked_sub(t.start_glyph_id.to_u32())
397 .filter(|_| glyph <= Uint24::MAX.to_u32())
398 .and_then(|i| t.class_value_array.get(i as usize))
399 .map(|c| c.to_u32()),
400 Self::Format4(t) => t
401 .class_range_records
402 .iter()
403 .find(|r| (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).contains(&glyph))
404 .map(|r| u32::from(r.class)),
405 }
406 }
407
408 pub fn class_count(&self) -> u32 {
409 self.iter()
410 .map(|(_, c)| c)
411 .chain(std::iter::once(0))
412 .collect::<HashSet<_>>()
413 .len() as u32
414 }
415
416 pub fn is_empty(&self) -> bool {
418 match self {
419 Self::Format1(t) => t.class_value_array.is_empty(),
420 Self::Format2(t) => t.class_range_records.is_empty(),
421 Self::Format3(t) => t.class_value_array.is_empty(),
422 Self::Format4(t) => t.class_range_records.is_empty(),
423 }
424 }
425}
426
427impl CoverageFormat1 {
428 fn iter(&self) -> impl Iterator<Item = GlyphId16> + '_ {
429 self.glyph_array.iter().copied()
430 }
431
432 fn len(&self) -> usize {
433 self.glyph_array.len()
434 }
435}
436
437impl CoverageFormat2 {
438 fn iter(&self) -> impl Iterator<Item = GlyphId16> + '_ {
439 self.range_records
440 .iter()
441 .flat_map(|rcd| iter_gids(rcd.start_glyph_id, rcd.end_glyph_id))
442 }
443
444 fn len(&self) -> usize {
445 self.range_records
446 .iter()
447 .map(|rcd| {
448 rcd.end_glyph_id
449 .to_u16()
450 .saturating_sub(rcd.start_glyph_id.to_u16()) as usize
451 + 1
452 })
453 .sum()
454 }
455}
456
457impl CoverageTable {
458 pub fn iter(&self) -> impl Iterator<Item = GlyphId> + '_ {
459 let (one, two, three, four) = match self {
460 Self::Format1(t) => (Some(t.iter().map(GlyphId::from)), None, None, None),
461 Self::Format2(t) => (None, Some(t.iter().map(GlyphId::from)), None, None),
462 Self::Format3(t) => (
463 None,
464 None,
465 Some(t.glyph_array.iter().copied().map(GlyphId::from)),
466 None,
467 ),
468 Self::Format4(t) => (
469 None,
470 None,
471 None,
472 Some(t.range_records.iter().flat_map(|r| {
473 (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).map(GlyphId::new)
474 })),
475 ),
476 };
477 one.into_iter()
478 .flatten()
479 .chain(two.into_iter().flatten())
480 .chain(three.into_iter().flatten())
481 .chain(four.into_iter().flatten())
482 }
483
484 pub fn len(&self) -> usize {
485 match self {
486 Self::Format1(t) => t.len(),
487 Self::Format2(t) => t.len(),
488 Self::Format3(t) => t.glyph_array.len(),
489 Self::Format4(t) => t
490 .range_records
491 .iter()
492 .map(|r| {
493 r.end_glyph_id
494 .to_u32()
495 .checked_sub(r.start_glyph_id.to_u32())
496 .map_or(0, |n| n as usize + 1)
497 })
498 .sum(),
499 }
500 }
501
502 pub fn is_empty(&self) -> bool {
503 self.len() == 0
504 }
505}
506
507impl FromIterator<GlyphId16> for CoverageTable {
508 fn from_iter<T: IntoIterator<Item = GlyphId16>>(iter: T) -> Self {
509 let glyphs = iter.into_iter().collect::<Vec<_>>();
510 builders::CoverageTableBuilder::from_glyphs(glyphs).build()
511 }
512}
513
514impl From<Vec<GlyphId16>> for CoverageTable {
515 fn from(value: Vec<GlyphId16>) -> Self {
516 builders::CoverageTableBuilder::from_glyphs(value).build()
517 }
518}
519
520impl FromIterator<(GlyphId16, u16)> for ClassDef {
521 fn from_iter<T: IntoIterator<Item = (GlyphId16, u16)>>(iter: T) -> Self {
522 builders::ClassDefBuilderImpl::from_iter(iter).build()
523 }
524}
525
526impl RangeRecord {
527 pub fn iter_for_glyphs(glyphs: &[GlyphId16]) -> impl Iterator<Item = RangeRecord> + '_ {
533 let mut cur_range = glyphs.first().copied().map(|g| (g, g));
534 let mut len = 0u16;
535 let mut iter = glyphs.iter().skip(1).copied();
536
537 #[allow(clippy::while_let_on_iterator)]
538 std::iter::from_fn(move || {
539 while let Some(glyph) = iter.next() {
540 match cur_range {
541 None => return None,
542 Some((a, b)) if are_sequential(b, glyph) => cur_range = Some((a, glyph)),
543 Some((a, b)) => {
544 let result = RangeRecord {
545 start_glyph_id: a,
546 end_glyph_id: b,
547 start_coverage_index: len,
548 };
549 cur_range = Some((glyph, glyph));
550 len += 1 + b.to_u16().saturating_sub(a.to_u16());
551 return Some(result);
552 }
553 }
554 }
555 cur_range
556 .take()
557 .map(|(start_glyph_id, end_glyph_id)| RangeRecord {
558 start_glyph_id,
559 end_glyph_id,
560 start_coverage_index: len,
561 })
562 })
563 }
564}
565
566fn iter_gids(gid1: GlyphId16, gid2: GlyphId16) -> impl Iterator<Item = GlyphId16> {
567 (gid1.to_u16()..=gid2.to_u16()).map(GlyphId16::new)
568}
569
570fn are_sequential(gid1: GlyphId16, gid2: GlyphId16) -> bool {
571 gid2.to_u16().saturating_sub(gid1.to_u16()) == 1
572}
573
574impl Device {
575 pub fn new(start_size: u16, end_size: u16, values: &[i8]) -> Self {
576 debug_assert_eq!(
577 (start_size..=end_size).count(),
578 values.len(),
579 "device range and values must match"
580 );
581 let delta_format: DeltaFormat = values
582 .iter()
583 .map(|val| match val {
584 -2..=1 => DeltaFormat::Local2BitDeltas,
585 -8..=7 => DeltaFormat::Local4BitDeltas,
586 _ => DeltaFormat::Local8BitDeltas,
587 })
588 .max()
589 .unwrap_or_default();
590 let delta_value = encode_delta(delta_format, values);
591
592 Device {
593 start_size,
594 end_size,
595 delta_format,
596 delta_value,
597 }
598 }
599}
600
601impl DeviceOrVariationIndex {
602 pub fn device(start_size: u16, end_size: u16, values: &[i8]) -> Self {
604 DeviceOrVariationIndex::Device(Device::new(start_size, end_size, values))
605 }
606}
607
608impl FontWrite for PendingVariationIndex {
609 fn write_into(&self, _writer: &mut TableWriter) {
610 panic!(
611 "Attempted to write PendingVariationIndex.\n\
612 VariationIndex tables should always be resolved before compilation.\n\
613 Please report this bug at <https://github.com/googlefonts/fontations/issues>"
614 )
615 }
616}
617
618fn encode_delta(format: DeltaFormat, values: &[i8]) -> Vec<u16> {
619 let (chunk_size, mask, bits) = match format {
620 DeltaFormat::Local2BitDeltas => (8, 0b11, 2),
621 DeltaFormat::Local4BitDeltas => (4, 0b1111, 4),
622 DeltaFormat::Local8BitDeltas => (2, 0b11111111, 8),
623 _ => panic!("invalid format"),
624 };
625 values
626 .chunks(chunk_size)
627 .map(|chunk| encode_chunk(chunk, mask, bits))
628 .collect()
629}
630
631fn encode_chunk(chunk: &[i8], mask: u8, bits: usize) -> u16 {
632 let mut out = 0u16;
633 for (i, val) in chunk.iter().enumerate() {
634 out |= ((val.to_be_bytes()[0] & mask) as u16) << ((16 - bits) - i * bits);
635 }
636 out
637}
638
639impl From<VariationIndex> for u32 {
640 fn from(value: VariationIndex) -> Self {
641 ((value.delta_set_outer_index as u32) << 16) | value.delta_set_inner_index as u32
642 }
643}
644
645impl ClassRangeRecord2 {
646 fn validate_glyph_range(&self, ctx: &mut ValidationCtx) {
647 if self.start_glyph_id > self.end_glyph_id {
648 ctx.report("start_glyph_id larger than end_glyph_id");
649 }
650 }
651}
652
653impl FromIterator<GlyphId> for CoverageTable {
654 fn from_iter<T: IntoIterator<Item = GlyphId>>(iter: T) -> Self {
655 let mut glyphs: Vec<_> = iter.into_iter().collect();
656 glyphs.sort_unstable();
657 glyphs.dedup();
658 if glyphs.iter().all(|g| g.to_u32() <= u16::MAX as u32) {
659 return glyphs
660 .into_iter()
661 .map(|g| GlyphId16::new(g.to_u32() as u16))
662 .collect();
663 }
664 let glyphs: Vec<_> = glyphs
665 .into_iter()
666 .map(|g| GlyphId24::checked_new(g.to_u32()).expect("glyph ID exceeds 24 bits"))
667 .collect();
668 let mut ranges: Vec<RangeRecord2> = Vec::new();
669 for (index, glyph) in glyphs.iter().enumerate() {
670 if let Some(last) = ranges.last_mut() {
671 if last.end_glyph_id.to_u32() + 1 == glyph.to_u32() {
672 last.end_glyph_id = *glyph;
673 continue;
674 }
675 }
676 ranges.push(RangeRecord2::new(*glyph, *glyph, Uint24::new(index as u32)));
677 }
678 if ranges.len() * 3 < glyphs.len() {
680 return Self::Format4(CoverageFormat4::new(ranges));
681 }
682 Self::Format3(CoverageFormat3::new(glyphs))
683 }
684}
685
686impl FromIterator<(GlyphId, u32)> for ClassDef {
687 fn from_iter<T: IntoIterator<Item = (GlyphId, u32)>>(iter: T) -> Self {
688 let items: std::collections::BTreeMap<_, _> = iter.into_iter().collect();
689 if items
690 .iter()
691 .all(|(g, c)| g.to_u32() <= u16::MAX as u32 && *c <= u16::MAX as u32)
692 {
693 return items
694 .into_iter()
695 .map(|(g, c)| (GlyphId16::new(g.to_u32() as u16), c as u16))
696 .collect();
697 }
698 if items.values().all(|c| *c <= u16::MAX as u32) {
699 let mut ranges: Vec<ClassRangeRecord2> = Vec::new();
700 for (g, c) in items {
701 let g = GlyphId24::checked_new(g.to_u32()).expect("glyph ID exceeds 24 bits");
702 if let Some(last) = ranges.last_mut() {
703 if last.class == c as u16 && last.end_glyph_id.to_u32() + 1 == g.to_u32() {
704 last.end_glyph_id = g;
705 continue;
706 }
707 }
708 ranges.push(ClassRangeRecord2::new(g, g, c as u16));
709 }
710 return Self::Format4(ClassDefFormat4::new(ranges));
711 }
712 let first = items.keys().next().unwrap().to_u32();
713 let last = items.keys().next_back().unwrap().to_u32();
714 let start = GlyphId24::checked_new(first).expect("glyph ID exceeds 24 bits");
715 assert!(last <= Uint24::MAX.to_u32(), "glyph ID exceeds 24 bits");
716 let values = (first..=last)
717 .map(|g| {
718 Uint24::checked_new(items.get(&GlyphId::new(g)).copied().unwrap_or(0))
719 .expect("class exceeds 24 bits")
720 })
721 .collect();
722 Self::Format3(ClassDefFormat3::new(start, values))
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 #[should_panic(expected = "array exceeds max length")]
732 fn array_len_smoke_test() {
733 let table = ScriptList {
734 script_records: vec![ScriptRecord {
735 script_tag: Tag::new(b"hihi"),
736 script: OffsetMarker::new(Script {
737 default_lang_sys: NullableOffsetMarker::new(None),
738 lang_sys_records: vec![LangSysRecord {
739 lang_sys_tag: Tag::new(b"coco"),
740 lang_sys: OffsetMarker::new(LangSys {
741 required_feature_index: 0xffff,
742 feature_indices: vec![69; (u16::MAX) as usize + 5],
743 }),
744 }],
745 }),
746 }],
747 };
748
749 table.validate().unwrap();
750 }
751
752 #[test]
753 #[should_panic(expected = "larger than end_glyph_id")]
754 fn validate_classdef_ranges() {
755 let classdef = ClassDefFormat2::new(vec![ClassRangeRecord::new(
756 GlyphId16::new(12),
757 GlyphId16::new(3),
758 7,
759 )]);
760
761 classdef.validate().unwrap();
762 }
763
764 #[test]
765 fn delta_encode() {
766 let inp = [1i8, 2, 3, -1];
767 let result = encode_delta(DeltaFormat::Local4BitDeltas, &inp);
768 assert_eq!(result.len(), 1);
769 assert_eq!(result[0], 0x123f_u16);
770
771 let inp = [1i8, 1, 1, 1, 1];
772 let result = encode_delta(DeltaFormat::Local2BitDeltas, &inp);
773 assert_eq!(result.len(), 1);
774 assert_eq!(result[0], 0x5540_u16);
775 }
776}