from collections import defaultdict
from collections.abc import Callable
import dataclasses
from dataclasses import dataclass
import glyphsLib
from importlib import resources
from lxml import etree
from math import ceil, floor
from pathlib import Path
from textwrap import dedent
from typing import Iterable, Optional, Self, Tuple
@dataclass(frozen=True)
class GlyphInfo:
codepoint: Optional[int]
name: str
category: str
subcategory: Optional[str]
production_name: Optional[str]
script: Optional[str]
def has_codepoint(self) -> bool:
return self.codepoint is not None
def production_name_or_empty(self) -> str:
if self.production_name is None:
return ""
return self.production_name
def production_name_file_bytes(self) -> bytes:
return ProdNamePattern.for_glyph_info(self).file_bytes(self)
@dataclass(frozen=True)
class ProdNamePattern:
display_name: str
pred: Callable[[GlyphInfo], bool]
file_bytes: Callable[[GlyphInfo], bytes]
def for_glyph_info(gi: GlyphInfo) -> Self:
for pattern in _PROD_NAME_PATTERNS:
if pattern.pred(gi):
return pattern
raise ValueError(
f"At least 1 pattern should always match, somehow {gi} defeated us"
)
_PROD_NAME_PATTERNS = (
ProdNamePattern(
"blank",
lambda gi: gi.production_name is None,
lambda _gi: b"b",
),
ProdNamePattern(
"uni-prefix",
lambda gi: gi.has_codepoint()
and gi.production_name == f"uni{gi.codepoint:04X}"
and gi.codepoint <= 0xFFFF,
lambda gi: b"u" + gi.codepoint.to_bytes(3, "little"),
),
ProdNamePattern(
"u-prefix",
lambda gi: gi.has_codepoint()
and gi.production_name == f"u{gi.codepoint:04X}"
and gi.codepoint > 0xFFFF,
lambda gi: b"v" + gi.codepoint.to_bytes(3, "little"),
),
ProdNamePattern(
"custom",
lambda gi: True,
lambda gi: b"s" + gi.production_name.encode("ascii"),
),
)
def prod_name_from_codepoint(cp: int) -> str:
if cp <= 0xFFFF:
return f"uni{cp:04x}"
else:
return f"u{cp:04x}"
def codename(name: Optional[str]) -> Optional[str]:
if name is None:
return None
return name.replace(" ", "")
def read_glyph_info(file: str) -> Tuple[GlyphInfo]:
file = resources.files(glyphsLib).joinpath("data").joinpath(file)
with open(file) as f:
tree = etree.parse(f)
by_name = {}
for e in tree.xpath("//glyph"):
codepoint = e.attrib.get("unicode", None)
if codepoint is not None:
codepoint = int(codepoint, 16)
info = GlyphInfo(
codepoint,
e.attrib["name"].strip(),
codename(e.attrib["category"]),
codename(e.attrib.get("subCategory", None)),
e.attrib.get("production", None),
e.attrib.get("script", None),
)
if info.name not in by_name:
by_name[info.name] = info
else:
print(f"We've already seen {info.name}!")
for e in tree.xpath("//glyph[@altNames]"):
for alt_name in e.attrib["altNames"].split(","):
alt_name = alt_name.strip()
if alt_name in by_name:
print(f'Ignoring alt name "{alt_name}", already taken')
continue
by_name[alt_name] = dataclasses.replace(
by_name[e.attrib["name"]],
name=alt_name,
codepoint=None,
production_name=e.attrib.get("production", None),
script=e.attrib.get("script", None),
)
return tuple(by_name.values())
def writeU24(file, value):
file.write(value.to_bytes(3, byteorder="little"))
def write_enum_conversion(name: str, values: Tuple[str], optional: bool) -> str:
lines = []
typ = name
if optional:
typ = f"Option<{name}>"
lines.append(f"impl BundledEntry for {typ} {{")
lines.append(" fn element_size() -> usize {")
lines.append(" 1")
lines.append(" }")
lines.append(f" fn from_slice(raw: &[u8]) -> {typ} {{")
lines.append(" match raw[0] {")
if optional:
for i, value in enumerate(values):
lines.append(f" {i} => Some({name}::{value}),")
lines.append(f" 255 => None,")
else:
for i, value in enumerate(values):
lines.append(f" {i} => {name}::{value},")
lines.append(f' v => panic!("What is {{v}}"),')
lines.append(" }")
lines.append(" }")
lines.append("}")
return "\n".join(lines) + "\n"
def write_list(offset_file: Path, data_file: Path, content: Iterable[bytes]):
offset = 0
with open(offset_file, "wb") as f_offsets:
with open(data_file, "wb") as f_data:
for data in content:
writeU24(f_offsets, offset)
f_data.write(data)
offset += len(data)
def camel_case(s: str) -> str:
parts = s.split(" ")
return "".join(p[0].upper() + p[1:] for p in parts)
def main():
glyph_infos = sorted(
set(read_glyph_info("GlyphData.xml"))
| set(read_glyph_info("GlyphData_Ideographs.xml")),
key=lambda g: g.name,
)
names = {g.name for g in glyph_infos}
categories = sorted({g.category for g in glyph_infos})
subcategories = sorted(
{g.subcategory for g in glyph_infos if g.subcategory is not None}
)
scripts = sorted(
{g.script for g in glyph_infos if g.script is not None}, key=camel_case
)
assert len(names) == len(glyph_infos), "Names aren't unique?"
codepoints = {}
for i, gi in enumerate(glyph_infos):
if gi.codepoint is None:
continue
codepoint = gi.codepoint
if codepoint not in codepoints:
codepoints[codepoint] = i
else:
print(
f"Multiple names are assigned 0x{codepoint:04x}, using the first one we saw"
)
for gi in glyph_infos:
if not gi.name.isascii():
raise ValueError(f"{gi.name} invalid")
if gi.production_name is not None and not gi.production_name.isascii():
raise ValueError(f"{gi.production_name} invalid")
name_offsets_file = Path(__file__).parent.parent / "resources" / "name_offsets.dat"
name_data_file = Path(__file__).parent.parent / "resources" / "names.dat"
codepoint_to_idx_file = (
Path(__file__).parent.parent / "resources" / "codepoints_to_idx.dat"
)
codepoint_file = Path(__file__).parent.parent / "resources" / "codepoints.dat"
category_file = Path(__file__).parent.parent / "resources" / "categories.dat"
subcategory_file = Path(__file__).parent.parent / "resources" / "subcategories.dat"
script_file = Path(__file__).parent.parent / "resources" / "scripts.dat"
enums_file = Path(__file__).parent.parent / "src" / "glyphslib_enums.rs"
prod_name_offsets_file = (
Path(__file__).parent.parent / "resources" / "prod_name_offsets.dat"
)
prod_name_data_file = Path(__file__).parent.parent / "resources" / "prod_names.dat"
prod_name_bitmap_file = (
Path(__file__).parent.parent / "resources" / "prod_name_bitmap.dat"
)
readme_file = Path(__file__).parent.parent / "resources" / "README.md"
write_list(
name_offsets_file,
name_data_file,
[gi.name.encode("ascii") for gi in glyph_infos],
)
prod_names_by_type = defaultdict(int)
for gi in glyph_infos:
pattern = ProdNamePattern.for_glyph_info(gi)
prod_names_by_type[pattern.display_name] += 1
print("Prod name types", prod_names_by_type)
max_codepoint = max(codepoints.keys())
prod_name_bitmap = bytearray(ceil(max_codepoint / 8))
num_predicted = 0
for cp in range(0, max_codepoint + 1):
if cp not in codepoints:
continue
gi = glyph_infos[codepoints[cp]]
if gi.production_name is None:
continue
if prod_name_from_codepoint(cp).lower() != gi.production_name.lower():
continue
i = floor(cp / 8)
prod_name_bitmap[i] |= 1 << (cp % 8)
num_predicted += 1
custom_prod_names = []
for i, gi in enumerate(glyph_infos):
if gi.production_name is None:
continue
if (
gi.has_codepoint()
and prod_name_from_codepoint(gi.codepoint).lower()
== gi.production_name.lower()
):
continue
custom_prod_names.append((i, gi))
print(
f"{num_predicted}/{num_predicted + len(custom_prod_names)} non-blank production names are predictable"
)
with open(prod_name_bitmap_file, "wb") as f:
f.write(prod_name_bitmap)
prod_names = [(gi.production_name, i) for (i, gi) in custom_prod_names]
prod_names.sort()
write_list(
prod_name_offsets_file,
prod_name_data_file,
[i.to_bytes(3, "little") + name.encode("ascii") for (name, i) in prod_names],
)
with open(codepoint_file, "wb") as f:
rev_codepoints = {i: cp for (cp, i) in codepoints.items()}
for i in range(0, len(glyph_infos)):
codepoint = rev_codepoints.get(i, 0)
writeU24(f, codepoint)
with open(codepoint_to_idx_file, "wb") as f:
for codepoint, i in sorted(codepoints.items()):
writeU24(f, codepoint)
writeU24(f, i)
with open(category_file, "wb") as f:
for gi in glyph_infos:
f.write(categories.index(gi.category).to_bytes(1))
with open(subcategory_file, "wb") as f:
for gi in glyph_infos:
i = 255
if gi.subcategory is not None:
i = subcategories.index(gi.subcategory)
f.write(i.to_bytes(1))
with open(script_file, "wb") as f:
for gi in glyph_infos:
i = 255
if gi.script is not None:
i = scripts.index(gi.script)
f.write(i.to_bytes(1))
with open(enums_file, "w") as f:
f.write("//! Generated by glyphs-reader/data/update.py\n")
f.write("\n")
f.write(
"use crate::{\n"
" glyphdata::{Category, Script, Subcategory},\n"
" glyphdata_bundled::BundledEntry,\n"
"};"
)
f.write("\n")
f.write(write_enum_conversion("Category", categories, False))
f.write("\n")
f.write("/// The subcategory of a given glyph\n")
f.write(write_enum_conversion("Subcategory", subcategories, True))
f.write("\n")
f.write("/// The script of a given glyph\n")
f.write(
write_enum_conversion("Script", tuple(camel_case(s) for s in scripts), True)
)
with open(readme_file, "w") as f:
f.write("# Dat files\n")
f.write("\n")
f.write(
f"*.dat content generated from glyphsLib {glyphsLib.__version__} by {Path(__file__).name}\n"
)
if __name__ == "__main__":
main()