1507
This commit is contained in:
1 parent
2fbf2e3d82
commit
5651d494ba
17 files changed
+4
-744
No files matched your search
@@ -7,11 +7,6 @@ on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_font_subset:
|
||||
description: 'Skip font subset step'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -86,162 +81,7 @@ jobs:
|
||||
echo "## ⏱️ Pre-check 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ============================================================
|
||||
# Job 1: Font Subset (字体子集化 - 可选并行任务)
|
||||
# ============================================================
|
||||
subset-fonts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
needs: pre-check
|
||||
if: |
|
||||
needs.pre-check.outputs.can_deploy == 'true' &&
|
||||
inputs.skip_font_subset != 'true' &&
|
||||
github.event_name == 'workflow_dispatch'
|
||||
outputs:
|
||||
font_updated: ${{ steps.update.outputs.font_updated }}
|
||||
char_count: ${{ steps.stats.outputs.char_count }}
|
||||
original_size: ${{ steps.stats.outputs.original_size }}
|
||||
subset_size: ${{ steps.stats.outputs.subset_size }}
|
||||
|
||||
steps:
|
||||
- name: Start timing
|
||||
id: start-subset
|
||||
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install fonttools brotli zopfli
|
||||
|
||||
- name: Generate font subset
|
||||
id: update
|
||||
run: |
|
||||
# 运行字体子集化脚本
|
||||
# 会自动扫描:
|
||||
# 1. 静态文件 (content, layouts, public)
|
||||
# 2. 本地 JSON 数据 (友链、朋友圈)
|
||||
# 3. 远程 API (api.usj.cc 的友链、订阅源、朋友圈)
|
||||
python scripts/subset-font-safe.py
|
||||
|
||||
# 检查是否有变化
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
git add themes/Ying/static/font/zql-v2-subset.woff2 themes/Ying/static/font/used_chars.txt
|
||||
if ! git diff --staged --quiet; then
|
||||
git commit -m "chore: update font subset [skip ci]"
|
||||
git push origin HEAD:main
|
||||
echo "font_updated=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ 字体子集已更新并推送"
|
||||
else
|
||||
echo "font_updated=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ 字体子集无变化,跳过提交"
|
||||
fi
|
||||
|
||||
- name: Collect stats
|
||||
id: stats
|
||||
if: always()
|
||||
run: |
|
||||
CHAR_COUNT=$(wc -m < themes/Ying/static/font/used_chars.txt | tr -d ' ')
|
||||
ORIGINAL_SIZE=$(stat -c%s themes/Ying/static/font/zql-v2.woff2)
|
||||
SUBSET_SIZE=$(stat -c%s themes/Ying/static/font/zql-v2-subset.woff2)
|
||||
echo "char_count=$CHAR_COUNT" >> $GITHUB_OUTPUT
|
||||
echo "original_size=$ORIGINAL_SIZE" >> $GITHUB_OUTPUT
|
||||
echo "subset_size=$SUBSET_SIZE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create font subset summary
|
||||
if: always()
|
||||
run: |
|
||||
ORIGINAL_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.original_size }} / 1024" | bc)
|
||||
SUBSET_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.subset_size }} / 1024" | bc)
|
||||
REDUCTION_KB=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) / 1024" | bc)
|
||||
PERCENTAGE=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) * 100 / ${{ steps.stats.outputs.original_size }}" | bc)
|
||||
|
||||
echo "## 🔤 字体子集化" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.update.outputs.font_updated }}" = "true" ]; then
|
||||
echo "✅ **字体子集已更新**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "ℹ️ **字体子集无变化**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📊 统计信息" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **字符数:** ${{ steps.stats.outputs.char_count }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **原始大小:** ${ORIGINAL_SIZE_KB} KB" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **子集大小:** ${SUBSET_SIZE_KB} KB" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **节省空间:** ${REDUCTION_KB} KB (${PERCENTAGE}%)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🌐 扫描的数据源" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 静态文件: content, layouts, public" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 本地 JSON: link_lite.json, friend_circle_data.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 远程 API: api.usj.cc (links, feeds, articles)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Send font subset notification
|
||||
if: steps.update.outputs.font_updated == 'true'
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||||
run: |
|
||||
ORIGINAL_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.original_size }} / 1024" | bc)
|
||||
SUBSET_SIZE_KB=$(echo "scale=1; ${{ steps.stats.outputs.subset_size }} / 1024" | bc)
|
||||
REDUCTION_KB=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) / 1024" | bc)
|
||||
PERCENTAGE=$(echo "scale=1; (${{ steps.stats.outputs.original_size }} - ${{ steps.stats.outputs.subset_size }}) * 100 / ${{ steps.stats.outputs.original_size }}" | bc)
|
||||
|
||||
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
|
||||
MESSAGE="✅ **字体子集化完成**
|
||||
|
||||
📦 项目: ${{ github.repository }}
|
||||
🌿 分支: ${{ github.ref_name }}
|
||||
|
||||
📊 优化统计:
|
||||
- 字符数: ${{ steps.stats.outputs.char_count }}
|
||||
- 原始大小: ${ORIGINAL_SIZE_KB} KB
|
||||
- 子集大小: ${SUBSET_SIZE_KB} KB
|
||||
- 节省空间: ${REDUCTION_KB} KB (${PERCENTAGE}%)
|
||||
|
||||
🌐 数据源:
|
||||
- 静态文件 + 本地JSON + 远程API"
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="${TELEGRAM_CHAT_ID}" \
|
||||
-d text="${MESSAGE}" \
|
||||
-d parse_mode="Markdown"
|
||||
fi
|
||||
|
||||
if [ -n "$FEISHU_WEBHOOK_URL" ]; then
|
||||
curl -s -X POST "${FEISHU_WEBHOOK_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": "✅ **字体子集化完成**\n\n📦 项目: ${{ github.repository }}\n🌿 分支: ${{ github.ref_name }}\n\n📊 优化统计:\n- 字符数: ${{ steps.stats.outputs.char_count }}\n- 原始大小: '"${ORIGINAL_SIZE_KB}"' KB\n- 子集大小: '"${SUBSET_SIZE_KB}"' KB\n- 节省空间: '"${REDUCTION_KB}"' KB ('"${PERCENTAGE}"'%)\n\n🌐 数据源: 静态文件 + 本地JSON + 远程API"
|
||||
}
|
||||
}'
|
||||
fi
|
||||
|
||||
- name: End timing
|
||||
id: end-subset
|
||||
run: |
|
||||
end_time=$(date +%s)
|
||||
start_time=${{ steps.start-subset.outputs.start_time }}
|
||||
duration=$((end_time - start_time))
|
||||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||||
echo "## ⏱️ Subset-fonts 耗时: ${duration}秒" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ============================================================
|
||||
# Job 2: Build (构建)
|
||||
# Job 1: Build (构建)
|
||||
# ============================================================
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -566,7 +406,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [pre-check, subset-fonts, build, deploy-edgeone, deploy-upyun]
|
||||
needs: [pre-check, build, deploy-edgeone, deploy-upyun]
|
||||
if: always() && needs.build.result == 'success' && needs.deploy-edgeone.result == 'success' && needs.deploy-upyun.result == 'success'
|
||||
|
||||
steps:
|
||||
@@ -729,7 +569,6 @@ jobs:
|
||||
echo "| 阶段 | 状态 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Pre-check | ${{ needs.pre-check.result == 'success' && '✅ 成功' || needs.pre-check.result == 'skipped' && '⏭️ 跳过' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Subset-fonts | ${{ needs.subset-fonts.result == 'success' && '✅ 成功' || needs.subset-fonts.result == 'skipped' && '⏭️ 跳过' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Build | ${{ needs.build.result == 'success' && '✅ 成功' || '❌ 失败' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Deploy EdgeOne | ${EDGEONE_DISPLAY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Deploy UpYun | ${UPYUN_DISPLAY} |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -745,7 +584,7 @@ jobs:
|
||||
timeout-minutes: 3
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [pre-check, subset-fonts, build, deploy-edgeone, deploy-upyun, finalize]
|
||||
needs: [pre-check, build, deploy-edgeone, deploy-upyun, finalize]
|
||||
if: always() && (needs.pre-check.result == 'failure' || needs.build.result == 'failure' || needs.deploy-edgeone.result == 'failure' || needs.deploy-upyun.result == 'failure' || needs.finalize.result == 'failure')
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,371 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
字体子集化脚本 - 安全版本
|
||||
从HTML和CSS文件中提取实际使用的字符,生成优化的子集字体
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.subset import Subsetter, Options
|
||||
|
||||
def extract_chars_from_files(directories, extensions=('.html', '.md', '.css')):
|
||||
"""从文件中提取使用的字符"""
|
||||
chars = set()
|
||||
|
||||
for directory in directories:
|
||||
if not os.path.exists(directory):
|
||||
print(f"⚠️ 目录不存在,跳过: {directory}")
|
||||
continue
|
||||
|
||||
print(f"🔍 扫描目录: {directory}")
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
filepath = os.path.join(root, file)
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 提取中文字符
|
||||
chinese_chars = re.findall(r'[一-鿿]', content)
|
||||
chars.update(chinese_chars)
|
||||
|
||||
# 提取中文标点
|
||||
cjk_punct = re.findall(r'[ -〿-]', content)
|
||||
chars.update(cjk_punct)
|
||||
|
||||
# 提取英文和数字
|
||||
ascii_chars = re.findall(r'[a-zA-Z0-9]', content)
|
||||
chars.update(ascii_chars)
|
||||
|
||||
# 提取常用英文标点
|
||||
en_punct = set('!@#$%^&*()_+-=[]{}|;:,.<>?/\\`~"\'-')
|
||||
chars.update(en_punct)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 读取文件失败 {filepath}: {e}")
|
||||
|
||||
return chars
|
||||
|
||||
def extract_chars_from_json(json_files):
|
||||
"""从JSON数据文件中提取字符(用于友链、朋友圈等动态内容)"""
|
||||
chars = set()
|
||||
|
||||
import json
|
||||
|
||||
for json_file in json_files:
|
||||
if not os.path.exists(json_file):
|
||||
print(f"⚠️ JSON文件不存在,跳过: {json_file}")
|
||||
continue
|
||||
|
||||
print(f"🔍 扫描JSON文件: {json_file}")
|
||||
|
||||
try:
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 将整个JSON转换为字符串,提取所有字符
|
||||
content = json.dumps(data, ensure_ascii=False)
|
||||
|
||||
# 提取中文字符
|
||||
chinese_chars = re.findall(r'[一-鿿]', content)
|
||||
chars.update(chinese_chars)
|
||||
|
||||
# 提取中文标点
|
||||
cjk_punct = re.findall(r'[ -〿-]', content)
|
||||
chars.update(cjk_punct)
|
||||
|
||||
# 提取英文和数字
|
||||
ascii_chars = re.findall(r'[a-zA-Z0-9]', content)
|
||||
chars.update(ascii_chars)
|
||||
|
||||
print(f" ✅ 从 {json_file} 提取了 {len(chinese_chars)} 个中文字符")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 读取JSON文件失败 {json_file}: {e}")
|
||||
|
||||
return chars
|
||||
|
||||
def fetch_and_extract_chars_from_api(urls):
|
||||
"""从API接口获取数据并提取字符(用于动态内容如友链、朋友圈)"""
|
||||
chars = set()
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
import ssl
|
||||
|
||||
# 创建不验证SSL的上下文(用于内网或自签名证书)
|
||||
context = ssl._create_unverified_context()
|
||||
|
||||
for url in urls:
|
||||
print(f"🌐 尝试从API获取: {url}")
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, context=context, timeout=15) as response:
|
||||
data = response.read().decode('utf-8')
|
||||
|
||||
try:
|
||||
json_data = json.loads(data)
|
||||
content = json.dumps(json_data, ensure_ascii=False)
|
||||
|
||||
# 提取中文字符
|
||||
chinese_chars = re.findall(r'[一-鿿]', content)
|
||||
chars.update(chinese_chars)
|
||||
|
||||
print(f" ✅ 从API提取了 {len(chinese_chars)} 个中文字符")
|
||||
except json.JSONDecodeError:
|
||||
print(f" ⚠️ 响应不是有效的JSON,跳过")
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
print(f" ❌ API请求失败: {e}")
|
||||
except Exception as e:
|
||||
print(f" ❌ 处理API响应失败: {e}")
|
||||
|
||||
except ImportError:
|
||||
print("⚠️ urllib不可用,跳过API扫描")
|
||||
|
||||
return chars
|
||||
|
||||
def subset_font(input_font_path, output_font_path, chars):
|
||||
"""生成子集字体"""
|
||||
print(f"\n📦 加载字体: {input_font_path}")
|
||||
|
||||
# 检查输入文件
|
||||
if not os.path.exists(input_font_path):
|
||||
print(f"❌ 字体文件不存在: {input_font_path}")
|
||||
return False
|
||||
|
||||
# 加载原始字体
|
||||
try:
|
||||
font = TTFont(input_font_path)
|
||||
except Exception as e:
|
||||
print(f"❌ 加载字体失败: {e}")
|
||||
return False
|
||||
|
||||
# 获取原始字符数
|
||||
cmap = font.getBestCmap()
|
||||
original_count = len(cmap) if cmap else 0
|
||||
original_size = os.path.getsize(input_font_path)
|
||||
|
||||
print(f"📊 原始字体信息:")
|
||||
print(f" 字符数: {original_count}")
|
||||
print(f" 文件大小: {original_size / 1024:.1f} KB")
|
||||
|
||||
# 配置子集化选项
|
||||
options = Options()
|
||||
if output_font_path.endswith('.woff2'):
|
||||
options.flavor = 'woff2'
|
||||
elif output_font_path.endswith('.woff'):
|
||||
options.flavor = 'woff'
|
||||
|
||||
options.desubroutinize = True
|
||||
options.layout_features = ['kern', 'liga'] # 保留基本的排版特性
|
||||
|
||||
# 创建子集化器
|
||||
subsetter = Subsetter(options=options)
|
||||
|
||||
# 填充字符集
|
||||
char_text = ''.join(chars)
|
||||
print(f"\n✂️ 正在生成子集字体...")
|
||||
print(f" 提取的字符数: {len(chars)}")
|
||||
|
||||
try:
|
||||
subsetter.populate(text=char_text)
|
||||
subsetter.subset(font)
|
||||
except Exception as e:
|
||||
print(f"❌ 子集化失败: {e}")
|
||||
return False
|
||||
|
||||
# 保存子集字体
|
||||
try:
|
||||
font.save(output_font_path)
|
||||
except Exception as e:
|
||||
print(f"❌ 保存字体失败: {e}")
|
||||
return False
|
||||
|
||||
# 获取子集信息
|
||||
subset_size = os.path.getsize(output_font_path)
|
||||
subset_font_obj = TTFont(output_font_path)
|
||||
subset_cmap = subset_font_obj.getBestCmap()
|
||||
subset_count = len(subset_cmap) if subset_cmap else 0
|
||||
|
||||
# 计算优化效果
|
||||
reduction = original_size - subset_size
|
||||
percentage = (reduction / original_size) * 100 if original_size > 0 else 0
|
||||
|
||||
print(f"\n✅ 子集化完成!")
|
||||
print(f"📊 优化结果:")
|
||||
print(f" 子集字符数: {subset_count}")
|
||||
print(f" 子集文件大小: {subset_size / 1024:.1f} KB")
|
||||
print(f" 减少: {reduction / 1024:.1f} KB ({percentage:.1f}%)")
|
||||
|
||||
# 验证是否真的优化了
|
||||
if subset_size >= original_size:
|
||||
print(f"\n⚠️ 警告: 子集字体反而变大了!")
|
||||
print(f" 原因可能是提取了太多字符")
|
||||
print(f" 建议检查扫描范围或使用更小的字符集")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("🔤 字体子集化工具(安全版本)")
|
||||
print("=" * 50)
|
||||
|
||||
# 配置路径
|
||||
font_dir = "themes/Ying/static/font"
|
||||
input_font = os.path.join(font_dir, "zql-v2.woff2")
|
||||
chars_file = os.path.join(font_dir, "used_chars.txt")
|
||||
|
||||
# 检查输入文件
|
||||
if not os.path.exists(input_font):
|
||||
print(f"❌ 字体文件不存在: {input_font}")
|
||||
print(f" 请确保在博客根目录运行此脚本")
|
||||
sys.exit(1)
|
||||
|
||||
# 读取现有的字符列表(如果存在)
|
||||
existing_chars = set()
|
||||
if os.path.exists(chars_file):
|
||||
with open(chars_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
existing_chars = set(content)
|
||||
print(f"📖 读取现有字符: {len(existing_chars)} 个")
|
||||
|
||||
# 检查特殊字符是否存在
|
||||
special_chars = ['—', '庶', '尉', '谏', '簿', '丞']
|
||||
found_special = [c for c in special_chars if c in existing_chars]
|
||||
print(f"🔍 检查特殊字符: {len(found_special)}/{len(special_chars)} 个已存在")
|
||||
if found_special:
|
||||
print(f" 已找到: {''.join(found_special)}")
|
||||
else:
|
||||
print(f"⚠️ 现有字符文件不存在,将创建新文件")
|
||||
|
||||
# 扫描目录
|
||||
scan_dirs = ["content", "layouts"]
|
||||
|
||||
# 检查public目录是否存在
|
||||
if os.path.exists("public"):
|
||||
scan_dirs.append("public")
|
||||
print(f"✅ 找到public目录,将扫描构建后的HTML")
|
||||
else:
|
||||
print(f"⚠️ 未找到public目录,请先运行: hugo --destination=public")
|
||||
print(f" 将只扫描content和layouts目录")
|
||||
|
||||
print(f"\n🔍 扫描目录: {', '.join(scan_dirs)}")
|
||||
|
||||
# 提取字符(静态文件)
|
||||
new_chars = extract_chars_from_files(scan_dirs)
|
||||
print(f"\n📝 从文件中提取了 {len(new_chars)} 个唯一字符")
|
||||
|
||||
# 扫描动态数据文件(友链、朋友圈等)
|
||||
json_data_files = [
|
||||
"themes/Ying/static/json/link_lite.json", # 友链数据
|
||||
"themes/Ying/static/json/friend_circle_data.json", # 朋友圈数据
|
||||
"themes/Ying/data/links.yaml", # 友链配置
|
||||
"themes/Ying/data/links.yml", # 友链配置(备用)
|
||||
]
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🔄 扫描动态数据文件(友链、朋友圈等)")
|
||||
dynamic_chars = extract_chars_from_json(json_data_files)
|
||||
print(f"📝 从动态数据中提取了 {len(dynamic_chars)} 个唯一字符")
|
||||
|
||||
# 从API获取动态数据(用于CI环境)
|
||||
api_urls = [
|
||||
"https://api.usj.cc/api/links?all=1", # 友链列表
|
||||
"https://api.usj.cc/api/feeds", # 订阅源列表
|
||||
"https://api.usj.cc/api/articles?limit=50", # 朋友圈文章
|
||||
]
|
||||
|
||||
# 如果有环境变量,追加额外的API地址
|
||||
if os.environ.get('FONT_SUBSET_API_URLS'):
|
||||
extra_urls = os.environ['FONT_SUBSET_API_URLS'].split(',')
|
||||
api_urls.extend(extra_urls)
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🌐 从环境变量获取额外API地址")
|
||||
print(f" 额外URLs: {', '.join(extra_urls)}")
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🌐 扫描远程API(友链、朋友圈等)")
|
||||
print(f" 共 {len(api_urls)} 个API端点")
|
||||
|
||||
api_chars = fetch_and_extract_chars_from_api(api_urls)
|
||||
print(f"📝 从API提取了 {len(api_chars)} 个唯一字符")
|
||||
dynamic_chars = dynamic_chars | api_chars
|
||||
|
||||
# 合并静态和动态字符
|
||||
new_chars = new_chars | dynamic_chars
|
||||
print(f"\n📊 合并后总字符数: {len(new_chars)}")
|
||||
|
||||
# 合并字符(保留现有字符 + 新提取的字符)
|
||||
all_chars = existing_chars | new_chars
|
||||
print(f"📊 合并后字符数: {len(all_chars)}")
|
||||
|
||||
# 验证特殊字符是否在合并后的字符集中
|
||||
special_chars = ['—', '庶', '尉', '谏', '簿', '丞']
|
||||
missing_special = [c for c in special_chars if c not in all_chars]
|
||||
if missing_special:
|
||||
print(f"⚠️ 警告:以下特殊字符不在合并后的字符集中: {''.join(missing_special)}")
|
||||
print(f" 将强制添加这些字符...")
|
||||
all_chars.update(special_chars)
|
||||
else:
|
||||
print(f"✅ 所有特殊字符都已保留在字符集中")
|
||||
|
||||
if len(all_chars) == 0:
|
||||
print("❌ 未找到任何字符,请检查扫描目录")
|
||||
sys.exit(1)
|
||||
|
||||
# 保存合并后的字符列表
|
||||
with open(chars_file, 'w', encoding='utf-8') as f:
|
||||
sorted_chars = sorted(all_chars)
|
||||
f.write(''.join(sorted_chars))
|
||||
print(f"💾 字符列表已保存到: {chars_file}")
|
||||
|
||||
# 显示部分字符(用于验证)
|
||||
sample_chars = sorted(list(all_chars))[:50]
|
||||
print(f"📋 前50个字符: {''.join(sample_chars)}")
|
||||
|
||||
# 生成woff2子集字体
|
||||
output_woff2 = os.path.join(font_dir, "zql-v2-subset.woff2")
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🎯 生成 woff2 子集字体...")
|
||||
success_woff2 = subset_font(input_font, output_woff2, all_chars)
|
||||
|
||||
# 生成woff子集字体
|
||||
output_woff = os.path.join(font_dir, "zql-v2-subset.woff")
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🎯 生成 woff 子集字体...")
|
||||
success_woff = subset_font(input_font, output_woff, all_chars)
|
||||
|
||||
# 总结
|
||||
print(f"\n{'='*50}")
|
||||
if success_woff2 and success_woff:
|
||||
print(f"🎉 所有子集字体生成成功!")
|
||||
|
||||
# 显示最终文件大小
|
||||
print(f"\n📂 最终文件:")
|
||||
for f in [output_woff2, output_woff]:
|
||||
if os.path.exists(f):
|
||||
size = os.path.getsize(f)
|
||||
print(f" {os.path.basename(f)}: {size / 1024:.1f} KB")
|
||||
|
||||
print(f"\n✅ 下一步:")
|
||||
print(f" 1. 更新CSS字体声明(将zql-v2.woff2改为zql-v2-subset.woff2)")
|
||||
print(f" 2. 重新构建Hugo: hugo server -D")
|
||||
print(f" 3. 测试字体显示是否正常")
|
||||
else:
|
||||
print(f"⚠️ 部分字体生成失败或未优化")
|
||||
print(f" 建议使用fallback方案(保留原始字体)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -75,8 +75,7 @@
|
||||
})();
|
||||
</script>
|
||||
<!-- Preload Fonts -->
|
||||
<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/zql-v2-subset.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 }}
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
|
||||
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~—、。〇《》「」【】一丁七万三上下不与丑专且世丘业东丝丞丢两严丧个中丰串临为主丽举久么义之乌乎乏乐乒乓乘乙九也习乡书买乱了予争事二于亏云互五井亚些亡交亦产享京亮亲人亿什仁仅今介仍从仓仔他仗付仙代令以仪们仰件价任份仿企伏伐休众优伙会伞传伤伦伪伫估伴伸似但位低住佐佑体何余佛作你佩佬佳使例供依侠侣侧便促俄俗保信俩俭修俱倍倒候借倦债值假偎偏做停健偶偷偿傅傍储催傲傻像僵僻儿兀允元兄充兆先光克免兑兔党兜入全八公六共关兴其具典养兼冀内册再冒冗写军农冠冤冥冬冰冲决况冷冻净准凉凌减凑凛几凡凭凳凶出击函刀刃分切划列刘则刚创初删判利别刮到制刷券刹刺刻剁剂前剔剖剥剧剩剪副割力劝办功加务劣动助努励劲劳势勃勇勉勒勤勾勿包匆匍匐匕化北匙匠匹区医匾匿十千升午半华协卓单卖南博占卡卢卦卧卫卯印危即却卷卸厂厅历厉压厌厕厘厚原厢厨去县参又及友双反发叒叔叕取受变叙叠口古句另叨只叫召可台史右叶号司叹吃各合吉同名后吐向吓吗吞吟否吧吨含听启吱吴吵吸吹吾呀呆呈告呕呗员呛呢呦周味呵呼命咄咋和咐咕咙咨咩咪咬咱咳咸咽哀品哄哆哇哈响哒哔哗哟哥哦哨哩哪哭哲唉唐唤唬售唯唱唾商啊啥啦啪啸喂善喉喊喜喝喧喵喷喻嗓嗖嗜嗡嗦嗨嗯嗽嘀嘎嘛嘱嘲嘴嘿器噩噪噶噼嚣囊四回因团园困围固国图圆圈土圣在地圳场圾址均坊坎坏坐坑块坚坛坞坟坠坡坦坷垃垄型垢垮埋城埔域培基堂堆堑堡堤堪堵塌塑塔塘塞填境墓墙增墨壁士壮声壳壶壹处备复夏夕外多夜够大天太夫夯失头夸夹夺奇奈奋奏契奔奖套奢女奶奸她好如妄妇妈妒妙妥妨妹妻始姐姑姓委姨姿威娃娘娜娱婆婚媒媚嫁嫉嫌嫖嫩子孔孕字存季孤学孩宁它宅守安宋完宏宗官定宛宜宝实宠审客宣室宫宰害宵家容宽宾宿寂寄密寇富寒寓寝察寸对寺寻导寿封射将尉尊小少尔尖尘尚尝尤尬就尴尸尺尼尽尾尿局屁层居屈届屋屎屏屑展属屠屯山岁岂岔岖岗岛岸峨峰崎崩嵌巍川州工左巧巨差己已巴巷巾币市布师希帖帘带帧席帮常帽幅幕幢干平年并幸幺幻幼幽广庄庆床序库应底店庙废度座庭庶康庸延建廿开异弃弄弈弊式引弟张弥弯弱弹强归当录形彩彰影彻彼往征径待很律徐徒徕得徙御循微德心必忆忍忐忑志忘忙忧快念忽怀态怎怒怔怕思怠怡急怦性怨怪怯总怼恋恍恐恒恢恨恩恭息恰恶恼悄悉悍悔悖悚悟患悦您悬悲情惊惑惕惜惠惧惨惫惬惯惰想惹愁愈愉意愚感愣愤愧愿慎慕慢慨憋憧憨憬憾懂懈懑懒懦懵戏成我或战戚截戳戴户戾房所扇手才扎扑扒打扔托扣执扩扫扬扰扳批扼找承技抄抉把抑抓投抖抗折抛抢护报抬抱抵抹抽担拆拉拌拍拒拓拔拖拘拙招拜拟拥拦拧拨择括拳拷拼拽拾拿持挂指按挑挖挚挡挣挤挥挨挫振挺捆捉捏捕捞损捡换捣捧据捷掂授掉掌掐排掘探接控推掩措掰掺描提插握揣揭搁搏搓搜搞搬搭携摁摄摆摇摊摔摘摧摩摸撑撒撞撤播撰撸撼擀擅操擎擦攀攒支收改攻放政故效敌敏救教敛敞敢散敬数敲整敷文斐斑斗料斜斤斥斧斩断斯新方施旁旅旋族旗无既日旦旧旨早旬时旷昂明易星映春昨是昵昼显晃晒晕晚晦晨普景晰晶智暂暇暑暖暗暮暴曦曲更曾替最月有朋服朗望朝期朦木未末本术朴朵机杀杂权杆李杏材村杜束杠条来杨杭杯杰東松板极构析枕林枚果枝枢枪枫枯架某柒染柔柜查柯柳柴柿栀标栈栋栏树栓校样核根格栽桂桃框案桌桑档桥桩桶梁梦梯械梳检棉棒棘棚森棵椅植椒楔楚楼概榜槛槽樊模横橙欠次欢欣欧欲欺款歉歌止正此步武歪死殉殊残段殿毁毅毋母每毒比毕毛毫氏民气氛氧水永汁求汇汉汕汗江池污汤汪汲沉沙沟没沦沮河油治沾沿泄泉泊泌法泛泡波泣泥注泪泯泰泳泽洁洋洒洗洛洞津洪洲活派流浅浇测济浏浑浓浩浪浮浴海浸涂消涉涌润涨涩涯液涵淀淋淘淡淫深淳混淹添清渐渗渠渡渣温港渲渴游湖湘湾湿溃源溢溯滋滑滙滚滞满滤滥滩滴漂漏漓演漫漱潘潜潦潭潮澎澡澳激瀑火灭灯灰灵灼灾炉炎炒炕炖炫炮炸点炼烁烂烈烟烤烦烧烫热烽焉焕焖無焦然煌煎照煲熄熊熙熟熬熵燕燥爆爬爱父爷爸爽片版牌牙牛物牲牵特牺犊犯状犹狂狐狗狠独狱狼猛猜猝猪猫献猴玄率玉王玛玩环现玻珍珠班球理琐琢瑞璃瓜瓢瓦瓶甚甜生甥用甩田由甲申电男画畅界留略番畴疆疏疑疗疙疫疮疯疲疼疾病症痍痒痕痘痛痪瘦瘩瘫瘾癌登白百的皇皑皮盆盈益监盒盖盗盘盛盟目盯盲直相盼盾省眉看真眠眨眩眯眶眼着睁睐睛睡Line truncated
|
||||
Reference in new issue
Block a user