// 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.
///|
/// Basic font attributes: stretch, weight and style.
///
/// Ported from `swash/src/attributes.rs` (swash is dual-licensed Apache-2.0 OR MIT).
///|
/// Variations that apply to attributes.
const WDTH : Tag = 0x77647468 // "wdth"
///|
const WGHT : Tag = 0x77676874 // "wght"
///|
const SLNT : Tag = 0x736C6E74 // "slnt"
///|
const ITAL : Tag = 0x6974616C // "ital"
///|
/// Primary attributes for font classification: stretch, weight and style.
///
/// This struct is created by the `attributes()` method on `FontRef`.
pub struct Attributes {
value : UInt
}
///|
pub fn Attributes::new(
stretch : Stretch,
weight : Weight,
style : Style,
) -> Attributes {
let stretch_u = stretch.value.to_int().reinterpret_as_uint() & 0x1FF
let weight_u = weight.value.to_int().reinterpret_as_uint() & 0x3FF
let style_u = style.pack()
Attributes::{ value: style_u | (weight_u << 9) | (stretch_u << 19) }
}
///|
/// Extracts the attributes from the specified font.
pub fn Attributes::from_font(font : FontRef) -> Attributes {
let attrs = Attributes::from_os2(font.os2())
let mut var_bits : UInt = 0
for v in font.variations().iter() {
match v.tag() {
WDTH => var_bits = var_bits | 1
WGHT => var_bits = var_bits | 2
SLNT => var_bits = var_bits | 4
ITAL => var_bits = var_bits | 8
_ => ()
}
}
Attributes::{ value: attrs.value | (var_bits << 28) }
}
///|
fn Attributes::from_os2(os2 : @internal.Os2?) -> Attributes {
match os2 {
None => Attributes::default()
Some(os2) => {
let flags = os2.selection_flags()
let style = if flags.italic() {
Style::Italic
} else if flags.oblique() {
Style::Oblique(ObliqueAngle::default())
} else {
Style::Normal
}
let weight = Weight::{ value: os2.weight_class().to_uint16() }
let stretch = Stretch::from_raw(os2.width_class().to_uint16())
Attributes::new(stretch, weight, style)
}
}
}
///|
pub fn Attributes::stretch(self : Attributes) -> Stretch {
Stretch::{ value: ((self.value >> 19) & 0x1FF).to_uint16() }
}
///|
pub fn Attributes::weight(self : Attributes) -> Weight {
Weight::{ value: ((self.value >> 9) & 0x3FF).to_uint16() }
}
///|
pub fn Attributes::style(self : Attributes) -> Style {
Style::unpack(self.value & 0x1FF)
}
///|
pub fn Attributes::parts(self : Attributes) -> (Stretch, Weight, Style) {
(self.stretch(), self.weight(), self.style())
}
///|
pub fn Attributes::has_variations(self : Attributes) -> Bool {
self.value >> 28 != 0
}
///|
pub fn Attributes::has_stretch_variation(self : Attributes) -> Bool {
let var_bits = self.value >> 28
(var_bits & 1) != 0
}
///|
pub fn Attributes::has_weight_variation(self : Attributes) -> Bool {
let var_bits = self.value >> 28
(var_bits & 2) != 0
}
///|
pub fn Attributes::has_oblique_variation(self : Attributes) -> Bool {
let var_bits = self.value >> 28
(var_bits & 4) != 0
}
///|
pub fn Attributes::has_italic_variation(self : Attributes) -> Bool {
let var_bits = self.value >> 28
(var_bits & 8) != 0
}
///|
/// Returns a synthesis analysis based on the requested attributes with
/// respect to this set of attributes.
pub fn Attributes::synthesize(
self : Attributes,
requested : Attributes,
) -> Synthesis {
if self.value << 4 == requested.value << 4 {
return Synthesis::default()
}
let vars : Array[VariationSetting] = []
if self.has_stretch_variation() {
let stretch = self.stretch()
let req_stretch = requested.stretch()
if stretch != req_stretch {
vars.push(Setting::new(WDTH, req_stretch.to_percentage()))
}
}
let weight = self.weight()
let req_weight = requested.weight()
let mut embolden = false
if weight != req_weight {
if self.has_weight_variation() {
if vars.length() < 4 {
vars.push(Setting::new(WGHT, req_weight.value.to_int().to_double()))
}
} else if req_weight > weight {
embolden = true
}
}
let style = self.style()
let req_style = requested.style()
let mut skew : Int = 0
if style != req_style {
match req_style {
Style::Normal => ()
Style::Italic =>
if style == Style::Normal {
if self.has_italic_variation() {
if vars.length() < 4 {
vars.push(Setting::new(ITAL, 1.0))
}
} else if self.has_oblique_variation() {
if vars.length() < 4 {
vars.push(Setting::new(SLNT, 14.0))
}
} else {
skew = 14
}
}
Style::Oblique(angle) =>
if style == Style::Normal {
let degrees = angle.to_degrees()
if self.has_oblique_variation() {
if vars.length() < 4 {
vars.push(Setting::new(SLNT, degrees))
}
} else if self.has_italic_variation() && degrees > 0.0 {
if vars.length() < 4 {
vars.push(Setting::new(ITAL, 1.0))
}
} else {
skew = degrees.to_int()
}
}
}
}
Synthesis::{ vars, embolden, skew }
}
///|
fn Attributes::default() -> Attributes {
Attributes::new(Stretch::normal(), Weight::normal(), Style::Normal)
}
///|
pub impl Eq for Attributes with equal(self, other) {
self.value << 4 == other.value << 4
}
///|
pub impl Show for Attributes with output(self, logger) {
let (stretch, weight, style) = self.parts()
if style == Style::Normal &&
weight == Weight::normal() &&
stretch == Stretch::normal() {
logger.write_string("regular")
return
}
let mut space = ""
if stretch != Stretch::normal() {
logger.write_string(stretch.to_string())
space = " "
}
if style != Style::Normal {
logger.write_string(space)
logger.write_string(style.to_string())
space = " "
}
if weight != Weight::normal() {
logger.write_string(space)
logger.write_string(weight.to_string())
}
}
///|
/// Angle of an oblique style in degrees from -90 to 90.
pub struct ObliqueAngle {
value : UInt16
}
///|
fn clamp_double(x : Double, lo : Double, hi : Double) -> Double {
if x < lo {
lo
} else if x > hi {
hi
} else {
x
}
}
///|
fn slice_or_none(s : StringView, start : Int, end : Int) -> StringView? {
try s[start:end] catch {
_ => None
} noraise {
v => Some(v)
}
}
///|
fn slice_from_or_none(s : StringView, start : Int) -> StringView? {
try s[start:] catch {
_ => None
} noraise {
v => Some(v)
}
}
///|
pub fn ObliqueAngle::from_degrees(degrees : Double) -> ObliqueAngle {
let clamped = clamp_double(degrees, -90.0, 90.0) + 90.0
ObliqueAngle::{ value: clamped.to_int().to_uint16() }
}
///|
pub fn ObliqueAngle::from_radians(radians : Double) -> ObliqueAngle {
ObliqueAngle::from_degrees(radians * 180.0 / 3.141592653589793)
}
///|
pub fn ObliqueAngle::from_gradians(gradians : Double) -> ObliqueAngle {
ObliqueAngle::from_degrees(gradians / 400.0 * 360.0)
}
///|
pub fn ObliqueAngle::from_turns(turns : Double) -> ObliqueAngle {
ObliqueAngle::from_degrees(turns * 360.0)
}
///|
pub fn ObliqueAngle::to_degrees(self : ObliqueAngle) -> Double {
self.value.to_int().to_double() - 90.0
}
///|
fn ObliqueAngle::default() -> ObliqueAngle {
ObliqueAngle::from_degrees(14.0)
}
///|
/// Visual style or 'slope' of a font.
pub(all) enum Style {
Normal
Italic
Oblique(ObliqueAngle)
}
///|
pub impl Eq for Style with equal(self, other) {
match (self, other) {
(Style::Normal, Style::Normal) => true
(Style::Italic, Style::Italic) => true
(Style::Oblique(a), Style::Oblique(b)) => a.value == b.value
_ => false
}
}
///|
pub fn Style::parse(s0 : String) -> Style? {
let s = s0.trim()
if s == "normal" {
Some(Style::Normal)
} else if s == "italic" {
Some(Style::Italic)
} else if s == "oblique" {
Some(Style::Oblique(ObliqueAngle::from_degrees(14.0)))
} else if s.has_prefix("oblique ") {
let rest = match slice_from_or_none(s, 8) {
None => return Some(Style::Oblique(ObliqueAngle::default()))
Some(v) => v
}
if rest.has_suffix("deg") {
let a_sv = match slice_or_none(rest, 0, rest.length() - 3) {
None => return Some(Style::Oblique(ObliqueAngle::default()))
Some(v) => v
}
try @strconv.parse_double(a_sv.trim()) catch {
_ => Some(Style::Oblique(ObliqueAngle::default()))
} noraise {
a => Some(Style::Oblique(ObliqueAngle::from_degrees(a)))
}
} else if rest.has_suffix("grad") {
let a_sv = match slice_or_none(rest, 0, rest.length() - 4) {
None => return Some(Style::Oblique(ObliqueAngle::default()))
Some(v) => v
}
try @strconv.parse_double(a_sv.trim()) catch {
_ => Some(Style::Oblique(ObliqueAngle::default()))
} noraise {
a => Some(Style::Oblique(ObliqueAngle::from_gradians(a)))
}
} else if rest.has_suffix("rad") {
let a_sv = match slice_or_none(rest, 0, rest.length() - 3) {
None => return Some(Style::Oblique(ObliqueAngle::default()))
Some(v) => v
}
try @strconv.parse_double(a_sv.trim()) catch {
_ => Some(Style::Oblique(ObliqueAngle::default()))
} noraise {
a => Some(Style::Oblique(ObliqueAngle::from_radians(a)))
}
} else if rest.has_suffix("turn") {
let a_sv = match slice_or_none(rest, 0, rest.length() - 4) {
None => return Some(Style::Oblique(ObliqueAngle::default()))
Some(v) => v
}
try @strconv.parse_double(a_sv.trim()) catch {
_ => Some(Style::Oblique(ObliqueAngle::default()))
} noraise {
a => Some(Style::Oblique(ObliqueAngle::from_turns(a)))
}
} else {
Some(Style::Oblique(ObliqueAngle::default()))
}
} else {
None
}
}
///|
pub fn Style::from_degrees(degrees : Double) -> Style {
Style::Oblique(ObliqueAngle::from_degrees(degrees))
}
///|
pub fn Style::to_degrees(self : Style) -> Double {
match self {
Style::Italic => 14.0
Style::Oblique(a) => a.to_degrees()
_ => 0.0
}
}
///|
fn Style::unpack(bits : UInt) -> Style {
if (bits & 1) != 0 {
Style::Oblique(ObliqueAngle::{ value: (bits >> 1).to_uint16() })
} else if bits == 0b110 {
Style::Italic
} else {
Style::Normal
}
}
///|
fn Style::pack(self : Style) -> UInt {
match self {
Style::Normal => 0b10
Style::Italic => 0b110
Style::Oblique(a) => 1 | (a.value.to_int().reinterpret_as_uint() << 1)
}
}
///|
pub fn Style::to_string(self : Style) -> String {
match self {
Style::Normal => "normal"
Style::Italic => "italic"
Style::Oblique(a) => {
let degrees = a.to_degrees()
if degrees == 14.0 {
"oblique"
} else {
"oblique(" + degrees.to_string() + "deg)"
}
}
}
}
///|
pub impl Show for Style with output(self, logger) {
logger.write_string(self.to_string())
}
///|
/// Visual weight class of a font on a scale from 1 to 1000.
pub struct Weight {
value : UInt16
}
///|
pub fn Weight::thin() -> Weight {
Weight::{ value: (100).to_uint16() }
}
///|
pub fn Weight::extra_light() -> Weight {
Weight::{ value: (200).to_uint16() }
}
///|
pub fn Weight::light() -> Weight {
Weight::{ value: (300).to_uint16() }
}
///|
pub fn Weight::normal() -> Weight {
Weight::{ value: (400).to_uint16() }
}
///|
pub fn Weight::medium() -> Weight {
Weight::{ value: (500).to_uint16() }
}
///|
pub fn Weight::semi_bold() -> Weight {
Weight::{ value: (600).to_uint16() }
}
///|
pub fn Weight::bold() -> Weight {
Weight::{ value: (700).to_uint16() }
}
///|
pub fn Weight::extra_bold() -> Weight {
Weight::{ value: (800).to_uint16() }
}
///|
pub fn Weight::black() -> Weight {
Weight::{ value: (900).to_uint16() }
}
///|
pub fn Weight::parse(s0 : String) -> Weight? {
let s = s0.trim()
if s == "normal" {
Some(Weight::normal())
} else if s == "bold" {
Some(Weight::bold())
} else {
try @strconv.parse_int(s, base=10) catch {
_ => None
} noraise {
v => Some(Weight::{ value: v.max(1).min(1000).to_uint16() })
}
}
}
///|
pub fn Weight::to_string(self : Weight) -> String {
if self == Weight::thin() {
"thin"
} else if self == Weight::extra_light() {
"extra-light"
} else if self == Weight::light() {
"light"
} else if self == Weight::normal() {
"normal"
} else if self == Weight::medium() {
"medium"
} else if self == Weight::semi_bold() {
"semi-bold"
} else if self == Weight::bold() {
"bold"
} else if self == Weight::extra_bold() {
"extra-bold"
} else if self == Weight::black() {
"black"
} else {
self.value.to_string()
}
}
///|
pub impl Show for Weight with output(self, logger) {
logger.write_string(self.to_string())
}
///|
pub impl Eq for Weight with equal(self, other) {
self.value == other.value
}
///|
pub impl Compare for Weight with compare(self, other) {
self.value.to_int() - other.value.to_int()
}
///|
///|
/// Visual width of a font-- a relative change from the normal aspect ratio.
pub struct Stretch {
value : UInt16
}
///|
pub fn Stretch::ultra_condensed() -> Stretch {
Stretch::{ value: (0).to_uint16() }
}
///|
pub fn Stretch::extra_condensed() -> Stretch {
Stretch::{ value: (25).to_uint16() }
}
///|
pub fn Stretch::condensed() -> Stretch {
Stretch::{ value: (50).to_uint16() }
}
///|
pub fn Stretch::semi_condensed() -> Stretch {
Stretch::{ value: (75).to_uint16() }
}
///|
pub fn Stretch::normal() -> Stretch {
Stretch::{ value: (100).to_uint16() }
}
///|
pub fn Stretch::semi_expanded() -> Stretch {
Stretch::{ value: (125).to_uint16() }
}
///|
pub fn Stretch::expanded() -> Stretch {
Stretch::{ value: (150).to_uint16() }
}
///|
pub fn Stretch::extra_expanded() -> Stretch {
Stretch::{ value: (200).to_uint16() }
}
///|
pub fn Stretch::ultra_expanded() -> Stretch {
Stretch::{ value: (300).to_uint16() }
}
///|
pub fn Stretch::from_percentage(percentage : Double) -> Stretch {
let p = clamp_double(percentage, 50.0, 200.0)
let value = ((p - 50.0) * 2.0).to_int().to_uint16()
Stretch::{ value, }
}
///|
pub fn Stretch::to_percentage(self : Stretch) -> Double {
self.value.to_int().to_double() * 0.5 + 50.0
}
///|
pub fn Stretch::is_normal(self : Stretch) -> Bool {
self == Stretch::normal()
}
///|
pub fn Stretch::is_condensed(self : Stretch) -> Bool {
self < Stretch::normal()
}
///|
pub fn Stretch::is_expanded(self : Stretch) -> Bool {
self > Stretch::normal()
}
///|
pub fn Stretch::parse(s0 : String) -> Stretch? {
let s = s0.trim()
if s == "ultra-condensed" {
Some(Stretch::ultra_condensed())
} else if s == "extra-condensed" {
Some(Stretch::extra_condensed())
} else if s == "condensed" {
Some(Stretch::condensed())
} else if s == "semi-condensed" {
Some(Stretch::semi_condensed())
} else if s == "normal" {
Some(Stretch::normal())
} else if s == "semi-expanded" {
Some(Stretch::semi_expanded())
} else if s == "expanded" {
Some(Stretch::expanded())
} else if s == "extra-expanded" {
Some(Stretch::extra_expanded())
} else if s == "ultra-expanded" {
Some(Stretch::ultra_expanded())
} else if s.has_suffix("%") {
let p_sv = match slice_or_none(s, 0, s.length() - 1) {
None => return None
Some(v) => v
}
try @strconv.parse_double(p_sv.trim()) catch {
_ => None
} noraise {
p => Some(Stretch::from_percentage(p))
}
} else {
None
}
}
///|
pub fn Stretch::raw(self : Stretch) -> UInt16 {
self.value
}
///|
fn Stretch::from_raw(raw : UInt16) -> Stretch {
match raw {
1 => Stretch::ultra_condensed()
2 => Stretch::extra_condensed()
3 => Stretch::condensed()
4 => Stretch::semi_condensed()
5 => Stretch::normal()
6 => Stretch::semi_expanded()
7 => Stretch::expanded()
8 => Stretch::extra_expanded()
9 => Stretch::ultra_expanded()
_ => Stretch::normal()
}
}
///|
pub fn Stretch::to_string(self : Stretch) -> String {
if self == Stretch::ultra_condensed() {
"ultra-condensed"
} else if self == Stretch::extra_condensed() {
"extra-condensed"
} else if self == Stretch::condensed() {
"condensed"
} else if self == Stretch::semi_condensed() {
"semi-condensed"
} else if self == Stretch::normal() {
"normal"
} else if self == Stretch::semi_expanded() {
"semi-expanded"
} else if self == Stretch::expanded() {
"expanded"
} else if self == Stretch::extra_expanded() {
"extra-expanded"
} else if self == Stretch::ultra_expanded() {
"ultra-expanded"
} else {
self.to_percentage().to_string() + "%"
}
}
///|
pub impl Show for Stretch with output(self, logger) {
logger.write_string(self.to_string())
}
///|
pub impl Eq for Stretch with equal(self, other) {
self.value == other.value
}
///|
pub impl Compare for Stretch with compare(self, other) {
self.value.to_int() - other.value.to_int()
}
///|
///|
/// Synthesis suggestions for mismatched font attributes.
///
/// This is generated by `Attributes::synthesize`.
pub struct Synthesis {
vars : Array[VariationSetting]
embolden : Bool
skew : Int
}
///|
fn Synthesis::default() -> Synthesis {
Synthesis::{ vars: [], embolden: false, skew: 0 }
}
///|
pub fn Synthesis::any(self : Synthesis) -> Bool {
self.vars.length() != 0 || self.embolden || self.skew != 0
}
///|
pub fn Synthesis::variations(self : Synthesis) -> ArrayView[VariationSetting] {
self.vars.op_as_view()
}
///|
pub fn Synthesis::embolden(self : Synthesis) -> Bool {
self.embolden
}
///|
pub fn Synthesis::skew(self : Synthesis) -> Double? {
if self.skew != 0 {
Some(self.skew.to_double())
} else {
None
}
}
///|
pub impl Eq for Synthesis with equal(self, other) {
if self.embolden != other.embolden || self.skew != other.skew {
return false
}
if self.vars.length() != other.vars.length() {
return false
}
for i in 0.. Attributes {
Attributes::from_font(self)
}