4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

アイコンを1つのフォントにまとめたい

Last updated at Posted at 2023-07-21

この記事は お題は不問!Qiita Engineer Festa 2023で記事投稿! - Qiita の参加記事です。

はじめに

最近、とあるフォントに含まれている文字を使いたいが、ひらがなにマップされているため普段遣いができず、普段遣いができるフォントの私用領域に入れて使えないかと思い、挑戦してみました。

やったこと

グリフが空ではない部分だけを普段遣いするフォントにコピーするようにしました。
実際のスクリプトは以下です。

patch.py
#!/usr/bin/env python3

import fontforge
import psMat

PROJECT_NAME = "ZUTOMOJI_HG"
PROJECT_VERSION = "0.1.0"


class Patcher:

    def __init__(self,
                 base_font_path: str,
                 ztmy_font_path: str,
                 font_name: str,
                 font_family_name: str,
                 base_start_code_point: int = 0x100000) -> None:
        self.__base_font = fontforge.open(base_font_path)
        self.__ztmy_font = fontforge.open(ztmy_font_path)
        self.__font_name = font_name
        self.__font_family_name = font_family_name
        self.__base_current_code_point = base_start_code_point

        assert self.__ztmy_font.is_cid, "{} is not CID font.".format(
            ztmy_font_path)
        subfont_count = self.__ztmy_font.cidsubfontcnt
        assert subfont_count == 1, "{} has {} subfonts. Expect only one.".format(
            ztmy_font_path, subfont_count)

        self.__em_scale: float = 0 if self.__base_font.em == self.__ztmy_font.em else (
            self.__base_font.em / self.__ztmy_font.em)

    def __patch_set(self):
        return [
            {
                "code_point_start": 0x3001,
                "code_point_end": 0x3002
            },
            {
                "code_point_start": 0xff1f,
                "code_point_end": 0xff1f
            },
            {
                "code_point_start": 0xff01,
                "code_point_end": 0xff01
            },
            {
                "code_point_start": 0x30fc,
                "code_point_end": 0x30fc
            },
            {
                "code_point_start": 0x300c,
                "code_point_end": 0x300d
            },
            {
                "code_point_start": 0x3041,
                "code_point_end": 0x308d
            },
            {
                "code_point_start": 0x308f,
                "code_point_end": 0x308f
            },
            {
                "code_point_start": 0x3092,
                "code_point_end": 0x3093
            },
            {
                "code_point_start": 0x30a1,
                "code_point_end": 0x30ed
            },
            {
                "code_point_start": 0x30ef,
                "code_point_end": 0x30ef
            },
            {
                "code_point_start": 0x30f2,
                "code_point_end": 0x30f4
            },
            {
                "code_point_start": 0x5f37,
                "code_point_end": 0x5f37
            },
            {
                "code_point_start": 0x6817,
                "code_point_end": 0x6817
            },
            {
                "code_point_start": 0x5263,
                "code_point_end": 0x5263
            },
            {
                "code_point_start": 0x9283,
                "code_point_end": 0x9283
            },
            {
                "code_point_start": 0x6ce5,
                "code_point_end": 0x6ce5
            },
            {
                "code_point_start": 0x571f,
                "code_point_end": 0x571f
            },
            {
                "code_point_start": 0x97ee,
                "code_point_end": 0x97ee
            },
            {
                "code_point_start": 0x732b,
                "code_point_end": 0x732b
            },
            {
                "code_point_start": 0x98ef,
                "code_point_end": 0x98ef
            },
            {
                "code_point_start": 0x591c,
                "code_point_end": 0x591c
            },
            {
                "code_point_start": 0x8e0a,
                "code_point_end": 0x8e0a
            },
        ]

    def __copy_glyphs(self, code_point_start: int,
                      code_point_end: int) -> None:
        self.__ztmy_font.selection.select(("ranges", "unicode"),
                                          code_point_start, code_point_end)
        glyphs = [
            g for g in self.__ztmy_font.selection.byGlyphs if g.unicode >= 0
        ]

        expect_glyph_count = code_point_end + 1 - code_point_start
        assert len(glyphs) == (
            expect_glyph_count
        ), "Invalid glyphs exist. Expected {} but actual {}.".format(
            expect_glyph_count, len(glyphs))

        for glyph in glyphs:
            print("Updating glyph: {}, at: 0x{:x}".format(
                glyph.glyphname, self.__base_current_code_point))

            self.__ztmy_font.selection.select(glyph.encoding)
            self.__ztmy_font.copy()
            self.__base_font.selection.select(self.__base_current_code_point)
            self.__base_font.paste()

            if self.__em_scale != 0:
                matrix = psMat.scale(self.__em_scale)
                self.__base_font[self.__base_current_code_point].transform(
                    matrix)

            self.__base_current_code_point += 1

    def __set_meta_data(self) -> None:
        # 略

    def patch(self) -> None:
        print("em scale: {}".format(self.__em_scale))

        for patch in self.__patch_set():
            code_point_start = patch["code_point_start"]
            code_point_end = patch["code_point_end"]

            self.__copy_glyphs(code_point_start, code_point_end)

        self.__set_meta_data()

    def generate(self, out_file_path: str) -> None:
        self.__base_font.generate(out_file_path)
        print("Generated {}".format(out_file_path))

    def close(self) -> None:
        self.__base_font.close()
        self.__ztmy_font.close()


def main():
    font_data = [
        {
            "base_font_path": "HackGenConsoleNF-Regular.ttf",
            "font_name": "ZUTOMOJI_HGC-Regular",
            "font_family_name": "ZUTOMOJI_HGC"
        },
        # 略
    ]
    ztmy_font_file = "ZTMY_MOJI-R.otf"

    for data in font_data:
        patcher = Patcher(data["base_font_path"], ztmy_font_file,
                          data["font_name"], data["font_family_name"])
        patcher.patch()

        out_file_path = "{}.ttf".format(data["font_name"])
        patcher.generate(out_file_path)
        patcher.close()


if __name__ == "__main__":
    main()

コピー元のフォントのグリフをselection.select()で選択するときに詰まったのが、

  • FontForgeで開いて一番上と一番下のUnicodeコードポイントをrangesで選択していたが、一部選択されていないものがあった
    • selection.select(('ranges', 'unicode'), 0x3001, 0x8e0a)でだめだった

で、途中CIDを選択するように変えたりしましたが、今後とあるフォント側のアップデートでCIDが変わると大変なことになりそうだったので、
最終的にはUnicodeコードポイントを細かい単位でrangesで選択するようにしました。
(CIDフォントだからなのか?なぜ細かい単位で選択すると動くようになったのかはわかっていない...)

また、上記でも出てきたフォントがアップデートされたときのことを気にして、今のCID順に私用領域に追加し、今後文字が増えた場合は随時後ろに追加していくような方針にしました。

課題

最終的にはシェルのプロンプトをつくりたいなと思っているのですが、それに向けて課題が2つあります。

1つ目は、フォントサイズが小さすぎると細かい線などが見えなくなってしまいます。
以下の例だとネコの目がほとんど見えないです。
(もともと細かい線で構成されているのが原因?)

image.png

2つ目は、ターミナル上で文字が重なってしまっている点です。(少なくとも僕の環境では)
調べてみるとAmbiguous Width問題というものらしく、僕の環境(Alacritty, Zsh, Zellij)でうまく表示できないかいじってみる予定です。

image.png

Refs

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?