Skip to content

Instantly share code, notes, and snippets.

@stefan6419846
Created July 14, 2025 14:27
Show Gist options
  • Select an option

  • Save stefan6419846/3d368b26ee5260a7886657909f26ca15 to your computer and use it in GitHub Desktop.

Select an option

Save stefan6419846/3d368b26ee5260a7886657909f26ca15 to your computer and use it in GitHub Desktop.
Convert Adobe core AFM files to a dataclass
import textwrap
import urllib.request
from io import BytesIO
from zipfile import ZipFile
from adobe_glyphs import adobe_glyphs
# FONT_URL = "http://web.archive.org/web/20110531171921if_/http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/Core14_AFMs.zip"
FONT_URL = "https://download.macromedia.com/pub/developer/opentype/tech-notes/Core14_AFMs.zip"
class Parser:
def __init__(self):
self.license_information = ""
self.files: dict[str, str] = {}
def get_fonts(self) -> None:
with (
urllib.request.urlopen(FONT_URL) as connection,
ZipFile(BytesIO(connection.read())) as font_zip
):
for filename in font_zip.namelist():
if filename.lower().endswith(".afm"):
with font_zip.open(filename, mode="r") as afm_font_file:
self.files[filename] = afm_font_file.read().decode("utf-8")
else:
with font_zip.open(filename, mode="r") as afm_font_file:
self.license_information = afm_font_file.read().decode("utf-8")
def get_disclaimer(self, width: int = 110) -> str:
pre = (
"This file is based upon the 14 core AFM files provided by Adobe/Macromedia at\n" +
FONT_URL +
"\nThe original copyright follows:\n\n" +
"-" * width +
"\n"
)
title = self.license_information.split("<title>")[1].split("</title>")[0]
text = self.license_information.split('<td width="300">')[1].split("<font color")[0]
post = "\n" + "-" * width + "\n\n"
return pre + title + "\n\n" + "\n".join(textwrap.wrap(text=text, width=width)) + post
def _handle_font(self, file_name: str, font_data: str) -> list[str]:
# AFM specification: https://adobe-type-tools.github.io/font-tech-notes/pdfs/5004.AFM_Spec.pdf
copyrights: list[str] = []
name: str = ""
family: str = ""
weight: str = ""
ascent: float = 0.0
descent: float = 0.0
cap_height: float = 0.0
x_height: float = 0.0
italic_angle: float = 0.0
flags: int = -1
bbox: tuple[float, float, float, float] = (0, 0, 0, 0)
character_widths: dict[str, int] = {}
for line in font_data.splitlines(keepends=False):
if not line.strip():
continue
if not " " in line:
continue
key, value = line.split(" ", maxsplit=1)
if not key:
continue
if key == "FontName":
name = value
if key == "Weight":
weight = value
if key == "FamilyName":
family = value
if key == "Ascender":
ascent = value
if key == "Descender":
descent = value
if key == "CapHeight":
cap_height = value
if key == "XHeight":
x_height = value
if key == "ItalicAngle":
italic_angle = value
if key == "IsFixedPitch":
flags = 64 * int(value.lower() == "true")
if key == "FontBBox":
bbox = tuple(map(float, value.split(" ")[:4])) # type: ignore
if key == "Comment" and value.startswith("Copyright"):
copyrights.append(value)
if key == "Notice" and value.startswith("Copyright"):
copyrights.append(value)
if key == "C":
# C integer ; WX number ; N name
key_value_pairs = line.split(";")
character_code = -1
character_width_x = -1
character_name = "dummy"
for pair in key_value_pairs:
if not pair.strip():
continue
key_of_pair, value_of_pair = pair.strip().split(" ", maxsplit=1)
if key_of_pair == "C":
character_code = int(value_of_pair)
if key_of_pair == "WX":
character_width_x = int(value_of_pair)
if key_of_pair == "N":
character_name = value_of_pair
if 0 <= character_code <= 255:
glyph = adobe_glyphs[f"/{character_name}"]
character_widths[glyph.encode("unicode_escape").decode("utf-8")] = character_width_x
if key == "CH":
raise NotImplementedError(name, line)
result = [
f" # Generated from {file_name}"
]
for copyright_entry in sorted(set(copyrights)):
for line in textwrap.wrap(text=copyright_entry, width=110):
result.append(f" # {line}")
result.append(f' "{name}": Font(')
result.append(f' name="{name}",')
result.append(f' family="{family}",')
result.append(f' weight="{weight}",')
result.append(f" ascent={ascent},")
result.append(f" descent={descent},")
result.append(f" cap_height={cap_height},")
result.append(f" x_height={x_height},")
result.append(f" italic_angle={italic_angle},")
result.append(f" flags={flags},")
result.append(f" bbox=({', '.join(map(str, bbox))}),")
result.append(" character_widths={")
for character, width in character_widths.items():
result.append(f' "{character}": {width},')
result.append(" },")
result.append(f" ),")
return result
def get_font_data(self) -> str:
data = [
"FONT_METRICS: dict[str, Font] = {",
]
for name, font_data in self.files.items():
data.extend(self._handle_font(name, font_data))
data.append("}\n")
return "\n".join(data)
parser = Parser()
parser.get_fonts()
print(parser.get_disclaimer())
print(parser.get_font_data())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment