feat(download-google-font): fetch subsetted fonts and local css
This commit is contained in:
Executable
+132
@@ -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()
|
||||
Reference in New Issue
Block a user