mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
159 lines
6.1 KiB
Python
159 lines
6.1 KiB
Python
"""Build the reading fonts the app bundles, from the webmail's copies.
|
|
|
|
Settings > Accessibility > Font offers the webmail's three reading fonts
|
|
(app/accessibility.css): Atkinson Hyperlegible Next, Lexend and OpenDyslexic,
|
|
all under the SIL Open Font License. The webmail ships them as woff2 subsets
|
|
(latin and latin-ext). The app needs them twice:
|
|
|
|
- Android draws UI text with fonts from android/app/src/main/assets/fonts,
|
|
looked up by family name, with a `_bold` file for bold text. Android can't
|
|
read woff2 or variable weights there, so each family becomes a static
|
|
Regular (400) and Bold (700) TTF, latin and latin-ext merged.
|
|
- A plain-text mail body renders in a WebView, which reads the woff2 files
|
|
directly; they go into src/theme/reading-fonts-data.ts as base64 so the
|
|
sandboxed page (CSP font-src data:) can load them without file access.
|
|
|
|
Run from the app root with the webmail checked out two levels up (or
|
|
WEBMAIL_DIR set to it):
|
|
python scripts/build-reading-fonts.py
|
|
Needs fontTools with brotli (pip install fonttools brotli).
|
|
"""
|
|
import base64
|
|
import io
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
from fontTools.merge import Merger
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools.varLib.instancer import instantiateVariableFont
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
# The webmail checkout (the app lives in its repos/react-native); WEBMAIL_DIR
|
|
# points elsewhere, e.g. from a separate worktree.
|
|
WEBMAIL_FONTS = os.path.join(os.environ.get('WEBMAIL_DIR') or os.path.join(ROOT, '..', '..'), 'app', 'fonts')
|
|
ASSETS = os.path.join(ROOT, 'android', 'app', 'src', 'main', 'assets', 'fonts')
|
|
DATA = os.path.join(ROOT, 'src', 'theme', 'reading-fonts-data.ts')
|
|
|
|
# Android family name -> the webmail's woff2 subsets (latin first).
|
|
FAMILIES = {
|
|
'AtkinsonHyperlegibleNext': {
|
|
'variable': ['atkinson-hyperlegible-next-latin-wght-normal.woff2',
|
|
'atkinson-hyperlegible-next-latin-ext-wght-normal.woff2'],
|
|
},
|
|
'Lexend': {
|
|
'variable': ['lexend-latin-wght-normal.woff2', 'lexend-latin-ext-wght-normal.woff2'],
|
|
},
|
|
'OpenDyslexic': {
|
|
400: ['opendyslexic-latin-400-normal.woff2'],
|
|
700: ['opendyslexic-latin-700-normal.woff2'],
|
|
},
|
|
}
|
|
|
|
# The woff2 files a mail body loads: (export name, css family, weight range, files).
|
|
WEB_FACES = [
|
|
('ATKINSON', 'Atkinson Hyperlegible Next', '200 800', FAMILIES['AtkinsonHyperlegibleNext']['variable']),
|
|
('LEXEND', 'Lexend', '100 900', FAMILIES['Lexend']['variable']),
|
|
('OPENDYSLEXIC', 'OpenDyslexic', '400', FAMILIES['OpenDyslexic'][400]),
|
|
]
|
|
|
|
|
|
def load(name):
|
|
return TTFont(os.path.join(WEBMAIL_FONTS, name))
|
|
|
|
|
|
def static_instance(name, weight):
|
|
font = load(name)
|
|
if 'fvar' in font:
|
|
font = instantiateVariableFont(font, {'wght': weight}, updateFontNames=False)
|
|
font.flavor = None
|
|
return font
|
|
|
|
|
|
def merge(fonts, out_path):
|
|
paths = []
|
|
for i, font in enumerate(fonts):
|
|
tmp = out_path + f'.part{i}.ttf'
|
|
font.save(tmp)
|
|
paths.append(tmp)
|
|
if len(paths) == 1:
|
|
os.replace(paths[0], out_path)
|
|
return
|
|
merged = Merger().merge(paths)
|
|
merged.save(out_path)
|
|
for p in paths:
|
|
os.remove(p)
|
|
|
|
|
|
def build_assets():
|
|
os.makedirs(ASSETS, exist_ok=True)
|
|
for family, spec in FAMILIES.items():
|
|
for weight, suffix in ((400, ''), (700, '_bold')):
|
|
files = spec.get('variable') or spec[weight]
|
|
fonts = [static_instance(f, weight) for f in files]
|
|
out = os.path.join(ASSETS, family + suffix + '.ttf')
|
|
merge(fonts, out)
|
|
print('wrote', os.path.relpath(out, ROOT), os.path.getsize(out))
|
|
|
|
|
|
def unicode_ranges():
|
|
"""woff2 file name -> its unicode-range in the webmail's app/accessibility.css."""
|
|
css_path = os.path.join(WEBMAIL_FONTS, '..', 'accessibility.css')
|
|
with open(css_path, encoding='utf-8') as fh:
|
|
css = fh.read()
|
|
out = {}
|
|
for block in re.findall(r'@font-face\s*\{([^}]*)\}', css):
|
|
src = re.search(r"fonts/([^\"')]+\.woff2)", block)
|
|
rng = re.search(r'unicode-range:\s*([^;]+);', block)
|
|
if src:
|
|
out[src.group(1)] = rng.group(1).strip() if rng else ''
|
|
return out
|
|
|
|
|
|
def build_data():
|
|
ranges = unicode_ranges()
|
|
lines = [
|
|
"// Generated by scripts/build-reading-fonts.py from the webmail's",
|
|
'// app/fonts (SIL Open Font License 1.1, see',
|
|
'// android/app/src/main/assets/fonts/OFL.txt). Do not edit by hand. The',
|
|
'// woff2 files a plain-text mail body loads as data: URIs, with the',
|
|
"// unicode ranges of the webmail's app/accessibility.css.",
|
|
'',
|
|
'export interface ReadingFontFace {',
|
|
' /** CSS font-family name, as the webmail uses it. */',
|
|
' family: string;',
|
|
' /** font-weight descriptor (a range for variable fonts). */',
|
|
' weight: string;',
|
|
' /** One per unicode subset: base64 woff2 and its unicode-range ("" = all). */',
|
|
' subsets: Array<{ woff2: string; unicodeRange: string }>;',
|
|
'}',
|
|
'',
|
|
]
|
|
for export, family, weight, files in WEB_FACES:
|
|
lines.append(f'export const {export}: ReadingFontFace = {{')
|
|
lines.append(f" family: '{family}',")
|
|
lines.append(f" weight: '{weight}',")
|
|
lines.append(' subsets: [')
|
|
for name in files:
|
|
if name not in ranges:
|
|
sys.exit(f"{name} is not in the webmail's accessibility.css")
|
|
with open(os.path.join(WEBMAIL_FONTS, name), 'rb') as fh:
|
|
blob = base64.b64encode(fh.read()).decode('ascii')
|
|
lines.append(' {')
|
|
lines.append(f" unicodeRange: '{ranges[name]}',")
|
|
lines.append(f" woff2: '{blob}',")
|
|
lines.append(' },')
|
|
lines.append(' ],')
|
|
lines.append('};')
|
|
lines.append('')
|
|
with open(DATA, 'w', encoding='utf-8', newline='\n') as fh:
|
|
fh.write('\n'.join(lines))
|
|
print('wrote', os.path.relpath(DATA, ROOT), os.path.getsize(DATA))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if not os.path.isdir(WEBMAIL_FONTS):
|
|
sys.exit(f'No webmail fonts at {WEBMAIL_FONTS}')
|
|
build_assets()
|
|
build_data()
|