use mlua::{Lua, Table, Value};
#[derive(Debug, Default, PartialEq)]
struct Info {
fontname: String,
familyname: String,
fullname: String,
version: String,
weight: String,
italicangle: f64,
units_per_em: u16,
}
fn weight_name(class: u16) -> &'static str {
match class {
0..=150 => "Thin",
151..=250 => "Extra-Light",
251..=350 => "Light",
351..=450 => "Book",
451..=550 => "Medium",
551..=650 => "Demi-Bold",
651..=750 => "Bold",
751..=850 => "Heavy",
_ => "Black",
}
}
fn opentype_info(data: &[u8]) -> Option<Info> {
let font = retex_font::font::OpenTypeFont::parse(data, 0).ok()?;
let name = |id| font.name(id).unwrap_or_default().to_owned();
let version = name(5);
Some(Info {
fontname: name(6),
familyname: name(1),
fullname: name(4),
version: version
.strip_prefix("Version ")
.unwrap_or(&version)
.to_owned(),
weight: weight_name(font.os2.map_or(400, |o| o.weight_class)).to_owned(),
italicangle: font.post.as_ref().map_or(0.0, |p| p.italic_angle_degrees()),
units_per_em: font.units_per_em(),
})
}
fn type1_cleartext(data: &[u8]) -> &[u8] {
if data.first() == Some(&0x80) && data.len() >= 6 {
let len = u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize;
return data.get(6..6 + len).unwrap_or(&data[6..]);
}
let end = data
.windows(5)
.position(|w| w == b"eexec")
.unwrap_or(data.len());
&data[..end]
}
fn type1_info(data: &[u8]) -> Option<Info> {
let text = String::from_utf8_lossy(type1_cleartext(data));
if !text.starts_with("%!") {
return None;
}
let value = |key: &str| -> Option<String> {
let at = text.find(&format!("/{key} "))? + key.len() + 2;
let rest = text[at..].trim_start();
if let Some(s) = rest.strip_prefix('(') {
return s.find(')').map(|e| s[..e].to_owned());
}
let rest = rest.strip_prefix('/').unwrap_or(rest);
let end = rest
.find(|c: char| c.is_whitespace() || "/[{(".contains(c))
.unwrap_or(rest.len());
Some(rest[..end].to_owned())
};
Some(Info {
fontname: value("FontName")?,
familyname: value("FamilyName").unwrap_or_default(),
fullname: value("FullName").unwrap_or_default(),
version: value("version").unwrap_or_default(),
weight: value("Weight").unwrap_or_default(),
italicangle: value("ItalicAngle")
.and_then(|v| v.parse().ok())
.unwrap_or(0.0),
units_per_em: 0,
})
}
fn info(data: &[u8]) -> Option<Info> {
opentype_info(data).or_else(|| type1_info(data))
}
fn to_table(lua: &Lua, i: Info) -> mlua::Result<Table> {
let t = lua.create_table()?;
t.set("fontname", i.fontname)?;
t.set("familyname", i.familyname)?;
t.set("fullname", i.fullname)?;
t.set("version", i.version)?;
t.set("weight", i.weight)?;
t.set("italicangle", i.italicangle)?;
t.set("units_per_em", i.units_per_em)?;
Ok(t)
}
pub fn install(lua: &Lua) -> mlua::Result<()> {
let t = lua.create_table()?;
t.set(
"info",
lua.create_function(|lua, name: String| {
let fail = |why: &str| {
Ok((
Value::Nil,
Value::String(
lua.create_string(format!("font loading failed for {name} ({why})"))?,
),
))
};
let Ok(data) = std::fs::read(&name) else {
return fail("read error");
};
match info(&data) {
Some(i) => Ok((Value::Table(to_table(lua, i)?), Value::Nil)),
None => fail("no font"),
}
})?,
)?;
lua.globals().set("fontloader", t)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type1_dictionary() {
let pfa = b"%!PS-AdobeFont-1.0: CMR10 003.002\n/FontInfo 7 dict dup begin\n\
/version (003.002) readonly def\n/FullName (CMR10) readonly def\n\
/FamilyName (Computer Modern) readonly def\n/Weight (Medium) readonly def\n\
/ItalicAngle 0 def\nend readonly def\n/FontName /CMR10 def\ncurrentfile eexec";
let i = info(pfa).unwrap();
assert_eq!(
(
&*i.fontname,
&*i.familyname,
&*i.fullname,
&*i.version,
&*i.weight
),
("CMR10", "Computer Modern", "CMR10", "003.002", "Medium")
);
}
#[test]
fn weights() {
assert_eq!(weight_name(400), "Book");
assert_eq!(weight_name(700), "Bold");
}
}