From 388cd0d2291b017b3c46d52d72ae55e04707da55 Mon Sep 17 00:00:00 2001 From: TakWolf Date: Sun, 21 Jul 2024 05:54:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=9E=84=E5=BB=BA=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/build.py | 13 ++--- tools/configs/__init__.py | 92 ++++++++++++++----------------- tools/configs/dump.py | 19 +++++++ tools/configs/dump_config.py | 19 ------- tools/configs/font.py | 24 ++++++++ tools/configs/font_config.py | 19 ------- tools/services/dump_service.py | 96 ++++++++++++++------------------ tools/services/font_service.py | 97 ++++++++++++--------------------- tools/services/image_service.py | 20 +++---- tools/utils/__init__.py | 0 tools/utils/glyph_util.py | 32 ----------- 11 files changed, 176 insertions(+), 255 deletions(-) create mode 100644 tools/configs/dump.py delete mode 100644 tools/configs/dump_config.py create mode 100644 tools/configs/font.py delete mode 100644 tools/configs/font_config.py delete mode 100644 tools/utils/__init__.py delete mode 100644 tools/utils/glyph_util.py diff --git a/tools/build.py b/tools/build.py index e47c88d..236372d 100644 --- a/tools/build.py +++ b/tools/build.py @@ -13,14 +13,11 @@ def main(): dump_service.dump_font(dump_config) for font_config in configs.font_configs: - character_mapping, glyph_file_infos = font_service.collect_glyph_files(font_config) - font_service.make_font_files(font_config, character_mapping, glyph_file_infos) - image_service.make_preview_image_file(font_config) - - shutil.copy( - path_define.www_static_dir.joinpath('index.html'), - path_define.outputs_dir.joinpath('index.html'), - ) + character_mapping, glyph_sequence = font_service.collect_glyph_files(font_config) + font_service.make_fonts(font_config, character_mapping, glyph_sequence) + image_service.make_preview_image(font_config) + + shutil.copy(path_define.www_static_dir.joinpath('index.html'), path_define.outputs_dir.joinpath('index.html')) if __name__ == '__main__': diff --git a/tools/configs/__init__.py b/tools/configs/__init__.py index 778192f..3c2d850 100644 --- a/tools/configs/__init__.py +++ b/tools/configs/__init__.py @@ -1,114 +1,106 @@ -from tools.configs.dump_config import DumpConfig -from tools.configs.font_config import FontConfig +from typing import Literal, get_args -font_configs = [ - FontConfig( - size=12, - ascent=9, - descent=-3, - x_height=6, - cap_height=8, - source_names=['ASC12', 'HZK12'], - ), - FontConfig( - size=16, - ascent=12, - descent=-4, - x_height=7, - cap_height=10, - source_names=['ASC16', 'HZK16'], - ), -] +from tools.configs.dump import DumpConfig +from tools.configs.font import FontConfig + +version = '1.4.0' dump_configs = [ DumpConfig( font_name='ASC12', font_type='asc', - glyph_width=6, - glyph_height=12, + font_size=12, ), DumpConfig( font_name='ASC16', font_type='asc', - glyph_width=8, - glyph_height=16, + font_size=16, ), DumpConfig( font_name='ASC48', font_type='asc', - glyph_width=24, - glyph_height=48, + font_size=48, ), DumpConfig( font_name='HZK12', font_type='hzk', - glyph_width=12, - glyph_height=12, + font_size=12, ), DumpConfig( font_name='HZK14', font_type='hzk', - glyph_width=14, - glyph_height=14, + font_size=14, ), DumpConfig( font_name='HZK16', font_type='hzk', - glyph_width=16, - glyph_height=16, + font_size=16, ), DumpConfig( font_name='HZK16F', font_type='hzk', - glyph_width=16, - glyph_height=16, + font_size=16, ), DumpConfig( font_name='HZK16S', font_type='hzk', - glyph_width=16, - glyph_height=16, + font_size=16, ), DumpConfig( font_name='HZK24F', font_type='hzk', - glyph_width=24, - glyph_height=24, + font_size=24, ), DumpConfig( font_name='HZK24H', font_type='hzk', - glyph_width=24, - glyph_height=24, + font_size=24, ), DumpConfig( font_name='HZK24K', font_type='hzk', - glyph_width=24, - glyph_height=24, + font_size=24, ), DumpConfig( font_name='HZK24S', font_type='hzk', - glyph_width=24, - glyph_height=24, + font_size=24, ), DumpConfig( font_name='HZK32', font_type='hzk', - glyph_width=32, - glyph_height=32, + font_size=32, ), DumpConfig( font_name='HZK40', font_type='hzk', - glyph_width=40, - glyph_height=40, + font_size=40, ), DumpConfig( font_name='HZK48', font_type='hzk', - glyph_width=48, - glyph_height=48, + font_size=48, ), ] + +font_configs = [ + FontConfig( + font_size=12, + ascent=9, + descent=-3, + x_height=6, + cap_height=8, + source_names=['ASC12', 'HZK12'], + ), + FontConfig( + font_size=16, + ascent=12, + descent=-4, + x_height=7, + cap_height=10, + source_names=['ASC16', 'HZK16'], + ), +] + +type FontFormat = Literal['otf', 'ttf', 'woff2', 'bdf', 'pcf'] +font_formats = list[FontFormat](get_args(FontFormat.__value__)) diff --git a/tools/configs/dump.py b/tools/configs/dump.py new file mode 100644 index 0000000..47da552 --- /dev/null +++ b/tools/configs/dump.py @@ -0,0 +1,19 @@ +from typing import Literal + +type FontType = Literal['asc', 'hzk'] + + +class DumpConfig: + font_name: str + font_type: FontType + font_size: int + + def __init__( + self, + font_name: str, + font_type: FontType, + font_size: int, + ): + self.font_name = font_name + self.font_type = font_type + self.font_size = font_size diff --git a/tools/configs/dump_config.py b/tools/configs/dump_config.py deleted file mode 100644 index 4d93bcd..0000000 --- a/tools/configs/dump_config.py +++ /dev/null @@ -1,19 +0,0 @@ -import math - -from tools.configs import path_define - - -class DumpConfig: - def __init__(self, font_name: str, font_type: str, glyph_width: int, glyph_height: int): - self.font_name = font_name - self.font_file_path = path_define.fonts_dir.joinpath(font_name) - self.dump_dir = path_define.dump_dir.joinpath(font_name) - self.font_type = font_type - - self.glyph_width = glyph_width - self.glyph_height = glyph_height - self.glyph_col_bytes_length = math.ceil(glyph_width / 8) - self.glyph_bytes_length = self.glyph_col_bytes_length * glyph_height - self.glyph_last_col_byte_bit_stop = glyph_width % 8 - if self.glyph_last_col_byte_bit_stop == 0: - self.glyph_last_col_byte_bit_stop = 8 diff --git a/tools/configs/font.py b/tools/configs/font.py new file mode 100644 index 0000000..349016d --- /dev/null +++ b/tools/configs/font.py @@ -0,0 +1,24 @@ + +class FontConfig: + font_size: int + ascent: int + descent: int + x_height: int + cap_height: int + source_names: list[str] + + def __init__( + self, + font_size: int, + ascent: int, + descent: int, + x_height: int, + cap_height: int, + source_names: list[str], + ): + self.font_size = font_size + self.ascent = ascent + self.descent = descent + self.x_height = x_height + self.cap_height = cap_height + self.source_names = source_names diff --git a/tools/configs/font_config.py b/tools/configs/font_config.py deleted file mode 100644 index 3611889..0000000 --- a/tools/configs/font_config.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Final - - -class FontConfig: - VERSION: Final[str] = '1.4.0' - FAMILY_NAME: Final[str] = 'HZK Pixel' - DESCRIPTION: Final[str] = 'HZK pixel font.' - VENDOR_URL: Final[str] = 'https://hzk-pixel-font.takwolf.com' - - def __init__(self, size: int, ascent: int, descent: int, x_height: int, cap_height: int, source_names: list[str]): - self.size = size - self.ascent = ascent - self.descent = descent - self.x_height = x_height - self.cap_height = cap_height - self.source_names = source_names - - self.outputs_name = f'{FontConfig.FAMILY_NAME.lower().replace(" ", "-")}-{size}px' - self.preview_image_file_name = f'preview-{size}px.png' diff --git a/tools/services/dump_service.py b/tools/services/dump_service.py index e9e22e1..ea9f457 100644 --- a/tools/services/dump_service.py +++ b/tools/services/dump_service.py @@ -1,67 +1,55 @@ -from typing import IO +import math from character_encoding_utils import gb2312 from character_encoding_utils.gb2312 import GB2312Exception from loguru import logger +from pixel_font_knife.mono_bitmap import MonoBitmap -from tools.configs import DumpConfig -from tools.utils import glyph_util +from tools.configs import DumpConfig, path_define -def _dump_glyph(dump_config: DumpConfig, c: str, glyph_bytes: bytes): - glyph_data = [] - for row_index in range(dump_config.glyph_height): - glyph_data_row = [] - for col_index in range(dump_config.glyph_col_bytes_length): - b = glyph_bytes[row_index * dump_config.glyph_col_bytes_length + col_index] - for bit_index in range(dump_config.glyph_last_col_byte_bit_stop if col_index == dump_config.glyph_col_bytes_length - 1 else 8): - if 0b1 << (7 - bit_index) & b: - glyph_data_row.append(1) - else: - glyph_data_row.append(0) - glyph_data.append(glyph_data_row) - hex_name = f'{ord(c): 04X}' - file_path = dump_config.dump_dir.joinpath(f'{hex_name}.png') - glyph_util.save_glyph_data_to_png(glyph_data, file_path) - logger.info('Dump {} {}*{} {} - {}', dump_config.font_name, dump_config.glyph_width, dump_config.glyph_height, c if c.isprintable() else ' ', hex_name) - - -def _dump_font_ascii(dump_config: DumpConfig, file: IO, num_start: int, num_end: int): - for num in range(num_start, num_end + 1): - c = chr(num) - glyph_offset = num * dump_config.glyph_bytes_length - file.seek(glyph_offset) - glyph_bytes = file.read(dump_config.glyph_bytes_length) - if len(glyph_bytes) == 0: - break - _dump_glyph(dump_config, c, glyph_bytes) - - -def _dump_font_gb2312(dump_config: DumpConfig, file: IO, row_start: int, row_end: int): - count = 0 - for row in range(row_start, row_end + 1): - for col in range(1, 94 + 1): - try: - c = gb2312.query_chr(row, col) - glyph_offset = ((row - 1) * 94 + (col - 1)) * dump_config.glyph_bytes_length - file.seek(glyph_offset) - glyph_bytes = file.read(dump_config.glyph_bytes_length) - _dump_glyph(dump_config, c, glyph_bytes) - count += 1 - except GB2312Exception: - pass - return count +def _parse_bitmap(bitmap_bytes: bytes, row_bytes_size: int, width: int, height: int) -> MonoBitmap: + bitmap = MonoBitmap() + bitmap.width = width + bitmap.height = height + for row_index in range(height): + bitmap_row = [] + for col_index in range(row_bytes_size): + b = bitmap_bytes[row_index * row_bytes_size + col_index] + row_string = f'{b:08b}' + bitmap_row.extend(int(c) for c in row_string) + bitmap.append(bitmap_row[:width]) + return bitmap def dump_font(dump_config: DumpConfig): - dump_config.dump_dir.mkdir(parents=True, exist_ok=True) + dump_dir = path_define.dump_dir.joinpath(dump_config.font_name) + dump_dir.mkdir(parents=True, exist_ok=True) - with dump_config.font_file_path.open('rb') as file: + with path_define.fonts_dir.joinpath(dump_config.font_name).open('rb') as file: if dump_config.font_type == 'asc': - _dump_font_ascii(dump_config, file, 0, 255) - elif dump_config.font_type == 'hzk': - assert _dump_font_gb2312(dump_config, file, 1, 9) == gb2312.get_other_count() - assert _dump_font_gb2312(dump_config, file, 16, 55) == gb2312.get_level_1_count() - assert _dump_font_gb2312(dump_config, file, 56, 87) == gb2312.get_level_2_count() + row_bytes_size = math.ceil(dump_config.font_size / 2 / 8) + bitmap_bytes_size = row_bytes_size * dump_config.font_size + for code_point in range(0, 255 + 1): + file.seek(code_point * bitmap_bytes_size) + bitmap_bytes = file.read(bitmap_bytes_size) + if len(bitmap_bytes) == 0: + break + bitmap = _parse_bitmap(bitmap_bytes, row_bytes_size, dump_config.font_size // 2, dump_config.font_size) + bitmap.save_png(dump_dir.joinpath(f'{code_point:04X}.png')) else: - raise Exception(f"Unknown font type: '{dump_config.font_type}'") + assert dump_config.font_type == 'hzk' + row_bytes_size = math.ceil(dump_config.font_size / 8) + bitmap_bytes_size = row_bytes_size * dump_config.font_size + for row in range(1, 94 + 1): + for col in range(1, 94 + 1): + try: + c = gb2312.query_chr(row, col) + except GB2312Exception: + continue + file.seek(((row - 1) * 94 + (col - 1)) * bitmap_bytes_size) + bitmap_bytes = file.read(bitmap_bytes_size) + bitmap = _parse_bitmap(bitmap_bytes, row_bytes_size, dump_config.font_size, dump_config.font_size) + bitmap.save_png(dump_dir.joinpath(f'{ord(c):04X}.png')) + + logger.info('Dump font: {}', dump_config.font_name) diff --git a/tools/services/font_service.py b/tools/services/font_service.py index d795990..856e611 100644 --- a/tools/services/font_service.py +++ b/tools/services/font_service.py @@ -3,95 +3,66 @@ from loguru import logger from pixel_font_builder import FontBuilder, WeightName, SerifStyle, SlantStyle, WidthStyle, Glyph from pixel_font_builder.opentype import Flavor +from pixel_font_knife import glyph_file_util +from pixel_font_knife.glyph_file_util import GlyphFile +from tools import configs from tools.configs import FontConfig from tools.configs import path_define -from tools.utils import glyph_util -def collect_glyph_files(font_config: FontConfig) -> tuple[dict[int, str], list[tuple[str, str]]]: - root_dirs = [path_define.glyphs_dir.joinpath(str(font_config.size))] +def collect_glyph_files(font_config: FontConfig) -> tuple[dict[int, str], list[GlyphFile]]: + context = glyph_file_util.load_context(path_define.glyphs_dir.joinpath(str(font_config.font_size))) for source_name in font_config.source_names: - root_dirs.append(path_define.dump_dir.joinpath(source_name)) + context.update(glyph_file_util.load_context(path_define.dump_dir.joinpath(source_name))) - registry = {} - for root_dir in reversed(root_dirs): - for glyph_file_dir, _, glyph_file_names in root_dir.walk(): - for glyph_file_name in glyph_file_names: - if not glyph_file_name.endswith('.png'): - continue - glyph_file_path = glyph_file_dir.joinpath(glyph_file_name) - if glyph_file_name == 'notdef.png': - code_point = -1 - else: - code_point = int(glyph_file_name.removesuffix('.png'), 16) - registry[code_point] = glyph_file_path + character_mapping = glyph_file_util.get_character_mapping(context) + glyph_sequence = glyph_file_util.get_glyph_sequence(context) + return character_mapping, glyph_sequence - sequence = list(registry.keys()) - sequence.sort() - character_mapping = {} - glyph_file_infos = [] - for code_point in sequence: - if code_point == -1: - glyph_name = '.notdef' - else: - glyph_name = f'uni{code_point:04X}' - character_mapping[code_point] = glyph_name - glyph_file_infos.append((glyph_name, registry[code_point])) - - return character_mapping, glyph_file_infos - - -def _create_builder(font_config: FontConfig, character_mapping: dict[int, str], glyph_file_infos: list[tuple[str, str]]) -> FontBuilder: +def _create_builder(font_config: FontConfig, character_mapping: dict[int, str], glyph_sequence: list[GlyphFile]) -> FontBuilder: builder = FontBuilder() - builder.font_metric.font_size = font_config.size + builder.font_metric.font_size = font_config.font_size builder.font_metric.horizontal_layout.ascent = font_config.ascent builder.font_metric.horizontal_layout.descent = font_config.descent + builder.font_metric.vertical_layout.ascent = font_config.font_size // 2 + builder.font_metric.vertical_layout.descent = -font_config.font_size // 2 builder.font_metric.x_height = font_config.x_height builder.font_metric.cap_height = font_config.cap_height - builder.meta_info.version = FontConfig.VERSION - builder.meta_info.family_name = f'{FontConfig.FAMILY_NAME} {font_config.size}px' + builder.meta_info.version = configs.version + builder.meta_info.family_name = f'HZK Pixel {font_config.font_size}px' builder.meta_info.weight_name = WeightName.REGULAR - builder.meta_info.serif_style = SerifStyle.SANS_SERIF + builder.meta_info.serif_style = SerifStyle.SERIF builder.meta_info.slant_style = SlantStyle.NORMAL builder.meta_info.width_style = WidthStyle.MONOSPACED - builder.meta_info.description = FontConfig.DESCRIPTION - builder.meta_info.vendor_url = FontConfig.VENDOR_URL + builder.meta_info.vendor_url = 'https://hzk-pixel-font.takwolf.com' builder.character_mapping.update(character_mapping) - for glyph_name, glyph_file_path in glyph_file_infos: - glyph_data, glyph_width, glyph_height = glyph_util.load_glyph_data_from_png(glyph_file_path) - offset_y = math.floor((font_config.ascent + font_config.descent - glyph_height) / 2) + for glyph_file in glyph_sequence: + horizontal_origin_y = math.floor((font_config.ascent + font_config.descent - glyph_file.height) / 2) builder.glyphs.append(Glyph( - name=glyph_name, - advance_width=glyph_width, - horizontal_origin=(0, offset_y), - bitmap=glyph_data, + name=glyph_file.glyph_name, + advance_width=glyph_file.width, + advance_height=font_config.font_size, + horizontal_origin=(0, horizontal_origin_y), + vertical_origin_y=0, + bitmap=glyph_file.bitmap.data, )) return builder -def make_font_files(font_config: FontConfig, character_mapping: dict[int, str], glyph_file_infos: list[tuple[str, str]]): +def make_fonts(font_config: FontConfig, character_mapping: dict[int, str], glyph_sequence: list[GlyphFile]): path_define.outputs_dir.mkdir(parents=True, exist_ok=True) - builder = _create_builder(font_config, character_mapping, glyph_file_infos) - - otf_file_path = path_define.outputs_dir.joinpath(f'{font_config.outputs_name}.otf') - builder.save_otf(otf_file_path) - logger.info("Make font file: '{}'", otf_file_path) - - woff2_file_path = path_define.outputs_dir.joinpath(f'{font_config.outputs_name}.woff2') - builder.save_otf(woff2_file_path, flavor=Flavor.WOFF2) - logger.info("Make font file: '{}'", woff2_file_path) - - ttf_file_path = path_define.outputs_dir.joinpath(f'{font_config.outputs_name}.ttf') - builder.save_ttf(ttf_file_path) - logger.info("Make font file: '{}'", ttf_file_path) - - bdf_file_path = path_define.outputs_dir.joinpath(f'{font_config.outputs_name}.bdf') - builder.save_bdf(bdf_file_path) - logger.info("Make font file: '{}'", bdf_file_path) + builder = _create_builder(font_config, character_mapping, glyph_sequence) + for font_format in configs.font_formats: + file_path = path_define.outputs_dir.joinpath(f'hzk-pixel-{font_config.font_size}px.{font_format}') + if font_format == 'woff2': + builder.save_otf(file_path, flavor=Flavor.WOFF2) + else: + getattr(builder, f'save_{font_format}')(file_path) + logger.info("Make font: '{}'", file_path) diff --git a/tools/services/image_service.py b/tools/services/image_service.py index 5f4c6ba..b4e5424 100644 --- a/tools/services/image_service.py +++ b/tools/services/image_service.py @@ -5,20 +5,20 @@ from tools.configs import path_define -def make_preview_image_file(font_config: FontConfig): - font = ImageFont.truetype(path_define.outputs_dir.joinpath(f'{font_config.outputs_name}.woff2'), font_config.size) +def make_preview_image(font_config: FontConfig): + font = ImageFont.truetype(path_define.outputs_dir.joinpath(f'hzk-pixel-{font_config.font_size}px.woff2'), font_config.font_size) text_color = (0, 0, 0, 255) - image = Image.new('RGBA', (font_config.size * 27, font_config.size * 11), (255, 255, 255, 255)) + image = Image.new('RGBA', (font_config.font_size * 27, font_config.font_size * 11), (255, 255, 255, 255)) draw = ImageDraw.Draw(image) - draw.text((font_config.size, font_config.size), '汉字库像素字体 / HZK Pixel Font', fill=text_color, font=font) - draw.text((font_config.size, font_config.size * 3), '我们度过的每个平凡的日常,也许就是连续发生的奇迹。', fill=text_color, font=font) - draw.text((font_config.size, font_config.size * 5), 'THE QUICK BROWN FOX JUMPS OVER A LAZY DOG.', fill=text_color, font=font) - draw.text((font_config.size, font_config.size * 7), 'the quick brown fox jumps over a lazy dog.', fill=text_color, font=font) - draw.text((font_config.size, font_config.size * 9), '0123456789', fill=text_color, font=font) + draw.text((font_config.font_size, font_config.font_size), '汉字库像素字体 / HZK Pixel Font', fill=text_color, font=font) + draw.text((font_config.font_size, font_config.font_size * 3), '我们度过的每个平凡的日常,也许就是连续发生的奇迹。', fill=text_color, font=font) + draw.text((font_config.font_size, font_config.font_size * 5), 'THE QUICK BROWN FOX JUMPS OVER A LAZY DOG.', fill=text_color, font=font) + draw.text((font_config.font_size, font_config.font_size * 7), 'the quick brown fox jumps over a lazy dog.', fill=text_color, font=font) + draw.text((font_config.font_size, font_config.font_size * 9), '0123456789', fill=text_color, font=font) image = image.resize((image.width * 2, image.height * 2), Image.Resampling.NEAREST) path_define.outputs_dir.mkdir(parents=True, exist_ok=True) - file_path = path_define.outputs_dir.joinpath(font_config.preview_image_file_name) + file_path = path_define.outputs_dir.joinpath(f'preview-{font_config.font_size}px.png') image.save(file_path) - logger.info("Make preview image file: '{}'", file_path) + logger.info("Make preview image: '{}'", file_path) diff --git a/tools/utils/__init__.py b/tools/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tools/utils/glyph_util.py b/tools/utils/glyph_util.py deleted file mode 100644 index 782d2f6..0000000 --- a/tools/utils/glyph_util.py +++ /dev/null @@ -1,32 +0,0 @@ -import png - - -def load_glyph_data_from_png(file_path: str) -> tuple[list[list[int]], int, int]: - width, height, bitmap, _ = png.Reader(filename=file_path).read() - data = [] - for bitmap_row in bitmap: - data_row = [] - for x in range(0, width * 4, 4): - alpha = bitmap_row[x + 3] - if alpha > 127: - data_row.append(1) - else: - data_row.append(0) - data.append(data_row) - return data, width, height - - -def save_glyph_data_to_png(data: list[list[int]], file_path: str): - bitmap = [] - for data_row in data: - bitmap_row = [] - for x in data_row: - bitmap_row.append(0) - bitmap_row.append(0) - bitmap_row.append(0) - if x == 0: - bitmap_row.append(0) - else: - bitmap_row.append(255) - bitmap.append(bitmap_row) - png.from_array(bitmap, 'RGBA').save(file_path)