Created
January 5, 2025 06:54
-
-
Save flodolo/ba0bd409bc1612945b8e436f175abc1f to your computer and use it in GitHub Desktop.
Pontoon: minimize Font Awesome
This file contains 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
#! /usr/bin/env python | |
""" | |
The script looks for TTF fonts in FontAwesome folder. It doesn't use WOFF2 | |
directly, because glyph names seem to be unreliable. | |
Optimized fonts are saved in `/pontoon/base/static/fonts/` as WOFF2. | |
The list of possible icons is done searching for `fa-*` in specific files (HTML, | |
JS). This is not ideal, but there is a lot of dynamic code inserting icons, so | |
searching just for the class syntax `fa[brs] fa-iconname` does't work. | |
Requirement: fonttools with woff2 support | |
pip install fonttools brotli | |
""" | |
from fontTools.ttLib import TTFont | |
from fontTools.subset import Subsetter, Options | |
from pathlib import Path | |
import argparse | |
import os | |
import re | |
import sys | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
"--source", | |
required=True, | |
help="Path to folder with Pontoon source files", | |
) | |
parser.add_argument( | |
"--fa", | |
required=True, | |
help="Path to Font Awesome folder", | |
) | |
args = parser.parse_args() | |
# Get list of files in Pontoon folder | |
root_folder = Path(args.source) | |
file_extensions = ( | |
".css", | |
".js", | |
".html", | |
".tsx", | |
) | |
excluded_folders = (".git", ".venv", "venv", "node_modules") | |
excluded_files = "fontawesome-all.css" | |
files = [ | |
file.relative_to(root_folder) | |
for file in root_folder.rglob("*") | |
if file.is_file() | |
and file.suffix in file_extensions | |
and file.name not in excluded_files | |
and not any( | |
excluded_folder in file.parts for excluded_folder in excluded_folders | |
) | |
] | |
# Exclude more folders, convert to absolute paths | |
excluded_roots = ( | |
"docs/", | |
"static/", | |
"translate/coverage/", | |
"translate/dist/", | |
) | |
files = [ | |
os.path.join(root_folder, f) | |
for f in files | |
if not str(f).startswith(excluded_roots) | |
] | |
files.sort() | |
# Find icons in HTML and JS searching for fa-SOMETHING | |
fa_icon = re.compile(r"[\s'\"]*(fa-\w[-\w]*)[\s'\"]*") | |
# Match all non-ASCII characters (non-7-bit) | |
special_characters = re.compile(r"[^\x00-\x7F]") | |
ignored_matches = ( | |
# Misc false positives | |
"fa-arab-ir", | |
# Font Awesome directives | |
"fa-fw", | |
"fa-w", | |
"fa-2x", | |
"fa-lg", | |
"fa-sm", | |
"fa-w" "fa-stack", | |
"fa-stack-1x", | |
"fa-stack-2x", | |
# Non-glyph directive | |
"fa-spin", | |
) | |
icons = [] | |
code_points = [] | |
for file in files: | |
with open(file) as f: | |
if file.endswith(".css"): | |
# Check for hard-coded special characters in CSS files | |
for line in f: | |
matches = special_characters.findall(line) | |
for char in matches: | |
char_code = ord(char) | |
# Check if the character is in the PUA range | |
if ( | |
(0xE000 <= char_code <= 0xF8FF) | |
or (0xF0000 <= char_code <= 0xFFFFD) | |
or (0x100000 <= char_code <= 0x10FFFD) | |
): | |
code_points.append(char_code) | |
else: | |
for line in f: | |
for match in fa_icon.findall(line): | |
if match not in ignored_matches: | |
if match not in icons: | |
# Drop "fa-" from the name | |
icons.append(match[3:]) | |
code_points = list(set(code_points)) | |
code_points.sort() | |
icons = list(set(icons)) | |
icons.sort() | |
# Search for font files | |
pontoon_font_folder = os.path.join( | |
root_folder, "pontoon", "base", "static", "fonts" | |
) | |
font_folder = Path(args.fa) | |
font_files = [ | |
file | |
for file in font_folder.rglob("fa-*.ttf") | |
if file.is_file() and "v4compatibility" not in file.name | |
] | |
# Icon name mapping to glyphs | |
mapping = { | |
"arrow-circle-left": "circle-arrow-left", | |
"arrow-circle-right": "circle-arrow-right", | |
"calendar-alt": "calendar-days", | |
"cloud-download-alt": "cloud-arrow-down", | |
"cloud-upload-alt": "cloud-arrow-up", | |
"cog": "gear", | |
"cogs": "gears", | |
"dot-circle": "circle-dot", | |
"exclamation-circle": "circle-exclamation", | |
"expand-arrows-alt": "maximize", | |
"pencil-alt": "pencil", | |
"search": "magnifying-glass", | |
"sign-in-alt": "right-to-bracket", | |
"sign-out-alt": "right-from-bracket", | |
"sync": "arrows-rotate", | |
"tasks": "list-check", | |
"times": "xmark", | |
"trash-alt": "trash-can", | |
} | |
found = [] | |
for font_file in font_files: | |
print(f"\n-----\nAnalizing: {font_file.name}\n") | |
# Load the font | |
font = TTFont(font_file) | |
# Mapping for glyph names and Unicode | |
cmap = font["cmap"].getBestCmap() # Unicode-to-glyph mapping | |
reverse_cmap = {v: k for k, v in cmap.items()} # Glyph-to-Unicode mapping | |
# Select known glyph names | |
selected_glyphs = set() | |
for icon in icons: | |
glyph_name = mapping.get(icon, icon) | |
if glyph_name in font.getGlyphOrder(): | |
selected_glyphs.add(glyph_name) | |
found.append(icon) | |
else: | |
print(f"Glyph not found: {glyph_name}") | |
# Store alternative code points for selected glyphs | |
alt_code_points = set() | |
for glyph_name in selected_glyphs: | |
code_point = reverse_cmap.get(glyph_name) | |
if code_point: | |
alt_code_points.add(code_point) | |
# Select glyphs by Unicode code points | |
for code_point in set(code_points + list(alt_code_points)): | |
glyph_name = cmap.get(code_point) | |
if glyph_name: | |
selected_glyphs.add(glyph_name) | |
found.append(f"0x{code_point:x}") | |
else: | |
print(f"Code point not found: 0x{code_point:x}") | |
# Print all selected glyphs | |
print("\nSelected glyphs:") | |
for glyph_name in selected_glyphs: | |
print( | |
f" {glyph_name} (Unicode: 0x{reverse_cmap.get(glyph_name, 'N/A'):04x})" | |
) | |
print(f"Total: {len(selected_glyphs)}") | |
# Subset the font | |
options = Options() | |
options.glyph_names = True | |
subsetter = Subsetter(options=options) | |
subsetter.populate(glyphs=selected_glyphs) | |
subsetter.subset(font) | |
# Save as .woff2 | |
new_filename = f"{font_file.stem}.woff2" | |
font.flavor = "woff2" | |
font.save(os.path.join(pontoon_font_folder, new_filename)) | |
print(f"\nGenerated new file: {new_filename}\n-----\n") | |
font.close() | |
missing = list(set(icons) - set(found)) | |
if missing: | |
missing.sort() | |
print(f"Missing glyphs: {', '.join(missing)}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment