fzg - oss pixel font, combining fairfax, zlabs and galmuri

[fzg] compile tt/ot only

do u know? eot and woff formats are just a compression method of specific ot font. like otf.gz file. ttf file can be treated as otf too.

Changed files
+260 -116
docs
tools
+2
README.md
··· 2 2 3 3 fzg 是 Z Labs Bitmap 的软 fork,最终的字体将基于 Fairfax 的 metrics,包含 Fairfax、Z Labs 和 Galmuri 的字形。 4 4 5 + [查看 VSCode 上的 FZG 编译指南。](docs/fzg_build_guide.md) 6 + 5 7 上游的 README 文件如下: 6 8 7 9 ---
+12
docs/fzg_build_guide.md
··· 1 + # VSCode 上的 FZG 编译指南。 2 + 3 + *(以 VSCodium 为例)* 4 + 5 + ``` 6 + $ git clone --recurse-submodules https://tangled.org/@dwn.dwnfonts.cc/fzg 7 + $ codium . 8 + ``` 9 + 10 + 这时候会打开 VSCodium。点击顶部搜索框,搜索 `>Python: Create Environment`,选择 `Venv`,**不要**勾选 `requirements.txt`。 11 + 12 + 导航到 [tools/build.py](../tools/build.py),点击 `Python Debugger: Debug Python File` 开始编译。你也可以使用 `python -m tools.build`
+241 -107
tools/build.py
··· 1 1 import math 2 2 import shutil 3 3 import zipfile 4 + import threading 5 + import logging # 导入logging模块 4 6 5 7 from fontTools.ttLib import TTFont 6 8 from kbitfont import KbitFont 7 - from pixel_font_builder import FontBuilder, WeightName, SerifStyle, SlantStyle, WidthStyle, Glyph 9 + from pixel_font_builder import ( 10 + FontBuilder, 11 + WeightName, 12 + SerifStyle, 13 + SlantStyle, 14 + WidthStyle, 15 + Glyph, 16 + ) 8 17 9 18 from tools import path_define, options 10 19 from tools.kbitx_marge_selected import advanced_merge_kbitx_files ··· 12 21 13 22 from fzg import fix_metrics 14 23 24 + # 配置logging 25 + logging.basicConfig( 26 + level=logging.INFO, 27 + format="%(asctime)s - %(levelname)s - %(message)s", 28 + datefmt="%Y-%m-%d %H:%M:%S", 29 + ) 30 + 31 + 15 32 def fix_mono_mode(font: TTFont): 16 - font['post'].isFixedPitch = 1 17 - font['OS/2'].panose.bFamilyType = 2 18 - font['OS/2'].panose.bProportion = 9 # 修改字体的 Panose 属性,使其能够被识别为等宽字体 19 - font['OS/2'].xAvgCharWidth = 600 # 修复 Panose 属性引起的字宽问题 20 - font['OS/2'].achVendID = "ZLAB" 21 - font['OS/2'].ulCodePageRange1 = 0b1100000000101100000000000001101 22 - font['OS/2'].ulCodePageRange2 = 0b10000110101010000000000000000 # 为字体添加微软编码页属性,防止某些程序不识别 33 + font["post"].isFixedPitch = 1 34 + font["OS/2"].panose.bFamilyType = 2 35 + font["OS/2"].panose.bProportion = ( 36 + 9 # 修改字体的 Panose 属性,使其能够被识别为等宽字体 37 + ) 38 + font["OS/2"].xAvgCharWidth = 600 # 修复 Panose 属性引起的字宽问题 39 + font["OS/2"].achVendID = "ZLAB" 40 + font["OS/2"].ulCodePageRange1 = 0b1100000000101100000000000001101 41 + font["OS/2"].ulCodePageRange2 = ( 42 + 0b10000110101010000000000000000 # 为字体添加微软编码页属性,防止某些程序不识别 43 + ) 23 44 24 45 25 - def main(): 26 - fix_metrics.main() 46 + def process_region(region): 47 + """处理单个地区的kbitx文件合并""" 48 + logging.info(f"开始处理地区: {region}") 27 49 28 - if path_define.build_dir.exists(): 29 - shutil.rmtree(path_define.build_dir) 30 - path_define.outputs_dir.mkdir(parents=True) 31 - path_define.releases_dir.mkdir(parents=True) 50 + if region != "CN": 51 + merge_kbitx_files( 52 + path_define.data_dir.joinpath(f"ZLabsBitmapCN.kbitx"), 53 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_diff.kbitx"), 54 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_fallback.kbitx"), 55 + ) 56 + else: 57 + shutil.copy( 58 + path_define.data_dir.joinpath("ZLabsBitmapCN.kbitx"), 59 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_fallback.kbitx"), 60 + ) 61 + merge_kbitx_files( 62 + path_define.data_dir.joinpath(f"Galmuri11.kbitx"), 63 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_fallback.kbitx"), 64 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_temp.kbitx"), 65 + ) 66 + merge_kbitx_files( 67 + path_define.data_dir.joinpath(f"ZLabsBitmap{region}_temp.kbitx"), 68 + path_define.data_dir.joinpath(f"Fairfax.kbitx"), 69 + path_define.data_dir.joinpath(f"FZG_{region}.kbitx"), 70 + ) 71 + merge_kbitx_files( 72 + path_define.data_dir.joinpath(f"FZG_{region}.kbitx"), 73 + path_define.src_dir.joinpath(f"ZLabsBitmap{region}_meta.kbitx"), 74 + path_define.data_dir.joinpath(f"FZG_{region}.kbitx"), 75 + ) 32 76 33 - # shutil.copy(path_define.src_dir.joinpath('ZLabsBitmapCN.kbitx'), path_define.data_dir) 34 - # there is fixed version of cn, don't copy again 35 - for region in ['CN', 'HC', 'JP']: 36 - # advanced_merge_kbitx_files(path_define.data_dir.joinpath(f'ZLabsBitmapCN.kbitx'), 37 - # path_define.data_dir.joinpath(f'ZLabsBitmap{region}_diff.kbitx'), 38 - # path_define.src_dir.joinpath(f'flags_{region}.txt'), 39 - # path_define.data_dir.joinpath(f'ZLabsBitmap{region}.kbitx')) 40 - # just make the fallback version 41 - if region != 'CN': 42 - merge_kbitx_files(path_define.data_dir.joinpath(f'ZLabsBitmapCN.kbitx'), 43 - path_define.data_dir.joinpath(f'ZLabsBitmap{region}_diff.kbitx'), 44 - path_define.data_dir.joinpath(f'ZLabsBitmap{region}_fallback.kbitx')) 45 - else: 46 - shutil.copy(path_define.data_dir.joinpath('ZLabsBitmapCN.kbitx'), path_define.data_dir.joinpath(f'ZLabsBitmap{region}_fallback.kbitx')) 47 - merge_kbitx_files(path_define.data_dir.joinpath(f'Galmuri11.kbitx'), 48 - path_define.data_dir.joinpath(f'ZLabsBitmap{region}_fallback.kbitx'), 49 - path_define.data_dir.joinpath(f'ZLabsBitmap{region}_temp.kbitx')) 50 - merge_kbitx_files(path_define.data_dir.joinpath(f'ZLabsBitmap{region}_temp.kbitx'), 51 - path_define.data_dir.joinpath(f'Fairfax.kbitx'), 52 - path_define.data_dir.joinpath(f'FZG_{region}.kbitx')) 53 - merge_kbitx_files(path_define.data_dir.joinpath(f'FZG_{region}.kbitx'), 54 - path_define.src_dir.joinpath(f'ZLabsBitmap{region}_meta.kbitx'), 55 - path_define.data_dir.joinpath(f'FZG_{region}.kbitx')) 77 + logging.info(f"完成处理地区: {region}") 56 78 57 79 58 - for language_flavor in options.language_flavors: 59 - kbit_font = KbitFont.load_kbitx(path_define.data_dir.joinpath(f'FZG_{language_flavor}.kbitx')) 80 + def process_otf(language_flavor, builder): 81 + """处理OTF字体格式""" 82 + otf_font = builder.to_otf_builder().font 83 + fix_mono_mode(otf_font) 60 84 85 + otf_font.save( 86 + path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.otf") 87 + ) 88 + logging.info(f"已创建 {language_flavor} otf") 61 89 62 - builder = FontBuilder() 63 - builder.font_metric.font_size = kbit_font.props.em_height 64 - builder.font_metric.horizontal_layout.ascent = kbit_font.props.line_ascent 65 - builder.font_metric.horizontal_layout.descent = -kbit_font.props.line_descent 66 - builder.font_metric.horizontal_layout.line_gap = 1 67 - builder.font_metric.vertical_layout.ascent = math.ceil(kbit_font.props.line_height / 2) 68 - builder.font_metric.vertical_layout.descent = -math.floor(kbit_font.props.line_height / 2) 69 - builder.font_metric.x_height = kbit_font.props.x_height 70 - builder.font_metric.cap_height = kbit_font.props.cap_height 90 + # otf_font.flavor = "woff" 91 + # otf_font.save( 92 + # path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.otf.woff") 93 + # ) 94 + # logging.info(f"已创建 {language_flavor} otf.woff") 95 + 96 + # otf_font.flavor = "woff2" 97 + # otf_font.save( 98 + # path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.otf.woff2") 99 + # ) 100 + # logging.info(f"已创建 {language_flavor} otf.woff2") 101 + 102 + 103 + def process_ttf(language_flavor, builder): 104 + """处理TTF字体格式""" 105 + ttf_font = builder.to_ttf_builder().font 106 + fix_mono_mode(ttf_font) 107 + 108 + ttf_font.save( 109 + path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.ttf") 110 + ) 111 + logging.info(f"已创建 {language_flavor} ttf") 112 + 113 + # ttf_font.flavor = "woff" 114 + # ttf_font.save( 115 + # path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.ttf.woff") 116 + # ) 117 + # logging.info(f"已创建 {language_flavor} ttf.woff") 71 118 72 - builder.meta_info.version = kbit_font.names.version 73 - builder.meta_info.weight_name = WeightName.REGULAR 74 - builder.meta_info.serif_style = SerifStyle.SERIF 75 - builder.meta_info.slant_style = SlantStyle.NORMAL 76 - builder.meta_info.width_style = WidthStyle.MONOSPACED 77 - builder.meta_info.manufacturer = kbit_font.names.manufacturer 78 - builder.meta_info.designer = kbit_font.names.designer 79 - builder.meta_info.description = kbit_font.names.description 80 - builder.meta_info.copyright_info = kbit_font.names.copyright 81 - builder.meta_info.license_info = kbit_font.names.license_description 82 - builder.meta_info.vendor_url = kbit_font.names.vendor_url 83 - builder.meta_info.designer_url = kbit_font.names.designer_url 84 - builder.meta_info.license_url = kbit_font.names.license_url 85 - builder.meta_info.sample_text = kbit_font.names.sample_text 119 + # ttf_font.flavor = "woff2" 120 + # ttf_font.save( 121 + # path_define.outputs_dir.joinpath(f"FZG_{language_flavor.upper()}.ttf.woff2") 122 + # ) 123 + # logging.info(f"已创建 {language_flavor} ttf.woff2") 86 124 87 - if language_flavor == 'HC_fallback' or language_flavor == 'JP_fallback': 88 - builder.meta_info.family_name = kbit_font.names.family + ' Fallback' 89 - else: 90 - builder.meta_info.family_name = kbit_font.names.family 91 125 126 + def process_language_flavor(language_flavor): 127 + """处理单个语言变体的字体生成,并行处理OTF和TTF""" 128 + logging.info(f"开始处理语言变体: {language_flavor}") 92 129 93 - k_glyph_notdef = kbit_font.named_glyphs['.notdef'] 94 - builder.glyphs.append(Glyph( 95 - name='.notdef', 96 - horizontal_offset=(k_glyph_notdef.x, k_glyph_notdef.y - k_glyph_notdef.height), 130 + kbit_font = KbitFont.load_kbitx( 131 + path_define.data_dir.joinpath(f"FZG_{language_flavor}.kbitx") 132 + ) 133 + 134 + builder = FontBuilder() 135 + builder.font_metric.font_size = kbit_font.props.em_height 136 + builder.font_metric.horizontal_layout.ascent = kbit_font.props.line_ascent 137 + builder.font_metric.horizontal_layout.descent = -kbit_font.props.line_descent 138 + builder.font_metric.horizontal_layout.line_gap = 1 139 + builder.font_metric.vertical_layout.ascent = math.ceil( 140 + kbit_font.props.line_height / 2 141 + ) 142 + builder.font_metric.vertical_layout.descent = -math.floor( 143 + kbit_font.props.line_height / 2 144 + ) 145 + builder.font_metric.x_height = kbit_font.props.x_height 146 + builder.font_metric.cap_height = kbit_font.props.cap_height 147 + 148 + builder.meta_info.version = kbit_font.names.version 149 + builder.meta_info.weight_name = WeightName.REGULAR 150 + builder.meta_info.serif_style = SerifStyle.SERIF 151 + builder.meta_info.slant_style = SlantStyle.NORMAL 152 + builder.meta_info.width_style = WidthStyle.MONOSPACED 153 + builder.meta_info.manufacturer = kbit_font.names.manufacturer 154 + builder.meta_info.designer = kbit_font.names.designer 155 + builder.meta_info.description = kbit_font.names.description 156 + builder.meta_info.copyright_info = kbit_font.names.copyright 157 + builder.meta_info.license_info = kbit_font.names.license_description 158 + builder.meta_info.vendor_url = kbit_font.names.vendor_url 159 + builder.meta_info.designer_url = kbit_font.names.designer_url 160 + builder.meta_info.license_url = kbit_font.names.license_url 161 + builder.meta_info.sample_text = kbit_font.names.sample_text 162 + 163 + if language_flavor == "HC_fallback" or language_flavor == "JP_fallback": 164 + builder.meta_info.family_name = kbit_font.names.family + " Fallback" 165 + else: 166 + builder.meta_info.family_name = kbit_font.names.family 167 + 168 + k_glyph_notdef = kbit_font.named_glyphs[".notdef"] 169 + builder.glyphs.append( 170 + Glyph( 171 + name=".notdef", 172 + horizontal_offset=( 173 + k_glyph_notdef.x, 174 + k_glyph_notdef.y - k_glyph_notdef.height, 175 + ), 97 176 advance_width=k_glyph_notdef.advance, 98 - vertical_offset=(k_glyph_notdef.width // 2, kbit_font.props.em_ascent - k_glyph_notdef.y), 177 + vertical_offset=( 178 + k_glyph_notdef.width // 2, 179 + kbit_font.props.em_ascent - k_glyph_notdef.y, 180 + ), 99 181 advance_height=kbit_font.props.em_height, 100 - bitmap=[[0 if color <= 127 else 1 for color in bitmap_row] for bitmap_row in k_glyph_notdef.bitmap], 101 - )) 182 + bitmap=[ 183 + [0 if color <= 127 else 1 for color in bitmap_row] 184 + for bitmap_row in k_glyph_notdef.bitmap 185 + ], 186 + ) 187 + ) 102 188 103 - for code_point, k_glyph in sorted(kbit_font.characters.items()): 104 - glyph_name = f'{code_point:04X}' 105 - builder.character_mapping[code_point] = glyph_name 106 - builder.glyphs.append(Glyph( 189 + for code_point, k_glyph in sorted(kbit_font.characters.items()): 190 + glyph_name = f"{code_point:04X}" 191 + builder.character_mapping[code_point] = glyph_name 192 + builder.glyphs.append( 193 + Glyph( 107 194 name=glyph_name, 108 195 horizontal_offset=(k_glyph.x, k_glyph.y - k_glyph.height), 109 196 advance_width=k_glyph.advance, 110 - vertical_offset=(k_glyph.width // 2, kbit_font.props.em_ascent - k_glyph.y), 197 + vertical_offset=( 198 + k_glyph.width // 2, 199 + kbit_font.props.em_ascent - k_glyph.y, 200 + ), 111 201 advance_height=kbit_font.props.em_height, 112 - bitmap=[[0 if color <= 127 else 1 for color in bitmap_row] for bitmap_row in k_glyph.bitmap], 113 - )) 202 + bitmap=[ 203 + [0 if color <= 127 else 1 for color in bitmap_row] 204 + for bitmap_row in k_glyph.bitmap 205 + ], 206 + ) 207 + ) 114 208 115 - otf_font = builder.to_otf_builder().font 116 - fix_mono_mode(otf_font) 209 + # 创建并启动OTF和TTF处理线程,实现并行处理 210 + otf_thread = threading.Thread(target=process_otf, args=(language_flavor, builder)) 211 + ttf_thread = threading.Thread(target=process_ttf, args=(language_flavor, builder)) 117 212 118 - otf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.otf')) 119 - print(f'Create {language_flavor} otf') 213 + otf_thread.start() 214 + ttf_thread.start() 120 215 121 - otf_font.flavor = 'woff' 122 - otf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.otf.woff')) 123 - print(f'Create {language_flavor} otf.woff') 216 + # 等待两个线程完成 217 + otf_thread.join() 218 + ttf_thread.join() 124 219 125 - otf_font.flavor = 'woff2' 126 - otf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.otf.woff2')) 127 - print(f'Create {language_flavor} otf.woff2') 220 + logging.info(f"完成处理语言变体: {language_flavor}") 128 221 129 - ttf_font = builder.to_ttf_builder().font 130 - fix_mono_mode(ttf_font) 222 + 223 + def process_font_format(font_format): 224 + """处理单个字体格式的压缩包生成""" 225 + logging.info(f"开始处理字体格式: {font_format}") 226 + 227 + with zipfile.ZipFile( 228 + path_define.releases_dir.joinpath(f"FZG_-{font_format}.zip"), "w" 229 + ) as file: 230 + file.write(path_define.project_root_dir.joinpath("LICENSE-OFL"), "LICENSE") 231 + for font_file_path in path_define.outputs_dir.iterdir(): 232 + if font_file_path.name.endswith(f".{font_format}"): 233 + file.write(font_file_path, font_file_path.name) 234 + 235 + logging.info(f"已创建 {font_format} zip") 236 + 237 + 238 + def main(): 239 + logging.info("开始执行字体处理程序") 240 + 241 + fix_metrics.main() 242 + 243 + if path_define.build_dir.exists(): 244 + shutil.rmtree(path_define.build_dir) 245 + path_define.outputs_dir.mkdir(parents=True) 246 + path_define.releases_dir.mkdir(parents=True) 247 + 248 + # 处理地区的多线程实现 249 + region_threads = [] 250 + for region in ["CN", "HC", "JP"]: 251 + thread = threading.Thread(target=process_region, args=(region,)) 252 + region_threads.append(thread) 253 + thread.start() 131 254 132 - ttf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.ttf')) 133 - print(f'Create {language_flavor} ttf') 255 + # 等待所有地区处理线程完成 256 + for thread in region_threads: 257 + thread.join() 134 258 135 - ttf_font.flavor = 'woff' 136 - ttf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.ttf.woff')) 137 - print(f'Create {language_flavor} ttf.woff') 259 + # 处理语言变体的多线程实现 260 + flavor_threads = [] 261 + for language_flavor in options.language_flavors: 262 + thread = threading.Thread( 263 + target=process_language_flavor, args=(language_flavor,) 264 + ) 265 + flavor_threads.append(thread) 266 + thread.start() 138 267 139 - ttf_font.flavor = 'woff2' 140 - ttf_font.save(path_define.outputs_dir.joinpath(f'FZG_{language_flavor.upper()}.ttf.woff2')) 141 - print(f'Create {language_flavor} ttf.woff2') 268 + # 等待所有语言变体处理线程完成 269 + for thread in flavor_threads: 270 + thread.join() 142 271 272 + # 处理字体格式的多线程实现 273 + format_threads = [] 143 274 for font_format in options.font_formats: 144 - with zipfile.ZipFile(path_define.releases_dir.joinpath(f'FZG_-{font_format}.zip'), 'w') as file: 145 - file.write(path_define.project_root_dir.joinpath('LICENSE-OFL'), 'LICENSE') 146 - for font_file_path in path_define.outputs_dir.iterdir(): 147 - if font_file_path.name.endswith(f'.{font_format}'): 148 - file.write(font_file_path, font_file_path.name) 149 - print(f'Create {font_format} zip') 275 + thread = threading.Thread(target=process_font_format, args=(font_format,)) 276 + format_threads.append(thread) 277 + thread.start() 278 + 279 + # 等待所有字体格式处理线程完成 280 + for thread in format_threads: 281 + thread.join() 282 + 283 + logging.info("字体处理程序执行完成") 150 284 151 285 152 - if __name__ == '__main__': 286 + if __name__ == "__main__": 153 287 main()
+5 -9
tools/options.py
··· 1 1 from typing import Literal, get_args 2 2 3 3 type LanguageFlavor = Literal[ 4 - 'CN', 5 - 'HC', 6 - 'JP', 4 + "CN", 5 + "HC", 6 + "JP", 7 7 ] 8 8 language_flavors = list[LanguageFlavor](get_args(LanguageFlavor.__value__)) 9 9 10 10 type FontFormat = Literal[ 11 - 'otf', 12 - 'otf.woff', 13 - 'otf.woff2', 14 - 'ttf', 15 - 'ttf.woff', 16 - 'ttf.woff2', 11 + "otf", 12 + "ttf", 17 13 ] 18 14 font_formats = list[FontFormat](get_args(FontFormat.__value__))