Skip to content

Instantly share code, notes, and snippets.

@SamusAranX
Created March 6, 2022 14:02
Show Gist options
  • Save SamusAranX/182cc80ecb99cbe787f1e17df67f0efd to your computer and use it in GitHub Desktop.
Save SamusAranX/182cc80ecb99cbe787f1e17df67f0efd to your computer and use it in GitHub Desktop.
A script that scans directories containing fonts and generates a CSS file containing @font-face blocks for each of them, complete with somewhat correct naming and font-style/-stretch/-weight values. To make this work with .woff2 files, run a search-and-replace on the resulting file.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
from glob import glob
from os.path import join, relpath
from pathlib import Path
from fontTools.ttLib import TTFont
FONT_SPECIFIER_NAME_ID = 4
FONT_SPECIFIER_FAMILY_ID = 1
FONT_WIDTHS = {
1: "ultra-condensed",
2: "extra-condensed",
3: "condensed",
4: "semi-condensed",
5: "normal",
6: "semi-expanded",
7: "expanded",
8: "extra-expanded",
9: "ultra-expanded",
}
class FontInfo:
@staticmethod
def font_short_name(font: TTFont) -> (str, str):
def get_record_string(record) -> str:
try:
if b"\x00" in record.string:
return record.string.decode("utf-16-be")
else:
return record.string.decode("utf-8")
except UnicodeError:
return record.string.decode("latin-1")
name = ""
family = ""
for record in font["name"].names:
if record.nameID == FONT_SPECIFIER_NAME_ID and not name:
name = get_record_string(record)
elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family:
family = get_record_string(record)
if name and family:
break
return name, family
@staticmethod
def font_modifiers(font) -> (int, bool, int):
return (
# weight as an integer
font["OS/2"].usWeightClass,
# italic
(font["OS/2"].fsSelection & 1 or font["head"].macStyle & 2) == 1,
# width as an integer
font["OS/2"].usWidthClass,
)
def __init__(self, font: TTFont):
self.name, self.family = self.font_short_name(font)
self.weight, self.italic, self.width = self.font_modifiers(font)
def __str__(self):
return f"{self.family}/{self.name} ({self.weight}, {self.italic}, {self.width})"
def main(args):
indir = args.indir
outfile = args.outfile
patterns = ["*.ttf", "*.otf"]
input_fonts = []
if args.recursive:
print(f"Recursively scanning directory {indir}")
for p in patterns:
for m in Path(indir).rglob(p):
input_fonts.append(str(m.resolve()))
input_fonts = list(set(input_fonts))
else:
print(f"Scanning directory {indir}")
for p in patterns:
input_fonts += glob(join(indir, p))
input_fonts = list(set(input_fonts))
if not input_fonts:
print("No fonts found")
return
font_infos = []
for f in input_fonts:
ttfont = TTFont(f)
font_infos.append((f, FontInfo(ttfont),))
font_infos.sort(key=lambda ft: (ft[1].family.split()[0], ft[1].width, ft[1].weight, ft[1].italic))
with open(outfile, "w", encoding="utf8") as css:
for font_path, font_info in font_infos:
print(font_info)
css.write("@font-face {\n")
css.write(f"\tfont-family: \"{font_info.family}\";\n")
if font_info.italic:
css.write(f"\tfont-style: italic;\n")
if font_info.width != 5:
font_width = FONT_WIDTHS[font_info.width]
css.write(f"\tfont-stretch: {font_width};\n")
if font_info.weight != 400:
css.write(f"\tfont-weight: {font_info.weight};\n")
font_path_rel = relpath(font_path, indir)
css.write(f"\tsrc: url(\"{font_path_rel}\") format(\"truetype\");\n")
css.write("}\n")
css.write("\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="@font-face generator")
parser.add_argument("--indir", "-i", type=str, required=True, help="Input directory")
parser.add_argument("--outfile", "-o", type=str, required=True, help="Output CSS file")
parser.add_argument("-r", "--recursive", action="store_true", help="Recursively scan subdirectories")
main(parser.parse_args())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment