Skip to content

Instantly share code, notes, and snippets.

@imliubo
Last active September 25, 2025 16:27
Show Gist options
  • Select an option

  • Save imliubo/d02d410ea76d451507b662d5b98ea80c to your computer and use it in GitHub Desktop.

Select an option

Save imliubo/d02d410ea76d451507b662d5b98ea80c to your computer and use it in GitHub Desktop.
Make custom font for M5GFX/LovyanGFX by use bdfconv and otf2bdf.
#
#!/usr/bin/env bash
set -euo pipefail
# mkgfxfont.sh - Generate u8g2 font header (.hpp) for LovyanGFX/M5GFX
#
#
# Author: imliubo
# License: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
# - TTF/OTF -> BDF via otf2bdf (-s size, default 16)
# - Read characters or Unicode codes from -t text.txt
# - User sets font name via -n <name>
# - Output file will be <name>.hpp
# - Provide extern <name>_data[], and static constexpr lgfx::v1::U8g2font <name>(<name>_data)
# - Replace bdfconv output line to const uint8_t <name>_data[...] =
print_usage() {
cat <<USAGE
Usage: $0 -f <font.(ttf|otf|bdf)> -n <name> [-s <size_px>] [-t <text_or_codes.txt>] [-A=0|1]
Options:
-f Font file (TTF/OTF/BDF) [required]
-n Final font name (e.g. font_my32) [required]
-s Pixel size when using TTF/OTF [default: 16]
-t Text/codes list file [default: text.txt]
* If file has non-hex/digit chars -> treat as characters and convert to Unicode.
* If file looks like codes (decimal or 0xHEX / HEXh) -> normalize to decimal.
-A Include ASCII 32-126 [default: 1; disable with -A=0]
Examples:
$0 -f /Path/To/Fonts/AlibabaPuHuiTi.ttf -s 32 -t text.txt -n font_my32
$0 -f ./puhui-20.bdf -t unicode.txt -A=0 -n font_cn_only
USAGE
}
SIZE=16
TEXT_FILE="text.txt"
INCLUDE_ASCII=1
FONT_IN=""
NAME=""
# -------------------------------
# Parse command-line arguments
# -------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
-f) FONT_IN="${2:-}"; shift 2;;
-s) SIZE="${2:-16}"; shift 2;;
-t) TEXT_FILE="${2:-text.txt}"; shift 2;;
-A=*) INCLUDE_ASCII="${1#*=}"; shift;;
-A) INCLUDE_ASCII=1; shift;;
-n) NAME="${2:-}"; shift 2;;
-h|--help) print_usage; exit 0;;
*) shift;;
esac
done
# -------------------------------
# Sanity checks
# -------------------------------
[[ -n "${FONT_IN}" ]] || { echo "Error: -f <font> is required." >&2; exit 1; }
[[ -f "${FONT_IN}" ]] || { echo "Error: font not found: ${FONT_IN}" >&2; exit 1; }
[[ -f "${TEXT_FILE}" ]] || { echo "Error: text/codes file not found: ${TEXT_FILE}" >&2; exit 1; }
[[ -n "${NAME}" ]] || { echo "Error: -n <name> is required." >&2; exit 1; }
# Build final symbol = name_size
SYMBOL="${NAME}_${SIZE}"
OUT_H="${SYMBOL}.hpp"
# -------------------------------
# Locate bdfconv binary
# -------------------------------
if command -v bdfconv >/dev/null 2>&1; then
BDFCONV="bdfconv"
elif [[ -x "./bdfconv" ]]; then
BDFCONV="./bdfconv"
elif [[ -x "./bdfconv.exe" ]]; then
BDFCONV="./bdfconv.exe"
else
echo "Error: bdfconv not found in PATH or current directory." >&2
exit 1
fi
# -------------------------------
# Convert TTF/OTF -> BDF if needed
# -------------------------------
EXT_LOWER="${FONT_IN##*.}"
EXT_LOWER=$(echo "$EXT_LOWER" | tr 'A-Z' 'a-z')
BDF_OUT=""
if [[ "${EXT_LOWER}" == "ttf" || "${EXT_LOWER}" == "otf" ]]; then
command -v otf2bdf >/dev/null 2>&1 || { echo "Error: otf2bdf not found. Install: brew install otf2bdf / sudo apt install otf2bdf" >&2; exit 1; }
BDF_OUT="$(basename "${FONT_IN}")"
BDF_OUT="${BDF_OUT%.*}-${SIZE}.bdf"
echo ">> Converting TTF/OTF to BDF: ${FONT_IN} -> ${BDF_OUT} (${SIZE}px)"
otf2bdf -p "${SIZE}" -r 72 -o "${BDF_OUT}" "${FONT_IN}"
else
BDF_OUT="${FONT_IN}"
fi
# -------------------------------
# Build code list from text file
# -------------------------------
TMP_CODES="$(mktemp)"
python3 - "$TEXT_FILE" <<'PYCODE' > "$TMP_CODES"
import sys, re
data = open(sys.argv[1],'r',encoding='utf-8').read()
def norm_codes(s):
tokens = re.split(r'[\s,]+', s.strip())
seen, out = set(), []
for tok in tokens:
if not tok: continue
t = tok.strip().lower()
try:
if t.startswith('0x'):
v = int(t,16)
elif t.endswith('h') and re.fullmatch(r'[0-9a-f]+h', t):
v = int(t[:-1],16)
else:
v = int(t,10)
except:
continue
if v not in seen:
seen.add(v); out.append(str(v))
return ",".join(out)
# If contains non-hex chars → treat as literal characters
if re.search(r'[^0-9xXa-fA-F,\s]', data):
seen, out = set(), []
for ch in data:
if ch.strip():
v = ord(ch)
if v not in seen:
seen.add(v); out.append(str(v))
print(",".join(out))
else:
print(norm_codes(data))
PYCODE
CODES="$(cat "$TMP_CODES")"
rm -f "$TMP_CODES"
[[ -n "${CODES}" ]] || { echo "Error: no codes parsed from ${TEXT_FILE}" >&2; exit 1; }
# Add ASCII range if requested
if [[ "${INCLUDE_ASCII}" == "1" ]]; then
MAP="32-126,${CODES}"
else
MAP="${CODES}"
fi
# -------------------------------
# Run bdfconv
# -------------------------------
echo ">> Running bdfconv ..."
"${BDFCONV}" -v -f 1 -m "${MAP}" "${BDF_OUT}" -o "${OUT_H}" -n "${SYMBOL}" -d "${BDF_OUT}"
# -------------------------------
# Post-process header:
# 1. Insert #pragma once + includes
# 2. Add extern <name>_data[] and lgfx wrapper
# 3. Rewrite array declaration to <name>_data
# -------------------------------
python3 - "$OUT_H" <<'PYEDIT'
import re, sys
path = sys.argv[1]
with open(path,'r',encoding='utf-8') as f:
src = f.read()
# Match first "const uint8_t ..." line
m = re.search(r'^(const\s+uint8_t\s+)([A-Za-z0-9_]+)(\s*\[\s*([0-9]+)\s*\]\s*)(?:U8G2_FONT_SECTION\([^)]+\)\s*)?=', src, re.M)
if not m:
sys.exit(0)
sym, size = m.group(2), m.group(4)
# Header banner to insert
banner = (
"#pragma once\n"
"#include <stdio.h>\n"
"#include \"lgfx/v1/lgfx_fonts.hpp\"\n"
"\n"
f"extern const uint8_t {sym}_data[{size}];\n"
f"static constexpr lgfx::v1::U8g2font {sym}({sym}_data);\n"
"\n"
)
# Insert after /* ... */ banner comment if found
m_comm = re.search(r'/\*.*?\*/\s*', src, re.S)
if m_comm:
src = src[:m_comm.end()] + banner + src[m_comm.end():]
else:
src = banner + src
# Rewrite the const array definition
src = re.sub(
r'^(const\s+uint8_t\s+)([A-Za-z0-9_]+)(\s*\[\s*[0-9]+\s*\]\s*)(?:U8G2_FONT_SECTION\([^)]+\)\s*)?=',
rf'\1\2_data\3=',
src, count=1, flags=re.M
)
with open(path,'w',encoding='utf-8') as f:
f.write(src)
PYEDIT
echo "Done. Output: ${OUT_H} (name: ${SYMBOL})"
路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常旳自己,到了另一世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫旳月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。
曲曲折折的荷塘上面,弥望旳是田田的叶子。叶子出水很高,像亭亭旳舞女旳裙。层层的叶子中间,零星地点缀着些白花,有袅娜(niǎo,nuó)地开着旳,有羞涩地打着朵儿旳;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉(mò)的流水,遮住了,不能见一些颜色;而叶子却更见风致了。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment