maple-font/source/py/utils.py
2025-02-20 00:02:39 +08:00

254 lines
7.7 KiB
Python

import hashlib
from os import environ, path, remove, walk
import platform
import shutil
import subprocess
from urllib.request import Request, urlopen
from zipfile import ZIP_DEFLATED, ZipFile
from fontTools.ttLib import TTFont
from glyphsLib import GSFont
def is_ci():
ci_envs = [
"JENKINS_HOME",
"TRAVIS",
"CIRCLECI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"TF_BUILD",
]
for env in ci_envs:
if environ.get(env):
return True
return False
def run(command, extra_args=None, log=not is_ci()):
"""
Run a command line interface (CLI) command.
"""
if extra_args is None:
extra_args = []
if isinstance(command, str):
command = command.split()
subprocess.run(command + extra_args, stdout=subprocess.DEVNULL if not log else None)
def set_font_name(font: TTFont, name: str, id: int):
font["name"].setName(name, nameID=id, platformID=1, platEncID=0, langID=0x0)
font["name"].setName(name, nameID=id, platformID=3, platEncID=1, langID=0x409)
def get_font_name(font: TTFont, id: int) -> str:
return (
font["name"]
.getName(nameID=id, platformID=3, platEncID=1, langID=0x409)
.__str__()
)
def del_font_name(font: TTFont, id: int):
font["name"].removeNames(nameID=id)
def joinPaths(*args: list[str]) -> str:
return "/".join(args)
def get_font_forge_bin():
WIN_FONTFORGE_PATH = "C:/Program Files (x86)/FontForgeBuilds/bin/fontforge.exe"
MAC_FONTFORGE_PATH = (
"/Applications/FontForge.app/Contents/Resources/opt/local/bin/fontforge"
)
LINUX_FONTFORGE_PATH = "/usr/bin/fontforge"
system_name = platform.uname()[0]
result = ""
if "Darwin" in system_name:
result = MAC_FONTFORGE_PATH
elif "Windows" in system_name:
result = WIN_FONTFORGE_PATH
else:
result = LINUX_FONTFORGE_PATH
if not path.exists(result):
result = shutil.which("fontforge")
return result
def parse_github_mirror(github_mirror: str) -> str:
github = environ.get("GITHUB") # custom github mirror, for CN users
if not github:
github = github_mirror
return f"https://{github}"
def download_file(url: str, target_path: str):
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
req = Request(url, headers={"User-Agent": user_agent})
not_ci = not is_ci()
with urlopen(req) as response, open(target_path, "wb") as out_file:
total_size = int(response.getheader("Content-Length").strip())
downloaded_size = 0
block_size = 8192
while True:
buffer = response.read(block_size)
if not buffer:
break
out_file.write(buffer)
if not_ci:
downloaded_size += len(buffer)
percent_downloaded = (downloaded_size / total_size) * 100
print(
f"Downloading: [{percent_downloaded:.2f}%] {downloaded_size} / {total_size}",
end="\r",
)
def download_zip_and_extract(
name: str, url: str, zip_path: str, output_dir: str, remove_zip: bool = False
) -> bool:
if not path.exists(zip_path):
print(f"{name} does not exist, download from {url}")
try:
download_file(url, target_path=zip_path)
except Exception as e:
print(
f"\nFail to download {name}. Please check your internet connection or download it manually from {url}, then put downloaded zip into project's root and run this script again. \n Error: {e}"
)
return False
try:
with ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(output_dir)
if remove_zip:
remove(zip_path)
return True
except Exception as e:
print(f"Fail to extract {name}. Error: {e}")
return False
def check_font_patcher(
version: str, github_mirror: str = "github.com", target_dir: str = "FontPatcher"
) -> bool:
bin_path = f"{target_dir}/font-patcher"
if path.exists(target_dir):
with open(bin_path, "r", encoding="utf-8") as f:
if f"# Nerd Fonts Version: {version}" in f.read():
return True
else:
print("FontPatcher version not match, delete it")
shutil.rmtree("FontPatcher", ignore_errors=True)
zip_path = "FontPatcher.zip"
url = f"https://{github_mirror}/ryanoasis/nerd-fonts/releases/download/v{version}/{zip_path}"
if not download_zip_and_extract(
name="Nerd Font Patcher", url=url, zip_path=zip_path, output_dir=target_dir
):
return False
with open(bin_path, "r", encoding="utf-8") as f:
if f"# Nerd Fonts Version: {version}" in f.read():
return True
print(f"FontPatcher version is not {version}, please download it from {url}")
return False
def download_cn_base_font(
tag: str, zip_path: str, target_dir: str, github_mirror: str = "github.com"
) -> bool:
url = f"https://{github_mirror}/subframe7536/maple-font/releases/download/{tag}/{zip_path}"
return download_zip_and_extract(
name=f"{'Static' if 'static' in zip_path else 'Variable'} CN Base Font",
url=url,
zip_path=zip_path,
output_dir=target_dir,
)
def match_unicode_names(file_path: str) -> dict[str, str]:
font = GSFont(file_path)
result = {}
for glyph in font.glyphs:
glyph_name = glyph.name
unicode_values = glyph.unicode
if glyph_name and unicode_values:
unicode_str = f"uni{''.join(unicode_values).upper().zfill(4)}"
result[unicode_str] = glyph_name
return result
# https://github.com/subframe7536/maple-font/issues/314
def verify_glyph_width(font: TTFont, expect_widths: list[int], file_name: str = None):
print("Verify glyph width...")
result: tuple[str, int] = []
for name in font.getGlyphNames():
width, _ = font["hmtx"][name]
if width not in expect_widths:
result.append([name, width])
if result.__len__() > 0:
print(f"Every glyph's width should be in {expect_widths}, but these are not:")
for item in result:
print(f"{item[0]} => {item[1]}")
raise Exception(
f"{file_name or 'The font'} may contain glyphs that width is not in {expect_widths}, which may broke monospace rule."
)
def compress_folder(
source_file_or_dir_path: str,
target_parent_dir_path: str,
family_name_compact: str,
suffix: str,
build_config_path: str,
) -> tuple[str, str]:
"""
Archive folder and return sha1 and file name
"""
source_folder_name = path.basename(source_file_or_dir_path)
zip_name_without_ext = f"{family_name_compact}-{source_folder_name}{suffix}"
zip_path = joinPaths(
target_parent_dir_path,
f"{zip_name_without_ext}.zip",
)
with ZipFile(zip_path, "w", compression=ZIP_DEFLATED, compresslevel=5) as zip_file:
for root, _, files in walk(source_file_or_dir_path):
for file in files:
file_path = joinPaths(root, file)
zip_file.write(
file_path, path.relpath(file_path, source_file_or_dir_path)
)
zip_file.write("OFL.txt", "LICENSE.txt")
if not source_file_or_dir_path.endswith("Variable"):
zip_file.write(
build_config_path,
"config.json",
)
zip_file.close()
sha256 = hashlib.sha256()
with open(zip_path, "rb") as zip_file:
while True:
data = zip_file.read(1024)
if not data:
break
sha256.update(data)
return sha256.hexdigest(), zip_name_without_ext