This commit is contained in:
Vaica committed 2026-06-25 13:53:18 +08:00
1 parent a01312d104
commit f19d91039a
15 files changed
+297 -19

No files matched your search

+2 -6
View File
@@ -8,14 +8,10 @@ on:
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
workflow_dispatch: workflow_dispatch:
inputs: inputs:
force_font_subset:
description: 'Force font subset regeneration'
type: boolean
default: false
skip_font_subset: skip_font_subset:
description: 'Skip font subset step' description: 'Skip font subset step'
type: boolean type: boolean
default: false default: true
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -101,7 +97,7 @@ jobs:
if: | if: |
needs.pre-check.outputs.can_deploy == 'true' && needs.pre-check.outputs.can_deploy == 'true' &&
inputs.skip_font_subset != 'true' && inputs.skip_font_subset != 'true' &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') github.event_name == 'workflow_dispatch'
outputs: outputs:
font_updated: ${{ steps.update.outputs.font_updated }} font_updated: ${{ steps.update.outputs.font_updated }}
char_count: ${{ steps.stats.outputs.char_count }} char_count: ${{ steps.stats.outputs.char_count }}
View File
Whitespace-only changes.
+206
View File
@@ -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()
+85
View File
@@ -0,0 +1,85 @@
/**
* 字体分片 - 按需加载
* 浏览器会根据页面实际使用的字符自动加载对应的分片
* 首屏通常只需要加载 ASCII + 1-3 个常用字分片(约 100-200KB)
*/
/* ASCII + 常用标点(基础,总是加载) */
@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;
}
/* 常用字分片 01: U+4E00-U+56FF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-01.woff2') format('woff2');
font-display: swap;
unicode-range: U+4E00-56FF;
}
/* 常用字分片 02: U+5700-U+5FFF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-02.woff2') format('woff2');
font-display: swap;
unicode-range: U+5700-5FFF;
}
/* 常用字分片 03: U+6000-U+68FF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-03.woff2') format('woff2');
font-display: swap;
unicode-range: U+6000-68FF;
}
/* 常用字分片 04: U+6900-U+71FF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-04.woff2') format('woff2');
font-display: swap;
unicode-range: U+6900-71FF;
}
/* 常用字分片 05: U+7200-U+7AFF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-05.woff2') format('woff2');
font-display: swap;
unicode-range: U+7200-7AFF;
}
/* 常用字分片 06: U+7B00-U+83FF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-06.woff2') format('woff2');
font-display: swap;
unicode-range: U+7B00-83FF;
}
/* 常用字分片 07: U+8400-U+8CFF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-common-07.woff2') format('woff2');
font-display: swap;
unicode-range: U+8400-8CFF;
}
/* 生僻字分片 01: U+8D00-U+95FF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-rare-01.woff2') format('woff2');
font-display: swap;
unicode-range: U+8D00-95FF;
}
/* 生僻字分片 02: U+9600-U+9FFF */
@font-face {
font-family: 'zql';
src: url('../font/slices/zql-rare-02.woff2') format('woff2');
font-display: swap;
unicode-range: U+9600-9FFF;
}
-11
View File
@@ -1,14 +1,3 @@
@font-face {
font-family: 'zql';
src: url('../font/zql-v2-subset.woff2') format('woff2'),
url('../font/zql-v2-subset.woff') format('woff');
font-display: swap;
}
@charset "UTF-8";:root { @charset "UTF-8";:root {
--background-color: #fafafa; --background-color: #fafafa;
--background-dark-color: #020617; --background-dark-color: #020617;
+4 -2
View File
@@ -75,7 +75,8 @@
})(); })();
</script> </script>
<!-- Preload Fonts --> <!-- Preload Fonts -->
<link rel="preload" href="{{ "font/zql-v2.woff2" | relURL }}" as="font" type="font/woff2" crossorigin> <link rel="preload" href="{{ "font/slices/zql-common-01.woff2" | relURL }}" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{{ "font/slices/zql-common-02.woff2" | relURL }}" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{{ "font/remixicon.woff2" | relURL }}" as="font" type="font/woff2" crossorigin> <link rel="preload" href="{{ "font/remixicon.woff2" | relURL }}" as="font" type="font/woff2" crossorigin>
{{ $echartsHead := resources.Get "js/libs/echarts.min.js" | resources.Fingerprint }} {{ $echartsHead := resources.Get "js/libs/echarts.min.js" | resources.Fingerprint }}
@@ -130,10 +131,11 @@
{{ $uikit := resources.Get "css/libs/uikit.min.css" }} {{ $uikit := resources.Get "css/libs/uikit.min.css" }}
{{ $nprogress := resources.Get "css/libs/nprogress.min.css" }} {{ $nprogress := resources.Get "css/libs/nprogress.min.css" }}
{{ $message := resources.Get "css/libs/message.min.css" }} {{ $message := resources.Get "css/libs/message.min.css" }}
{{ $fontSlices := resources.Get "css/font-slices.css" }}
{{ $main := resources.Get "css/main.css" }} {{ $main := resources.Get "css/main.css" }}
{{ $shortcodes := resources.Get "css/shortcodes.css" }} {{ $shortcodes := resources.Get "css/shortcodes.css" }}
{{ $style := slice $normalize $remixicon $uikit $nprogress $message $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }} {{ $style := slice $normalize $remixicon $uikit $nprogress $message $fontSlices $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}"> <link rel="stylesheet" href="{{ $style.RelPermalink }}">
{{- if and .Site.Params.style.enableBackgroundImage .Site.Params.style.backgroundImageUrl -}} {{- if and .Site.Params.style.enableBackgroundImage .Site.Params.style.backgroundImageUrl -}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.