// Copyright 2025 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.
///|
/// Axes of variation in a variable font.
///
/// Ported from `fontations/skrifa/src/variation.rs` (Apache-2.0 OR MIT).
const TAG_FVAR : UInt = 0x66766172 // "fvar"
///|
const TAG_AVAR : UInt = 0x61766172 // "avar"
///|
const F2DOT14_SCALE : Double = 16384.0
///|
fn variation_read_u16_be(view : BytesView, offset : Int) -> Int? {
if offset < 0 || offset + 2 > view.length() {
None
} else {
let b0 = view.at(offset).to_int()
let b1 = view.at(offset + 1).to_int()
Some((b0 << 8) | b1)
}
}
///|
fn variation_read_i16_be(view : BytesView, offset : Int) -> Int? {
match variation_read_u16_be(view, offset) {
None => None
Some(u) => if u >= 0x8000 { Some(u - 0x10000) } else { Some(u) }
}
}
///|
fn variation_read_u32_be(view : BytesView, offset : Int) -> UInt? {
if offset < 0 || offset + 4 > view.length() {
None
} else {
let b0 = view.at(offset).to_uint()
let b1 = view.at(offset + 1).to_uint()
let b2 = view.at(offset + 2).to_uint()
let b3 = view.at(offset + 3).to_uint()
Some((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)
}
}
///|
fn variation_read_i32_be(view : BytesView, offset : Int) -> Int? {
match variation_read_u32_be(view, offset) {
None => None
Some(u) => Some(u.reinterpret_as_int())
}
}
///|
fn variation_read_fixed_16_16(view : BytesView, offset : Int) -> Double? {
match variation_read_i32_be(view, offset) {
None => None
Some(bits) => Some(bits.to_double() / 65536.0)
}
}
///|
fn variation_clamp(x : Double, min : Double, max : Double) -> Double {
if x < min {
min
} else if x > max {
max
} else {
x
}
}
///|
fn variation_normalize_coord(
coord : Double,
min_value : Double,
default_value : Double,
max_value : Double,
) -> NormalizedCoord {
let c = variation_clamp(coord, min_value, max_value)
if c == default_value {
return 0
}
if c < default_value {
let denom = default_value - min_value
if denom == 0.0 {
0
} else {
let n = (c - default_value) / denom
(n * F2DOT14_SCALE).round().to_int()
}
} else {
let denom = max_value - default_value
if denom == 0.0 {
0
} else {
let n = (c - default_value) / denom
(n * F2DOT14_SCALE).round().to_int()
}
}
}
///|
priv struct AxisRecord {
tag : Tag
min_value : Double
default_value : Double
max_value : Double
flags : Int
name_id : StringId
}
///|
pub struct Axis {
priv index : Int
priv record : AxisRecord
}
///|
pub fn Axis::tag(self : Axis) -> Tag {
self.record.tag
}
///|
pub fn Axis::index(self : Axis) -> Int {
self.index
}
///|
pub fn Axis::name_id(self : Axis) -> StringId {
self.record.name_id
}
///|
pub fn Axis::is_hidden(self : Axis) -> Bool {
// fvar hidden axis flag: 0x0001
(self.record.flags & 0x1) != 0
}
///|
pub fn Axis::min_value(self : Axis) -> Double {
self.record.min_value
}
///|
pub fn Axis::default_value(self : Axis) -> Double {
self.record.default_value
}
///|
pub fn Axis::max_value(self : Axis) -> Double {
self.record.max_value
}
///|
pub fn Axis::normalize(self : Axis, coord : Double) -> NormalizedCoord {
variation_normalize_coord(
coord,
self.record.min_value,
self.record.default_value,
self.record.max_value,
)
}
///|
priv struct AvarAxisMap {
from_coords : Array[Int]
to_coords : Array[Int]
}
///|
priv struct AvarData {
axes : Array[AvarAxisMap]
}
///|
fn avar_apply(map : AvarAxisMap, coord : Int) -> Int {
let n = map.from_coords.length()
if n <= 0 || map.to_coords.length() != n {
return coord
}
let from0 = map.from_coords.at(0)
let to0 = map.to_coords.at(0)
if coord <= from0 {
return to0
}
let from_last = map.from_coords.at(n - 1)
let to_last = map.to_coords.at(n - 1)
if coord >= from_last {
return to_last
}
for i in 0..<(n - 1) {
let from_a = map.from_coords.at(i)
let from_b = map.from_coords.at(i + 1)
if coord >= from_a && coord <= from_b {
let to_a = map.to_coords.at(i)
let to_b = map.to_coords.at(i + 1)
let denom = from_b - from_a
if denom == 0 {
return to_a
}
let t = (coord - from_a).to_double() / denom.to_double()
return (to_a.to_double() + t * (to_b - to_a).to_double()).round().to_int()
}
}
coord
}
///|
fn avar_parse(table : BytesView, axis_count : Int) -> AvarData? {
// avar header: major u16, minor u16, axisCount u16, reserved u16
if table.length() < 8 {
return None
}
let major = variation_read_u16_be(table, 0).unwrap_or(-1)
let minor = variation_read_u16_be(table, 2).unwrap_or(-1)
if major != 1 || minor != 0 {
return None
}
let count = variation_read_u16_be(table, 4).unwrap_or(-1)
if count != axis_count || count < 0 {
return None
}
let mut off = 8
let axes : Array[AvarAxisMap] = Array::new()
for _ in 0.. table.length() {
return None
}
let map_count = variation_read_u16_be(table, off).unwrap_or(-1)
off = off + 2
if map_count <= 0 {
return None
}
let from_coords : Array[Int] = Array::new()
let to_coords : Array[Int] = Array::new()
for _ in 0.. AxisCollection {
let fvar = match font.table(TAG_FVAR) {
None => return { axes: Array::new(), instances: Array::new(), avar: None }
Some(v) => v
}
if fvar.length() < 16 {
return { axes: Array::new(), instances: Array::new(), avar: None }
}
let major = variation_read_u16_be(fvar, 0).unwrap_or(-1)
let minor = variation_read_u16_be(fvar, 2).unwrap_or(-1)
if major != 1 || minor != 0 {
return { axes: Array::new(), instances: Array::new(), avar: None }
}
let axes_offset = variation_read_u16_be(fvar, 4).unwrap_or(-1)
let axis_count = variation_read_u16_be(fvar, 8).unwrap_or(-1)
let axis_size = variation_read_u16_be(fvar, 10).unwrap_or(-1)
let instance_count = variation_read_u16_be(fvar, 12).unwrap_or(-1)
let instance_size = variation_read_u16_be(fvar, 14).unwrap_or(-1)
if axes_offset < 0 ||
axis_count < 0 ||
axis_size < 20 ||
instance_count < 0 ||
instance_size < 4 {
return { axes: Array::new(), instances: Array::new(), avar: None }
}
let axes : Array[AxisRecord] = Array::new()
let axis_count_i = axis_count
let axis_size_i = axis_size
let axes_end = axes_offset + axis_count_i * axis_size_i
if axes_end < axes_offset || axes_end > fvar.length() {
return { axes: Array::new(), instances: Array::new(), avar: None }
}
for i in 0.. fvar.length() {
return { axes, instances: Array::new(), avar: None }
}
let instances : Array[InstanceRecord] = Array::new()
for i in 0.. fvar.length() {
continue
}
let user_coords : Array[Double] = Array::new()
for j in 0.. None
Some(avar_tbl) => avar_parse(avar_tbl, axis_count_i)
}
{ axes, instances, avar }
}
///|
pub fn AxisCollection::len(self : AxisCollection) -> Int {
self.axes.length()
}
///|
pub fn AxisCollection::is_empty(self : AxisCollection) -> Bool {
self.axes.is_empty()
}
///|
pub fn AxisCollection::get(self : AxisCollection, index : Int) -> Axis? {
match self.axes.get(index) {
None => None
Some(r) => Some({ index, record: r })
}
}
///|
pub fn AxisCollection::get_by_tag(self : AxisCollection, tag : Tag) -> Axis? {
let n = self.axes.length()
for i in 0.. Location {
let n = self.axes.length()
let coords : Array[NormalizedCoord] = Array::make(n, 0)
for i in 0.. 0
Some(v) => axis.normalize(v)
}
let remapped = match self.avar {
None => c
Some(a) =>
if i < 0 || i >= a.axes.length() {
c
} else {
avar_apply(a.axes.at(i), c)
}
}
coords.set(i, remapped)
}
Location::from_array(coords)
}
///|
pub struct NamedInstance {
priv axes : AxisCollection
priv record : InstanceRecord
}
///|
pub fn NamedInstance::subfamily_name_id(self : NamedInstance) -> StringId {
self.record.subfamily_name_id
}
///|
pub fn NamedInstance::postscript_name_id(self : NamedInstance) -> StringId? {
self.record.postscript_name_id
}
///|
pub fn NamedInstance::user_coords(self : NamedInstance) -> ArrayView[Double] {
self.record.user_coords.op_as_view()
}
///|
pub fn NamedInstance::location(self : NamedInstance) -> Location {
let n = self.axes.len()
let coords : Array[NormalizedCoord] = Array::make(n, 0)
for i in 0.. c
Some(a) =>
if i < 0 || i >= a.axes.length() {
c
} else {
avar_apply(a.axes.at(i), c)
}
}
coords.set(i, remapped)
}
Location::from_array(coords)
}
///|
pub struct NamedInstanceCollection {
priv axes : AxisCollection
}
///|
pub fn NamedInstanceCollection::NamedInstanceCollection(
font : FontRef,
) -> NamedInstanceCollection {
{ axes: AxisCollection(font) }
}
///|
pub fn NamedInstanceCollection::len(self : NamedInstanceCollection) -> Int {
self.axes.instances.length()
}
///|
pub fn NamedInstanceCollection::is_empty(
self : NamedInstanceCollection,
) -> Bool {
self.len() == 0
}
///|
pub fn NamedInstanceCollection::get(
self : NamedInstanceCollection,
index : Int,
) -> NamedInstance? {
match self.axes.instances.get(index) {
None => None
Some(r) => Some({ axes: self.axes, record: r })
}
}
///|
pub fn FontRef::axes(self : FontRef) -> AxisCollection {
AxisCollection(self)
}
///|
pub fn FontRef::named_instances(self : FontRef) -> NamedInstanceCollection {
NamedInstanceCollection(self)
}