Created
July 14, 2025 14:27
-
-
Save stefan6419846/3d368b26ee5260a7886657909f26ca15 to your computer and use it in GitHub Desktop.
Convert Adobe core AFM files to a dataclass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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