1353
This commit is contained in:
1 parent
a01312d104
commit
f19d91039a
15 files changed
+297
-19
No files matched your search
@@ -0,0 +1,206 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user