Files
raven/scripts/provision-text.py
T

198 lines
8.7 KiB
Python

#!/usr/bin/env python3
import argparse
import hashlib
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import urllib.request
FONT_PACKAGE = "http://archive.ubuntu.com/ubuntu/pool/main/f/fonts-liberation/fonts-liberation_2.1.5-3build1_all.deb"
FONT_PACKAGE_SHA256 = "47f156bc945cfc69cf2f2a0c19b8c82b67a685738d3846673462de6a73266e1b"
FONTS = {
"LiberationSans-Regular.ttf": "075e71be4c58beda172ecb574a799308f04c3d054c6a5b7434954d3b0f1b28d8",
"LiberationMono-Regular.ttf": "d18235f1e874771f4eaec3ddd6ddb4db849d334bdc6930c5c2185d9344834e62",
"LiberationSerif-Regular.ttf": "350f4ffd773cae53560c63dca3d89a5070595614fde12f772bf43feb63d81064",
}
DEJAVU_RELEASE = "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2"
DEJAVU_RELEASE_SHA256 = "fa9ca4d13871dd122f61258a80d01751d603b4d3ee14095d65453b4e846e17d7"
# Raven Sans stands in for the dialog face Tahoma. It is DejaVu Sans Condensed with every advance narrowed so the weighted
# average width of Latin text matches Tahoma's, and with Tahoma's vertical metrics so a cell has the same height. The
# targets are the published figures of @capsizecss/metrics 4.4.0 (Tahoma: ascent 2049, descent 423, average 917 regular
# and 1044 bold on 2048 units); 937 and 1058 are the same average measured on the pinned DejaVu faces.
DIALOG_FACES = {
"RavenSans-Regular.ttf": ("DejaVuSansCondensed.ttf", "Regular", 917 / 937),
"RavenSans-Bold.ttf": ("DejaVuSansCondensed-Bold.ttf", "Bold", 1044 / 1058),
}
DIALOG_ASCENT = 2049
DIALOG_DESCENT = 423
SKIA_REPOSITORY = "https://skia.googlesource.com/skia.git"
SKIA_COMMIT = "a21aae83d6e4e3da284100a30f0a5eafc443b60f"
SKIA_ARGUMENTS = """is_official_build = true
is_debug = false
cc = "clang"
cxx = "clang++"
skia_enable_ganesh = false
skia_enable_graphite = false
skia_enable_pdf = false
skia_enable_skottie = false
skia_enable_svg = false
skia_enable_tools = false
skia_use_partition_alloc = false
skia_use_gl = false
skia_use_x11 = false
skia_use_fontconfig = false
skia_use_freetype = true
skia_use_harfbuzz = false
skia_use_icu = false
skia_use_expat = false
skia_use_piex = false
skia_use_wuffs = false
skia_use_libjpeg_turbo_decode = false
skia_use_libjpeg_turbo_encode = false
skia_use_libpng_decode = false
skia_use_libpng_encode = false
skia_use_libwebp_decode = false
skia_use_libwebp_encode = false
skia_use_zlib = false
skia_use_perfetto = false
skia_enable_fontmgr_empty = true
skia_enable_fontmgr_custom_empty = true
skia_use_system_freetype2 = true
"""
def ar_member(archive, prefix):
at = 8
while at + 60 <= len(archive):
name = archive[at:at + 16].decode().strip()
size = int(archive[at + 48:at + 58].decode())
if name.startswith(prefix):
return name, archive[at + 60:at + 60 + size]
at += 60 + size + (size & 1)
raise RuntimeError(f"the font package has no {prefix} member")
def provision_fonts(output):
fonts = output / "fonts"
fonts.mkdir(exist_ok=True)
package = output / "fonts-liberation.deb"
if not package.exists():
partial = package.with_suffix(".part")
urllib.request.urlretrieve(FONT_PACKAGE, partial)
partial.replace(package)
data = package.read_bytes()
if hashlib.sha256(data).hexdigest() != FONT_PACKAGE_SHA256:
raise RuntimeError("font package checksum differs; delete it to download it again")
name, payload = ar_member(data, "data.tar")
tarball = output / name
tarball.write_bytes(payload)
subprocess.run(["tar", "-xf", tarball, "-C", fonts, "--strip-components=6", "--wildcards", "*/Liberation*-Regular.ttf"], check=True)
tarball.unlink()
for font, digest in FONTS.items():
if hashlib.sha256((fonts / font).read_bytes()).hexdigest() != digest:
raise RuntimeError(f"font checksum differs: {font}")
return fonts
def dialog_face(source, target, style, ratio):
from fontTools.ttLib import TTFont
font = TTFont(source, recalcTimestamp=False)
metrics = font["hmtx"].metrics
# Only the advances change: outlines, side bearings on the left and hinting stay as designed.
for glyph, (advance, bearing) in metrics.items():
metrics[glyph] = (round(advance * ratio), bearing)
font["hhea"].ascent = DIALOG_ASCENT
font["hhea"].descent = -DIALOG_DESCENT
font["hhea"].lineGap = 0
table = font["OS/2"]
table.usWinAscent = table.sTypoAscender = DIALOG_ASCENT
table.usWinDescent = DIALOG_DESCENT
table.sTypoDescender = -DIALOG_DESCENT
table.sTypoLineGap = 0
# Version 1 of the table defines the average on lower-case letters, which narrow by the same ratio.
table.xAvgCharWidth = round(table.xAvgCharWidth * ratio)
font["hhea"].recalc(font)
# The Bitstream Vera and Arev terms allow modified fonts only under names without their words; the notices stay.
names = font["name"]
full = "Raven Sans" if style == "Regular" else f"Raven Sans {style}"
replaced = {1: "Raven Sans", 2: style, 3: f"Raven Sans {style} 2.37", 4: full, 5: "Version 2.37",
6: f"RavenSans-{style}", 8: "Raven"}
names.names = [record for record in names.names if record.nameID not in (16, 17)]
for record in names.names:
if record.nameID in replaced:
record.string = replaced[record.nameID]
names.setName("Derived from DejaVu Sans Condensed 2.37 with narrower advances and taller cells.", 10, 3, 1, 0x409)
for word in ("Bitstream Vera", "Arev", "DejaVu Sans Condensed"):
for record in names.names:
if record.nameID in (1, 3, 4, 6) and word in str(record):
raise RuntimeError(f"the dialog face is still named after {word}")
font.save(target)
def provision_dialog_faces(output, fonts):
archive = output / "dejavu-fonts-ttf-2.37.tar.bz2"
if not archive.exists():
partial = archive.with_suffix(".part")
urllib.request.urlretrieve(DEJAVU_RELEASE, partial)
partial.replace(archive)
if hashlib.sha256(archive.read_bytes()).hexdigest() != DEJAVU_RELEASE_SHA256:
raise RuntimeError("DejaVu release checksum differs; delete it to download it again")
source = output / "dejavu"
source.mkdir(exist_ok=True)
# Python reads the bzip2 archive itself, so the host needs no bzip2 program.
with tarfile.open(archive, "r:bz2") as release:
for name in [f"ttf/{face}" for face, _, _ in DIALOG_FACES.values()] + ["LICENSE"]:
member = release.extractfile(f"dejavu-fonts-ttf-2.37/{name}")
(source / Path(name).name).write_bytes(member.read())
for target, (face, style, ratio) in DIALOG_FACES.items():
dialog_face(source / face, fonts / target, style, ratio)
shutil.copyfile(source / "LICENSE", fonts / "RavenSans-LICENSE.txt")
shutil.rmtree(source)
def provision_skia(output, jobs):
source = output / "skia"
if not source.exists():
subprocess.run(["git", "init", "-q", source], check=True)
subprocess.run(["git", "-C", source, "fetch", "-q", "--depth=1", SKIA_REPOSITORY, SKIA_COMMIT], check=True)
subprocess.run(["git", "-C", source, "checkout", "-q", "FETCH_HEAD"], check=True)
subprocess.run([sys.executable, "tools/git-sync-deps"], cwd=source, check=True)
subprocess.run([sys.executable, "bin/fetch-gn"], cwd=source, check=True)
subprocess.run([sys.executable, "bin/fetch-ninja"], cwd=source, check=True)
build = output / "cpu-fonts"
build.mkdir(exist_ok=True)
(build / "args.gn").write_text(SKIA_ARGUMENTS)
subprocess.run([source / "bin/gn", "gen", build], cwd=source, check=True)
subprocess.run([source / "third_party/ninja/ninja", "-C", build, f"-j{jobs}", "skia"], check=True)
return source, build / "libskia.a"
def main():
parser = argparse.ArgumentParser(description="Provide the pinned Liberation fonts, the Raven Sans dialog faces and the CPU build of Skia that text in guest windows needs")
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--jobs", type=int, default=2)
parser.add_argument("--fonts-only", action="store_true", help="only the fonts, as Skia takes a long time to build")
args = parser.parse_args()
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
for tool in ("tar", "git", "clang", "clang++"):
if not shutil.which(tool):
raise RuntimeError(f"required tool is missing: {tool}")
fonts = provision_fonts(output)
provision_dialog_faces(output, fonts)
if args.fonts_only:
print(f"configure Raven with -DRAVEN_FONT_DIRECTORY={fonts}")
return
source, library = provision_skia(output, args.jobs)
print(f"configure Raven with -DRAVEN_SKIA_SOURCE={source} -DRAVEN_SKIA_LIBRARY={library} -DRAVEN_FONT_DIRECTORY={fonts}")
if __name__ == "__main__":
os.umask(0o022)
main()