#![feature(type_info)]
#![feature(specialization)]
#![allow(incomplete_features)]
use std::{
fmt::Debug,
io::{BufWriter, Write, stdout},
};
use termal::{eprintac, printac, printacln};
use ttf_view::{
tables::*,
types::{Tag, tags},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Action {
PrintHelp,
PrintVersion,
ListTables,
DumpTable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Format {
Debug,
Binary,
Json,
Ttx,
}
macro_rules! error_exit {
($($arg:tt)*) => {{
eprintac!("{'bold r}error{'_}: ");
eprintln!($($arg)*);
std::process::exit(1)
}};
}
fn main() {
let mut path = None;
let mut action = Action::PrintHelp;
let mut format = None;
let mut table = None;
let mut args = std::env::args();
args.next();
while let Some(arg) = args.next() {
match &*arg {
"-h" | "--help" => {
action = Action::PrintHelp;
},
"-v" | "-V" | "--version" => {
action = Action::PrintVersion;
},
"--list-tables" => {
action = Action::ListTables;
},
"-f" | "--format" => {
if format.is_some() {
error_exit!("-f/--format was specified more than once");
}
let Some(arg) = args.next() else {
error_exit!("Expected a format identifier after -f/--format");
};
format = Some(match &*arg {
"dbg" | "debug" => Format::Debug,
"bin" | "binary" => Format::Binary,
"json" => Format::Json,
"ttx" => Format::Ttx,
unknown => error_exit!("Got an unknown format identifier '{unknown}'"),
});
},
"-t" | "--table" => {
if table.is_some() {
error_exit!("-t/--table was specified more than once");
}
let Some(arg) = args.next() else {
error_exit!("Expected a table tag after -t/--table");
};
if arg == "directory" {
table = Some(None);
} else {
table = match Tag::from_str(&arg) {
Ok(tag) => Some(Some(tag)),
Err(tag_error) => error_exit!("Got an invalid table tag ({tag_error})"),
};
}
},
_ => {
if arg.starts_with('-') {
error_exit!("Got an unknown option '{}'", arg);
}
if let Some(x) = path {
error_exit!("Got more than one path in arguments ('{}', '{}')", &x, arg);
}
path = Some(arg);
action = Action::DumpTable;
},
};
}
let format = format.unwrap_or(Format::Debug);
let table = table.unwrap_or(None);
let mut font_data = None;
let load_font = || {
font_data = path.map(|path| match std::fs::read(&path) {
Ok(data) => data,
Err(err) => error_exit!("{} '{}'", err, &path),
});
font_data.as_ref().map(|data| {
return TableDirectory::new(&data).expect("Font should be well-formed");
})
};
match action {
Action::PrintVersion => print_version(),
Action::PrintHelp => print_help(),
Action::ListTables => print_tables(load_font()),
Action::DumpTable => {
let Some(dir) = load_font() else { error_exit!("Font file was not specified") };
dump(dir, table, format);
},
};
}
const DIR_TAG: Tag = if let Ok(tag) = Tag::from_str("dir") { tag } else { unreachable!() };
macro_rules! implement_tables {
($dir:ident { $( $tag:path, $name:expr $(, $get_table:expr => $Table:ty)? );* $(;)? }) => {
fn dump($dir: &TableDirectory, tag: Option<Tag>, format: Format) {
if format == Format::Binary {
let bytes = match tag {
None => $dir.directory_as_bytes(),
Some(DIR_TAG) if $dir.table_record_raw(DIR_TAG).is_none() => $dir.directory_as_bytes(),
Some(tag) => $dir.table_record(tag).map_or_default(|t| t.table_as_bytes()),
};
stdout().write_all(bytes).unwrap();
return;
}
let table: &dyn DumpDebug = match tag {
None => $dir,
Some(DIR_TAG) if $dir.table_record_raw(DIR_TAG).is_none() => $dir,
$($( Some($tag) => &$get_table.expect("Table should be well-formed"), )?)*
Some(table_tag @ _) => {
if table_tag.is_known() {
error_exit!("OpenType table '{table_tag}' is not implemented yet");
} else if $dir.table_record_raw(table_tag).is_none() {
error_exit!("Could not find a table with tag '{table_tag}'");
} else {
error_exit!("Unknown table '{table_tag}'. Consider exporting in binary format");
}
},
};
match format {
Format::Binary => {
unreachable!(); },
Format::Debug => {
if !table.debug_is_supported() {
error_exit!("debug format is not yet implemented for '{}'", tag.unwrap());
}
let mut out = BufWriter::new(stdout());
writeln!(out, "{:#?}", std::fmt::from_fn(|f| table.debug_dump(f))).unwrap();
out.flush().unwrap();
},
Format::Json => {
error_exit!("json format is not implemented yet");
},
Format::Ttx => {
error_exit!("ttx format is not implemented yet");
},
};
}
struct TableSupport {
name: &'static str,
tag: Tag,
dbg: bool,
json: bool,
ttx: bool,
}
const SUPPORTED_TABLES: &[TableSupport] = &[
$(TableSupport {
tag: $tag, name: $name,
dbg: implement_tables!(@impls_trait $($Table)? : Debug),
json: false,
ttx: false,
},)*
];
};
(@impls_trait $Table:ty : $Trait:ident) => {
std::any::TypeId::of::<$Table>().trait_info_of::<dyn $Trait>().is_some()
};
(@impls_trait : $Trait:ty) => { false };
}
implement_tables!(dir {
tags::avar, "Axis Variations Table"; tags::BASE, "Baseline Table"; tags::CBDT, "Color Bitmap Data Table"; tags::CBLC, "Color Bitmap Location Table"; tags::CFF , "Compact Font Format (Version 1)"; tags::CFF2, "Compact Font Format (Version 2)"; tags::cmap, "Character to Glyph Index Mapping", dir.cmap() => cmap::Cmap;
tags::COLR, "Color Table"; tags::CPAL, "Color Palette Table"; tags::cvar, "CVT Variations Table"; tags::cvt , "Control Value Table"; tags::DSIG, "Digital Signature Table"; tags::EBDT, "Embedded Bitmap Data Table"; tags::EBLC, "Embedded Bitmap Location Table"; tags::EBSC, "Embedded Bitmap Scaling Table"; tags::fpgm, "Font Program"; tags::fvar, "Font Variations Table"; tags::gasp, "Grid-fitting and Scan-conversion"; tags::GDEF, "Glyph Definition Table"; tags::glyf, "Glyph Data Table"; tags::GPOS, "Glyph Positioning Table"; tags::GSUB, "Glyph Substitution Table"; tags::gvar, "Glyph Variations Table"; tags::hdmx, "Horizontal Device Metrics"; tags::head, "Font Header Table", dir.head() => head::Head;
tags::hhea, "Horizontal Header Table", dir.hhea() => hhea::Hhea;
tags::hmtx, "Horizontal Metrics Table", dir.hmtx() => hmtx::Hmtx;
tags::HVAR, "Horizontal Metrics Variations Table"; tags::JSTF, "Justification Table"; tags::kern, "Kerning Table"; tags::loca, "Index to Location Table", dir.loca() => loca::Loca;
tags::LTSH, "Linear Threshold Table"; tags::MATH, "Mathematical Typesetting Table"; tags::maxp, "Maximum Profile", dir.maxp() => maxp::Maxp;
tags::MERG, "Merge Table"; tags::meta, "Metadata Table"; tags::MVAR, "Metrics Variations Table"; tags::name, "Naming Table", dir.name() => name::Name;
tags::OS_2, "OS/2 and Windows Metrics Table", dir.os_2() => os_2::Os_2;
tags::PCLT, "PCL 5 Table"; tags::post, "PostScript Table"; tags::prep, "Control Value Program"; tags::sbix, "Standard Bitmap Graphics Table"; tags::STAT, "Style Attributes Table"; tags::SVG , "Scalable Vector Graphics Table"; tags::VDMX, "Vertical Device Metrics Table"; tags::vhea, "Vertical Header Table"; tags::vmtx, "Vertical Metrics Table"; tags::VORG, "Vertical Origin Table"; tags::VVAR, "Vertical Metrics Variations Table"; });
fn print_version() {
println!("ttf-view {}", env!("CARGO_PKG_VERSION"));
}
fn print_help() {
printacln!(
r#"A TrueType/OpenType font parsing/viewing Rust library and a CLI tool.
The project's GitHub repository: https://github.com/Chasmical/ttf-view
{'bold u}Usage:{'_u} ttf-view{'_bold} [OPTIONS] <FONT>
{'bold u}Arguments:{'_}
<FONT> Path to the OpenType font file to view (.ttf, .otf)
{'bold u}Options:{'_}
{'bold}-t, --table{'_} <TAG> The table to dump [default: table directory]
{'bold}-f, --format{'_} <FORMAT> The format to dump the data in [default: debug]
[options: dbg/debug, bin/binary]
{'bold} --list-tables{'_} List all supported OpenType tables and formats
[with <FONT>, only shows ones in the font file]
{'bold}-h, --help{'_} Print help
{'bold}-V, --version{'_} Print version"#
);
}
fn print_tables(dir: Option<&TableDirectory>) {
if dir.is_some() {
println!("The specified font contains the following tables:");
} else {
println!("Currently only the following OpenType tables are implemented:");
}
printacln!(
r#"
{'bold u}OT Tag{'_} {'bold u}OT Table Name{'_} {'bold u}Supported Formats{'_}
{'bold i} dir {'_} Table directory [alias {'bold i}directory{'_}] bin,dbg"#
);
let tags = if let Some(dir) = dir {
Vec::from_iter(dir.table_records_raw().iter().map(|x| x.table_tag))
} else {
Vec::from_iter(SUPPORTED_TABLES.iter().map(|x| x.tag))
};
for tag in tags {
let known = SUPPORTED_TABLES.iter().find(|x| x.tag == tag);
if dir.is_none() && known.is_some_and(|x| !x.dbg) {
continue;
}
let name = known.map_or("UNKNOWN TABLE", |x| x.name);
if let Some(known) = known
&& known.dbg
{
printac!("{'bold}{:?}{'_bold} {:35} bin,dbg", tag, name);
known.json.then(|| print!(",json"));
known.ttx.then(|| print!(",ttx"));
println!();
}
else {
if known.is_none() {
printac!("{'r}");
}
printacln!("{'bold f}{:?}{'_bold} {'f}{:35}{'_fg} bin{'_}", tag, name);
}
}
printacln!("\nNote: {'bold}binary{'_} format is always supported, even for unknown tables.");
}
trait DumpDebug {
fn debug_is_supported(&self) -> bool;
fn debug_dump(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result;
}
impl<T> DumpDebug for T {
default fn debug_is_supported(&self) -> bool {
false
}
default fn debug_dump(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
Err(std::fmt::Error)
}
}
impl<T: Debug> DumpDebug for T {
fn debug_is_supported(&self) -> bool {
true
}
fn debug_dump(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
Debug::fmt(self, f)
}
}