├── .github └── workflows │ └── font-unpacker.yml ├── .gitignore ├── README.md ├── dc-bios-font-layout.svg ├── font-unpacker.py ├── requirements.txt └── sega-katana-r9-font-sample.png /.github/workflows/font-unpacker.yml: -------------------------------------------------------------------------------- 1 | name: font-unpacker 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 3.10 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.10' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Unpack bootROM glyphs 35 | run: | 36 | # retrieve one of the main bios file 37 | curl -LO https://github.com/japanese-cake/dc-bios/raw/main/retail/bootROM_v1.01d.bin 38 | # run the unpack command 39 | python font-unpacker.py ./bootROM_v1.01d.bin 40 | FNT_NAME=$(ls output/ | head -n 1) 41 | echo "fnt_name = $FNT_NAME" >> $GITHUB_ENV 42 | - name: Archive bootROM glyphs 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: bootROM-glyphs 46 | path: output/${{ env.fnt_name }} 47 | if-no-files-found: error 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore python virtual env 2 | venv 3 | 4 | # Ignore generated data 5 | output -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SEGA Dreamcast bootROM Font Unpacker 2 | 3 | A small python script to unpack all the glyphs from the Dreamcast bootROM embeded fonts. 4 | 5 | ## Purpose 6 | 7 | While working on the custom bootROM, I needed specific characters and didn't really know whether they had a representation in the bootROM fonts. 8 | 9 | Additionally, giving a way extract the glyphs could help designing new fonts for our beloved Dreamcast. 10 | 11 | ## Getting started 12 | 13 | ### Requirements 14 | 15 | * Python 3.10+ 16 | 17 | ### Prepare your environment 18 | 19 | ```bash 20 | python3 -m venv venv 21 | . ./venv/bin/activate 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | ### Usage 26 | 27 | ```bash 28 | ./font-unpacker.py --help 29 | Usage: font-unpacker.py [options] bootROM.bin 30 | 31 | Unpack all bootROM font glyphs 32 | 33 | Options: 34 | -h, --help show this help message and exit 35 | -f, --font-id Read only the font ID 36 | -d, --debug Change logger level to 'DEBUG' 37 | ``` 38 | 39 | ### Example 40 | 41 | ```bash 42 | $ ./font-unpacker.py bootROM_v1.01d.bin 43 | font-unpacker INFO: Opening bootROM 'bootROM_v1.01d.bin' 44 | font-unpacker INFO: Font ID @0x00100018 is FNT90427 45 | font-unpacker INFO: Creating output directory 'output/FNT90427' 46 | font-unpacker INFO: Unpacking 7bit fontset ascii glyphs in output/FNT90427/7bit/ascii 47 | font-unpacker INFO: Unpacking 8bit fontset iso-8859-1 glyphs in output/FNT90427/8bit/iso-8859-1 48 | font-unpacker INFO: Unpacking 8bit fontset jis-x-0201 glyphs in output/FNT90427/8bit/jis-x-0201 49 | font-unpacker INFO: Unpacking 16bit fontset jis-x-0208 glyphs in output/FNT90427/16bit/jis-x-0208 50 | font-unpacker INFO: Unpacking 16bit fontset sega-gaij glyphs in output/FNT90427/16bit/sega-gaij 51 | font-unpacker INFO: Unpacking 16bit fontset sega-vmu-icon glyphs in output/FNT90427/16bit/sega-vmu-icon 52 | font-unpacker INFO: Done. 53 | ``` 54 | 55 | ## Technical details 56 | 57 | A glyph is the graphic representation of a character and the Dreamcast bootROM contains a section made up of series of glyphs that can be used by a software to display Latin, Japanese as well as SEGA specific graphics on the screen. 58 | 59 | ### Glyphs layout 60 | 61 | ![Dreamcast BIOS font layout](./dc-bios-font-layout.svg) 62 | 63 | ### SEGA Dreamcast Fontsets 64 | 65 | The *SEGA KATANA R9 SDK* has a sample application that let you pick-up a fontset, set a char code in the selected fontset: 66 | 67 | ![Font Sample Screenshot](./sega-katana-r9-font-sample.png) 68 | 69 | This application, just like any other commercial games developed with the SEGA SDKs, relies on the `sg_byFnt` library which defines the following charsets (or fontsets): 70 | 71 | | SEGA charset name | Closest charset in computer wording | 72 | |:-----------------:|------------------------------------------------------| 73 | | **WESTERN_24** | a subset of *ISO 8859-1* | 74 | | **JP_JIS_24** | *JIS X 0201* with extensions | 75 | | **JP_LVL2_24** | a subnet of *JIS X 0208* (also known as *Shift JIS*) | 76 | | **JP_GAIJ_24** | N/A, SEGA proprietary charset for controller icons | 77 | | **VMSICON_32** | N.A, SEGA proprietary charset for VMU icons | 78 | 79 | ### General comment about charsets 80 | 81 | A charset, from code point of view, is nothing but the mapping of a code, encoded on either 7, 8 or 16 bits depending on the charset, to a glyph, all done in a not so bad way to optimize the lookup and avoid glyph duplication. From one charset to another, you usually don't have the exact same set of characters represented as each was designed for a specific usage or language. It was especially true before Unicode standard came in. That said, the charsets, in general, try to have some sort of interoperability, so that a range of codes in one charset can be handled or understood by another one without complex transformations. 82 | 83 | ### The `¥` and the `‾` glyphs 84 | 85 | You may have noticed that the `¥` and the `‾` glyphs in the glyphs layout surround the 7-bit ASCII glyphs. I am not sure about the real reasons behing this but at least I think I understand a part of it. 86 | 87 | There is a certain logic in the code-glyph mapping. For instance, given the *ISO 8859-1* charset, a `0x41` char code leads to the glyph of the letter `A` and `0x42` to the glyph of the letter `B`. *JIS X 0201* charset re-uses a part of the *ISO 8859-1* charset, meaning that in our example the code for the letter `A` is also `0x41`. So far so good. With a quick look at the character tables you will easily come to the conclusion that the codes in the `0x20-0x7f` range, coding for 7-bit ASCII characters, are (almost!) similar in both *ISO 8859-1* and *JIS X 0201* charsets. It means that the code that does the mapping for *JIS X 0201* does not really have to do much for handling this range, it just has to delegate it to the code that handles 7-bit ASCII characters. Smart, right? 88 | 89 | Previously, I said that the codes were almost similar. Indeed! Now take the `0x5c` and `0x7e` char codes. In *ISO 8859-1*, those two char codes code for `\` (backslash) and `~` (tilde) glyphs, respectively. For interoperability sake, *JIS X 0201* re-uses the same ASCII 7-bit code mapping for codes between the `[0x20;0x7f]` range and extends its with the `[0xa0;0xdf]` range for Japanase kana. However, the JIS committee decided that those two char codes would now code for `¥` (yen sign) and `‾` (overline) glyphs (see the *JIS X 0201* Wikipedia page). So now, assuming you want to make *ISO 8859-1* the default unaltered charset, and also want to support the *JIS X 0201* one and factorize the glyphs to use as much as possible, you have to change your code mapping for those two codes so that they are mapped to two new glyph locations. Based on the `sg_byFnt` library, SEGA seems to have replaced the standard ASCII space character `0x20` with the overline character and appended the yen sign right after the last known ASCII 7-bit printable character, i.e. the character you could have obtained with the `0x7f` ASCII 7-bit code. But what about the ASCII space character `0x20` then? An additional mapping fixes it: the `0x20` code in *ISO 8859-1* returns the same glyph as for the `0xa0` code, the non-breaking space glyph which in the case of the Dreamcast bootROM font is a blank glyph. Problem solved (phew!). 90 | 91 | ## TODOs 92 | 93 | * Generate an HTML page to visualize the glyphs, gather them in fontsets and detail the code mapping 94 | * Pack PNGs to create a new font 95 | 96 | ## Resources 97 | 98 | * http://mc.pp.se/dc/syscalls.html#vecB4 99 | * https://github.com/KallistiOS/KallistiOS 100 | * https://www.ascii-code.com/ISO-8859-1 101 | * https://en.wikipedia.org/wiki/JIS_X_0201 102 | * https://en.wikipedia.org/wiki/JIS_X_0208 103 | * https://en.wikipedia.org/wiki/Katakana 104 | * https://japanese-cake.io/ 105 | -------------------------------------------------------------------------------- /font-unpacker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | import optparse 5 | import struct 6 | import sys 7 | import traceback 8 | 9 | from pathlib import Path 10 | from PIL import Image, ImageColor 11 | 12 | 13 | # 14 | # charset/fontset type 15 | # 16 | SYBTFNT_TYPE_7BIT = "7bit" 17 | SYBTFNT_TYPE_8BIT = "8bit" 18 | SYBTFNT_TYPE_16BIT = "16bit" 19 | 20 | # 21 | # glyph data and image sizes 22 | # 23 | SYBTFNT_12X24_GLYPH_DATA_SIZE = int(12 * 24 / 8) 24 | SYBTFNT_12X24_GLYPH_IMAGE_SIZE = (12, 24) 25 | SYBTFNT_24X24_GLYPH_DATA_SIZE = int(24 * 24 / 8) 26 | SYBTFNT_24X24_GLYPH_IMAGE_SIZE = (24, 24) 27 | SYBTFNT_32X32_GLYPH_DATA_SIZE = int(32 * 32 / 8) 28 | SYBTFNT_32X32_GLYPH_IMAGE_SIZE = (32, 32) 29 | 30 | # 31 | # font glyph specs 32 | # 33 | SYBTFNT_ASCII = "ascii" 34 | SYBTFNT_ASCII_OFFSET = 0 35 | SYBTFNT_ASCII_GLYPH_GLOBAL_INDEX_START = 0 36 | SYBTFNT_ASCII_GLYPH_DATA_SIZE = SYBTFNT_12X24_GLYPH_DATA_SIZE 37 | SYBTFNT_ASCII_GLYPH_COUNT = 96 38 | SYBTFNT_ASCII_SIZE = SYBTFNT_ASCII_GLYPH_COUNT * SYBTFNT_12X24_GLYPH_DATA_SIZE 39 | 40 | SYBTFNT_ISO_8859_1 = "iso-8859-1" 41 | SYBTFNT_ISO_8859_1_OFFSET = int(SYBTFNT_ASCII_OFFSET + SYBTFNT_ASCII_SIZE) 42 | SYBTFNT_ISO_8859_1_GLYPH_GLOBAL_INDEX_START = SYBTFNT_ASCII_GLYPH_GLOBAL_INDEX_START + SYBTFNT_ASCII_GLYPH_COUNT 43 | SYBTFNT_ISO_8859_1_GLYPH_DATA_SIZE = SYBTFNT_12X24_GLYPH_DATA_SIZE 44 | SYBTFNT_ISO_8859_1_GLYPH_COUNT = 96 45 | SYBTFNT_ISO_8859_1_SIZE = SYBTFNT_ISO_8859_1_GLYPH_COUNT * SYBTFNT_12X24_GLYPH_DATA_SIZE 46 | 47 | SYBTFNT_JIS_X_0201 = "jis-x-0201" 48 | SYBTFNT_JIS_X_0201_OFFSET = int(SYBTFNT_ISO_8859_1_OFFSET + SYBTFNT_ISO_8859_1_SIZE) 49 | SYBTFNT_JIS_X_0201_GLYPH_GLOBAL_INDEX_START = SYBTFNT_ISO_8859_1_GLYPH_GLOBAL_INDEX_START + SYBTFNT_ISO_8859_1_GLYPH_COUNT 50 | SYBTFNT_JIS_X_0201_GLYPH_DATA_SIZE = SYBTFNT_12X24_GLYPH_DATA_SIZE 51 | SYBTFNT_JIS_X_0201_GLYPH_COUNT = 96 52 | SYBTFNT_JIS_X_0201_SIZE = SYBTFNT_JIS_X_0201_GLYPH_COUNT * SYBTFNT_12X24_GLYPH_DATA_SIZE 53 | 54 | SYBTFNT_JISX_0208 = "jis-x-0208" 55 | SYBTFNT_JISX_0208_OFFSET = int(SYBTFNT_JIS_X_0201_OFFSET + SYBTFNT_JIS_X_0201_SIZE) 56 | SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START = SYBTFNT_JIS_X_0201_GLYPH_GLOBAL_INDEX_START + SYBTFNT_JIS_X_0201_GLYPH_COUNT 57 | SYBTFNT_JISX_0208_GLYPH_DATA_SIZE = SYBTFNT_24X24_GLYPH_DATA_SIZE 58 | SYBTFNT_JISX_0208_GLYPH_COUNT = 7056 59 | SYBTFNT_JISX_0208_SIZE = SYBTFNT_JISX_0208_GLYPH_COUNT * SYBTFNT_24X24_GLYPH_DATA_SIZE 60 | 61 | SYBTFNT_GAIJ = "sega-gaij" 62 | SYBTFNT_GAIJ_OFFSET = int(SYBTFNT_JISX_0208_OFFSET + SYBTFNT_JISX_0208_SIZE) 63 | SYBTFNT_GAIJ_GLYPH_GLOBAL_INDEX_START = SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START + SYBTFNT_JISX_0208_GLYPH_COUNT 64 | SYBTFNT_GAIJ_GLYPH_DATA_SIZE = SYBTFNT_24X24_GLYPH_DATA_SIZE 65 | SYBTFNT_GAIJ_GLYPH_COUNT = 22 66 | SYBTFNT_GAIJ_SIZE = SYBTFNT_GAIJ_GLYPH_COUNT * SYBTFNT_24X24_GLYPH_DATA_SIZE 67 | 68 | SYBTFNT_VMU_ICON = "sega-vmu-icon" 69 | SYBTFNT_VMU_ICON_OFFSET = int(SYBTFNT_GAIJ_OFFSET + SYBTFNT_GAIJ_SIZE) 70 | SYBTFNT_VMU_ICON_GLYPH_GLOBAL_INDEX_START = SYBTFNT_GAIJ_GLYPH_GLOBAL_INDEX_START + SYBTFNT_GAIJ_GLYPH_COUNT 71 | SYBTFNT_VMU_ICON_GLYPH_DATA_SIZE = SYBTFNT_32X32_GLYPH_DATA_SIZE 72 | SYBTFNT_VMU_ICON_GLYPH_COUNT = 129 73 | SYBTFNT_VMU_ICON_SIZE = SYBTFNT_VMU_ICON_GLYPH_COUNT * SYBTFNT_32X32_GLYPH_DATA_SIZE 74 | 75 | # 76 | # font ID offset and size in the bios 77 | # 78 | SYBTFNT_ID_OFFSET = 0x00100018 79 | SYBTFNT_ID_SIZE = 0x08 80 | 81 | # 82 | # font data offset in the bios 83 | # 84 | SYBTFNT_DATA_OFFSET = SYBTFNT_ID_OFFSET + SYBTFNT_ID_SIZE 85 | SYBTFNT_DATA_END_OFFSET = SYBTFNT_DATA_OFFSET + SYBTFNT_VMU_ICON_OFFSET + SYBTFNT_VMU_ICON_SIZE 86 | 87 | # 88 | # global logger to use 89 | # 90 | logger = logging.getLogger(Path(__file__).stem) 91 | 92 | 93 | class FontUnpackerError(Exception): 94 | 95 | def __init__(self, message): 96 | self.message = message 97 | 98 | def __str__(self): 99 | return self.message 100 | 101 | 102 | def get_glyph_ascii_code(glyph_global_index): 103 | if glyph_global_index >= SYBTFNT_ASCII_GLYPH_GLOBAL_INDEX_START and \ 104 | glyph_global_index < SYBTFNT_ISO_8859_1_GLYPH_GLOBAL_INDEX_START: 105 | 106 | if glyph_global_index == SYBTFNT_ASCII_GLYPH_GLOBAL_INDEX_START: 107 | return f"{SYBTFNT_ASCII}-0x{glyph_global_index + 33 - 1:02x}-overline" 108 | 109 | if glyph_global_index == SYBTFNT_ISO_8859_1_GLYPH_GLOBAL_INDEX_START - 1: 110 | return f"{SYBTFNT_ASCII}-0x{glyph_global_index + 33 - 1:02x}-yen-sign" 111 | 112 | return f"{SYBTFNT_ASCII}-0x{glyph_global_index + 33 - 1:02x}" 113 | 114 | return "unknown" 115 | 116 | 117 | def get_glyph_iso_8859_1_code(glyph_global_index): 118 | if glyph_global_index >= SYBTFNT_ISO_8859_1_GLYPH_GLOBAL_INDEX_START and \ 119 | glyph_global_index < SYBTFNT_JIS_X_0201_GLYPH_GLOBAL_INDEX_START: 120 | return f"{SYBTFNT_ISO_8859_1}-0x{glyph_global_index + 64:02x}" 121 | 122 | return "unknown" 123 | 124 | 125 | def get_glyph_jis_x_0201_code(glyph_global_index): 126 | if glyph_global_index >= SYBTFNT_JIS_X_0201_GLYPH_GLOBAL_INDEX_START and \ 127 | glyph_global_index < SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START: 128 | if glyph_global_index == 256: 129 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-wi" 130 | if glyph_global_index == 257: 131 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-we" 132 | if glyph_global_index == 258: 133 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-small-wa" 134 | if glyph_global_index == 259: 135 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-small-ka" 136 | if glyph_global_index == 259: 137 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-small-ke" 138 | if glyph_global_index >= 260: 139 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}-ext-diacritics" 140 | 141 | return f"{SYBTFNT_JIS_X_0201}-0x{glyph_global_index - 32:02x}" 142 | 143 | return "unknown" 144 | 145 | 146 | def get_glyph_jis_x_0208_code(glyph_global_index): 147 | if glyph_global_index >= SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START and \ 148 | glyph_global_index < SYBTFNT_GAIJ_GLYPH_GLOBAL_INDEX_START: 149 | 150 | column_index = int((glyph_global_index - SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START) % 94) + 0x21 151 | row_index = int((glyph_global_index - SYBTFNT_JISX_0208_GLYPH_GLOBAL_INDEX_START) / 94) 152 | # Are we in the 7th first row ranges (which correspond to 33-39 non-Kanji rows)? 153 | if row_index < 39 - 33 + 1: 154 | row_index += 33 155 | else: 156 | # Otherwise we are in the 48-116 range. 157 | row_index += 48 - 7 158 | 159 | return f"{SYBTFNT_JISX_0208}-0x{row_index:02x}{column_index:02x}" 160 | 161 | return "unknown" 162 | 163 | 164 | def get_glyph_gaij_code(glyph_global_index): 165 | return f"{SYBTFNT_GAIJ}-0x{glyph_global_index - SYBTFNT_GAIJ_GLYPH_GLOBAL_INDEX_START:04x}" 166 | 167 | 168 | def get_glyph_vmu_icon_code(glyph_global_index): 169 | return f"{SYBTFNT_VMU_ICON}-0x{glyph_global_index - SYBTFNT_VMU_ICON_GLYPH_GLOBAL_INDEX_START:04x}" 170 | 171 | 172 | def read_font_id(filehandle): 173 | try: 174 | filehandle.seek(SYBTFNT_ID_OFFSET) 175 | font_id, = struct.unpack(f"<{SYBTFNT_ID_SIZE}s", filehandle.read(SYBTFNT_ID_SIZE)) 176 | font_id = font_id.decode(encoding='UTF-8') 177 | logger.info(f"Font ID @0x{SYBTFNT_ID_OFFSET:08x} is {font_id}") 178 | 179 | return font_id 180 | except Exception as error: 181 | raise FontUnpackerError(f"Unable to decode font ID @0x{SYBTFNT_ID_OFFSET:08x}") from error 182 | 183 | 184 | def unpack_glyphs_fontsets(bootROM_filehandle, output_path): 185 | logger.info(f"Creating output directory '{output_path}'") 186 | output_path.mkdir(parents=True, exist_ok=True) 187 | 188 | glyph_global_index = 0 189 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_7BIT, 190 | SYBTFNT_ASCII, SYBTFNT_ASCII_OFFSET, SYBTFNT_ASCII_SIZE, 191 | SYBTFNT_ASCII_GLYPH_DATA_SIZE, SYBTFNT_12X24_GLYPH_IMAGE_SIZE, 192 | get_glyph_ascii_code) 193 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_8BIT, 194 | SYBTFNT_ISO_8859_1, SYBTFNT_ISO_8859_1_OFFSET, SYBTFNT_ISO_8859_1_SIZE, 195 | SYBTFNT_ISO_8859_1_GLYPH_DATA_SIZE, SYBTFNT_12X24_GLYPH_IMAGE_SIZE, 196 | get_glyph_iso_8859_1_code) 197 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_8BIT, 198 | SYBTFNT_JIS_X_0201, SYBTFNT_JIS_X_0201_OFFSET, SYBTFNT_JIS_X_0201_SIZE, 199 | SYBTFNT_JIS_X_0201_GLYPH_DATA_SIZE, SYBTFNT_12X24_GLYPH_IMAGE_SIZE, 200 | get_glyph_jis_x_0201_code) 201 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_16BIT, 202 | SYBTFNT_JISX_0208, SYBTFNT_JISX_0208_OFFSET, SYBTFNT_JISX_0208_SIZE, 203 | SYBTFNT_JISX_0208_GLYPH_DATA_SIZE, SYBTFNT_24X24_GLYPH_IMAGE_SIZE, 204 | get_glyph_jis_x_0208_code) 205 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_16BIT, 206 | SYBTFNT_GAIJ, SYBTFNT_GAIJ_OFFSET, SYBTFNT_GAIJ_SIZE, 207 | SYBTFNT_GAIJ_GLYPH_DATA_SIZE, SYBTFNT_24X24_GLYPH_IMAGE_SIZE, 208 | get_glyph_gaij_code) 209 | glyph_global_index = unpack_glyphs_fontset(bootROM_filehandle, output_path, glyph_global_index, SYBTFNT_TYPE_16BIT, 210 | SYBTFNT_VMU_ICON, SYBTFNT_VMU_ICON_OFFSET, SYBTFNT_VMU_ICON_SIZE, 211 | SYBTFNT_VMU_ICON_GLYPH_DATA_SIZE, SYBTFNT_32X32_GLYPH_IMAGE_SIZE, 212 | get_glyph_vmu_icon_code) 213 | 214 | 215 | def unpack_glyphs_fontset(filehandle, output_path, glyph_global_index, fontset_type, 216 | fontset_name, fontset_offset: int, fontset_size, fontset_glyph_size, font_size, fontset_get_code): 217 | fontset_output_path = output_path / fontset_type / fontset_name 218 | logger.info(f"Unpacking {fontset_type} fontset {fontset_name} glyphs in {fontset_output_path}") 219 | fontset_output_path.mkdir(parents=True, exist_ok=True) 220 | 221 | offset = SYBTFNT_DATA_OFFSET + fontset_offset 222 | while offset < SYBTFNT_DATA_OFFSET + fontset_offset + fontset_size: 223 | filehandle.seek(offset) 224 | 225 | output_filepath = fontset_output_path / f"glyph-{glyph_global_index}-{fontset_get_code(glyph_global_index)}.png" 226 | font_data = filehandle.read(fontset_glyph_size) 227 | 228 | image = Image.new("RGB", font_size) 229 | 230 | pixel_index = 0 231 | for byte in font_data: 232 | for bit_index in range(8): 233 | if ((byte << bit_index) & 0x80) != 0: 234 | color = ImageColor.getcolor("black", "RGB") 235 | else: 236 | color = ImageColor.getcolor("white", "RGB") 237 | image.putpixel((pixel_index % font_size[0], int(pixel_index / font_size[0])), color) 238 | pixel_index += 1 239 | image.save(output_filepath) 240 | 241 | offset += fontset_glyph_size 242 | glyph_global_index += 1 243 | 244 | return glyph_global_index 245 | 246 | 247 | def main(argv=None): 248 | logging.basicConfig(format='%(module)s %(levelname)s: %(message)s', level=logging.INFO) 249 | 250 | if argv is None: 251 | argv = sys.argv 252 | 253 | optparser = optparse.OptionParser(usage="usage: %prog [options] bootROM.bin", 254 | description="Unpack all bootROM font glyphs") 255 | optparser.add_option("-f", "--font-id", 256 | action="store_true", dest="font_id_only", default=False, 257 | help="Read only the font ID") 258 | optparser.add_option("-d", "--debug", 259 | action="store_true", dest="debug", default=False, 260 | help="Change logger level to 'DEBUG'") 261 | (options, args) = optparser.parse_args() 262 | 263 | try: 264 | if options.debug: 265 | logger.setLevel(logging.DEBUG) 266 | 267 | if len(args) == 0: 268 | raise FontUnpackerError('No bootROM (BIOS) file specified') 269 | 270 | basepath = Path(__file__).resolve().parent 271 | input_file = Path(args[0]) 272 | output_path = (basepath / "output").relative_to(basepath) 273 | 274 | logger.info(f"Opening bootROM '{input_file}'") 275 | with open(input_file, mode='rb') as inputfile_handle: 276 | output_path /= read_font_id(inputfile_handle) 277 | if not options.font_id_only: 278 | unpack_glyphs_fontsets(inputfile_handle, output_path) 279 | 280 | logger.info('Done.') 281 | except FontUnpackerError as error: 282 | logger.error(error) 283 | logger.debug(traceback.format_exc()) 284 | optparser.print_help(sys.stderr) 285 | return 1 286 | 287 | 288 | if __name__ == '__main__': 289 | sys.exit(main()) 290 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow==10.4.0 2 | -------------------------------------------------------------------------------- /sega-katana-r9-font-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/japanese-cake/jc-bootrom-font/fbda1b06a5d8f263be133b8da97e94c32b20daf0/sega-katana-r9-font-sample.png --------------------------------------------------------------------------------