Skip to content

Instantly share code, notes, and snippets.

@ziap
Last active January 28, 2025 12:15
Show Gist options
  • Save ziap/0d9bb64d3639962059b97eae5fbfbcbe to your computer and use it in GitHub Desktop.
Save ziap/0d9bb64d3639962059b97eae5fbfbcbe to your computer and use it in GitHub Desktop.
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