// 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.
///|
/// Color palettes (OpenType `CPAL` table) MVP.
///
/// Ported from `fontations/skrifa/src/color/*` (Apache-2.0 OR MIT).
const COLOR_TAG_CPAL : UInt = 0x4350414C // "CPAL"
///|
const COLOR_CPAL_VERSION_1 : Int = 1
///|
const COLOR_CPAL_PALETTE_USABLE_WITH_LIGHT_BACKGROUND : UInt = 0x00000001
///|
const COLOR_CPAL_PALETTE_USABLE_WITH_DARK_BACKGROUND : UInt = 0x00000002
///|
fn color_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 color_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 color_read_u32_be_int(view : BytesView, offset : Int) -> Int? {
match color_read_u32_be(view, offset) {
None => None
Some(u) => Some(u.to_uint64().to_int())
}
}
///|
pub struct Color {
r : Byte
g : Byte
b : Byte
a : Byte
}
///|
pub fn Color::r(self : Color) -> Byte {
self.r
}
///|
pub fn Color::g(self : Color) -> Byte {
self.g
}
///|
pub fn Color::b(self : Color) -> Byte {
self.b
}
///|
pub fn Color::a(self : Color) -> Byte {
self.a
}
///|
pub struct ColorPaletteType {
bits : UInt
}
///|
pub fn ColorPaletteType::raw(self : ColorPaletteType) -> UInt {
self.bits
}
///|
pub fn ColorPaletteType::usable_with_light_background(
self : ColorPaletteType,
) -> Bool {
(self.bits & COLOR_CPAL_PALETTE_USABLE_WITH_LIGHT_BACKGROUND) != 0
}
///|
pub fn ColorPaletteType::usable_with_dark_background(
self : ColorPaletteType,
) -> Bool {
(self.bits & COLOR_CPAL_PALETTE_USABLE_WITH_DARK_BACKGROUND) != 0
}
///|
pub struct ColorPalettes {
priv cpal : BytesView?
priv version : Int
priv num_entries : Int
priv num_palettes : Int
priv num_records : Int
priv color_records_off : Int
priv indices_off : Int
priv palette_types_off : Int?
priv palette_labels_off : Int?
priv palette_entry_labels_off : Int?
}
///|
pub struct ColorPalette {
priv palettes : ColorPalettes
priv index : Int
}
///|
fn ColorPalettes::empty() -> ColorPalettes {
{
cpal: None,
version: 0,
num_entries: 0,
num_palettes: 0,
num_records: 0,
color_records_off: 0,
indices_off: 0,
palette_types_off: None,
palette_labels_off: None,
palette_entry_labels_off: None,
}
}
///|
pub fn ColorPalettes::ColorPalettes(font : FontRef) -> ColorPalettes {
match font.table(COLOR_TAG_CPAL) {
None => ColorPalettes::empty()
Some(cpal) => {
if cpal.length() < 12 {
return ColorPalettes::empty()
}
let version = color_read_u16_be(cpal, 0).unwrap_or(-1)
if version < 0 || version > COLOR_CPAL_VERSION_1 {
return ColorPalettes::empty()
}
let num_entries = color_read_u16_be(cpal, 2).unwrap_or(-1)
let num_palettes = color_read_u16_be(cpal, 4).unwrap_or(-1)
let num_records = color_read_u16_be(cpal, 6).unwrap_or(-1)
let records_off = color_read_u32_be_int(cpal, 8).unwrap_or(-1)
if num_entries < 0 ||
num_palettes < 0 ||
num_records < 0 ||
records_off < 0 {
return ColorPalettes::empty()
}
let indices_off = if version >= COLOR_CPAL_VERSION_1 { 24 } else { 12 }
if indices_off > cpal.length() {
return ColorPalettes::empty()
}
let indices_end = indices_off + num_palettes * 2
let records_end = records_off + num_records * 4
if indices_end < indices_off || indices_end > cpal.length() {
return ColorPalettes::empty()
}
if records_end < records_off || records_end > cpal.length() {
return ColorPalettes::empty()
}
let mut palette_types_off : Int? = None
let mut palette_labels_off : Int? = None
let mut palette_entry_labels_off : Int? = None
if version >= COLOR_CPAL_VERSION_1 {
if cpal.length() < 24 {
return ColorPalettes::empty()
}
let types_off = color_read_u32_be_int(cpal, 12).unwrap_or(0)
let labels_off = color_read_u32_be_int(cpal, 16).unwrap_or(0)
let entry_labels_off = color_read_u32_be_int(cpal, 20).unwrap_or(0)
if types_off > 0 {
let end = types_off + num_palettes * 4
if end >= types_off && end <= cpal.length() {
palette_types_off = Some(types_off)
}
}
if labels_off > 0 {
let end = labels_off + num_palettes * 2
if end >= labels_off && end <= cpal.length() {
palette_labels_off = Some(labels_off)
}
}
if entry_labels_off > 0 {
let end = entry_labels_off + num_entries * 2
if end >= entry_labels_off && end <= cpal.length() {
palette_entry_labels_off = Some(entry_labels_off)
}
}
}
{
cpal: Some(cpal),
version,
num_entries,
num_palettes,
num_records,
color_records_off: records_off,
indices_off,
palette_types_off,
palette_labels_off,
palette_entry_labels_off,
}
}
}
}
///|
pub fn ColorPalettes::len(self : ColorPalettes) -> Int {
self.num_palettes
}
///|
pub fn ColorPalettes::version(self : ColorPalettes) -> Int {
self.version
}
///|
pub fn ColorPalettes::is_empty(self : ColorPalettes) -> Bool {
self.num_palettes == 0
}
///|
pub fn ColorPalettes::entries_per_palette(self : ColorPalettes) -> Int {
self.num_entries
}
///|
fn ColorPalettes::palette_start_index(
self : ColorPalettes,
palette : Int,
) -> Int? {
let cpal = match self.cpal {
None => return None
Some(v) => v
}
if palette < 0 || palette >= self.num_palettes {
return None
}
color_read_u16_be(cpal, self.indices_off + palette * 2)
}
///|
pub fn ColorPalettes::color(
self : ColorPalettes,
palette : Int,
entry : Int,
) -> Color? {
let cpal = match self.cpal {
None => return None
Some(v) => v
}
if entry < 0 || entry >= self.num_entries {
return None
}
let start_index = match self.palette_start_index(palette) {
None => return None
Some(v) => v
}
if start_index < 0 {
return None
}
let rec = start_index + entry
if rec < 0 || rec >= self.num_records {
return None
}
let pos = self.color_records_off + rec * 4
let b = cpal.at(pos)
let g = cpal.at(pos + 1)
let r = cpal.at(pos + 2)
let a = cpal.at(pos + 3)
Some({ r, g, b, a })
}
///|
pub fn ColorPalettes::get(self : ColorPalettes, index : Int) -> ColorPalette? {
let start_index = match self.palette_start_index(index) {
None => return None
Some(v) => v
}
if start_index < 0 || start_index + self.num_entries > self.num_records {
return None
}
Some({ palettes: self, index })
}
///|
pub fn ColorPalettes::color_label(
self : ColorPalettes,
color_index : Int,
) -> StringId? {
let cpal = match self.cpal {
None => return None
Some(v) => v
}
let off = match self.palette_entry_labels_off {
None => return None
Some(v) => v
}
if color_index < 0 || color_index >= self.num_entries {
return None
}
let name_id = color_read_u16_be(cpal, off + color_index * 2).unwrap_or(-1)
if name_id < 0 || name_id == 0xFFFF {
None
} else {
Some(name_id.to_uint16())
}
}
///|
pub fn ColorPalette::index(self : ColorPalette) -> Int {
self.index
}
///|
pub fn ColorPalette::colors(self : ColorPalette) -> Array[Color] {
let count = self.palettes.entries_per_palette()
let out : Array[Color] = Array::new(capacity=count)
for i in 0.. Color? {
self.palettes.color(self.index, entry)
}
///|
pub fn ColorPalette::palette_type(self : ColorPalette) -> ColorPaletteType? {
let cpal = match self.palettes.cpal {
None => return None
Some(v) => v
}
let off = match self.palettes.palette_types_off {
None => return None
Some(v) => v
}
let raw = color_read_u32_be(cpal, off + self.index * 4).unwrap_or(0)
Some({ bits: raw })
}
///|
pub fn ColorPalette::label(self : ColorPalette) -> StringId? {
let cpal = match self.palettes.cpal {
None => return None
Some(v) => v
}
let off = match self.palettes.palette_labels_off {
None => return None
Some(v) => v
}
let name_id = color_read_u16_be(cpal, off + self.index * 2).unwrap_or(-1)
if name_id < 0 || name_id == 0xFFFF {
None
} else {
Some(name_id.to_uint16())
}
}
///|
pub fn FontRef::color_palettes(self : FontRef) -> ColorPalettes {
ColorPalettes(self)
}