#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 鸿蒙字体压缩工具 使用 fonttools 库压缩 TTF 字体文件,减小文件大小 """ import os import sys import subprocess import shutil from pathlib import Path from typing import List, Tuple def check_dependencies(): """检查必要的依赖是否已安装""" missing_packages = [] # 检查 fonttools try: import fontTools except ImportError: missing_packages.append('fonttools') # 检查 brotli try: import brotli except ImportError: missing_packages.append('brotli') # 检查 pyftsubset 命令是否可用 try: result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True) if result.returncode != 0: missing_packages.append('fonttools[subset]') except FileNotFoundError: if 'fonttools' not in missing_packages: missing_packages.append('fonttools[subset]') if missing_packages: print(f"缺少必要的依赖包: {', '.join(missing_packages)}") print("请运行以下命令安装:") print(f"pip install {' '.join(missing_packages)}") return False return True def get_file_size(file_path: str) -> int: """获取文件大小(字节)""" return os.path.getsize(file_path) def format_file_size(size_bytes: int) -> str: """格式化文件大小显示""" if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 * 1024: return f"{size_bytes / 1024:.2f} KB" else: return f"{size_bytes / (1024 * 1024):.2f} MB" def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool: """ 压缩单个字体文件 Args: input_path: 输入字体文件路径 output_path: 输出字体文件路径 compression_level: 压缩级别 ("basic", "medium", "aggressive") Returns: bool: 压缩是否成功 """ try: # 基础压缩参数 base_args = [ "pyftsubset", input_path, "--output-file=" + output_path, "--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高 "--with-zopfli", # 使用 Zopfli 算法进一步压缩 ] # 根据压缩级别设置不同的参数 if compression_level == "basic": # 基础压缩:保留常用字符和功能 args = base_args + [ "--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等 "--layout-features=*", # 保留所有布局特性 "--glyph-names", # 保留字形名称 "--symbol-cmap", # 保留符号映射 "--legacy-cmap", # 保留传统字符映射 "--notdef-glyph", # 保留 .notdef 字形 "--recommended-glyphs", # 保留推荐字形 "--name-IDs=*", # 保留所有名称ID "--name-legacy", # 保留传统名称 ] elif compression_level == "medium": # 中等压缩:移除一些不常用的功能 args = base_args + [ "--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围 "--layout-features=kern,liga,clig", # 只保留关键布局特性 "--no-glyph-names", # 移除字形名称 "--notdef-glyph", "--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID ] else: # aggressive # 激进压缩:最大程度减小文件大小 args = base_args + [ "--unicodes=U+0020-007F", # 只保留基本ASCII字符 "--no-layout-features", # 移除所有布局特性 "--no-glyph-names", # 移除字形名称 "--no-symbol-cmap", # 移除符号映射 "--no-legacy-cmap", # 移除传统映射 "--notdef-glyph", "--name-IDs=1,2", # 只保留最基本的名称 "--desubroutinize", # 去子程序化(可能减小CFF字体大小) ] # 执行压缩命令 result = subprocess.run(args, capture_output=True, text=True) if result.returncode == 0: return True else: print(f"压缩失败: {result.stderr}") return False except Exception as e: print(f"压缩过程中出现错误: {str(e)}") return False def find_font_files(directory: str) -> List[str]: """查找目录中的所有字体文件""" font_extensions = ['.ttf', '.otf', '.woff', '.woff2'] font_files = [] for root, dirs, files in os.walk(directory): for file in files: if any(file.lower().endswith(ext) for ext in font_extensions): font_files.append(os.path.join(root, file)) return font_files def compress_fonts_batch(font_directory: str, compression_level: str = "basic"): """ 批量压缩字体文件 Args: font_directory: 字体文件目录 compression_level: 压缩级别 """ if not os.path.exists(font_directory): print(f"错误: 目录 {font_directory} 不存在") return # 查找所有字体文件 font_files = find_font_files(font_directory) if not font_files: print("未找到字体文件") return print(f"找到 {len(font_files)} 个字体文件") print(f"压缩级别: {compression_level}") print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2") print("-" * 60) total_original_size = 0 total_compressed_size = 0 successful_compressions = 0 for i, font_file in enumerate(font_files, 1): print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}") # 获取原始文件大小 original_size = get_file_size(font_file) total_original_size += original_size # 生成输出文件名(保持原文件名,只改变扩展名) file_dir = os.path.dirname(font_file) base_name = os.path.splitext(os.path.basename(font_file))[0] output_file = os.path.join(file_dir, f"{base_name}.woff2") # 压缩字体 if compress_font(font_file, output_file, compression_level): if os.path.exists(output_file): compressed_size = get_file_size(output_file) total_compressed_size += compressed_size successful_compressions += 1 # 计算压缩率 compression_ratio = (1 - compressed_size / original_size) * 100 print(f" ✓ 成功: {format_file_size(original_size)} → {format_file_size(compressed_size)} " f"(压缩 {compression_ratio:.1f}%)") else: print(f" ✗ 失败: 输出文件未生成") else: print(f" ✗ 失败: 压缩过程出错") print() # 显示总结 print("=" * 60) print("压缩完成!") print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件") if successful_compressions > 0: total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100 print(f"总大小: {format_file_size(total_original_size)} → {format_file_size(total_compressed_size)}") print(f"总压缩率: {total_compression_ratio:.1f}%") print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}") def main(): """主函数""" print("鸿蒙字体压缩工具") print("=" * 60) # 检查依赖 if not check_dependencies(): return # 获取当前脚本所在目录 current_dir = os.path.dirname(os.path.abspath(__file__)) # 设置默认字体目录 font_directory = current_dir print(f"字体目录: {font_directory}") # 让用户选择压缩级别 print("\n请选择压缩级别:") print("1. 基础压缩 (保留大部分功能,适合网页使用)") print("2. 中等压缩 (平衡文件大小和功能)") print("3. 激进压缩 (最小文件大小,可能影响显示效果)") while True: choice = input("\n请输入选择 (1-3): ").strip() if choice == "1": compression_level = "basic" break elif choice == "2": compression_level = "medium" break elif choice == "3": compression_level = "aggressive" break else: print("无效选择,请输入 1、2 或 3") # 开始批量压缩 compress_fonts_batch(font_directory, compression_level=compression_level) if __name__ == "__main__": main()