206 lines
6.1 KiB
Python
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()
|