from __future__ import annotations import argparse import hashlib import shutil import sys import tempfile import urllib.request import zipfile from pathlib import Path from fontTools.ttLib import TTFont ROOT = Path(__file__).resolve().parent CACHE = ROOT / ".cache" DEFAULT_OUTPUT = ROOT / "dist" INTER_URL = "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" INTER_SHA256 = "9883fdd4a49d4fb66bd8177ba6625ef9a64aa45899767dde3d36aa425756b11e" LICENSE_SHA256 = "262481e844521b326f5ecd053e59b98c8b2da78c8ee1bdbb6e8174305e54935a" FONTS_URL = ( "https://raw.githubusercontent.com/BloCamLimb/ModernUI-MC/" "ce9862da07068fed3a44d84f7ca921e897d6fa1b/libs/ModernUI-Fonts-5.0.jar" ) FONTS_SHA256 = "095260084d42d14ed73087b6066772d02083bc2a4f0c58fffcc8adef553e7e37" WEIGHTS = ( ("Thin", "thin"), ("ExtraLight", "extra-light"), ("Light", "light"), ("Regular", "regular"), ("Medium", "medium"), ("SemiBold", "semi-bold"), ("Bold", "bold"), ("ExtraBold", "extra-bold"), ("Black", "black"), ) MODIFIED_TIMES = {False: 3838697782, True: 3838700032} MEDIUM_REFERENCES = { False: ( "assets/modernui/font/inter-frozen-medium.otf", "b12e9633978feeb319a1f66deb7236cc069b4390b651573eeda73ce26085d997", ), True: ( "assets/modernui/font/inter-frozen-medium-italic.otf", "be8a39d64e5a60b8d670fb3e9be5ca7c072d25aecdf33ce5f462341b7c2e230a", ), } EXPECTED_OUTPUTS = { "inter-frozen-thin.otf": "010b50357cd57ae233eef5fdbb54d8b544775617a972fa8ce34af89ab021b604", "inter-frozen-thin-italic.otf": "a03f545277cd0e7d9ea80e266b75bdbc6e74de29cb3d9b892c7939b48e478ed8", "inter-frozen-extra-light.otf": "45d9ff50f7feacce319b808f963df535d9052ead26773851598e564c6e056429", "inter-frozen-extra-light-italic.otf": "54534bb1bf6538eded2ea9b7ce56081f69eb3613e8cf59d748d7dc94d17ee2b0", "inter-frozen-light.otf": "6e85d7acc844c36928556042fde0dc314beff9de337f113bfa9ccad946dbf0ba", "inter-frozen-light-italic.otf": "cb06ff9f830eaed7a187ae8281f11a8151f7e6da2b16f3d49254006c2916f4ea", "inter-frozen-regular.otf": "e1e51859a61e4cd94e9b7354871013a346c7e0938e95f8595b9eee560ea870da", "inter-frozen-italic.otf": "56823188393701ba87499d961f060728509e937265f118729de5762dca350493", "inter-frozen-medium.otf": "b12e9633978feeb319a1f66deb7236cc069b4390b651573eeda73ce26085d997", "inter-frozen-medium-italic.otf": "be8a39d64e5a60b8d670fb3e9be5ca7c072d25aecdf33ce5f462341b7c2e230a", "inter-frozen-semi-bold.otf": "aba3d0479cebed344b0b89db9020ef44ea72000045babf1f0a9c46778c65df81", "inter-frozen-semi-bold-italic.otf": "0b708c4f258188dd22c323d80f263752b8afe339b7d0aa0726ab9cb55184e268", "inter-frozen-bold.otf": "8edd421890765fffba993c241824230f120283fce01208a8df88ef99004f8238", "inter-frozen-bold-italic.otf": "67cc5264f5b2a28290217850ee17bc4ae0d5d114a08e3eee6b7277566cb741a0", "inter-frozen-extra-bold.otf": "96e334e23f6799efdf2911dbf263620ad8195a1db74de067e5c3655a9ef1357f", "inter-frozen-extra-bold-italic.otf": "db77a6ad1e4a7a05eb49b8b281d12f394edccbdb7c510801a6973c9e8a0a192e", "inter-frozen-black.otf": "945f77bd2c670870e310f95995d0f22ede1878f99392e09f673fda1705037733", "inter-frozen-black-italic.otf": "375d5cb426a9930efd1214bff3516a14855b9bb5f4d591df5d76511852a6edcd", } UNTOUCHED_TABLES = ("GDEF", "hhea", "hmtx", "maxp", "post") def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as file: for chunk in iter(lambda: file.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def require_hash(path: Path, expected: str) -> None: actual = sha256(path) if actual != expected: raise RuntimeError(f"SHA-256 mismatch for {path}: expected {expected}, got {actual}") def download(url: str, target: Path, expected: str) -> None: if target.exists(): require_hash(target, expected) print(f"Using cached {target.name}") return print(f"Downloading {url}") temporary = target.with_suffix(target.suffix + ".part") with urllib.request.urlopen(url, timeout=120) as response, temporary.open("wb") as file: shutil.copyfileobj(response, file) require_hash(temporary, expected) temporary.replace(target) def archive( supplied: Path | None, cached: Path, url: str, expected: str, offline: bool, ) -> Path: if supplied is not None: if not supplied.is_file(): raise RuntimeError(f"Archive does not exist: {supplied}") require_hash(supplied, expected) return supplied if cached.is_file(): require_hash(cached, expected) return cached if offline: raise RuntimeError(f"Offline mode requires archive: {cached.name}") cached.parent.mkdir(parents=True, exist_ok=True) download(url, cached, expected) return cached def extract(archive: Path, member: str, target: Path, expected: str | None = None) -> None: with zipfile.ZipFile(archive) as zipped: data = zipped.read(member) actual = hashlib.sha256(data).hexdigest() if expected is not None and actual != expected: raise RuntimeError( f"SHA-256 mismatch for {archive.name}:{member}: expected {expected}, got {actual}" ) target.write_bytes(data) def feature_record(font: TTFont, tag: str): records = [ record for record in font["GSUB"].table.FeatureList.FeatureRecord if record.FeatureTag == tag ] if len(records) != 1: raise RuntimeError(f"Expected one GSUB feature {tag}, found {len(records)}") return records[0] def single_substitutions(font: TTFont, lookup_indexes: list[int]) -> dict[str, str]: substitutions: dict[str, str] = {} lookups = font["GSUB"].table.LookupList.Lookup for lookup_index in lookup_indexes: lookup = lookups[lookup_index] if lookup.LookupType != 1: raise RuntimeError( f"Expected GSUB SingleSubst lookup at index {lookup_index}, " f"found type {lookup.LookupType}" ) for subtable in lookup.SubTable: substitutions.update(subtable.mapping) return substitutions def name_value(font: TTFont, name_id: int) -> str | None: record = font["name"].getName(name_id, 3, 1, 0x409) return record.toUnicode() if record is not None else None def frozen_name(value: str) -> str: if value == "Inter": return "Inter Frozen" if value.startswith("Inter-"): return "InterFrozen-" + value.removeprefix("Inter-") if value.startswith("Inter "): return "Inter Frozen " + value.removeprefix("Inter ") raise RuntimeError(f"Unexpected Inter name: {value}") def set_names(font: TTFont, italic: bool) -> None: license_text = ( "This Font Software is licensed under the SIL Open Font License, Version 1.1." if italic else "This Font Software is licensed under the SIL Open Font License, Version 1.1" ) license_url = "http://scripts.sil.org/OFL" if italic else "https://scripts.sil.org/OFL" values = { 0: "Copyright 2016 The Inter Project Authors, modified by BloCamLimb 2025", 5: "Version 4.001", 10: ( "Modified from Inter Version 4.001;git-9221beed3. " "Activates cv05, tnum. Deactivates calt." ), 13: license_text, 14: license_url, } for name_id in (1, 4, 6, 16, 21): original = name_value(font, name_id) if original is not None: values[name_id] = frozen_name(original) values[3] = f"4.001;{values[6]}" names = font["name"] for name_id, value in values.items(): names.setName(value, name_id, 3, 1, 0x409) def build_font(source: Path, output: Path, modified: int, italic: bool) -> None: font = TTFont(source, recalcBBoxes=False, recalcTimestamp=False) font.ensureDecompiled() untouched = {tag: font.getTableData(tag) for tag in UNTOUCHED_TABLES} calt = feature_record(font, "calt") cv05 = feature_record(font, "cv05") tnum = feature_record(font, "tnum") cv05_lookups = list(cv05.Feature.LookupListIndex) tnum_lookups = list(tnum.Feature.LookupListIndex) tnum_substitutions = single_substitutions(font, tnum_lookups) cv05_substitutions = single_substitutions(font, cv05_lookups) if len(tnum_substitutions) != 61 or len(cv05_substitutions) != 15: raise RuntimeError( "Unexpected feature sizes: " f"tnum={len(tnum_substitutions)}, cv05={len(cv05_substitutions)}" ) substitutions = tnum_substitutions | cv05_substitutions unicode_cmaps = 0 for table in font["cmap"].tables: if table.isUnicode(): unicode_cmaps += 1 changed = sum(glyph in substitutions for glyph in table.cmap.values()) if changed != 76: raise RuntimeError( f"Expected 76 cmap changes in format {table.format}, found {changed}" ) table.cmap = { codepoint: substitutions.get(glyph, glyph) for codepoint, glyph in table.cmap.items() } if unicode_cmaps != 4: raise RuntimeError(f"Expected four Unicode cmap tables, found {unicode_cmaps}") calt.FeatureTag = "DELT" calt.Feature.FeatureParams = None calt.Feature.LookupListIndex = [] cv05.FeatureTag = "calt" cv05.Feature.FeatureParams = None cv05.Feature.LookupListIndex = tnum_lookups + cv05_lookups tnum.FeatureTag = "DELT" tnum.Feature.FeatureParams = None tnum.Feature.LookupListIndex = [] font["OS/2"].fsSelection &= ~(1 << 7) font["head"].modified = modified set_names(font, italic) font.save(output, reorderTables=True) built = TTFont(output, recalcBBoxes=False, recalcTimestamp=False) for tag, expected in untouched.items(): if built.getTableData(tag) != expected: raise RuntimeError(f"Unexpected change in {tag} table for {output.name}") if built["OS/2"].fsSelection & (1 << 7): raise RuntimeError(f"USE_TYPO_METRICS remains enabled in {output.name}") built_calt = feature_record(built, "calt") if list(built_calt.Feature.LookupListIndex) != tnum_lookups + cv05_lookups: raise RuntimeError(f"Unexpected calt lookups in {output.name}") if any( record.FeatureTag in {"cv05", "tnum"} for record in built["GSUB"].table.FeatureList.FeatureRecord ): raise RuntimeError(f"Unfrozen cv05 or tnum feature remains in {output.name}") def source_member(weight: str, italic: bool) -> str: style = "Italic" if weight == "Regular" and italic else weight + ("Italic" if italic else "") return f"extras/otf/Inter-{style}.otf" def output_name(weight: str, slug: str, italic: bool) -> str: if weight == "Regular" and italic: return "inter-frozen-italic.otf" suffix = "-italic" if italic else "" return f"inter-frozen-{slug}{suffix}.otf" def write_package(output_dir: Path, outputs: list[Path]) -> None: checksums = output_dir / "SHA256SUMS" packaged = [output_dir / "OFL-1.1.txt", *outputs] checksums.write_text( "".join(f"{sha256(path)} {path.name}\n" for path in sorted(packaged)), encoding="ascii", ) package = output_dir / "inter-frozen-all-weights.zip" with zipfile.ZipFile(package, "w", zipfile.ZIP_STORED) as zipped: for path in [*packaged, checksums]: info = zipfile.ZipInfo(path.name, (2025, 8, 23, 0, 0, 0)) info.compress_type = zipfile.ZIP_STORED info.external_attr = 0o644 << 16 zipped.writestr(info, path.read_bytes()) print(f"Packaged {len(outputs)} fonts: {package}") def arguments() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Build the Inter Frozen font family") parser.add_argument("--inter-archive", type=Path) parser.add_argument("--reference-archive", type=Path) parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("--offline", action="store_true") return parser.parse_args() def main() -> None: args = arguments() output_dir = args.output_dir.resolve() output_dir.mkdir(parents=True, exist_ok=True) inter_zip = archive( args.inter_archive, CACHE / "Inter-4.1.zip", INTER_URL, INTER_SHA256, args.offline, ) fonts_jar = None if args.reference_archive is not None or not args.offline: fonts_jar = archive( args.reference_archive, CACHE / "ModernUI-Fonts-5.0.jar", FONTS_URL, FONTS_SHA256, args.offline, ) extract(inter_zip, "LICENSE.txt", output_dir / "OFL-1.1.txt", LICENSE_SHA256) outputs: list[Path] = [] with tempfile.TemporaryDirectory(prefix="inter-frozen-") as temporary: sources = Path(temporary) for weight, slug in WEIGHTS: for italic in (False, True): member = source_member(weight, italic) source = sources / Path(member).name output = output_dir / output_name(weight, slug, italic) extract(inter_zip, member, source) build_font(source, output, MODIFIED_TIMES[italic], italic) require_hash(output, EXPECTED_OUTPUTS[output.name]) outputs.append(output) if weight == "Medium" and fonts_jar is not None: reference_member, expected = MEDIUM_REFERENCES[italic] reference = sources / f"reference-{output.name}" extract(fonts_jar, reference_member, reference, expected) if output.read_bytes() != reference.read_bytes(): raise RuntimeError(f"Generated font differs from reference: {output}") print(f"Verified byte-identical reference: {output}") else: print(f"Verified generated output: {output}") if len(outputs) != 18 or len({output.name for output in outputs}) != 18: raise RuntimeError("Expected 18 unique generated fonts") write_package(output_dir, outputs) if __name__ == "__main__": try: main() except Exception as error: print(f"ERROR: {error}", file=sys.stderr) raise