Last active
September 25, 2025 16:27
-
-
Save imliubo/d02d410ea76d451507b662d5b98ea80c to your computer and use it in GitHub Desktop.
Make custom font for M5GFX/LovyanGFX by use bdfconv and otf2bdf.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| #!/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})" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常旳自己,到了另一世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫旳月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。 | |
| 曲曲折折的荷塘上面,弥望旳是田田的叶子。叶子出水很高,像亭亭旳舞女旳裙。层层的叶子中间,零星地点缀着些白花,有袅娜(niǎo,nuó)地开着旳,有羞涩地打着朵儿旳;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉(mò)的流水,遮住了,不能见一些颜色;而叶子却更见风致了。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment