blob: cff180199638a25e1d9f7729be34f29da8538a10 [file] [edit]
//! Print the contents of font tables.
//!
//! This accepts command line arguments similar to what is present in ttx,
//! although it does not produce xml output.
use std::{collections::HashSet, str::FromStr};
use font_types::Tag;
use read_fonts::{traversal::SomeTable, FileRef, FontRef, ReadError, TableProvider, TopLevelTable};
mod print;
mod query;
use print::PrettyPrinter;
use query::Query;
fn main() -> Result<(), Error> {
let args = flags::Otexplorer::from_env_or_exit();
let bytes = std::fs::read(&args.input).unwrap();
let font = FileRef::new(&bytes)
.unwrap()
.fonts()
.nth(args.index.unwrap_or(0) as usize)
.unwrap()
.unwrap();
if args.list {
list_tables(&font);
return Ok(());
}
if let Some(query) = &args.query {
return query::print_query(&font, query).map_err(Error);
}
let filter = TableFilter::from_args(&args)?;
print_tables(&font, &filter);
Ok(())
}
fn list_tables(font: &FontRef) {
println!("Tag Offset Length Checksum");
println!("-------------------------------");
let offset_pad = get_offset_width(font);
for record in font.table_directory().table_records() {
println!(
"{0} 0x{1:02$X} {3:8} 0x{4:08X} ",
record.tag(),
record.offset(),
offset_pad,
record.length(),
record.checksum()
);
}
}
fn print_tables(font: &FontRef, filter: &TableFilter) {
let mut printed = HashSet::new();
for tag in font
.table_directory()
.table_records()
.iter()
.map(|rec| rec.tag())
.filter(|tag| filter.should_print(*tag))
{
printed.insert(tag);
print_table(font, tag)
}
if let TableFilter::Include(to_print) = filter {
for unprinted in to_print.difference(&printed) {
println!("Table {unprinted} not found");
}
}
}
fn get_offset_width(font: &FontRef) -> usize {
// pick how much padding we use for offsets based on the max offset in directory
let max_off = font
.table_directory()
.table_records()
.iter()
.map(|rec| rec.offset())
.max()
.unwrap_or_default();
hex_width(max_off)
}
/// Return the number of hex digits required to represent a given number.
fn hex_width(val: u32) -> usize {
match val {
0..=0xffff => 4usize,
0x10000..=0xffffff => 6,
0x1000000.. => 8,
}
}
/// Given a font and a tag, return the appropriate table as a [`dyn SomeTable`][SomeTable].
fn get_some_table<'a>(
font: &FontRef<'a>,
tag: Tag,
) -> Result<Box<dyn SomeTable<'a> + 'a>, ReadError> {
use read_fonts::tables;
match tag {
tables::gpos::Gpos::TAG => font.gpos().map(|x| Box::new(x) as _),
tables::gsub::Gsub::TAG => font.gsub().map(|x| Box::new(x) as _),
tables::cmap::Cmap::TAG => font.cmap().map(|x| Box::new(x) as _),
tables::fvar::Fvar::TAG => font.fvar().map(|x| Box::new(x) as _),
tables::avar::Avar::TAG => font.avar().map(|x| Box::new(x) as _),
tables::gdef::Gdef::TAG => font.gdef().map(|x| Box::new(x) as _),
tables::glyf::Glyf::TAG => font.glyf().map(|x| Box::new(x) as _),
tables::head::Head::TAG => font.head().map(|x| Box::new(x) as _),
tables::hhea::Hhea::TAG => font.hhea().map(|x| Box::new(x) as _),
tables::hmtx::Hmtx::TAG => font.hmtx().map(|x| Box::new(x) as _),
tables::loca::Loca::TAG => font.loca(None).map(|x| Box::new(x) as _),
tables::maxp::Maxp::TAG => font.maxp().map(|x| Box::new(x) as _),
tables::name::Name::TAG => font.name().map(|x| Box::new(x) as _),
tables::post::Post::TAG => font.post().map(|x| Box::new(x) as _),
tables::colr::Colr::TAG => font.colr().map(|x| Box::new(x) as _),
tables::stat::Stat::TAG => font.stat().map(|x| Box::new(x) as _),
tables::vhea::Vhea::TAG => font.vhea().map(|x| Box::new(x) as _),
tables::vmtx::Vmtx::TAG => font.vmtx().map(|x| Box::new(x) as _),
tables::svg::Svg::TAG => font.svg().map(|x| Box::new(x) as _),
_ => Err(ReadError::TableIsMissing(tag)),
}
}
fn print_table(font: &FontRef, tag: Tag) {
match get_some_table(font, tag) {
Ok(table) => fancy_print_table(&table).unwrap(),
Err(err) => println!("{tag}: Error '{err}'"),
}
}
enum TableFilter {
All,
Include(HashSet<Tag>),
Exclude(HashSet<Tag>),
}
impl TableFilter {
fn from_args(args: &flags::Otexplorer) -> Result<Self, Error> {
if args.tables.is_some() && args.exclude.is_some() {
return Err(Error::new("pass only one of --tables and --exclude"));
}
if let Some(tags) = &args.tables {
make_tag_set(tags).map(TableFilter::Include)
} else if let Some(tags) = &args.exclude {
make_tag_set(tags).map(TableFilter::Exclude)
} else {
Ok(TableFilter::All)
}
}
fn should_print(&self, tag: Tag) -> bool {
match self {
TableFilter::All => true,
TableFilter::Include(tags) => tags.contains(&tag),
TableFilter::Exclude(tags) => !tags.contains(&tag),
}
}
}
fn make_tag_set(inp: &str) -> Result<HashSet<Tag>, Error> {
inp.split(' ')
.map(|raw| match Tag::from_str(raw) {
Ok(tag) => Ok(tag),
Err(e) => Err(Error(format!(
"Invalid tag '{}': {e}",
raw.escape_default()
))),
})
.collect()
}
#[derive(Debug, Clone)]
struct Error(String);
impl Error {
fn new(t: impl std::fmt::Display) -> Self {
Self(t.to_string())
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for Error {}
fn fancy_print_table<'a>(table: &(dyn SomeTable<'a> + 'a)) -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut locked = stdout.lock();
let mut formatter = PrettyPrinter::new(&mut locked);
formatter.print_root_table(table)
}
mod flags {
use super::Query;
use std::path::PathBuf;
xflags::xflags! {
/// Explore the contents of binary font files
///
/// otexplorer can print a textual representation of the tables in
/// a font file, or some subset of those tables.
///
/// In addition to this, the -q option allows the user to provide a query,
/// which can be used to print some particular subtable or value.
///
/// Queries begin with a table tag, and then zero or more path elements,
/// separated by a period ('.'). Path elements can be either the names
/// of fields, or indexes into an array.
///
/// For instance, to print the first subtable of the second lookup in
/// the GPOS table, you can do,
///
/// otexplorer $myfont -q GPOS.lookup_list.lookup_offsets.1.subtable_offsets.0
///
/// Queries are case-insensitive, and fuzzily matched. The following
/// two examples produce identical results to the example above:
///
/// otexplorer $myfont -q GPOS.lookupListOffset.lookupOffsets.1.subtableOffsets.0
/// otexplorer $myfont -q GPOS.look.off.1.off.0
cmd otexplorer {
required input: PathBuf
optional -i, --index index: u32
optional -l, --list
optional -q, --query query: Query
optional -t, --tables include: String
optional -x, --exclude exclude: String
}
}
}