// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
suberror FaceTableError {
TableNotFound
TableError(@sfnt.SfntError)
InvalidFaceIndex
UnexpectedEof
InvalidFormat
} derive(Eq, Show, ToJson)
///|
fn read_u32(data : BytesView, offset : Int) -> Result[Int, FaceTableError] {
if offset + 4 > data.length() {
return Err(UnexpectedEof)
}
let b0 = data[offset].to_uint()
let b1 = data[offset + 1].to_uint()
let b2 = data[offset + 2].to_uint()
let b3 = data[offset + 3].to_uint()
let value = ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3).reinterpret_as_int()
if value < 0 { Err(InvalidFormat) } else { Ok(value) }
}
///|
fn ttc_font_offset(
data : BytesView,
index : Int,
) -> Result[Int, FaceTableError] {
let count = read_u32(data, 8)
match count {
Err(err) => Err(err)
Ok(count) => {
if index < 0 || index >= count {
return Err(InvalidFaceIndex)
}
let offset = 12 + index * 4
read_u32(data, offset)
}
}
}
///|
/// Build and cache the SFNT table directory from the face blob.
pub fn Face::ensure_sfnt_tables(self : Face) -> Result[Unit, FaceTableError] {
if self.tables.length() > 0 {
return Ok(())
}
let data = self.blob.as_view()
if data.length() < 4 {
return Err(UnexpectedEof)
}
let tag = @common.Tag::from_bytes(data[0], data[1], data[2], data[3])
let ttcf = @common.Tag::from_chars('t', 't', 'c', 'f')
let offset =
if tag == ttcf {
match ttc_font_offset(data, self.index) {
Err(err) => return Err(err)
Ok(value) => value
}
} else {
0
}
match @sfnt.Sfnt::parse_at(self.blob, offset) {
Err(err) => Err(TableError(err))
Ok(directory) => {
for record in directory.tables() {
match directory.table(record.tag) {
Err(err) => return Err(TableError(err))
Ok(blob) => self.tables[record.tag] = blob
}
}
Ok(())
}
}
}
///|
/// Reference a table blob by tag, falling back to sfnt directory if needed.
pub fn Face::reference_table(
self : Face,
tag : @common.Tag,
) -> Result[@blob.Blob, FaceTableError] {
match self.tables.get(tag) {
Some(blob) => Ok(blob)
None =>
match self.ensure_sfnt_tables() {
Err(err) => Err(err)
Ok(_) =>
match self.tables.get(tag) {
Some(blob) => Ok(blob)
None => Err(TableNotFound)
}
}
}
}
///|
/// Reference a table blob, returning None when the table is missing.
pub fn Face::reference_table_optional(
self : Face,
tag : @common.Tag,
) -> Result[@blob.Blob?, FaceTableError] {
match self.reference_table(tag) {
Ok(blob) => Ok(Some(blob))
Err(TableNotFound) => Ok(None)
Err(err) => Err(err)
}
}
///|
/// List table tags starting at `start_offset`.
/// Returns (total_count, tags_slice).
pub fn Face::table_tags(
self : Face,
start_offset? : Int = 0,
count? : Int = -1,
) -> Result[(Int, Array[@common.Tag]), FaceTableError] {
let offset = if start_offset < 0 { 0 } else { start_offset }
match self.ensure_sfnt_tables() {
Err(err) => Err(err)
Ok(_) => {
let tags = Array::from_iter(self.tables.keys())
let total = tags.length()
if offset >= total {
return Ok((total, []))
}
let remaining = total - offset
let take = if count < 0 || count > remaining { remaining } else { count }
let out : Array[@common.Tag] = []
for i in offset..<(offset + take) {
out.push(tags[i])
}
Ok((total, out))
}
}
}
///|
/// Collect all Unicode codepoints mapped by the face cmap.
pub fn Face::collect_unicodes(
self : Face,
) -> Result[@common.CodepointSet, FaceTableError] {
match self.collect_nominal_glyph_mapping() {
Err(err) => Err(err)
Ok(map) => Ok(map.keys_set())
}
}
///|
/// Collect nominal glyph mappings from the face cmap.
pub fn Face::collect_nominal_glyph_mapping(
self : Face,
) -> Result[@common.CodepointMap, FaceTableError] {
let tag = @common.Tag::from_chars('c', 'm', 'a', 'p')
let blob = match self.reference_table(tag) {
Err(err) => return Err(err)
Ok(value) => value
}
let cmap = match @sfnt.CmapTable::parse(blob) {
Err(err) => return Err(TableError(err))
Ok(value) => value
}
match cmap.collect_nominal_mapping() {
Err(err) => Err(TableError(err))
Ok(map) => Ok(map)
}
}