150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""收集剧本字符并生成字体字符集文件。
|
|
|
|
用法:
|
|
python Tools/collect_font_characters.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
SOURCE_RELATIVE_PATH = Path("Assets/Yarn/FP")
|
|
TARGET_RELATIVE_PATH = Path("Assets/Font/字符统计.txt")
|
|
LINE_WIDTH = 20
|
|
EXCLUDED_CHARACTERS = {"\ufeff", "\r", "\n"}
|
|
|
|
|
|
class CharacterCollectionError(RuntimeError):
|
|
"""字符统计失败。"""
|
|
|
|
|
|
def find_source_files(source_root: Path) -> tuple[list[Path], list[Path]]:
|
|
"""递归查找 Yarn 和 CSV 文件,并返回稳定排序后的路径。"""
|
|
if not source_root.is_dir():
|
|
raise CharacterCollectionError(f"剧本目录不存在: {source_root}")
|
|
|
|
yarn_files: list[Path] = []
|
|
csv_files: list[Path] = []
|
|
for path in source_root.rglob("*"):
|
|
if not path.is_file():
|
|
continue
|
|
|
|
suffix = path.suffix.lower()
|
|
if suffix == ".yarn":
|
|
yarn_files.append(path)
|
|
elif suffix == ".csv":
|
|
csv_files.append(path)
|
|
|
|
sort_key = lambda path: path.as_posix()
|
|
yarn_files.sort(key=sort_key)
|
|
csv_files.sort(key=sort_key)
|
|
return yarn_files, csv_files
|
|
|
|
|
|
def read_characters(path: Path) -> set[str]:
|
|
"""读取 UTF-8 文本并返回其中可写入字符集文件的字符。"""
|
|
try:
|
|
text = path.read_text(encoding="utf-8-sig")
|
|
except (OSError, UnicodeError) as error:
|
|
raise CharacterCollectionError(f"无法读取文件 {path}: {error}") from error
|
|
|
|
return {character for character in text if character not in EXCLUDED_CHARACTERS}
|
|
|
|
|
|
def collect_characters(
|
|
source_root: Path, target_path: Path
|
|
) -> tuple[set[str], int, int]:
|
|
"""合并全部剧本文件与现有字符统计文件中的字符。"""
|
|
yarn_files, csv_files = find_source_files(source_root)
|
|
characters: set[str] = set()
|
|
|
|
for path in [*yarn_files, *csv_files]:
|
|
characters.update(read_characters(path))
|
|
|
|
if target_path.exists():
|
|
characters.update(read_characters(target_path))
|
|
|
|
return characters, len(yarn_files), len(csv_files)
|
|
|
|
|
|
def format_characters(characters: set[str], line_width: int = LINE_WIDTH) -> str:
|
|
"""按 Unicode 码点排序,并按固定字符数生成 CRLF 文本。"""
|
|
if line_width <= 0:
|
|
raise ValueError("line_width 必须大于 0")
|
|
|
|
ordered = sorted(
|
|
character
|
|
for character in characters
|
|
if character not in EXCLUDED_CHARACTERS
|
|
)
|
|
lines = [
|
|
"".join(ordered[index : index + line_width])
|
|
for index in range(0, len(ordered), line_width)
|
|
]
|
|
return "\r\n".join(lines) + ("\r\n" if lines else "")
|
|
|
|
|
|
def write_atomic_utf8_bom(target_path: Path, content: str) -> None:
|
|
"""以 UTF-8 BOM 原子覆盖目标文件,避免失败时留下半截内容。"""
|
|
if not target_path.parent.is_dir():
|
|
raise CharacterCollectionError(f"输出目录不存在: {target_path.parent}")
|
|
|
|
temporary_path: Path | None = None
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="wb",
|
|
prefix=f".{target_path.name}.",
|
|
suffix=".tmp",
|
|
dir=target_path.parent,
|
|
delete=False,
|
|
) as temporary_file:
|
|
temporary_path = Path(temporary_file.name)
|
|
temporary_file.write(content.encode("utf-8-sig"))
|
|
temporary_file.flush()
|
|
os.fsync(temporary_file.fileno())
|
|
|
|
os.replace(temporary_path, target_path)
|
|
temporary_path = None
|
|
except (OSError, UnicodeError) as error:
|
|
raise CharacterCollectionError(f"无法写入文件 {target_path}: {error}") from error
|
|
finally:
|
|
if temporary_path is not None:
|
|
try:
|
|
temporary_path.unlink(missing_ok=True)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def generate_character_file(project_root: Path = PROJECT_ROOT) -> tuple[int, int, int]:
|
|
"""收集项目字符、覆盖目标文件,并返回 Yarn/CSV/字符数量。"""
|
|
source_root = project_root / SOURCE_RELATIVE_PATH
|
|
target_path = project_root / TARGET_RELATIVE_PATH
|
|
characters, yarn_count, csv_count = collect_characters(source_root, target_path)
|
|
write_atomic_utf8_bom(target_path, format_characters(characters))
|
|
return yarn_count, csv_count, len(characters)
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
yarn_count, csv_count, character_count = generate_character_file()
|
|
except CharacterCollectionError as error:
|
|
print(f"[错误] {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"扫描 Yarn 文件: {yarn_count}")
|
|
print(f"扫描 CSV 文件: {csv_count}")
|
|
print(f"唯一字符数量: {character_count}")
|
|
print(f"已写入: {PROJECT_ROOT / TARGET_RELATIVE_PATH}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|