Skip to content

Commit

Permalink
Merge pull request #578 from googlefonts/layout-repr
Browse files Browse the repository at this point in the history
Add tool for generating a normalized text representation of mark/kerning rules
  • Loading branch information
cmyr authored Nov 20, 2023
2 parents a05fd6b + 0ccce5b commit 87c72e6
Show file tree
Hide file tree
Showing 14 changed files with 2,161 additions and 8 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ rayon = "1.6"

# fontations etc
font-types = { version = "0.4.0", features = ["serde"] }
read-fonts = "0.13.0"
read-fonts = "0.13.1"
write-fonts = { version = "0.18.0", features = ["serde", "read"] }
skrifa = "0.12.0"
norad = "0.12"
Expand All @@ -48,4 +48,6 @@ members = [
"fontc",
"fea-rs",
"fea-lsp",
"layout-normalizer",
]

17 changes: 17 additions & 0 deletions layout-normalizer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "layout-normalizer"
version = "0.1.0"
edition = "2021"
license = "MIT/Apache-2.0"
publish = false

[dependencies]
clap.workspace = true
thiserror.workspace = true
smol_str.workspace = true
read-fonts.workspace = true
indexmap.workspace = true

# cargo-release settings
[package.metadata.release]
release = false
695 changes: 695 additions & 0 deletions layout-normalizer/aglfn.txt

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions layout-normalizer/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Build script to generate our glyph name lookup table.

use std::{
env,
fs::File,
io::{BufWriter, Write},
path::Path,
str::FromStr,
};

const GLYPH_NAMES_FILE: &str = "glyph_names_codegen.rs";
static AGLFN: &str = include_str!("aglfn.txt");

fn main() {
println!("cargo:rerun-if-changed=resources/aglfn.txt");
generate_glyph_names();
}

fn generate_glyph_names() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(GLYPH_NAMES_FILE);
let mut file = BufWriter::new(File::create(path).unwrap());

let mut entries = AGLFN
.lines()
.filter(|l| !l.starts_with('#'))
.map(NameEntry::from_str)
.collect::<Result<Vec<_>, _>>()
.unwrap();

entries.sort_by(|a, b| a.chr.cmp(&b.chr));
let formatted = entries
.iter()
.map(|NameEntry { chr, name }| format!("({chr}, SmolStr::new_inline(\"{name}\"))"))
.collect::<Vec<_>>();
writeln!(
&mut file,
"static GLYPH_NAMES: &[(u32, SmolStr)] = &[\n{}];\n",
formatted.join(",\n")
)
.unwrap();
}

struct NameEntry {
chr: u32,
name: String,
}

impl FromStr for NameEntry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.split(';');
match (split.next(), split.next(), split.next(), split.next()) {
(Some(cpoint), Some(postscript_name), Some(_unic_name), None) => {
let chr = u32::from_str_radix(cpoint, 16).unwrap();
let postscript_name = postscript_name.to_string();
Ok(NameEntry {
chr,
name: postscript_name,
})
}
_ => Err(s.to_string()),
}
}
}
38 changes: 38 additions & 0 deletions layout-normalizer/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::{path::PathBuf, str::FromStr};

#[derive(Clone, Debug, clap::Parser)]
pub(crate) struct Args {
pub font_path: PathBuf,
#[arg(short, long)]
/// Optional destination path for writing output. Default is stdout.
pub out: Option<PathBuf>,
/// Target table to print, one of gpos/gsub/all (case insensitive)
#[arg(short, long)]
pub table: Option<Table>,
/// Index of font to examine, if target is a font collection
#[arg(short, long)]
pub index: Option<u32>,
}

/// What table to print
#[derive(Clone, Debug, Default)]
pub enum Table {
#[default]
All,
Gpos,
Gsub,
}

impl FromStr for Table {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
static ERR_MSG: &str = "expected one of 'gsub', 'gpos', 'all'";
match s.to_ascii_lowercase().trim() {
"gpos" => Ok(Self::Gpos),
"gsub" => Ok(Self::Gsub),
"all" => Ok(Self::All),
_ => Err(ERR_MSG),
}
}
}
180 changes: 180 additions & 0 deletions layout-normalizer/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! general common utilities and types

use std::{collections::BTreeSet, fmt::Display};

use read_fonts::{
tables::layout::{FeatureList, ScriptList},
types::{GlyphId, Tag},
};

use crate::glyph_names::NameMap;

/// A set of lookups for a specific feature and language system
pub(crate) struct Feature {
pub(crate) feature: Tag,
pub(crate) script: Tag,
pub(crate) lang: Tag,
pub(crate) lookups: Vec<u16>,
}

/// A type to represent either one or multiple glyphs
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub(crate) enum GlyphSet {
Single(GlyphId),
Multiple(BTreeSet<GlyphId>),
}

impl Feature {
fn sort_key(&self) -> impl Ord {
// make it so we always put DFLT/dflt above other tags
fn tag_to_int(tag: Tag) -> u32 {
if tag == Tag::new(b"DFLT") {
0
} else if tag == Tag::new(b"dflt") {
1
} else {
u32::from_be_bytes(tag.to_be_bytes())
}
}

(
tag_to_int(self.feature),
tag_to_int(self.script),
tag_to_int(self.lang),
)
}
}

pub(crate) fn get_lang_systems(
script_list: &ScriptList,
feature_list: &FeatureList,
) -> Vec<Feature> {
let data = script_list.offset_data();

let mut result = script_list
.script_records()
.iter()
// first iterate all (script, lang, feature indices)
.flat_map(|script| {
let script_tag = script.script_tag();
let script = script.script(data).unwrap();
let maybe_default = script
.default_lang_sys()
.transpose()
.unwrap()
.map(|dflt| (script_tag, Tag::new(b"dflt"), dflt.feature_indices()));
let lang_sys_iter = script.lang_sys_records().iter().map(move |lang_sys| {
let lang_tag = lang_sys.lang_sys_tag();
let lang = lang_sys.lang_sys(script.offset_data()).unwrap();
(script_tag, lang_tag, lang.feature_indices())
});
maybe_default.into_iter().chain(lang_sys_iter)
})
// then convert these into script/lang/feature/lookup indices
.flat_map(|(script, lang, indices)| {
indices.iter().map(move |idx| {
let rec = feature_list
.feature_records()
.get(idx.get() as usize)
.unwrap();
let feature = rec.feature(feature_list.offset_data()).unwrap();
let lookups = feature
.lookup_list_indices()
.iter()
.map(|x| x.get())
.collect();
Feature {
feature: rec.feature_tag(),
script,
lang,
lookups,
}
})
})
.collect::<Vec<_>>();

result.sort_unstable_by_key(|sys| sys.sort_key());

result
}

impl GlyphSet {
pub(crate) fn make_set(&mut self) {
if let GlyphSet::Single(gid) = self {
*self = GlyphSet::Multiple(BTreeSet::from([*gid]))
}
}

pub(crate) fn combine(&mut self, other: GlyphSet) {
self.make_set();
let GlyphSet::Multiple(gids) = self else {
unreachable!()
};
match other {
GlyphSet::Single(gid) => {
gids.insert(gid);
}
GlyphSet::Multiple(mut multi) => gids.append(&mut multi),
}
}

pub(crate) fn add(&mut self, gid: GlyphId) {
// if we're a single glyph, don't turn into a set if we're adding ourselves
if matches!(self, GlyphSet::Single(x) if *x == gid) {
return;
}
self.make_set();
if let GlyphSet::Multiple(set) = self {
set.insert(gid);
}
}

pub(crate) fn printer<'a>(&'a self, names: &'a NameMap) -> impl Display + 'a {
// A helper for printing one or more glyphs
struct GlyphPrinter<'a> {
glyphs: &'a GlyphSet,
names: &'a NameMap,
}

impl Display for GlyphPrinter<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.glyphs {
GlyphSet::Single(single) => {
let name = self.names.get(*single);
f.write_str(name)
}
GlyphSet::Multiple(glyphs) => {
f.write_str("[")?;
let mut first = true;
for gid in glyphs {
let name = self.names.get(*gid);
if !first {
f.write_str(",")?;
}
f.write_str(name)?;
first = false;
}
f.write_str("]")
}
}
}
}

GlyphPrinter {
glyphs: self,
names,
}
}
}

impl From<GlyphId> for GlyphSet {
fn from(src: GlyphId) -> GlyphSet {
GlyphSet::Single(src)
}
}

impl FromIterator<GlyphId> for GlyphSet {
fn from_iter<T: IntoIterator<Item = GlyphId>>(iter: T) -> Self {
GlyphSet::Multiple(iter.into_iter().collect())
}
}
23 changes: 23 additions & 0 deletions layout-normalizer/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::path::PathBuf;

use read_fonts::{types::Tag, ReadError};

#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("could not read path '{path}': '{inner}'")]
Load {
path: PathBuf,
inner: std::io::Error,
},
#[error("could not create file '{path}': '{inner}'")]
FileWrite {
path: PathBuf,
inner: std::io::Error,
},
#[error("write error: '{0}'")]
Write(#[from] std::io::Error),
#[error("could not read font data: '{0}")]
FontRead(ReadError),
#[error("missing table '{0}'")]
MissingTable(Tag),
}
Loading

0 comments on commit 87c72e6

Please sign in to comment.