use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use crate::parse::Parser;
use crate::parse_ui::{UIFontFaceOwned, UIFontParser};
#[derive(serde::Serialize)]
pub struct WasmFontAnalysisResult {
pub success: bool,
pub family_name: String,
pub has_italic: bool,
pub has_upright: bool,
pub strategy: String,
pub scenario: String,
pub recipe_count: usize,
pub variable_font_info: Option<WasmVariableFontInfo>,
pub face_info: Vec<WasmFaceInfo>,
}
#[derive(serde::Serialize)]
pub struct WasmVariableFontInfo {
pub axes: Vec<WasmFontAxis>,
pub instances: Vec<WasmFontInstance>,
}
#[derive(serde::Serialize)]
pub struct WasmFontAxis {
pub tag: String,
pub name: String,
pub min: f32,
pub default: f32,
pub max: f32,
}
#[derive(serde::Serialize)]
pub struct WasmFontInstance {
pub name: String,
pub coordinates: HashMap<String, f32>,
}
#[derive(serde::Serialize)]
pub struct WasmFaceInfo {
pub face_id: String,
pub family_name: String,
pub subfamily_name: String,
pub postscript_name: String,
pub weight_class: u16,
pub width_class: u16,
pub is_variable: bool,
pub features: Vec<WasmFontFeature>,
}
#[derive(serde::Serialize)]
pub struct WasmFontFeature {
pub tag: String,
pub name: String,
pub tooltip: Option<String>,
pub sample_text: Option<String>,
}
#[derive(serde::Serialize)]
pub struct WasmFaceRecord {
pub face_id: String,
pub ps_name: String,
pub family_name: String,
pub subfamily_name: String,
pub is_variable: bool,
pub os2_italic_bit: bool,
pub weight_class: u16,
pub width_class: u16,
pub user_font_style_italic: Option<bool>,
pub axes_count: usize,
}
#[derive(serde::Serialize)]
pub struct WasmError {
pub error: bool,
pub message: String,
}
impl From<crate::parse_ui::UIFontFamilyResult> for WasmFontAnalysisResult {
fn from(result: crate::parse_ui::UIFontFamilyResult) -> Self {
Self {
success: true,
family_name: result.family_name,
has_italic: result.italic_capability.has_italic,
has_upright: result.italic_capability.has_upright,
strategy: format!("{:?}", result.italic_capability.strategy),
scenario: format!("{:?}", result.italic_capability.scenario),
recipe_count: result.italic_capability.recipes.len(),
variable_font_info: result.variable_font_info.map(Into::into),
face_info: result.face_info.into_iter().map(Into::into).collect(),
}
}
}
impl From<crate::parse_ui::UIFontVariableInfo> for WasmVariableFontInfo {
fn from(vfi: crate::parse_ui::UIFontVariableInfo) -> Self {
Self {
axes: vfi.axes.into_iter().map(Into::into).collect(),
instances: vfi.instances.into_iter().map(Into::into).collect(),
}
}
}
impl From<crate::parse_ui::UIFontAxis> for WasmFontAxis {
fn from(axis: crate::parse_ui::UIFontAxis) -> Self {
Self {
tag: axis.tag,
name: axis.name,
min: axis.min,
default: axis.default,
max: axis.max,
}
}
}
impl From<crate::parse_ui::UIFontInstance> for WasmFontInstance {
fn from(instance: crate::parse_ui::UIFontInstance) -> Self {
Self {
name: instance.name,
coordinates: instance.coordinates,
}
}
}
impl From<crate::parse_ui::UIFontFaceInfo> for WasmFaceInfo {
fn from(face: crate::parse_ui::UIFontFaceInfo) -> Self {
Self {
face_id: face.face_id,
family_name: face.family_name,
subfamily_name: face.subfamily_name,
postscript_name: face.postscript_name,
weight_class: face.weight_class,
width_class: face.width_class,
is_variable: face.is_variable,
features: face.features.into_iter().map(Into::into).collect(),
}
}
}
impl From<crate::parse_ui::UIFontFeature> for WasmFontFeature {
fn from(feature: crate::parse_ui::UIFontFeature) -> Self {
Self {
tag: feature.tag,
name: feature.name,
tooltip: feature.tooltip,
sample_text: feature.sample_text,
}
}
}
impl From<crate::selection::FaceRecord> for WasmFaceRecord {
fn from(record: crate::selection::FaceRecord) -> Self {
Self {
face_id: record.face_id,
ps_name: record.ps_name,
family_name: record.family_name,
subfamily_name: record.subfamily_name,
is_variable: record.is_variable,
os2_italic_bit: record.os2_italic_bit,
weight_class: record.weight_class,
width_class: record.width_class,
user_font_style_italic: record.user_font_style_italic,
axes_count: record.axes.len(),
}
}
}
unsafe fn convert_c_arrays_to_faces(
font_count: usize,
face_ids: *const *const c_char,
font_data_ptrs: *const *const u8,
font_data_sizes: *const usize,
user_italic_flags: *const i32,
) -> Result<Vec<UIFontFaceOwned>, String> {
if font_count == 0 {
return Err("font_count cannot be zero".to_string());
}
if face_ids.is_null() || font_data_ptrs.is_null() || font_data_sizes.is_null() {
return Err("face_ids, font_data_ptrs, and font_data_sizes cannot be null".to_string());
}
let mut faces = Vec::with_capacity(font_count);
for i in 0..font_count {
let face_id_ptr = *face_ids.add(i);
if face_id_ptr.is_null() {
return Err(format!("face_ids[{}] cannot be null", i));
}
let face_id = CStr::from_ptr(face_id_ptr).to_string_lossy().to_string();
let data_ptr = *font_data_ptrs.add(i);
let data_size = *font_data_sizes.add(i);
if data_ptr.is_null() || data_size == 0 {
return Err(format!("font_data[{}] cannot be null or empty", i));
}
let font_data = std::slice::from_raw_parts(data_ptr, data_size).to_vec();
let user_font_style_italic = if user_italic_flags.is_null() {
None
} else {
let flag = *user_italic_flags.add(i);
match flag {
-1 => None,
0 => Some(false),
1 => Some(true),
_ => return Err(format!("Invalid user_italic_flags[{}]: {}", i, flag)),
}
};
faces.push(UIFontFaceOwned {
face_id,
data: font_data,
user_font_style_italic,
});
}
Ok(faces)
}
#[no_mangle]
pub unsafe extern "C" fn grida_fonts_analyze_family(
family_name: *const c_char,
font_count: usize,
face_ids: *const *const c_char,
font_data_ptrs: *const *const u8,
font_data_sizes: *const usize,
user_italic_flags: *const i32, ) -> *mut c_char {
let result = (|| -> Result<String, String> {
let family_name = if family_name.is_null() {
None
} else {
let c_str = CStr::from_ptr(family_name);
Some(c_str.to_string_lossy().to_string())
};
let faces = convert_c_arrays_to_faces(
font_count,
face_ids,
font_data_ptrs,
font_data_sizes,
user_italic_flags,
)?;
let parser = UIFontParser::new();
let result = parser.analyze_family_owned(family_name, faces)?;
let response = WasmFontAnalysisResult::from(result);
serde_json::to_string(&response).map_err(|e| format!("Failed to serialize result: {}", e))
})();
match result {
Ok(json) => CString::new(json).unwrap().into_raw(),
Err(error) => {
let error_response = WasmError {
error: true,
message: error,
};
CString::new(serde_json::to_string(&error_response).unwrap())
.unwrap()
.into_raw()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn grida_fonts_parse_font(
font_data_ptr: *const u8,
font_data_size: usize,
face_id: *const c_char,
user_font_style_italic: *const c_char,
) -> *mut c_char {
let result = (|| -> Result<String, String> {
if font_data_ptr.is_null() || font_data_size == 0 {
return Err("font_data cannot be null or empty".to_string());
}
let font_data = std::slice::from_raw_parts(font_data_ptr, font_data_size).to_vec();
let face_id_str = CStr::from_ptr(face_id).to_string_lossy().to_string();
let user_font_style_italic = if user_font_style_italic.is_null() {
None
} else {
let style_str = CStr::from_ptr(user_font_style_italic)
.to_string_lossy()
.to_string();
match style_str.as_str() {
"true" => Some(true),
"false" => Some(false),
_ => {
return Err(format!(
"Invalid user_font_style_italic: {}. Must be 'true' or 'false'",
style_str
))
}
}
};
let parser = Parser::new(&font_data).map_err(|e| format!("Failed to parse font: {}", e))?;
let face_record = parser.extract_face_record(face_id_str, user_font_style_italic)?;
let response = WasmFaceRecord::from(face_record);
serde_json::to_string(&response).map_err(|e| format!("Failed to serialize result: {}", e))
})();
match result {
Ok(json) => CString::new(json).unwrap().into_raw(),
Err(error) => {
let error_response = WasmError {
error: true,
message: error,
};
CString::new(serde_json::to_string(&error_response).unwrap())
.unwrap()
.into_raw()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn grida_fonts_free(ptr: *mut c_char) {
if !ptr.is_null() {
let _ = CString::from_raw(ptr);
}
}
#[no_mangle]
pub unsafe extern "C" fn grida_fonts_version() -> *mut c_char {
let version = env!("CARGO_PKG_VERSION");
CString::new(version).unwrap().into_raw()
}