diff --git a/download_google_font.py b/download_google_font.py new file mode 100755 index 0000000..205ca3c --- /dev/null +++ b/download_google_font.py @@ -0,0 +1,132 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = ["httpx"] +# /// + +import argparse +import logging +import re +from pathlib import Path + +import httpx + + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + +HEADERS = { + "sec-ch-ua": '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("url", help="Google Fonts CSS URL") + parser.add_argument("-o", "--output", type=Path, default=Path.cwd(), help="Output directory") + parser.add_argument("--subsets", default="latin,latin-ext", help="Comma-separated unicode subsets to include") + return parser.parse_args() + + +def filter_css_by_subsets(css_content: str, subsets: list[str]) -> str: + """Filter @font-face rules to only include specified unicode subsets.""" + font_faces = re.findall(r"@font-face\s*{[^}]+}", css_content, re.DOTALL) + filtered_faces = [] + + for face in font_faces: + if any(subset in face for subset in subsets): + filtered_faces.append(face) + + return "\n".join(filtered_faces) + + +def parse_googlefont_stylesheet(css_content: str) -> list[dict]: + """Parse Google Fonts CSS and return list of font entries with metadata.""" + comments = re.findall(r"/\*\s*([^*]+)\s*\*/", css_content) + families = re.findall(r"font-family:\s*['\"]([^'\"]+)['\"]", css_content) + styles = re.findall(r"font-style:\s*(\w+)", css_content) + weights = re.findall(r"font-weight:\s*([^;]+)", css_content) + displays = re.findall(r"font-display:\s*(\w+)", css_content) + urls = re.findall(r"url\(([^)]+)\)", css_content) + ranges = re.findall(r"unicode-range:\s*([^;]+)", css_content) + + fonts = [] + for comment, family, style, weight, display, url, unicode_range in zip(comments, families, styles, weights, displays, urls, ranges): + entry = { + "subset": comment.strip().lower(), + "family": family, + "style": style, + "weight": weight.strip(), + "display": display, + "url": url.strip("'\""), + "unicode_range": unicode_range.strip(), + } + fonts.append(entry) + + return fonts + + +def download_google_fonts(url: str, output_dir: Path, subsets: list[str]) -> None: + client = httpx.Client(headers=HEADERS) + output_dir = output_dir.resolve() + output_dir.mkdir(parents=True, exist_ok=True) + fonts_dir = output_dir / "fonts" + fonts_dir.mkdir(parents=True, exist_ok=True) + + logger.info(f"Downloading from {url}") + response = client.get(url) + response.raise_for_status() + css_content = response.text + + fonts = parse_googlefont_stylesheet(css_content=css_content) + + filtered_fonts = [] + for font in fonts: + if font.get("subset") in subsets: + filtered_fonts.append(font) + + logger.info(f"Found {len(filtered_fonts)} font entries for subsets: {', '.join(subsets)}") + + for font in filtered_fonts: + font_url = font["url"] + logger.info(f"Downloading {font_url}") + font_response = client.get(url=font_url) + font_response.raise_for_status() + + font_filename = Path(font_url).name + font_path = fonts_dir / font_filename + font_path.write_bytes(font_response.content) + logger.info(f"Saved to {font_path}") + + css_lines = [] + for font in filtered_fonts: + css_lines.append(f"/* {font['subset']} */") + css_lines.append("@font-face {") + css_lines.append(f" font-family: '{font['family']}';") + css_lines.append(f" font-style: {font['style']};") + css_lines.append(f" font-weight: {font['weight']};") + css_lines.append(f" font-display: {font['display']};") + font_filename = Path(font["url"]).name + css_lines.append(f" src: url(fonts/{font_filename}) format('woff2');") + css_lines.append(f" unicode-range: {font['unicode_range']};") + css_lines.append("}") + + local_css_content = "\n".join(css_lines) + + css_file = output_dir / "fonts.css" + css_file.write_text(local_css_content) + logger.info(f"CSS file saved to {css_file}") + + +def main() -> None: + args = parse_args() + subset_list = [s.strip() for s in args.subsets.split(",")] + download_google_fonts( + url=args.url, + output_dir=args.output, + subsets=subset_list, + ) + + +if __name__ == "__main__": + main()