#!/usr/bin/env python3 """ 字体分片工具 将中文字体按 Unicode 区块分成多个小文件,配合 CSS unicode-range 使用 浏览器按需加载,大幅提升首屏加载速度 """ import os import re from fontTools.ttLib import TTFont from fontTools.subset import Subsetter, Options FONT_SRC = 'themes/Ying/static/font/zql-v2.woff2' FONT_OUT_DIR = 'themes/Ying/static/font/slices' CSS_OUT_PATH = 'themes/Ying/assets/css/font-slices.css' CJK_UNIFIED_START = 0x4E00 CJK_UNIFIED_END = 0x9FFF def generate_slices(num_slices=15): """ 将 CJK 统一汉字区分成多个分片 常用字区域(开头)分得细一些,生僻字区域分得粗一些 """ total_chars = CJK_UNIFIED_END - CJK_UNIFIED_START + 1 slices = [] # 策略:前30%常用区分得细,后70%生僻字区分得粗 common_ratio = 0.3 common_end = int(CJK_UNIFIED_START + total_chars * common_ratio) common_slices = int(num_slices * 0.6) # 60%的分片给常用字 rare_slices = num_slices - common_slices # 常用字区分片 common_range = common_end - CJK_UNIFIED_START common_per_slice = common_range // common_slices for i in range(common_slices): start = CJK_UNIFIED_START + i * common_per_slice end = CJK_UNIFIED_START + (i + 1) * common_per_slice - 1 if i == common_slices - 1: end = common_end slices.append((start, end, f'common-{i+1:02d}')) # 生僻字区分片 rare_range = CJK_UNIFIED_END - common_end rare_per_slice = rare_range // rare_slices for i in range(rare_slices): start = common_end + 1 + i * rare_per_slice end = common_end + (i + 1) * rare_per_slice if i == rare_slices - 1: end = CJK_UNIFIED_END slices.append((start, end, f'rare-{i+1:02d}')) return slices def get_all_chars(font): """获取字体中所有可用的字符""" chars = set() cmap = font.getBestCmap() for codepoint in cmap: if CJK_UNIFIED_START <= codepoint <= CJK_UNIFIED_END: chars.add(codepoint) return chars def subset_font(font_path, output_path, codepoints): """生成子集字体""" font = TTFont(font_path) options = Options() options.flavor = 'woff2' options.desubroutinize = True subsetter = Subsetter(options=options) subsetter.populate(unicodes=codepoints) subsetter.subset(font) font.save(output_path) font.close() def generate_css(slices_info, font_dir_rel='../font/slices'): """生成 CSS 文件""" css_parts = [] # ASCII + 标点符号(第一优先级,总是加载) css_parts.append("""/** * 字体分片 - 按需加载 * 浏览器会根据页面实际使用的字符自动加载对应的分片 */ @font-face { font-family: 'zql'; src: url('../font/zql-v2.woff2') format('woff2'); font-display: swap; unicode-range: U+0000-00FF, U+2000-206F, U+3000-303F, U+FF00-FFEF; } """) for name, codepoints, size in slices_info: if not codepoints: continue # 生成 unicode-range ranges = [] sorted_points = sorted(codepoints) range_start = sorted_points[0] prev = sorted_points[0] for cp in sorted_points[1:]: if cp == prev + 1: prev = cp else: if range_start == prev: ranges.append(f'U+{range_start:04X}') else: ranges.append(f'U+{range_start:04X}-{prev:04X}') range_start = cp prev = cp if range_start == prev: ranges.append(f'U+{range_start:04X}') else: ranges.append(f'U+{range_start:04X}-{prev:04X}') unicode_range = ', '.join(ranges) css_parts.append(f"""@font-face {{ font-family: 'zql'; src: url('{font_dir_rel}/zql-{name}.woff2') format('woff2'); font-display: swap; unicode-range: {unicode_range}; }} """) return '\n'.join(css_parts) def main(): print('🚀 开始字体分片...') print(f'📄 源字体: {FONT_SRC}') if not os.path.exists(FONT_SRC): print(f'❌ 源字体不存在: {FONT_SRC}') return os.makedirs(FONT_OUT_DIR, exist_ok=True) # 读取源字体,获取所有字符 print('🔍 读取源字体...') font = TTFont(FONT_SRC) all_chars = get_all_chars(font) font.close() print(f'✅ 共 {len(all_chars)} 个 CJK 字符') # 生成分片方案 slices = generate_slices(num_slices=12) print(f'📦 分成 {len(slices)} 个分片') # 处理每个分片 slices_info = [] for start, end, name in slices: # 获取这个区间内字体实际包含的字符 slice_chars = [cp for cp in all_chars if start <= cp <= end] if not slice_chars: print(f' ⏭️ 分片 {name} (U+{start:04X}-U+{end:04X}): 无字符,跳过') slices_info.append((name, [], 0)) continue output_path = os.path.join(FONT_OUT_DIR, f'zql-{name}.woff2') print(f' ✂️ 分片 {name} (U+{start:04X}-U+{end:04X}): {len(slice_chars)} 字') subset_font(FONT_SRC, output_path, slice_chars) size = os.path.getsize(output_path) slices_info.append((name, slice_chars, size)) print(f' → {size/1024:.1f} KB') # 生成 CSS print('\n📝 生成 CSS 文件...') css = generate_css(slices_info) with open(CSS_OUT_PATH, 'w', encoding='utf-8') as f: f.write(css) print(f'✅ CSS 已保存: {CSS_OUT_PATH}') # 统计 total_size = sum(size for _, _, size in slices_info) original_size = os.path.getsize(FONT_SRC) print('\n' + '='*50) print('📊 统计信息') print('='*50) print(f'原始字体大小: {original_size/1024:.1f} KB') print(f'所有分片总大小: {total_size/1024:.1f} KB') print(f'分片数量: {len(slices_info)}') print() print('预估首屏加载: 50-150 KB (常用字 3-5 个分片)') print('='*50) print('✅ 字体分片完成!') if __name__ == '__main__': main()