Files
blog/scripts/slice_font.py
T
2026-06-25 14:48:46 +08:00

206 lines
6.1 KiB
Python

#!/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'
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()