普通 svg 字形文件批量转换为 ttf/woff2等任意字体文件
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:50
- 发布: 2025-10-17 11:12
- 最后更新: 2025-10-17 11:12
由于要做一个几乎全部汉字的在线字典,好多生僻字需要进行字体的特殊处理。电脑端访问基本所有汉字都可正常显示,但是手机端,一般只支持基本汉字,扩展A,扩展B ~ 扩展H,几乎都不支持。显示都变成了“豆腐块”。 所以,我需要想办法做一批自定义的字体文件,用 webfont 来支持手机端生僻字的显示。
方案选择
字体的生成目前比较成熟的方案就是实用 fontforge 来进行 svg 到不同类型字体文件的转换。所以核心库,需要有fontforge 支持。考虑到 fontforge 对代码的支持,python 是比较成熟的。所以库和开发语言就确定为: python + fontforge。
另外为了保证环境的隔离性,目前有两种方案:
- 方案一:conda (windows)新建运行环境,安装 fontforge 库,和 python。
- 方案二:docker + linux(ubuntu/centos)+ fontforge + python。
方案一踩坑过程:经过一上午的验证,发现 conda 各种难用,以前搞 AI 大模型环境的时候,感觉还可以的。但是今天一用,各种卡,网上各种查,发现是由于依赖复杂,导致 conda 解析依赖太慢,同时,又发现 mamba 对依赖解析很快。所以又开始捣鼓将 annaconda 换成 mamba。然后 conda 安装mamba 也卡。。。怎么也装不上。无奈就卸载了conda,安装了 miniforge,这才用上了 mamba。
本以为,memba 能够很方便的安装上 fontforge,结果各种查找,fontforge、python-fontforge 都没有。pip 安装也没有这个库。最终结论就是,windows 环境下,没有合适的 fontforge 库能安装。搞了大半天没成功,干脆不搞了,换方案二。
方案二:验证很顺利。由于无法直接下载已有的 fontforge 镜像,只能自己装。我的docker 实在 windows 的wsl2 下安装的 ubuntu 系统中。在linux 下,应该可以拜托之前找不到库的问题。具体看下边环境搭建。
环境搭建
通过 docker 环境来实现,主要是为了保证环境的隔离。电脑上会运行各种类型的环境和软件,为了不互相冲突,且用过之后清理方便。所以选用了 docker 环境。
由于国外 docker hub 的封禁,目前国内好多镜像无法访问,所以想找个现有合适的镜像基本就不可能了。没干系,可以自己做一个镜像。
先建立一个目录:
bash
mkdir fontforge
cd fontforge
然后在里面建立 Dockerfile 文件,构建镜像。
--- Dockerfile 核心安装流程如下 ---
- 首先更新基础包列表
- 安装 software-properties-common,它提供了 add-apt-repository 命令
- 使用 add-apt-repository universe 启用 universe 软件源
- 再次更新包列表,以包含 universe 源里的新包
- 安装所有需要的软件:
- fontforge: FontForge 主程序
- python3: Python 3 解释器
- python3-fontforge: FontForge 的 Python 3 绑定(关键!)
- vim: 一个文本编辑器,方便在容器内修改文件
- 清理 apt 缓存,减小镜像体积
Dockerfile
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository universe && \
apt-get update && \
apt-get install -y fontforge python3 python3-fontforge vim && \
rm -rf /var/lib/apt/lists/*
WORKDIR /data/svg2ttf
然后构建镜像:
bash
# 通过当前目录的 Dockerfile 构建镜像:ubuntu-fontforge
docker build -t ubuntu-fontforge .
# 构建成功后,可以查看镜像
docker images
运行环境:
bash
# 将本地目录 /mnt/d/php-project/svg2ttf 挂载到容器中 /data/svg2ttf 项目下,方便文件拷贝共享
docker run -d \
--name fontforge-test-container \
-v "/mnt/d/php-project/svg2ttf:/data/svg2ttf" \
ubuntu-fontforge \
sleep infinity
# 执行后可以拉起一个运行中的容器 fontforge-test-container,查看运行容器列表
docker ps
# 进入容器
docker exec -it fontforge-test-container bash
svg 转字体文件脚本
进入容器后,后边就是核心业务,将 svg 转为字体文件的过程了。可能涉及的脚本如下:
svg 字形文件源码示例:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 200 200" width="200" height="200">
<g fill="black">
<path d="M 84.5 66.0 L 84.5 70.0 L 51.0 70.0 L 51.0 183.5 L 41.0 188.5 L 41.0 70.0 L 11.8 70.0 L 11.8 66.0 L 62.5 66.0 L 72.5 56.0 Z M 121.1 126.3 L 120.03469390869141 135.0 L 146.8 135.0 L 146.8 100.0 L 158.8 102.0 L 161.8 104.0 L 158.8 105.8 L 158.8 135.0 L 165.5 135.0 L 175.5 125.0 L 187.5 135.0 L 187.5 139.0 L 158.8 139.0 L 158.8 181.0 L 146.8 187.0 L 146.8 139.0 L 119.26956520080566 139.0 L 117.9 145.3 L 115.3 153.6 L 111.8 161.2 L 107.7 168.0 L 102.8 173.9 L 97.3 178.9 L 91.1 182.9 L 84.3 185.9 L 76.9 187.8 L 76.3 186.1 L 82.6 182.5 L 88.1 178.4 L 92.9 174.0 L 97.0 169.0 L 100.5 163.4 L 103.4 157.3 L 105.7 150.4 L 107.5 142.9 L 108.01686744689941 139.0 L 80.4 139.0 L 80.4 135.0 L 108.54698791503907 135.0 L 108.6 134.6 L 109.2 125.6 L 109.2 100.0 L 121.2 102.0 L 124.2 104.0 L 121.2 105.8 L 121.2 126.0 L 121.0037735939026 126.29433965682983 Z M 30.7 88.0 L 33.4 90.0 L 30.7 91.72340421676635 L 30.7 120.0 L 30.2 128.2 L 29.5 135.9 L 28.5 143.0 L 27.2 149.5 L 25.6 155.4 L 23.7 160.7 L 21.4 165.3 L 18.7 169.2 L 15.7 172.3 L 12.4 174.6 L 11.3 173.3 L 13.0 170.2 L 14.5 166.7 L 15.9 162.8 L 17.1 158.4 L 18.0 153.5 L 18.8 148.0 L 19.4 141.9 L 19.7 135.2 L 19.8 127.9 L 19.7 120.0 L 19.7 86.0 Z M 74.1 88.0 L 76.8 90.0 L 74.1 91.72340421676635 L 74.1 155.2 L 63.1 160.7 L 63.1 86.0 Z M 83.0 93.7 L 128.0 93.7 L 128.0 63.4 L 94.3 63.4 L 94.3 59.4 L 128.0 59.4 L 128.0 29.6 L 88.9 29.6 L 88.9 25.6 L 157.0 25.6 L 167.0 15.6 L 179.0 25.6 L 179.0 29.6 L 140.0 29.6 L 140.0 59.4 L 151.6 59.4 L 161.6 49.4 L 173.6 59.4 L 173.6 63.4 L 140.0 63.4 L 140.0 93.7 L 162.9 93.7 L 172.9 83.7 L 184.9 93.7 L 184.9 97.7 L 83.0 97.7 Z M 75.3 30.0 L 75.3 34.0 L 20.2 34.0 L 20.2 30.0 L 53.3 30.0 L 63.3 20.0 Z " />
</g>
</svg>
批量将 svg 字体文件生成为 ttf/woff2 等字体文件
注意输入 svg 文件源码里需要只有一个 path 标签。要求输入文件的名称为 u{unicode}.svg 形式,名称字母全部小写。
可以指定输入目录,输出目录,输出字体文件类型,以及字体文件的名称。
此脚本可以将每个 svg 文件生成一个对应的字体文件(输入文件数=输出字体文件数)。
需要将所有字体文件打包成一个字体文件的,见后边脚本。
python
import fontforge
import os
import re
import tempfile
# --------------------------
# Centralized Configuration (Modify these)
# --------------------------
CONFIG = {
# Input/Output
"INPUT_DIR": "./svg_g/wiki", # Directory containing "u{unicode}.svg" files
"OUTPUT_DIR": "./output/g/", # Auto-generate if None (same as input dir)
# --- New Configuration: Font Format ---
# Possible values: 'ttf', 'woff', 'woff2', 'svg' (depending on FontForge support)
"FONT_FORMAT": "woff2",
# Font Metrics (adjust if glyph is cut off)
"ASCENT_RATIO": 0.9, # Space above baseline (90% of SVG width)
"DESCENT_RATIO": 0.1, # Space below baseline (10% of SVG width)
# Font Internal Names (will be embedded in all generated font files)
"FONT_FAMILY": "HanDianJiFont",
"FONT_FULLNAME": "HanDianJiFont",
"FONT_FONTNAME": "HanDianJi-Regular",
# Temporary SVG Template (for old FontForge compatibility)
"TEMP_SVG_TPL": '''<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{width}" viewBox="0 0 {width} {width}">
<path d="{path_d}" fill="black"/>
</svg>'''
}
# --------------------------
def extract_svg_data_from_file(svg_file_path):
"""Extract Unicode, path data, and width from a single SVG file."""
try:
filename = os.path.basename(svg_file_path)
unicode_match = re.match(r'^u([0-9a-f]{1,6})\.svg$', filename.lower())
if not unicode_match:
return None, None, None, None, "Invalid filename format"
unicode_code = int(unicode_match.group(1), 16)
unicode_hex = hex(unicode_code)[2:].upper()
with open(svg_file_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
path_match = re.search(r'<path[^>]*?d="([^"]+)"', svg_content, re.IGNORECASE)
if not path_match:
return None, None, None, None, "No <path> tag or 'd' attribute"
path_d = path_match.group(1)
width_match = re.search(r'width\s*=\s*["\']?(\d+)["\']?', svg_content, re.IGNORECASE)
svg_width = int(width_match.group(1)) if width_match else 200
return unicode_code, unicode_hex, path_d, svg_width, None
except Exception as e:
return None, None, None, None, f"Parsing failed: {str(e)}"
def create_temp_svg(path_d, svg_width):
"""Create a minimal temporary SVG for old FontForge to import."""
temp_svg_content = CONFIG["TEMP_SVG_TPL"].format(width=svg_width, path_d=path_d)
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.svg', delete=False, encoding='utf-8')
temp_file.write(temp_svg_content)
temp_file_path = temp_file.name
temp_file.close()
return temp_file_path
def generate_font_for_svg(svg_file_path, output_dir, font_format):
"""Generate a single font file for one SVG based on the specified format."""
unicode_code, unicode_hex, path_d, svg_width, err = extract_svg_data_from_file(svg_file_path)
if err:
print(f"Warning: Skipping invalid file '{os.path.basename(svg_file_path)}' -> {err}")
return False
# Prepare output font path with the correct extension
font_filename = os.path.basename(svg_file_path).replace('.svg', f'.{font_format}')
output_font_path = os.path.join(output_dir, font_filename)
font = fontforge.font()
font.em = svg_width
font.ascent = int(svg_width * CONFIG["ASCENT_RATIO"])
font.descent = int(svg_width * CONFIG["DESCENT_RATIO"])
font.familyname = CONFIG["FONT_FAMILY"]
font.fullname = CONFIG["FONT_FULLNAME"]
font.fontname = CONFIG["FONT_FONTNAME"]
glyph = font.createChar(unicode_code)
glyph.width = svg_width
temp_svg_path = create_temp_svg(path_d, svg_width)
glyph.importOutlines(temp_svg_path)
os.unlink(temp_svg_path)
try:
# FontForge uses the filename extension to determine the output format
font.generate(output_font_path)
print(f"Success: Generated {font_format.upper()} -> '{font_filename}' (U+{unicode_hex})")
return True
except Exception as e:
print(f"Error: Failed to generate {font_format.upper()} for '{font_filename}' -> {str(e)}")
return False
finally:
font.close()
def batch_convert():
"""Main function to batch convert all valid SVGs in a directory."""
input_dir = CONFIG["INPUT_DIR"]
output_dir = CONFIG["OUTPUT_DIR"] if CONFIG["OUTPUT_DIR"] else input_dir
font_format = CONFIG["FONT_FORMAT"].lower() # Normalize to lowercase
supported_formats = ['ttf', 'woff', 'woff2', 'svg']
if font_format not in supported_formats:
print(f"Error: Unsupported font format '{font_format}'. Supported formats are: {', '.join(supported_formats)}")
return
if not os.path.isdir(input_dir):
print(f"Error: Input directory not found -> {input_dir}")
return
os.makedirs(output_dir, exist_ok=True)
print(f"Info: Scanning for SVG files in '{input_dir}'...")
print(f"Info: Target font format: {font_format.upper()}")
total_files = 0
success_count = 0
for filename in os.listdir(input_dir):
if filename.lower().endswith('.svg'):
full_path = os.path.join(input_dir, filename)
if generate_font_for_svg(full_path, output_dir, font_format):
success_count += 1
total_files += 1
print("\nBatch conversion complete.")
print(f"Summary: Processed {total_files} SVG file(s), successfully generated {success_count} {font_format.upper()} file(s).")
if __name__ == "__main__":
batch_convert()
将多个 svg 字形文件打包成一个字体文件合集
svg 文件要求同上。
配置输入文件夹,和输出字体文件类型,扩展名决定最终的字体文件类型。
python
import fontforge
import os
import re
import tempfile
# --------------------------
# Centralized Configuration (Modify these)
# --------------------------
CONFIG = {
# Input/Output
"INPUT_DIR": "./svg_g/wiki", # Directory containing "u{unicode}.svg" files
"OUTPUT_FILE": "./output/handianji-collection.woff2", # Path for the single output font file
# Font Metrics (adjust if glyphs are cut off)
# All glyphs will be scaled to fit this统一的尺寸
"UNIFIED_EM_SIZE": 1024,
"ASCENT_RATIO": 0.85, # Space above baseline (85% of EM size)
"DESCENT_RATIO": 0.15, # Space below baseline (15% of EM size)
# Font Internal Names (will be embedded in the generated font file)
"FONT_FAMILY": "HanDianJiCollection",
"FONT_FULLNAME": "HanDianJi Collection",
"FONT_FONTNAME": "HanDianJi-Collection-Regular",
# Temporary SVG Template (for old FontForge compatibility)
"TEMP_SVG_TPL": '''<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{width}" viewBox="0 0 {width} {width}">
<path d="{path_d}" fill="black"/>
</svg>'''
}
# --------------------------
def extract_svg_data_from_file(svg_file_path):
"""Extract Unicode, path data, and width from a single SVG file."""
try:
filename = os.path.basename(svg_file_path)
unicode_match = re.match(r'^u([0-9a-f]{1,6})\.svg$', filename.lower())
if not unicode_match:
return None, None, None, f"Invalid filename format: {filename}"
unicode_code = int(unicode_match.group(1), 16)
with open(svg_file_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
path_match = re.search(r'<path[^>]*?d="([^"]+)"', svg_content, re.IGNORECASE)
if not path_match:
return None, None, None, f"No <path> tag or 'd' attribute: {filename}"
path_d = path_match.group(1)
width_match = re.search(r'width\s*=\s*["\']?(\d+)["\']?', svg_content, re.IGNORECASE)
svg_width = int(width_match.group(1)) if width_match else 200
return unicode_code, path_d, svg_width, None
except Exception as e:
return None, None, None, f"Parsing failed for {os.path.basename(svg_file_path)}: {str(e)}"
def create_temp_svg(path_d, svg_width):
"""Create a minimal temporary SVG for old FontForge to import."""
temp_svg_content = CONFIG["TEMP_SVG_TPL"].format(width=svg_width, path_d=path_d)
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.svg', delete=False, encoding='utf-8')
temp_file.write(temp_svg_content)
temp_file_path = temp_file.name
temp_file.close()
return temp_file_path
def merge_svgs_to_single_font():
"""Main function to merge all valid SVGs into a single font file."""
input_dir = CONFIG["INPUT_DIR"]
output_file = CONFIG["OUTPUT_FILE"]
if not os.path.isdir(input_dir):
print(f"Error: Input directory not found -> {input_dir}")
return
# Create output directory if it doesn't exist
os.makedirs(os.path.dirname(output_file), exist_ok=True)
print(f"Info: Scanning for SVG files in '{input_dir}'...")
# Initialize a single FontForge font object
font = fontforge.font()
font.em = CONFIG["UNIFIED_EM_SIZE"]
font.ascent = int(font.em * CONFIG["ASCENT_RATIO"])
font.descent = int(font.em * CONFIG["DESCENT_RATIO"])
font.familyname = CONFIG["FONT_FAMILY"]
font.fullname = CONFIG["FONT_FULLNAME"]
font.fontname = CONFIG["FONT_FONTNAME"]
success_count = 0
error_count = 0
# Process each SVG file and add it as a glyph to the same font
for filename in os.listdir(input_dir):
if filename.lower().endswith('.svg'):
full_path = os.path.join(input_dir, filename)
unicode_code, path_d, svg_width, err = extract_svg_data_from_file(full_path)
if err:
print(f"Warning: {err}")
error_count += 1
continue
try:
# Create a new glyph in the font for this Unicode code point
glyph = font.createChar(unicode_code)
glyph.width = font.em # Set glyph width to the unified EM size
# Create a temporary SVG with the path data
temp_svg_path = create_temp_svg(path_d, svg_width)
# Import the path. FontForge will handle scaling to the glyph's width.
glyph.importOutlines(temp_svg_path)
# Clean up the temporary file
os.unlink(temp_svg_path)
unicode_hex = hex(unicode_code)[2:].upper()
print(f"Success: Added glyph -> '{filename}' (U+{unicode_hex})")
success_count += 1
except Exception as e:
print(f"Error: Failed to add glyph from '{filename}' -> {str(e)}")
error_count += 1
# Clean up in case of failure
if 'temp_svg_path' in locals() and os.path.exists(temp_svg_path):
os.unlink(temp_svg_path)
if success_count == 0:
print("\nError: No valid glyphs were added. Font file will not be generated.")
font.close()
return
# Generate the final single font file
try:
# FontForge determines the format based on the output file's extension
font.generate(output_file)
print(f"\nSuccess: All glyphs merged into a single font file! -> {output_file}")
print(f"Summary: Successfully added {success_count} glyph(s). {error_count} error(s) occurred.")
except Exception as e:
print(f"\nError: Failed to generate the final font file -> {str(e)}")
finally:
font.close()
print("\nInfo: Font file closed.")
if __name__ == "__main__":
merge_svgs_to_single_font()
使用转换成功的字体文件
通过 @font-face 来加载字体文件。示例如下,将以下 css 放到页面中可以测试下生成的字体文件是否成功。
css
@font-face {
font-family: 'hdjFont';
src: url('/assets/fonts/ttf/u30a00.ttf') format('truetype');
font-weight: normal;
font-style: normal;
unicode-range: U+30A00-30A00;
}
.test {
font-family: 'hdjFont', sans-serif !important;
}
以上代码均能正常使用,相关自己文件也已经上线完成。