|
#!/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() |