#!/usr/bin/env python3 """ Download Google Fonts woff2 files (latin subset only) for self-hosting. Fonts downloaded: - Source Serif 4: 400, 600, 400-italic - Inter: 400, 500, 600, 700 - JetBrains Mono: 400 Files are saved to src/fonts/ with naming: {family}-{weight}{-italic}.woff2 """ import os import re import urllib.request # Chrome User-Agent to get woff2 format from Google Fonts CSS API USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) # Google Fonts CSS2 API URLs FONT_REQUESTS = [ # Source Serif 4: 400 normal, 600 normal, 400 italic ( "https://fonts.googleapis.com/css2?" "family=Source+Serif+4:ital,wght@0,400;0,600;1,400" "&display=swap" ), # Inter: 400, 500, 600, 700 ( "https://fonts.googleapis.com/css2?" "family=Inter:wght@400;500;600;700" "&display=swap" ), # JetBrains Mono: 400 ( "https://fonts.googleapis.com/css2?" "family=JetBrains+Mono:wght@400" "&display=swap" ), ] SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) FONTS_DIR = os.path.join(PROJECT_ROOT, "src", "fonts") def fetch_css(url): """Fetch CSS from Google Fonts with Chrome User-Agent to get woff2.""" req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req) as resp: return resp.read().decode("utf-8") def parse_latin_font_faces(css_text): """Parse CSS for latin @font-face blocks and extract metadata + URL. Returns list of dicts with keys: family, weight, style, url """ results = [] # Split into blocks by looking for /* latin */ comment followed by @font-face # Google Fonts CSS2 uses comments like /* latin */ before each @font-face block parts = re.split(r'/\*\s*([^*]+?)\s*\*/', css_text) # parts alternates: [text_before, comment1, text_after1, comment2, text_after2, ...] i = 1 while i < len(parts): subset_label = parts[i].strip() block_text = parts[i + 1] if i + 1 < len(parts) else "" i += 2 if subset_label != "latin": continue # Extract the @font-face block face_match = re.search(r'@font-face\s*\{([^}]+)\}', block_text) if not face_match: continue face_body = face_match.group(1) # Extract font-family family_match = re.search(r"font-family:\s*'([^']+)'", face_body) if not family_match: continue family = family_match.group(1) # Extract font-weight weight_match = re.search(r'font-weight:\s*(\d+)', face_body) weight = weight_match.group(1) if weight_match else "400" # Extract font-style style_match = re.search(r'font-style:\s*(\w+)', face_body) style = style_match.group(1) if style_match else "normal" # Extract woff2 URL url_match = re.search(r"url\(([^)]+\.woff2[^)]*)\)", face_body) if not url_match: continue font_url = url_match.group(1) results.append({ "family": family, "weight": weight, "style": style, "url": font_url, }) return results def font_filename(family, weight, style): """Generate filename: {family}-{weight}{-italic}.woff2""" # Normalize family name: "Source Serif 4" -> "source-serif-4" slug = family.lower().replace(" ", "-") suffix = "-italic" if style == "italic" else "" return f"{slug}-{weight}{suffix}.woff2" def download_font(url, dest_path): """Download a font file.""" req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req) as resp: data = resp.read() with open(dest_path, "wb") as f: f.write(data) return len(data) def main(): os.makedirs(FONTS_DIR, exist_ok=True) downloaded = [] for css_url in FONT_REQUESTS: print(f"\nFetching CSS: {css_url[:80]}...") css_text = fetch_css(css_url) faces = parse_latin_font_faces(css_text) print(f" Found {len(faces)} latin @font-face block(s)") for face in faces: filename = font_filename(face["family"], face["weight"], face["style"]) dest = os.path.join(FONTS_DIR, filename) print(f" Downloading {filename}...", end=" ") size = download_font(face["url"], dest) print(f"({size:,} bytes)") downloaded.append(filename) print(f"\nDone! Downloaded {len(downloaded)} font files to src/fonts/:") for f in sorted(downloaded): print(f" {f}") if __name__ == "__main__": main()