1use super::{
4 layout::DeviceOrVariationIndex,
5 variations::{DeltaSetIndex, ItemVariationStore},
6};
7
8include!("../../generated/generated_base.rs");
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum BaseAxis {
16 Horizontal,
18 Vertical,
20}
21
22#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
32pub struct BaseValue {
33 pub value: F48Dot16,
35 pub delta_px: i32,
39}
40
41const DEFAULT_SCRIPT: Tag = Tag::new(b"DFLT");
44
45pub mod baseline_tags {
47 use super::Tag;
48
49 pub const ROMAN: Tag = Tag::new(b"romn");
52 pub const HANGING: Tag = Tag::new(b"hang");
54 pub const IDEO_FACE_BOTTOM_OR_LEFT: Tag = Tag::new(b"icfb");
56 pub const IDEO_FACE_TOP_OR_RIGHT: Tag = Tag::new(b"icft");
58 pub const IDEO_FACE_CENTRAL: Tag = Tag::new(b"Icfc");
60 pub const IDEO_EMBOX_BOTTOM_OR_LEFT: Tag = Tag::new(b"ideo");
62 pub const IDEO_EMBOX_TOP_OR_RIGHT: Tag = Tag::new(b"idtp");
64 pub const IDEO_EMBOX_CENTRAL: Tag = Tag::new(b"Idce");
66 pub const MATH: Tag = Tag::new(b"math");
68}
69
70const HANGING_SCRIPTS: &[Tag] = &[
74 Tag::new(b"Beng"),
76 Tag::new(b"Deva"),
77 Tag::new(b"Gujr"),
78 Tag::new(b"Guru"),
79 Tag::new(b"Tibt"),
81 Tag::new(b"Limb"),
83 Tag::new(b"Sylo"),
85 Tag::new(b"Phag"),
87 Tag::new(b"Mtei"),
89 Tag::new(b"Shrd"),
91 Tag::new(b"Takr"),
92 Tag::new(b"Modi"),
94 Tag::new(b"Sidd"),
95 Tag::new(b"Tirh"),
96 Tag::new(b"Marc"),
98 Tag::new(b"Newa"),
99 Tag::new(b"Soyo"),
101 Tag::new(b"Zanb"),
102 Tag::new(b"Dogr"),
104 Tag::new(b"Gong"),
105 Tag::new(b"Nand"),
107];
108
109const IDEOGRAPHIC_SCRIPTS: &[Tag] = &[
111 Tag::new(b"Hang"),
113 Tag::new(b"Hani"),
114 Tag::new(b"Hira"),
115 Tag::new(b"Kana"),
116 Tag::new(b"Bopo"),
118 Tag::new(b"Tang"),
120 Tag::new(b"Nshu"),
122 Tag::new(b"Kits"),
124];
125
126pub fn horizontal_baseline_tag_for_script(script: Tag) -> Tag {
144 if HANGING_SCRIPTS.contains(&script) {
145 baseline_tags::HANGING
146 } else if IDEOGRAPHIC_SCRIPTS.contains(&script) {
147 baseline_tags::IDEO_FACE_BOTTOM_OR_LEFT
148 } else {
149 baseline_tags::ROMAN
150 }
151}
152
153impl<'a> Base<'a> {
154 pub fn axis(&self, axis: BaseAxis) -> Option<Axis<'a>> {
157 match axis {
158 BaseAxis::Horizontal => self.horiz_axis(),
159 BaseAxis::Vertical => self.vert_axis(),
160 }?
161 .ok()
162 }
163}
164
165impl<'a> Axis<'a> {
166 pub fn base_script(&self, script_tag: Tag) -> Option<BaseScript<'a>> {
169 let list = self.base_script_list().ok()?;
170 let records = list.base_script_records();
171 let record =
172 find_script(records, script_tag).or_else(|| find_script(records, DEFAULT_SCRIPT))?;
173 record.base_script(list.offset_data()).ok()
174 }
175
176 pub fn baseline_coord(&self, baseline_tag: Tag, script_tag: Tag) -> Option<BaseCoord<'a>> {
183 let values = self.base_script(script_tag)?.base_values()?.ok()?;
184 let tags = self.base_tag_list()?.ok()?;
185 let index = tags
188 .baseline_tags()
189 .binary_search_by_key(&baseline_tag, |tag| tag.get())
190 .ok()?;
191 values.base_coords().get(index).ok()
192 }
193}
194
195fn find_script(records: &[BaseScriptRecord], tag: Tag) -> Option<&BaseScriptRecord> {
197 let index = records
198 .binary_search_by_key(&tag, |record| record.base_script_tag())
199 .ok()?;
200 records.get(index)
201}
202
203impl<'a> BaseCoord<'a> {
204 pub fn device(&self) -> Option<DeviceOrVariationIndex<'a>> {
208 match self {
209 BaseCoord::Format3(coord) => coord.device()?.ok(),
210 _ => None,
211 }
212 }
213}
214
215#[derive(Clone)]
221pub struct BaseInstance<'a> {
222 base: Base<'a>,
223 var_store: Option<ItemVariationStore<'a>>,
224 coords: &'a [F2Dot14],
225}
226
227impl<'a> BaseInstance<'a> {
228 pub fn new(base: Base<'a>) -> Self {
231 Self::with_coords(base, &[])
232 }
233
234 pub fn with_coords(base: Base<'a>, coords: &'a [F2Dot14]) -> Self {
240 let var_store = base.item_var_store().and_then(|store| store.ok());
241 Self {
242 base,
243 var_store,
244 coords,
245 }
246 }
247
248 pub fn baseline(&self, baseline_tag: Tag, axis: BaseAxis, script_tag: Tag) -> Option<F48Dot16> {
254 Some(
255 self.baseline_for_ppem(baseline_tag, axis, script_tag, 0)?
256 .value,
257 )
258 }
259
260 pub fn baseline_for_ppem(
267 &self,
268 baseline_tag: Tag,
269 axis: BaseAxis,
270 script_tag: Tag,
271 ppem: u16,
272 ) -> Option<BaseValue> {
273 let coord = self
274 .base
275 .axis(axis)?
276 .baseline_coord(baseline_tag, script_tag)?;
277 Some(self.coord_for_ppem(&coord, ppem))
278 }
279
280 pub fn coord(&self, coord: &BaseCoord) -> F48Dot16 {
282 self.coord_for_ppem(coord, 0).value
283 }
284
285 pub fn coord_for_ppem(&self, coord: &BaseCoord, ppem: u16) -> BaseValue {
287 let mut value = F48Dot16::from_i32(coord.coordinate() as i32);
291 let mut delta_px = 0;
292 match coord.device() {
293 Some(DeviceOrVariationIndex::Device(device)) => {
294 delta_px = device.delta_for_ppem(ppem);
295 }
296 Some(DeviceOrVariationIndex::VariationIndex(index)) => {
297 if let Some(delta) = self.delta(index.into()) {
298 value += delta;
299 }
300 }
301 None => {}
302 }
303 BaseValue { value, delta_px }
304 }
305
306 fn delta(&self, index: DeltaSetIndex) -> Option<F48Dot16> {
307 if self.coords.is_empty() {
308 return None;
309 }
310 self.var_store.as_ref()?.compute_delta(index, self.coords)
311 }
312}
313
314impl<'a> core::ops::Deref for BaseInstance<'a> {
315 type Target = Base<'a>;
316
317 fn deref(&self) -> &Self::Target {
318 &self.base
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use font_test_data::bebuffer::BeBuffer;
325 use font_types::MajorMinor;
326
327 use super::*;
328
329 #[test]
330 fn example_1() {
332 let data = BeBuffer::new()
333 .push(MajorMinor::VERSION_1_0)
334 .push(8u16) .push(0x10c_u16) .push(4u16) .push(0x12_u16) .push(3u16) .push(Tag::new(b"hang"))
342 .push(Tag::new(b"ideo"))
343 .push(Tag::new(b"romn"))
344 .push(4u16) .push(Tag::new(b"cyrl"))
347 .push(0x1a_u16)
348 .push(Tag::new(b"devn"))
349 .push(0x60_u16)
350 .push(Tag::new(b"hani"))
351 .push(0x8a_u16)
352 .push(Tag::new(b"latn"))
353 .push(0xb4_u16);
354
355 let base = Base::read(data.data().into()).unwrap();
356 assert_eq!(base.version(), MajorMinor::VERSION_1_0);
357 let horiz = base.horiz_axis().unwrap().unwrap();
358 let base_tag = horiz.base_tag_list().unwrap().unwrap();
359 assert_eq!(
360 base_tag.baseline_tags(),
361 &[Tag::new(b"hang"), Tag::new(b"ideo"), Tag::new(b"romn")]
362 );
363 assert_eq!(base_tag.min_byte_range().end, 14);
364 let base_script = horiz.base_script_list().unwrap();
365 assert_eq!(
366 base_script.base_script_records()[3].base_script_tag(),
367 Tag::new(b"latn")
368 );
369 }
370
371 const HANG: Tag = Tag::new(b"hang");
372 const IDEO: Tag = Tag::new(b"ideo");
373 const ROMN: Tag = Tag::new(b"romn");
374 const LATN: Tag = Tag::new(b"latn");
375 const HANI: Tag = Tag::new(b"hani");
376 const ARAB: Tag = Tag::new(b"arab");
377
378 const ROMN_DELTA: i32 = -40;
381
382 fn base_table() -> BeBuffer {
389 let mut buf = BeBuffer::new()
390 .push(MajorMinor::VERSION_1_1)
391 .push_with_tag(0u16, "horiz_axis_offset")
392 .push(0u16) .push_with_tag(0u32, "var_store_offset");
394
395 let axis = buf.len();
397 buf = buf
398 .push_with_tag(0u16, "base_tag_list_offset")
399 .push_with_tag(0u16, "base_script_list_offset");
400
401 let tag_list = buf.len();
403 buf = buf.push(3u16).push(HANG).push(IDEO).push(ROMN);
404
405 let script_list = buf.len();
407 buf = buf
408 .push(3u16) .push(Tag::new(b"DFLT"))
410 .push_with_tag(0u16, "dflt_script_offset")
411 .push(HANI)
412 .push_with_tag(0u16, "hani_script_offset")
413 .push(LATN)
414 .push_with_tag(0u16, "latn_script_offset");
415
416 let latn_script = buf.len();
418 buf = buf
419 .push_with_tag(0u16, "latn_values_offset")
420 .push(0u16) .push(0u16); let latn_values = buf.len();
423 buf = buf
424 .push(2u16) .push(3u16) .push_with_tag(0u16, "latn_hang_offset")
427 .push_with_tag(0u16, "latn_ideo_offset")
428 .push_with_tag(0u16, "latn_romn_offset");
429
430 let hani_script = buf.len();
432 buf = buf.push(0u16).push(0u16).push(0u16);
433
434 let dflt_script = buf.len();
436 buf = buf
437 .push_with_tag(0u16, "dflt_values_offset")
438 .push(0u16)
439 .push(0u16);
440 let dflt_values = buf.len();
441 buf = buf
442 .push(2u16)
443 .push(3u16)
444 .push_with_tag(0u16, "dflt_hang_offset")
445 .push_with_tag(0u16, "dflt_ideo_offset")
446 .push_with_tag(0u16, "dflt_romn_offset");
447
448 let latn_hang = buf.len();
450 buf = buf
451 .push(3u16) .push(700i16)
453 .push_with_tag(0u16, "latn_hang_device");
454 let latn_ideo = buf.len();
455 buf = buf.push(1u16).push(-120i16);
456 let latn_romn = buf.len();
457 buf = buf
458 .push(3u16)
459 .push(0i16)
460 .push_with_tag(0u16, "latn_romn_varidx");
461
462 let dflt_hang = buf.len();
465 buf = buf.push(1u16).push(600i16);
466 let dflt_ideo = buf.len();
467 buf = buf.push(2u16).push(-100i16).push(9u16).push(3u16);
468 let dflt_romn = buf.len();
469 buf = buf.push(1u16).push(5i16);
470
471 let latn_hang_device = buf.len();
472 buf = buf
473 .push(10u16) .push(11u16) .push(3u16) .push(0x02FEu16); let latn_romn_varidx = buf.len();
478 buf = buf.push(0u16).push(0u16).push(0x8000u16);
479
480 let var_store = buf.len();
482 buf = buf
483 .push(1u16) .push_with_tag(0u32, "region_list_offset")
485 .push(1u16) .push_with_tag(0u32, "var_data_offset");
487 let region_list = buf.len();
488 buf = buf
489 .push(1u16) .push(1u16) .push(F2Dot14::from_f32(0.0))
492 .push(F2Dot14::from_f32(1.0))
493 .push(F2Dot14::from_f32(1.0));
494 let var_data = buf.len();
495 buf = buf
496 .push(1u16) .push(1u16) .push(1u16) .push(0u16) .push(ROMN_DELTA as i16);
501
502 buf.write_at("horiz_axis_offset", axis as u16);
503 buf.write_at("var_store_offset", var_store as u32);
504 buf.write_at("base_tag_list_offset", (tag_list - axis) as u16);
505 buf.write_at("base_script_list_offset", (script_list - axis) as u16);
506 buf.write_at("dflt_script_offset", (dflt_script - script_list) as u16);
507 buf.write_at("hani_script_offset", (hani_script - script_list) as u16);
508 buf.write_at("latn_script_offset", (latn_script - script_list) as u16);
509 buf.write_at("latn_values_offset", (latn_values - latn_script) as u16);
510 buf.write_at("dflt_values_offset", (dflt_values - dflt_script) as u16);
511 buf.write_at("latn_hang_offset", (latn_hang - latn_values) as u16);
512 buf.write_at("latn_ideo_offset", (latn_ideo - latn_values) as u16);
513 buf.write_at("latn_romn_offset", (latn_romn - latn_values) as u16);
514 buf.write_at("dflt_hang_offset", (dflt_hang - dflt_values) as u16);
515 buf.write_at("dflt_ideo_offset", (dflt_ideo - dflt_values) as u16);
516 buf.write_at("dflt_romn_offset", (dflt_romn - dflt_values) as u16);
517 buf.write_at("latn_hang_device", (latn_hang_device - latn_hang) as u16);
518 buf.write_at("latn_romn_varidx", (latn_romn_varidx - latn_romn) as u16);
519 buf.write_at("region_list_offset", (region_list - var_store) as u32);
520 buf.write_at("var_data_offset", (var_data - var_store) as u32);
521 buf
522 }
523
524 fn base(buf: &BeBuffer) -> Base<'_> {
525 Base::read(buf.data().into()).unwrap()
526 }
527
528 fn value(v: i32) -> F48Dot16 {
529 F48Dot16::from_i32(v)
530 }
531
532 #[test]
533 fn scripts_map_to_their_baseline() {
534 use baseline_tags::{HANGING, IDEO_FACE_BOTTOM_OR_LEFT, ROMAN};
535
536 assert_eq!(
537 horizontal_baseline_tag_for_script(Tag::new(b"Deva")),
538 HANGING
539 );
540 assert_eq!(
541 horizontal_baseline_tag_for_script(Tag::new(b"Nand")),
542 HANGING
543 );
544 assert_eq!(
545 horizontal_baseline_tag_for_script(Tag::new(b"Hani")),
546 IDEO_FACE_BOTTOM_OR_LEFT
547 );
548 assert_eq!(
549 horizontal_baseline_tag_for_script(Tag::new(b"Kits")),
550 IDEO_FACE_BOTTOM_OR_LEFT
551 );
552 assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Latn")), ROMAN);
553 assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Arab")), ROMAN);
554 assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Zzzz")), ROMAN);
556
557 assert_eq!(
560 horizontal_baseline_tag_for_script(Tag::new(b"Hang")),
561 IDEO_FACE_BOTTOM_OR_LEFT
562 );
563 assert_ne!(Tag::new(b"Hang"), HANGING);
564
565 assert_eq!(HANGING_SCRIPTS.len(), 21);
567 assert_eq!(IDEOGRAPHIC_SCRIPTS.len(), 8);
568 for script in HANGING_SCRIPTS {
569 assert!(!IDEOGRAPHIC_SCRIPTS.contains(script), "{script} is in both");
570 }
571 }
572
573 #[test]
574 fn baselines_are_found_by_tag() {
575 let buf = base_table();
576 let instance = BaseInstance::new(base(&buf));
577
578 assert_eq!(
579 instance.baseline(HANG, BaseAxis::Horizontal, LATN),
580 Some(value(700))
581 );
582 assert_eq!(
583 instance.baseline(IDEO, BaseAxis::Horizontal, LATN),
584 Some(value(-120))
585 );
586 assert_eq!(
587 instance.baseline(ROMN, BaseAxis::Horizontal, LATN),
588 Some(value(0))
589 );
590
591 assert_eq!(
593 instance.baseline(Tag::new(b"math"), BaseAxis::Horizontal, LATN),
594 None
595 );
596 assert_eq!(instance.baseline(ROMN, BaseAxis::Vertical, LATN), None);
598 }
599
600 #[test]
601 fn an_unlisted_script_falls_back_to_the_default() {
602 let buf = base_table();
603 let instance = BaseInstance::new(base(&buf));
604
605 assert_eq!(
607 instance.baseline(HANG, BaseAxis::Horizontal, ARAB),
608 Some(value(600))
609 );
610 assert_eq!(
612 instance.baseline(IDEO, BaseAxis::Horizontal, ARAB),
613 Some(value(-100))
614 );
615
616 assert_eq!(instance.baseline(HANG, BaseAxis::Horizontal, HANI), None);
619 }
620
621 #[test]
622 fn variation_deltas_come_from_the_tables_own_store() {
623 let buf = base_table();
624 let coords = [F2Dot14::from_f32(1.0)];
625 let instance = BaseInstance::with_coords(base(&buf), &coords);
626
627 assert_eq!(
629 instance.baseline(ROMN, BaseAxis::Horizontal, LATN),
630 Some(value(ROMN_DELTA))
631 );
632 assert_eq!(
634 instance.baseline(IDEO, BaseAxis::Horizontal, LATN),
635 Some(value(-120))
636 );
637
638 let default = BaseInstance::new(base(&buf));
640 assert_eq!(
641 default.baseline(ROMN, BaseAxis::Horizontal, LATN),
642 Some(value(0))
643 );
644 }
645
646 #[test]
647 fn device_tables_adjust_by_ppem() {
648 let buf = base_table();
649 let instance = BaseInstance::new(base(&buf));
650
651 let at = |ppem| instance.baseline_for_ppem(HANG, BaseAxis::Horizontal, LATN, ppem);
654 assert_eq!(
655 at(10),
656 Some(BaseValue {
657 value: value(700),
658 delta_px: 2
659 })
660 );
661 assert_eq!(
662 at(11),
663 Some(BaseValue {
664 value: value(700),
665 delta_px: -2
666 })
667 );
668 assert_eq!(
670 at(12),
671 Some(BaseValue {
672 value: value(700),
673 delta_px: 0
674 })
675 );
676
677 let coords = [F2Dot14::from_f32(1.0)];
679 let varying = BaseInstance::with_coords(base(&buf), &coords);
680 assert_eq!(
681 varying.baseline_for_ppem(ROMN, BaseAxis::Horizontal, LATN, 10),
682 Some(BaseValue {
683 value: value(ROMN_DELTA),
684 delta_px: 0
685 })
686 );
687 }
688}