mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 03:28:22 -06:00
Settings: 56 of 59 not-yet-wired settings now change behaviour (nested-comp switches, constant vertex count, related-item time sync, match-name pick whip, split-layer order, Home on open, motion path span, internal wireframes, zoom quality, label-coloured tabs, mask colour cycling, gradients, grid/guide styles, text engine, font preview/recent/native names, missing frames, unlabeled alpha, drag import, default output folder, segmenting, bit depth names, preview sample rate, disk/media/conformed-audio caches, RAM reserve and low-memory reduction, Video Preview window, 3D reference axes/extended viewer/draft shadows, script file gate + File.execute warning, expression editor options and error banner). Three stay display-only and are documented. Features: Convert Audio to Keyframes, RPF Camera Import (JSON/CSV camera data), keyframe colour labels + Select Keyframe Label Group, Recent Animation Presets. Menus: Edit > History, File > Import Recent Footage, Replace Footage > With Layered Comp, Save a Copy As XML (.ecprojx, opens again), Composition > VR (Create VR Environment, Extract Cubemap), Assign Shortcut to 3D view / workspace, Window viewer labels + open viewers, Help > System Compatibility Report / Enable Logging / Reveal Logging File. Dynamic menu nodes (@dynamic:…) shared by the egui and native menus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
6.2 KiB
Rust
166 lines
6.2 KiB
Rust
//! Minimal SFNT (TrueType / OpenType / collection) header reader for font discovery: reads only
|
|
//! the table directory, `name` and `OS/2` of each face, so scanning a system font folder does not
|
|
//! load whole files.
|
|
|
|
use std::io::{Read, Seek, SeekFrom};
|
|
|
|
/// Names and style attributes of one face in a font file.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct FaceNames {
|
|
pub index: u32,
|
|
pub family: String,
|
|
pub style: String,
|
|
pub full_name: String,
|
|
pub weight: u16,
|
|
pub italic: bool,
|
|
/// The family name in another language than English (CJK, Arabic… fonts), when the font
|
|
/// has one (Settings ▸ Type ▸ Show Font Names in English off shows it).
|
|
pub native_family: Option<String>,
|
|
}
|
|
|
|
fn be16(b: &[u8], o: usize) -> Option<u16> {
|
|
Some(u16::from_be_bytes([*b.get(o)?, *b.get(o + 1)?]))
|
|
}
|
|
fn be32(b: &[u8], o: usize) -> Option<u32> {
|
|
Some(u32::from_be_bytes([*b.get(o)?, *b.get(o + 1)?, *b.get(o + 2)?, *b.get(o + 3)?]))
|
|
}
|
|
|
|
fn read_at<R: Read + Seek>(r: &mut R, off: u64, len: usize) -> Option<Vec<u8>> {
|
|
if len > 4 << 20 {
|
|
return None;
|
|
}
|
|
r.seek(SeekFrom::Start(off)).ok()?;
|
|
let mut v = vec![0u8; len];
|
|
r.read_exact(&mut v).ok()?;
|
|
Some(v)
|
|
}
|
|
|
|
/// Read the names of every face in a font file (a single font or a collection).
|
|
pub fn read_faces<R: Read + Seek>(r: &mut R) -> Vec<FaceNames> {
|
|
let Some(head) = read_at(r, 0, 12) else { return Vec::new() };
|
|
let offsets: Vec<u32> = if &head[0..4] == b"ttcf" {
|
|
let n = be32(&head, 8).unwrap_or(0).min(256) as usize;
|
|
let Some(t) = read_at(r, 12, n * 4) else { return Vec::new() };
|
|
(0..n).filter_map(|i| be32(&t, i * 4)).collect()
|
|
} else {
|
|
vec![0]
|
|
};
|
|
offsets.iter().enumerate().filter_map(|(i, &o)| read_face(r, o as u64, i as u32)).collect()
|
|
}
|
|
|
|
/// Names from in-memory font data.
|
|
pub fn read_faces_bytes(data: &[u8]) -> Vec<FaceNames> {
|
|
read_faces(&mut std::io::Cursor::new(data))
|
|
}
|
|
|
|
fn read_face<R: Read + Seek>(r: &mut R, off: u64, index: u32) -> Option<FaceNames> {
|
|
let h = read_at(r, off, 12)?;
|
|
let tag = &h[0..4];
|
|
if tag != [0, 1, 0, 0] && tag != b"OTTO" && tag != b"true" {
|
|
return None;
|
|
}
|
|
let n = be16(&h, 4)? as usize;
|
|
let dir = read_at(r, off + 12, n * 16)?;
|
|
let mut name = None;
|
|
let mut os2 = None;
|
|
for i in 0..n {
|
|
let rec = &dir[i * 16..i * 16 + 16];
|
|
let (o, l) = (be32(rec, 8)? as u64, be32(rec, 12)? as usize);
|
|
match &rec[0..4] {
|
|
b"name" => name = Some((o, l)),
|
|
b"OS/2" => os2 = Some((o, l)),
|
|
_ => {}
|
|
}
|
|
}
|
|
let (no, nl) = name?;
|
|
let nb = read_at(r, no, nl)?;
|
|
let get = |id: u16| name_string(&nb, id);
|
|
let family = get(16).or_else(|| get(1))?;
|
|
let style = get(17).or_else(|| get(2)).unwrap_or_else(|| "Regular".into());
|
|
let full_name = get(4).unwrap_or_else(|| format!("{family} {style}"));
|
|
let native_family = native_name(&nb, 16).or_else(|| native_name(&nb, 1)).filter(|n| *n != family);
|
|
let (mut weight, mut italic) = (400u16, false);
|
|
if let Some((oo, ol)) = os2
|
|
&& let Some(ob) = read_at(r, oo, ol.min(78))
|
|
{
|
|
weight = be16(&ob, 4).unwrap_or(400);
|
|
italic = be16(&ob, 62).is_some_and(|fs| fs & 1 != 0);
|
|
}
|
|
let sl = style.to_ascii_lowercase();
|
|
italic |= sl.contains("italic") || sl.contains("oblique");
|
|
Some(FaceNames { index, family, style, full_name, weight, italic, native_family })
|
|
}
|
|
|
|
/// A Windows Unicode name-table string by id in a language other than US English.
|
|
fn native_name(b: &[u8], id: u16) -> Option<String> {
|
|
let count = be16(b, 2)? as usize;
|
|
let storage = be16(b, 4)? as usize;
|
|
for i in 0..count {
|
|
let r = 6 + i * 12;
|
|
let (plat, enc, lang, nid, len, off) =
|
|
(be16(b, r)?, be16(b, r + 2)?, be16(b, r + 4)?, be16(b, r + 6)?, be16(b, r + 8)? as usize, be16(b, r + 10)? as usize);
|
|
if nid != id || plat != 3 || !(enc == 1 || enc == 10) || lang == 0x409 {
|
|
continue;
|
|
}
|
|
let raw = b.get(storage + off..storage + off + len)?;
|
|
let u: Vec<u16> = raw.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
|
let s = String::from_utf16_lossy(&u);
|
|
if !s.trim().is_empty() {
|
|
return Some(s);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// A name-table string by id, preferring Windows Unicode English, then any Unicode, then Mac Roman.
|
|
fn name_string(b: &[u8], id: u16) -> Option<String> {
|
|
let count = be16(b, 2)? as usize;
|
|
let storage = be16(b, 4)? as usize;
|
|
let mut best: Option<(u8, String)> = None;
|
|
for i in 0..count {
|
|
let r = 6 + i * 12;
|
|
let (plat, enc, lang, nid, len, off) =
|
|
(be16(b, r)?, be16(b, r + 2)?, be16(b, r + 4)?, be16(b, r + 6)?, be16(b, r + 8)? as usize, be16(b, r + 10)? as usize);
|
|
if nid != id {
|
|
continue;
|
|
}
|
|
let raw = b.get(storage + off..storage + off + len)?;
|
|
let (rank, s) = match plat {
|
|
3 if enc == 1 || enc == 10 => {
|
|
let u: Vec<u16> = raw.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
|
(if lang == 0x409 { 0 } else { 1 }, String::from_utf16_lossy(&u))
|
|
}
|
|
0 => {
|
|
let u: Vec<u16> = raw.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
|
(2, String::from_utf16_lossy(&u))
|
|
}
|
|
1 if enc == 0 => (3, raw.iter().map(|&c| if c < 128 { c as char } else { '?' }).collect()),
|
|
_ => continue,
|
|
};
|
|
if best.as_ref().is_none_or(|(r0, _)| rank < *r0) {
|
|
best = Some((rank, s));
|
|
}
|
|
}
|
|
best.map(|(_, s)| s).filter(|s| !s.trim().is_empty())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn reads_bundled_names() {
|
|
let f = read_faces_bytes(crate::fonts::INTER_BOLD);
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(f[0].family, "Inter");
|
|
assert_eq!(f[0].style, "Bold");
|
|
assert_eq!(f[0].weight, 700);
|
|
assert!(!f[0].italic);
|
|
let i = read_faces_bytes(crate::fonts::INTER_ITALIC);
|
|
assert!(i[0].italic, "{:?}", i[0]);
|
|
let n = read_faces_bytes(crate::fonts::NOTO_SERIF_REGULAR);
|
|
assert_eq!(n[0].family, "Noto Serif");
|
|
assert!(read_faces_bytes(b"not a font").is_empty());
|
|
}
|
|
}
|