Last active
January 28, 2025 12:15
-
-
Save ziap/0d9bb64d3639962059b97eae5fbfbcbe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncio | |
from base64 import urlsafe_b64encode | |
from hashlib import sha384 | |
import os | |
import aiofiles | |
from aiohttp.client import ClientSession | |
# Fake user agent to trick the google font api to give us the .woff2 fonts | |
# Retrive one by typing `navigator.userAgent` in the browser's console | |
UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36' | |
async def download_css(url: str, session: ClientSession) -> str: | |
async with session.get(url, headers={'User-Agent': UA}) as response: | |
return await response.text() | |
async def download_resource( | |
url: str, session: ClientSession, dir_prefix: str, url_prefix: str | |
) -> str: | |
url = url.strip() | |
while len(url) and url[0] == url[-1] and url[0] in "'\"": | |
url = url[1:-1] | |
async with session.get(url, headers={'User-Agent': UA}) as response: | |
font = await response.read() | |
# Create a 192-bit hash by xor-ing the two halves of a 384-bit hash | |
hash = sha384(font).digest() | |
h1 = int.from_bytes(hash[:24], byteorder='big') | |
h2 = int.from_bytes(hash[24:], byteorder='big') | |
h = (h1 ^ h2).to_bytes(24, byteorder='big') | |
subtype, ext = response.content_type.split(';')[0].split('/') | |
name = urlsafe_b64encode(h).decode('ascii') | |
prefix = subtype + 's' | |
filename = f'{prefix}/{name}.{ext}' | |
os.makedirs(dir_prefix + prefix, exist_ok=True) | |
async with aiofiles.open(dir_prefix + filename, 'wb') as f: | |
await f.write(font) | |
return url_prefix + filename | |
async def main( | |
fonts_url: list[str], dir_prefix: str, url_prefix: str, output: str | |
) -> None: | |
async with ClientSession() as session: | |
css_tasks = [download_css(url, session) for url in fonts_url] | |
css = ''.join(await asyncio.gather(*css_tasks)) | |
segments: list[str] = [] | |
urls: list[str] = [] | |
# Extract anything between these two patterns | |
pat_start = 'url(' | |
pat_end = ')' | |
while (start := css.find(pat_start)) != -1: | |
start += len(pat_start) | |
segments.append(css[:start]) | |
end = css.find(pat_end, start) | |
urls.append(css[start:end]) | |
css = css[end:] | |
segments.append(css) | |
resource_tasks = [ | |
download_resource(url, session, dir_prefix, url_prefix) | |
for url in urls | |
] | |
filenames = await asyncio.gather(*resource_tasks) | |
result = ''.join( | |
f'{segment}{filename}' | |
for segment, filename in zip(segments, filenames) | |
) + segments[-1] | |
async with aiofiles.open(output, 'w') as f: | |
await f.write(result) | |
DIR_PREFIX = './' | |
URL_PREFIX = '/' | |
asyncio.run(main([ | |
'https://fonts.googleapis.com/css2?family=Open+Sans:ital@0;1&display=swap', | |
'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,700;1,700&display=swap', | |
'https://fonts.googleapis.com/css2?family=Merriweather+Sans:ital,wght@0,700;1,700&display=swap', | |
'https://fonts.googleapis.com/css2?family=Fira+Code:wght@500&display=swap', | |
], DIR_PREFIX, URL_PREFIX, 'fonts.css')) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment