-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathfont_management.py
More file actions
170 lines (134 loc) · 6.28 KB
/
font_management.py
File metadata and controls
170 lines (134 loc) · 6.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
Font Management Module
Handles font loading, Google Fonts integration, and caching.
"""
import os
import re
from pathlib import Path
from typing import Optional
import requests
FONTS_DIR = "fonts"
FONTS_CACHE_DIR = Path(FONTS_DIR) / "cache"
def download_google_font(font_family: str, weights: list = None) -> Optional[dict]:
"""
Download a font family from Google Fonts and cache it locally.
Returns dict with font paths for different weights, or None if download fails.
:param font_family: Google Fonts family name (e.g., 'Noto Sans JP', 'Open Sans')
:param weights: List of font weights to download (300=light, 400=regular, 700=bold)
:return: Dict with 'light', 'regular', 'bold' keys mapping to font file paths
"""
if weights is None:
weights = [300, 400, 700]
# Create fonts cache directory
FONTS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
# Normalize font family name for file paths
font_name_safe = font_family.replace(" ", "_").lower()
font_files = {}
try:
# Google Fonts API endpoint - request all weights at once
weights_str = ";".join(map(str, weights))
api_url = "https://fonts.googleapis.com/css2"
# Use requests library for cleaner HTTP handling
params = {"family": f"{font_family}:wght@{weights_str}"}
headers = {
"User-Agent": "Mozilla/5.0" # Get .woff2 files (better compression)
}
# Fetch CSS file
response = requests.get(api_url, params=params, headers=headers, timeout=10)
response.raise_for_status()
css_content = response.text
# Parse CSS to extract weight-specific URLs
# Google Fonts CSS has @font-face blocks with font-weight and src: url()
weight_url_map = {}
# Split CSS into font-face blocks
font_face_blocks = re.split(r"@font-face\s*\{", css_content)
for block in font_face_blocks[1:]: # Skip first empty split
# Extract font-weight
weight_match = re.search(r"font-weight:\s*(\d+)", block)
if not weight_match:
continue
weight = int(weight_match.group(1))
# Extract URL (prefer woff2, fallback to ttf)
url_match = re.search(r"url\((https://[^)]+\.(woff2|ttf))\)", block)
if url_match:
weight_url_map[weight] = url_match.group(1)
# Map weights to our keys
weight_map = {300: "light", 400: "regular", 700: "bold"}
# Download each weight
for weight in weights:
weight_key = weight_map.get(weight, "regular")
# Find URL for this weight
weight_url = weight_url_map.get(weight)
# If exact weight not found, try to find closest
if not weight_url and weight_url_map:
# Find closest weight
closest_weight = min(
weight_url_map.keys(), key=lambda x: abs(x - weight)
)
weight_url = weight_url_map[closest_weight]
print(
f" Using weight {closest_weight} for {weight_key} (requested {weight} not available)"
)
if weight_url:
# Determine file extension
file_ext = "woff2" if weight_url.endswith(".woff2") else "ttf"
# Download font file
font_filename = f"{font_name_safe}_{weight_key}.{file_ext}"
font_path = FONTS_CACHE_DIR / font_filename
if not font_path.exists():
print(f" Downloading {font_family} {weight_key} ({weight})...")
try:
font_response = requests.get(weight_url, timeout=10)
font_response.raise_for_status()
font_path.write_bytes(font_response.content)
except Exception as e:
print(f" ⚠ Failed to download {weight_key}: {e}")
continue
else:
print(f" Using cached {font_family} {weight_key}")
font_files[weight_key] = str(font_path)
# Ensure we have at least regular weight
if "regular" not in font_files and font_files:
# Use first available as regular
font_files["regular"] = list(font_files.values())[0]
print(f" Using {list(font_files.keys())[0]} weight as regular")
# If we don't have all three weights, duplicate available ones
if "bold" not in font_files and "regular" in font_files:
font_files["bold"] = font_files["regular"]
print(" Using regular weight as bold")
if "light" not in font_files and "regular" in font_files:
font_files["light"] = font_files["regular"]
print(" Using regular weight as light")
return font_files if font_files else None
except Exception as e:
print(f"⚠ Error downloading Google Font '{font_family}': {e}")
return None
def load_fonts(font_family: Optional[str] = None) -> Optional[dict]:
"""
Load fonts from local directory or download from Google Fonts.
Returns dict with font paths for different weights.
:param font_family: Google Fonts family name (e.g., 'Noto Sans JP', 'Open Sans').
If None, uses local Roboto fonts.
:return: Dict with 'bold', 'regular', 'light' keys mapping to font file paths,
or None if all loading methods fail
"""
# If custom font family specified, try to download from Google Fonts
if font_family and font_family.lower() != "roboto":
print(f"Loading Google Font: {font_family}")
fonts = download_google_font(font_family)
if fonts:
print(f"✓ Font '{font_family}' loaded successfully")
return fonts
print(f"⚠ Failed to load '{font_family}', falling back to local Roboto")
# Default: Load local Roboto fonts
fonts = {
"bold": os.path.join(FONTS_DIR, "Roboto-Bold.ttf"),
"regular": os.path.join(FONTS_DIR, "Roboto-Regular.ttf"),
"light": os.path.join(FONTS_DIR, "Roboto-Light.ttf"),
}
# Verify fonts exist
for _weight, path in fonts.items():
if not os.path.exists(path):
print(f"⚠ Font not found: {path}")
return None
return fonts