1353
This commit is contained in:
1 parent
a01312d104
commit
f19d91039a
15 files changed
+297
-19
No files matched your search
@@ -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 }}
|
||||||
|
|||||||
Whitespace-only changes.
@@ -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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
Reference in new issue
Block a user