Skip to content

Instantly share code, notes, and snippets.

@tsmd
Created July 11, 2025 05:47
Show Gist options
  • Save tsmd/38ad1360af8db4f60817d66a71a1c06d to your computer and use it in GitHub Desktop.
Save tsmd/38ad1360af8db4f60817d66a71a1c06d to your computer and use it in GitHub Desktop.

Font Modifier Tool for Noto Sans

Noto Sansフォントの数字と記号に指定したem単位のパディングを追加し、超軽量なwoff2フォントファイルを生成するPythonコマンドラインツールです。

特徴

  • 🔢 数字と記号のみに特化: 0-9と■•◦●*の15個のグリフのみを処理
  • 📏 統一幅調整: 「0」グリフを基準に全グリフを同じ幅に統一
  • 🎯 中央配置: 各グリフが統一幅の中央に配置される
  • 📦 超軽量WOFF2: サブセット化により最小限のファイルサイズを実現
  • 🎨 デモHTML生成: 視覚的な確認用デモファイルを自動生成
  • ⚙️ コマンドライン対応: 簡単なコマンドで一括処理

インストール

依存関係のインストール

pip install fonttools[woff] brotli

必要なパッケージ

fonttools[woff]>=4.38.0
brotli>=1.0.9

使用方法

基本的な使用方法

python font-modifier.py --em <パディング値> <フォントファイルパス>

コマンドライン引数

引数 必須 説明
font_path str 入力フォントファイルのパス(TTF/OTF)
--em float 追加するパディングのサイズ(em単位)
-o, --output str 出力ファイルのパス(省略時は自動生成)

使用例

# 基本的な使用(0.3emのパディングを追加)
python font-modifier.py --em 0.3 NotoSansMono-Regular.ttf

# 出力ファイル名を指定
python font-modifier.py --em 0.2 NotoSansMono-Regular.ttf -o custom-font.woff2

# より大きなパディングを追加
python font-modifier.py --em 0.5 NotoSansMono-Regular.ttf

対象グリフ

数字グリフ(10個)

  • 0-9 (U+0030-U+0039)

記号グリフ(5個)

  • ■ (U+25A0) BLACK SQUARE
  • • (U+2022) BULLET
  • ◦ (U+25E6) WHITE BULLET
  • ● (U+25CF) BLACK CIRCLE
    • (U+002A) ASTERISK

処理内容

1. 基準グリフ設定

  • 「0」(数字のゼロ)グリフを基準として使用
  • 基準グリフの幅 + 左右のパディング = 統一幅を計算

2. 統一幅調整

  • すべてのグリフを統一幅に調整
  • 統一幅 = 基準グリフの幅 + (2 × パディング)
  • 各グリフが統一幅の中央に配置されるよう調整

3. メトリクス計算

各グリフについて以下を計算:

  • 新しい幅: 統一幅
  • 新しいLSB: (統一幅 - 元のグリフ幅) / 2
  • 中央配置: グリフが新しい幅の中央に配置される

4. サブセット化処理

  • 対象15個以外のグリフを削除
  • 不要なテーブルを削除してファイルサイズを最適化

5. WOFF2変換

  • 最新のWOFF2形式で出力
  • 最適な圧縮率でファイルサイズを最小化

出力ファイル

生成されるファイル

NotoSansMono-Regular_padded_0.3em.woff2  # 変換されたフォント
demo.html                                 # デモファイル

ファイル名規則

{入力ファイル名}_padded_{em値}em.woff2

デモファイル

生成される demo.html では以下を確認できます:

  • 📊 全グリフ表示: 15個すべてのグリフをグリッド表示
  • 📏 等幅性テスト: 全文字が左端で揃うことを確認
  • 🔢 数字テスト: 0-9の数字の表示確認
  • 🔸 記号テスト: ■•◦●*の記号の表示確認

ライセンス

このツールはMITライセンスの下で公開されています。


Author: SHIMADA Takayuki

#!/usr/bin/env python3
"""
Font Modifier Tool for Noto Sans
数字と記号にパディングを追加してWOFF2フォントを生成するツール
"""
import argparse
import sys
import warnings
from pathlib import Path
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter
from fontTools.ttLib.tables._g_l_y_f import Glyph
from fontTools.ttLib.tables._h_m_t_x import table__h_m_t_x
def get_target_glyphs():
"""対象グリフのUnicodeコードポイントを返す"""
target_glyphs = {}
# 数字 (0-9)
for i in range(10):
unicode_value = 0x0030 + i # U+0030-U+0039
target_glyphs[unicode_value] = str(i)
# 記号
symbols = {
0x25A0: "■", # BLACK SQUARE
0x2022: "•", # BULLET
0x25E6: "◦", # WHITE BULLET
0x25CF: "●", # BLACK CIRCLE
0x002A: "*" # ASTERISK
}
target_glyphs.update(symbols)
return target_glyphs
def modify_font_padding(font_path, em_padding, output_path=None):
"""フォントにパディングを追加してサブセット化"""
# フォントを読み込み
font = TTFont(font_path)
# 対象グリフを取得
target_glyphs = get_target_glyphs()
# フォントのunits per emを取得
units_per_em = font['head'].unitsPerEm
padding_units = int(em_padding * units_per_em)
print(f"📝 処理対象グリフ: 10個の数字 + 5個の記号")
print(f"📏 Units per EM: {units_per_em}")
print(f"📐 パディング: {em_padding}em = {padding_units} units")
# 対象グリフのglyph nameを取得
cmap = font.getBestCmap()
glyph_names = []
for unicode_value, char in target_glyphs.items():
if unicode_value in cmap:
glyph_name = cmap[unicode_value]
glyph_names.append(glyph_name)
print(f" ✓ {char} (U+{unicode_value:04X}) -> {glyph_name}")
else:
print(f" ✗ {char} (U+{unicode_value:04X}) not found in font")
if not glyph_names:
print("❌ 対象グリフが見つかりません")
return False
# hmtxテーブルを取得(水平メトリクス)
hmtx = font['hmtx']
# 「0」のグリフを基準として統一幅を計算
zero_glyph_name = cmap.get(0x0030) # U+0030 = '0'
if not zero_glyph_name or zero_glyph_name not in hmtx.metrics:
print("❌ 基準グリフ '0' が見つかりません")
return False
zero_advance_width, zero_lsb = hmtx.metrics[zero_glyph_name]
# 統一幅 = 「0」の幅 + 左右のパディング
unified_width = zero_advance_width + (2 * padding_units)
print(f"📏 基準グリフ '0': width={zero_advance_width}, lsb={zero_lsb}")
print(f"📐 統一幅: {unified_width} units (基準幅 + 2×{padding_units})")
# 各グリフのメトリクスを調整
for glyph_name in glyph_names:
if glyph_name in hmtx.metrics:
advance_width, lsb = hmtx.metrics[glyph_name]
# グリフの実際の幅を計算(RSB = advance_width - lsb - glyph_width)
# 中央揃えのため、左右に同量のパディングを追加
remaining_space = unified_width - advance_width
left_padding = remaining_space // 2
right_padding = remaining_space - left_padding
new_advance_width = unified_width
new_lsb = lsb + left_padding
hmtx.metrics[glyph_name] = (new_advance_width, new_lsb)
print(f" 📊 {glyph_name}: width {advance_width} -> {new_advance_width}, lsb {lsb} -> {new_lsb} (left+{left_padding}, right+{right_padding})")
# サブセット化(対象グリフのみを残す)
subsetter = Subsetter()
subsetter.options.desubroutinize = True
subsetter.options.hinting = False
subsetter.options.layout_features = []
subsetter.options.name_IDs = []
subsetter.options.name_legacy = True
subsetter.options.name_languages = []
subsetter.options.drop_tables = [
'DSIG', 'GPOS', 'GSUB', 'kern', 'mort', 'morx'
]
# 対象グリフのUnicodeコードポイントを設定
subsetter.populate(unicodes=list(target_glyphs.keys()))
subsetter.subset(font)
# 出力ファイル名を生成
if output_path is None:
input_path = Path(font_path)
output_path = input_path.parent / f"{input_path.stem}_padded_{em_padding}em.woff2"
else:
output_path = Path(output_path)
if not output_path.suffix:
output_path = output_path.with_suffix('.woff2')
# WOFF2として保存
font.flavor = 'woff2'
font.save(output_path)
# ファイルサイズを取得
file_size = output_path.stat().st_size
file_size_kb = file_size / 1024
print(f"📦 数字と記号のみを残してファイルサイズを最適化しました({len(glyph_names)}個の文字)")
print(f"✓ フォントの処理が完了しました: {output_path}")
print(f"📊 ファイルサイズ: {file_size:,} bytes ({file_size_kb:.1f} KB)")
return output_path
def create_demo_html(font_path, em_padding):
"""デモHTMLファイルを作成"""
font_name = Path(font_path).stem
target_glyphs = get_target_glyphs()
# グリフを順番に並べる(数字→記号)
digits = [str(i) for i in range(10)]
symbols = ["■", "•", "◦", "●", "*"]
all_chars = digits + symbols
html_content = f"""<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Font Demo - {font_name} (Padded {em_padding}em)</title>
<style>
@font-face {{
font-family: 'PaddedFont';
src: url('{font_name}_padded_{em_padding}em.woff2') format('woff2');
font-display: swap;
}}
body {{
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}}
.container {{
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
h1 {{
color: #333;
text-align: center;
margin-bottom: 30px;
}}
.info {{
background: #e8f4f8;
padding: 15px;
border-radius: 5px;
margin-bottom: 30px;
border-left: 4px solid #2196F3;
}}
.demo-section {{
margin-bottom: 30px;
}}
.demo-section h2 {{
color: #555;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}}
.char-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 15px;
margin-bottom: 30px;
}}
.char-box {{
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
text-align: center;
font-family: 'PaddedFont', monospace;
font-size: 24px;
position: relative;
}}
.char-box::before {{
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px dashed #999;
border-radius: 5px;
pointer-events: none;
}}
.alignment-test {{
font-family: 'PaddedFont', monospace;
font-size: 32px;
line-height: 1.5;
background: #f9f9f9;
padding: 20px;
border-radius: 5px;
border-left: 3px solid #red;
white-space: pre-line;
}}
.alignment-test span {{
background: rgba(255, 0, 0, 0.1);
border-left: 1px solid red;
}}
.size-info {{
background: #fff3cd;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
border-left: 4px solid #ffc107;
}}
.tech-details {{
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-family: 'Courier New', monospace;
font-size: 14px;
}}
</style>
</head>
<body>
<div class="container">
<h1>📝 Font Demo: {font_name}</h1>
<div class="info">
<strong>パディング:</strong> {em_padding}em<br>
<strong>含まれるグリフ:</strong> 15個(数字10個 + 記号5個)<br>
<strong>フォント形式:</strong> WOFF2
</div>
<div class="demo-section">
<h2>📊 全グリフ表示</h2>
<div class="char-grid">
"""
# 各文字のボックスを生成
for char in all_chars:
unicode_value = ord(char)
html_content += f' <div class="char-box">{char}<br><small>U+{unicode_value:04X}</small></div>\n'
html_content += f""" </div>
</div>
<div class="demo-section">
<h2>📏 等幅性テスト</h2>
<div class="alignment-test">"""
# 各文字を縦に並べて左端揃えテスト
for char in all_chars:
html_content += f'<span>{char}</span>\n'
html_content += f"""</div>
<p><small>🔍 すべての文字が左端で揃っていることを確認してください</small></p>
</div>
<div class="demo-section">
<h2>🔢 数字テスト</h2>
<div class="alignment-test">0123456789</div>
</div>
<div class="demo-section">
<h2>🔸 記号テスト</h2>
<div class="alignment-test">■•◦●*</div>
</div>
<div class="size-info">
<strong>💡 使用方法:</strong><br>
このフォントは数字と記号のみを含む超軽量フォントです。<br>
CSS: <code>font-family: 'PaddedFont', monospace;</code>
</div>
<div class="tech-details">
<strong>🛠️ 技術詳細:</strong><br>
フォントファイル: {font_name}_padded_{em_padding}em.woff2<br>
パディング: {em_padding}em (左右各{em_padding/2}em)<br>
含まれる文字: 0-9 ■•◦●*<br>
最適化: サブセット化によりファイルサイズを最小化
</div>
</div>
</body>
</html>"""
demo_path = Path(font_path).parent / "demo.html"
with open(demo_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"📄 デモファイルを作成しました: {demo_path}")
return demo_path
def main():
"""メイン関数"""
parser = argparse.ArgumentParser(
description="Noto Sansフォントの数字と記号にパディングを追加してWOFF2フォントを生成",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用例:
python font-modifier.py --em 0.3 NotoSansMono-Regular.ttf
python font-modifier.py --em 0.2 NotoSansMono-Regular.ttf -o custom-font.woff2
対象グリフ:
数字: 0-9 (10個)
記号: ■•◦●* (5個)
"""
)
parser.add_argument(
'font_path',
help='入力フォントファイルのパス(TTF/OTF)'
)
parser.add_argument(
'--em',
type=float,
required=True,
help='追加するパディングのサイズ(em単位)'
)
parser.add_argument(
'-o', '--output',
help='出力ファイルのパス(省略時は自動生成)'
)
args = parser.parse_args()
# 入力ファイルの存在確認
font_path = Path(args.font_path)
if not font_path.exists():
print(f"❌ フォントファイルが見つかりません: {font_path}")
sys.exit(1)
# パディング値の検証
if args.em <= 0:
print("❌ パディング値は0より大きい値を指定してください")
sys.exit(1)
if args.em > 1.0:
print("⚠️ パディング値が1.0emより大きいです。非常に大きなパディングになります。")
# 警告を抑制
warnings.filterwarnings('ignore')
try:
print("🚀 フォント処理を開始します...")
print(f"📁 入力ファイル: {font_path}")
print(f"📐 パディング: {args.em}em")
# フォントを処理
output_path = modify_font_padding(
str(font_path),
args.em,
args.output
)
if output_path:
# デモHTMLを作成
create_demo_html(str(font_path), args.em)
print("\n🎉 すべての処理が完了しました!")
print(f"📦 WOFF2フォント: {output_path}")
print(f"📄 デモファイル: demo.html")
else:
print("❌ フォント処理に失敗しました")
sys.exit(1)
except Exception as e:
print(f"❌ エラーが発生しました: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
fonttools[woff]>=4.38.0
brotli>=1.0.9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment