├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── img ├── ascii.png └── emu_example.png └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler32" 7 | version = "1.0.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" 10 | 11 | [[package]] 12 | name = "ansi_term" 13 | version = "0.11.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 16 | dependencies = [ 17 | "winapi", 18 | ] 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.0.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.2.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 42 | 43 | [[package]] 44 | name = "byteorder" 45 | version = "1.3.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 48 | 49 | [[package]] 50 | name = "cfg-if" 51 | version = "0.1.10" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 54 | 55 | [[package]] 56 | name = "chrono" 57 | version = "0.4.11" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" 60 | dependencies = [ 61 | "num-integer", 62 | "num-traits", 63 | "time", 64 | ] 65 | 66 | [[package]] 67 | name = "clap" 68 | version = "2.33.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" 71 | dependencies = [ 72 | "ansi_term", 73 | "atty", 74 | "bitflags", 75 | "strsim", 76 | "textwrap", 77 | "unicode-width", 78 | "vec_map", 79 | ] 80 | 81 | [[package]] 82 | name = "colored" 83 | version = "1.9.3" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" 86 | dependencies = [ 87 | "atty", 88 | "lazy_static", 89 | "winapi", 90 | ] 91 | 92 | [[package]] 93 | name = "crc32fast" 94 | version = "1.2.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 97 | dependencies = [ 98 | "cfg-if", 99 | ] 100 | 101 | [[package]] 102 | name = "deflate" 103 | version = "0.8.4" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" 106 | dependencies = [ 107 | "adler32", 108 | "byteorder", 109 | ] 110 | 111 | [[package]] 112 | name = "gbtile" 113 | version = "0.2.0" 114 | dependencies = [ 115 | "clap", 116 | "log", 117 | "png", 118 | "simple_logger", 119 | ] 120 | 121 | [[package]] 122 | name = "hermit-abi" 123 | version = "0.1.12" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" 126 | dependencies = [ 127 | "libc", 128 | ] 129 | 130 | [[package]] 131 | name = "inflate" 132 | version = "0.4.5" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" 135 | dependencies = [ 136 | "adler32", 137 | ] 138 | 139 | [[package]] 140 | name = "lazy_static" 141 | version = "1.4.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 144 | 145 | [[package]] 146 | name = "libc" 147 | version = "0.2.69" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 150 | 151 | [[package]] 152 | name = "log" 153 | version = "0.4.8" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 156 | dependencies = [ 157 | "cfg-if", 158 | ] 159 | 160 | [[package]] 161 | name = "num-integer" 162 | version = "0.1.42" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 165 | dependencies = [ 166 | "autocfg", 167 | "num-traits", 168 | ] 169 | 170 | [[package]] 171 | name = "num-traits" 172 | version = "0.2.11" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 175 | dependencies = [ 176 | "autocfg", 177 | ] 178 | 179 | [[package]] 180 | name = "png" 181 | version = "0.16.3" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "2c68a431ed29933a4eb5709aca9800989758c97759345860fa5db3cfced0b65d" 184 | dependencies = [ 185 | "bitflags", 186 | "crc32fast", 187 | "deflate", 188 | "inflate", 189 | ] 190 | 191 | [[package]] 192 | name = "simple_logger" 193 | version = "1.6.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "fea0c4611f32f4c2bac73754f22dca1f57e6c1945e0590dae4e5f2a077b92367" 196 | dependencies = [ 197 | "atty", 198 | "chrono", 199 | "colored", 200 | "log", 201 | "winapi", 202 | ] 203 | 204 | [[package]] 205 | name = "strsim" 206 | version = "0.8.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 209 | 210 | [[package]] 211 | name = "textwrap" 212 | version = "0.11.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 215 | dependencies = [ 216 | "unicode-width", 217 | ] 218 | 219 | [[package]] 220 | name = "time" 221 | version = "0.1.43" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 224 | dependencies = [ 225 | "libc", 226 | "winapi", 227 | ] 228 | 229 | [[package]] 230 | name = "unicode-width" 231 | version = "0.1.7" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 234 | 235 | [[package]] 236 | name = "vec_map" 237 | version = "0.8.2" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 240 | 241 | [[package]] 242 | name = "winapi" 243 | version = "0.3.8" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 246 | dependencies = [ 247 | "winapi-i686-pc-windows-gnu", 248 | "winapi-x86_64-pc-windows-gnu", 249 | ] 250 | 251 | [[package]] 252 | name = "winapi-i686-pc-windows-gnu" 253 | version = "0.4.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 256 | 257 | [[package]] 258 | name = "winapi-x86_64-pc-windows-gnu" 259 | version = "0.4.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 262 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbtile" 3 | description = "A small command line utility to convert PNG images to GBDK compliant Game Boy tiles" 4 | license = "MIT" 5 | version = "0.2.0" 6 | authors = ["Blake Smith "] 7 | edition = "2018" 8 | repository = "https://crates.io/crates/gbtile" 9 | readme = "README.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | png = "0.16" 15 | clap = "2.33" 16 | log = "0.4" 17 | simple_logger = "1.6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GB Tile 2 | 3 | A small command line utility to convert PNG images 4 | to [GBDK](http://gbdk.sourceforge.net/) or [RGBDS](https://rgbds.gbdev.io/) 5 | compliant Game Boy tiles. Tiles are generated as C `unsigned char` arrays for 6 | GBDK tiles, and an array of byte literals in ROM for RGBDS tiles. 7 | 8 | Takes input like: 9 | 10 | ![ASCII PNG](https://raw.github.com/blakesmith/gbtile/master/img/ascii.png) 11 | 12 | That you can use in your Game Boy programs like: 13 | 14 | ![Emulator Example](https://raw.github.com/blakesmith/gbtile/master/img/emu_example.png) 15 | 16 | ## Install 17 | 18 | Install [cargo, and rust](https://rustup.rs/) 19 | 20 | ### From cargo 21 | 22 | ``` 23 | cargo install gbtile 24 | ``` 25 | 26 | ### From source 27 | 28 | ``` 29 | $ git clone git@github.com:blakesmith/gbtile.git 30 | $ cd gbtile/ 31 | $ cargo install --path . 32 | ``` 33 | 34 | The `gbtile` executable will be installed in `$HOME/.cargo/bin/` by default. 35 | 36 | ### From nix 37 | 38 | If you use the nix package manager, from the root of this repo, with flakes enabled: 39 | 40 | ``` 41 | $ nix build 42 | ``` 43 | 44 | You can also depend on it from another repo via the flake directly. 45 | 46 | The `gbtile` executable will be in `result/bin/gbtile`. 47 | 48 | ## Usage 49 | 50 | ``` 51 | USAGE: 52 | gbtile [FLAGS] [OPTIONS] -i -o 53 | 54 | FLAGS: 55 | -d Enable debug logging 56 | -h, --help Prints help information 57 | -V, --version Prints version information 58 | 59 | OPTIONS: 60 | -i The PNG image to generate tiles from. Example: 'image.png' 61 | -o The output file to generate. Usually something like 'tiles.h' for GBDK output, or 62 | 'tiles.asm' for RGBDS 63 | -t The output type. Either 'gbdk' or 'rgbds'. Defaults to 'gbdk' 64 | ``` 65 | 66 | Find an image that matches the image criteria below, or make your own 67 | in your favorite photo editor, then convert it like so: 68 | 69 | ### GBDK 70 | 71 | ``` 72 | $ gbtile -t gbdk -i ascii.png -o ascii.tile.h 73 | 2020-05-19 21:07:02,154 INFO [gbtile] File: ascii.png, Tile rows: 14, 74 | columns: 16, unique colors: 2 75 | ``` 76 | 77 | Make sure the tile count and unique colors match your 78 | expectations. The output will be a valid C array like so: 79 | 80 | ```c 81 | unsigned char ascii[] = { 82 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 83 | 0x00,0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x00,0x40,0x40,0x00,0x00, 84 | ... 85 | 0x10,0x10,0x38,0x38,0x54,0x54,0x50,0x50,0x38,0x38,0x14,0x14,0x54,0x54,0x38,0x38, 86 | }; 87 | ``` 88 | 89 | The variable name should match the input file name. 90 | 91 | You can now include the tile array in your GBDK Game Boy projects, and 92 | load it using the `set_bkg_data` or `set_sprite_data` C functions. 93 | 94 | ### RGBDS 95 | 96 | ``` 97 | $ gbtile -t rgbds -i ascii.png -o tiles.asm 98 | 2023-03-19 08:29:04,797 INFO [gbtile] File: img/ascii.png, Tile rows: 14, columns: 16, unique colors: 2 99 | ``` 100 | 101 | The label name of the tiles will match the file name, will be placed in ROM, and exported 102 | for other .asm files to reference. 103 | 104 | You'll get an output file that looks something like this: 105 | 106 | ```asm 107 | SECTION "Tiles for 'ascii'", ROM0 108 | 109 | EXPORT ascii, ascii_end 110 | 111 | ascii: 112 | db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00, 113 | db $00,$00,$40,$40,$40,$40,$40,$40,$40,$40,$00,$00,$40,$40,$00,$00, 114 | db $00,$00,$6c,$6c,$24,$24,$48,$48,$00,$00,$00,$00,$00,$00,$00,$00, 115 | db $00,$00,$24,$24,$7e,$7e,$24,$24,$24,$24,$7e,$7e,$24,$24,$00,$00, 116 | ... 117 | db $00,$00,$28,$28,$00,$00,$44,$44,$44,$44,$28,$28,$10,$10,$60,$60 118 | ascii_end: 119 | ``` 120 | 121 | You can assemble the file along with the rest of your project with something like: 122 | 123 | ``` 124 | rgbasm -L -o tiles.o tiles.asm 125 | ``` 126 | 127 | Once the tile data is assembled with the rest of you're project, you'll need to copy 128 | the tiles into video memory correctly using some sort of `Memcopy` routine like so: 129 | 130 | ```asm 131 | ; Called at game startup 132 | InitGame: 133 | ; Call routine to initialize tile data 134 | call InitTileData 135 | ; Jump to main game loop after initializing tile data 136 | jp Main 137 | 138 | ; Initialize the tile data. In this example, we've converted a tile image 139 | ; named 'ascii_tiles' from gbtile, so we should have two symbols exported 140 | ; for our project to use: 'ascii_files' and 'ascii_tiles_end', which should reference 141 | ; to the beginning and end address of the tile data in ROM. 142 | InitTileData: 143 | ; Copy tile data from the exported tile named 'ascii_tiles' 144 | ld de, ascii_tiles 145 | ; Load the tiles into the start of video memory address 146 | ld hl, $9000 147 | ; The length of the copy is the difference between the start 148 | ; of the ascii_tiles symbol, and ascii_tiles_end symbol. 149 | ld bc, ascii_tiles_end - ascii_tiles 150 | call Memcopy 151 | ret 152 | 153 | ; Copy bytes from one area to another. 154 | ; @param de: Source 155 | ; @param hl: Destination 156 | ; @param bc: Length 157 | Memcopy: 158 | ld a, [de] 159 | ld [hli], a 160 | inc de 161 | dec bc 162 | ld a, b 163 | or a, c 164 | jp nz, Memcopy 165 | ret 166 | ``` 167 | 168 | ## Images 169 | 170 | For my workflow, I'm using the following image setup: 171 | 172 | 1. Create an image that has a pixel dimension that's divisible by 8, and no greater than 256x256 173 | 2. Use 4 distinct colors. 0xFFFFFF for white, 0x000000 for black. Dark gray, any RGB value between 0xbfbfbf and 0x7f7f7f. For light gray, any RGB color between 0x7f7f7f and 0x3f3f3f. 174 | 3. The image will be cut into tiles that are 8x8 pixels wide each. 175 | 4. I've been using RGB formatted PNGs, but others should theoretically work. 176 | 177 | ## License 178 | 179 | MIT Licensed. 180 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, rustPlatform }: 2 | 3 | with rustPlatform; 4 | 5 | buildRustPackage rec { 6 | pname = "gbtile"; 7 | version = "0.2.0"; 8 | src = builtins.filterSource 9 | (path: type: type != "directory" || baseNameOf path != "target") 10 | ./.; 11 | cargoSha256 = "sha256-k2abebOeoa6X9W95YN3WCnwDhU0JcYhQaypDX3QHq5M="; 12 | doCheck = false; 13 | 14 | meta = with lib; { 15 | description = "GameBoy tile generator. Converts PNG images to GBDK or RGDDS data"; 16 | homepage = "https://github.com/blakesmith/gbtile"; 17 | license = with licenses; [ mit ]; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1678901627, 6 | "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1653936696, 21 | "narHash": "sha256-M6bJShji9AIDZ7Kh7CPwPBPb/T7RiVev2PAcOi4fxDQ=", 22 | "owner": "nixos", 23 | "repo": "nixpkgs", 24 | "rev": "ce6aa13369b667ac2542593170993504932eb836", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "nixos", 29 | "ref": "22.05", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "GBTile GameBoy tile generator"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/22.05"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, flake-utils, nixpkgs }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | in 14 | rec { 15 | packages = { 16 | gbtile = pkgs.callPackage ./default.nix {}; 17 | }; 18 | apps.${system}.gbtile = { 19 | type = "app"; 20 | program = "${packages.gbtile}/bin/gbtile"; 21 | }; 22 | defaultPackage = packages.gbtile; 23 | } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /img/ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakesmith/gbtile/cc4540c83e30d9728312ef920ebb1de8f7ea269f/img/ascii.png -------------------------------------------------------------------------------- /img/emu_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakesmith/gbtile/cc4540c83e30d9728312ef920ebb1de8f7ea269f/img/emu_example.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, Arg}; 2 | use log; 3 | use log::Level; 4 | use png::Decoder; 5 | use std::collections::{BTreeSet, HashMap}; 6 | use std::fs::{File, OpenOptions}; 7 | use std::io; 8 | use std::io::Write; 9 | use std::path::Path; 10 | 11 | const GB_MAX_COLOR_COUNT: usize = 4; 12 | 13 | #[derive(Copy, Clone, Debug)] 14 | enum OutputType { 15 | Gbdk, 16 | Rgbds, 17 | } 18 | 19 | #[derive(Debug)] 20 | struct CommandArguments { 21 | pub input: String, 22 | pub output: String, 23 | pub output_type: OutputType, 24 | } 25 | 26 | #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Hash, Ord, Eq)] 27 | struct RGB { 28 | r: u8, 29 | g: u8, 30 | b: u8, 31 | } 32 | 33 | impl RGB { 34 | fn round(&self) -> RGB { 35 | RGB { 36 | r: (self.r / 48) * 48, 37 | g: (self.g / 48) * 48, 38 | b: (self.b / 48) * 48, 39 | } 40 | } 41 | } 42 | 43 | struct DecodedImage { 44 | input_filename: String, 45 | info: png::OutputInfo, 46 | image_data: Vec, 47 | color_numbers: HashMap, 48 | } 49 | 50 | struct EncodedTile { 51 | input_filename: String, 52 | tile_data: Vec, 53 | } 54 | 55 | impl DecodedImage { 56 | fn lookup_color(&self, pixel: &RGB) -> u8 { 57 | *self.color_numbers.get(&pixel).unwrap() 58 | } 59 | } 60 | 61 | #[derive(Debug)] 62 | enum ImageReadError { 63 | Png(png::DecodingError), 64 | Io(io::Error), 65 | UnsupportedColorType(png::ColorType), 66 | TooManyColors, 67 | } 68 | 69 | impl From for ImageReadError { 70 | fn from(err: io::Error) -> Self { 71 | ImageReadError::Io(err) 72 | } 73 | } 74 | 75 | impl From for ImageReadError { 76 | fn from(err: png::DecodingError) -> Self { 77 | ImageReadError::Png(err) 78 | } 79 | } 80 | 81 | fn rgbs_to_color_number(unique_colors: &BTreeSet) -> HashMap { 82 | let mut color_numbers = HashMap::new(); 83 | for (i, rgb) in unique_colors.iter().rev().enumerate() { 84 | color_numbers.insert(*rgb, i as u8); 85 | } 86 | color_numbers 87 | } 88 | 89 | fn read_image_data(info: &png::OutputInfo, image_buf: Vec) -> Result, ImageReadError> { 90 | log::debug!("PNG info: {:?}", info); 91 | let mut image_data = Vec::new(); 92 | match info.color_type { 93 | png::ColorType::RGB => { 94 | for color in image_buf.chunks(3) { 95 | let rgb = RGB { 96 | r: color[0], 97 | g: color[1], 98 | b: color[2], 99 | }; 100 | log::debug!("Original RGB is: {:?}", rgb); 101 | image_data.push(rgb.round()); 102 | } 103 | } 104 | png::ColorType::RGBA => { 105 | for color in image_buf.chunks(4) { 106 | let rgb = RGB { 107 | r: color[0], 108 | g: color[1], 109 | b: color[2], 110 | }; 111 | log::debug!("Original RGB is: {:?}", rgb); 112 | image_data.push(rgb.round()); 113 | } 114 | } 115 | png::ColorType::Grayscale => { 116 | for color in image_buf { 117 | let rgb = RGB { 118 | r: color, 119 | g: color, 120 | b: color, 121 | }; 122 | log::debug!("Original RGB is: {:?}", rgb); 123 | image_data.push(rgb.round()); 124 | } 125 | } 126 | png::ColorType::GrayscaleAlpha => { 127 | for color in image_buf.chunks(2) { 128 | let rgb = RGB { 129 | r: color[0], 130 | g: color[0], 131 | b: color[0], 132 | }; 133 | log::debug!("Original RGB is: {:?}", rgb); 134 | image_data.push(rgb.round()); 135 | } 136 | } 137 | color_type => { 138 | return Err(ImageReadError::UnsupportedColorType(color_type)); 139 | } 140 | } 141 | 142 | Ok(image_data) 143 | } 144 | 145 | fn decode_image(image_input: &str) -> Result { 146 | let file = File::open(image_input)?; 147 | let mut unique_colors = BTreeSet::new(); 148 | let decoder = Decoder::new(file); 149 | let (info, mut png_reader) = decoder.read_info()?; 150 | 151 | let mut image_buf = vec![0; info.buffer_size()]; 152 | png_reader.next_frame(&mut image_buf)?; 153 | let image_data = read_image_data(&info, image_buf)?; 154 | 155 | log::debug!("Image data size is: {}", image_data.len()); 156 | 157 | for (i, color) in image_data.iter().enumerate() { 158 | unique_colors.insert(*color); 159 | if unique_colors.len() > GB_MAX_COLOR_COUNT { 160 | log::debug!("Unique colors are: {:?}, stopped at: {}", unique_colors, i,); 161 | return Err(ImageReadError::TooManyColors); 162 | } 163 | } 164 | let color_numbers = rgbs_to_color_number(&unique_colors); 165 | log::debug!("Color numbers are: {:?}", color_numbers); 166 | 167 | let decoded = DecodedImage { 168 | input_filename: image_input.to_string(), 169 | image_data, 170 | info, 171 | color_numbers, 172 | }; 173 | Ok(decoded) 174 | } 175 | 176 | const PIXELS_PER_LINE: u8 = 8; 177 | 178 | fn encode_tile(decoded_image: DecodedImage) -> EncodedTile { 179 | let rows = decoded_image.info.height / 8; 180 | let columns = decoded_image.info.width / 8; 181 | log::info!( 182 | "File: {}, Tile rows: {}, columns: {}, unique colors: {}", 183 | decoded_image.input_filename, 184 | rows, 185 | columns, 186 | decoded_image.color_numbers.len() 187 | ); 188 | let mut tile_data = Vec::new(); 189 | for row in 0..rows { 190 | for column in 0..columns { 191 | for tile_row in 0..8 { 192 | let mut low_byte = 0; 193 | let mut high_byte = 0; 194 | for tile_column in 0..8 { 195 | let pixel_index = (column * 8 + tile_column) 196 | + ((decoded_image.info.width * tile_row) 197 | + (row * 8 * decoded_image.info.width)); 198 | let pixel = decoded_image.image_data[pixel_index as usize]; 199 | let color = decoded_image.lookup_color(&pixel); 200 | low_byte |= (color & 0x01) << (PIXELS_PER_LINE - tile_column as u8 - 1); 201 | high_byte |= ((color >> 1) & 0x01) << (PIXELS_PER_LINE - tile_column as u8 - 1); 202 | } 203 | tile_data.push(low_byte); 204 | tile_data.push(high_byte); 205 | } 206 | } 207 | } 208 | 209 | let input_filename = decoded_image.input_filename.clone(); 210 | 211 | EncodedTile { 212 | input_filename, 213 | tile_data, 214 | } 215 | } 216 | 217 | fn write_tile_gbdk(variable_name: &str, encoded_tile: &EncodedTile) -> String { 218 | let preamble = format!("unsigned char {}[] = {{", variable_name); 219 | let mut body = Vec::new(); 220 | for line in encoded_tile.tile_data.chunks(16) { 221 | let mut formatted_bytes = Vec::new(); 222 | for byte in line { 223 | formatted_bytes.push(format!("{:#04X}", byte)); 224 | } 225 | body.push(format!(" {}", formatted_bytes.join(","))); 226 | } 227 | 228 | format!("{}\n{}\n}};\n", preamble, body.join(",\n")) 229 | } 230 | 231 | fn write_tile_rgbds(variable_name: &str, encoded_tile: &EncodedTile) -> String { 232 | let end_symbol = format!("{}_end", variable_name); 233 | let preamble = format!( 234 | "SECTION \"Tiles for '{}'\", ROM0\n\nEXPORT {}, {}\n\n{}:", 235 | variable_name, variable_name, end_symbol, variable_name 236 | ); 237 | let mut body = Vec::new(); 238 | for line in encoded_tile.tile_data.chunks(16) { 239 | let mut formatted_bytes = Vec::new(); 240 | for byte in line { 241 | formatted_bytes.push(format!("${:02x}", byte)); 242 | } 243 | body.push(format!(" db {}", formatted_bytes.join(","))); 244 | } 245 | 246 | format!("{}\n{}\n{}:\n", preamble, body.join(",\n"), end_symbol) 247 | } 248 | 249 | fn write_tile( 250 | encoded_tile: &EncodedTile, 251 | out_file: &str, 252 | output_type: OutputType, 253 | ) -> Result<(), io::Error> { 254 | let variable_name = Path::new(&encoded_tile.input_filename) 255 | .file_stem() 256 | .map(|stem| stem.to_string_lossy()) 257 | .expect(&format!( 258 | "Invalid file name: {}", 259 | encoded_tile.input_filename 260 | )); 261 | let formatted_result = match output_type { 262 | OutputType::Gbdk => write_tile_gbdk(&variable_name, encoded_tile), 263 | OutputType::Rgbds => write_tile_rgbds(&variable_name, encoded_tile), 264 | }; 265 | let mut file = OpenOptions::new() 266 | .read(true) 267 | .write(true) 268 | .create(true) 269 | .truncate(true) 270 | .open(out_file)?; 271 | file.write_all(formatted_result.as_bytes())?; 272 | Ok(()) 273 | } 274 | 275 | fn main() { 276 | let matches = App::new("Gameboy Tile Generator") 277 | .version("0.2.0") 278 | .author("Blake Smith ") 279 | .about("Generate GBDK or RGBDS Game Boy tiles from PNG images") 280 | .arg( 281 | Arg::with_name("debug") 282 | .help("Enable debug logging") 283 | .short("d"), 284 | ) 285 | .arg( 286 | Arg::with_name("input") 287 | .help("The PNG image to generate tiles from. Example: 'image.png'") 288 | .short("i") 289 | .takes_value(true) 290 | .required(true), 291 | ) 292 | .arg( 293 | Arg::with_name("output") 294 | .help("The output file to generate. Usually something like 'tiles.h' for GBDK output, or 'tiles.asm' for RGBDS") 295 | .short("o") 296 | .takes_value(true) 297 | .required(true), 298 | ) 299 | .arg( 300 | Arg::with_name("output-type") 301 | .help("The output type. Either 'gbdk' or 'rgbds'. Defaults to 'gbdk'") 302 | .takes_value(true) 303 | .short("t"), 304 | ) 305 | .get_matches(); 306 | 307 | if matches.is_present("debug") { 308 | simple_logger::init_with_level(Level::Debug).unwrap(); 309 | } else { 310 | simple_logger::init_with_level(Level::Info).unwrap(); 311 | } 312 | let output_type = match matches.value_of("output-type") { 313 | Some("gbdk") => OutputType::Gbdk, 314 | Some("rgbds") => OutputType::Rgbds, 315 | _ => OutputType::Gbdk, 316 | }; 317 | 318 | let args = CommandArguments { 319 | input: matches.value_of("input").unwrap().to_string(), 320 | output: matches.value_of("output").unwrap().to_string(), 321 | output_type: output_type, 322 | }; 323 | 324 | let decoded_image = decode_image(&args.input).expect("Could not decode image"); 325 | let encoded_tile = encode_tile(decoded_image); 326 | write_tile(&encoded_tile, &args.output, args.output_type).expect("Could not write out tile"); 327 | 328 | log::debug!("Arguments are: {:?}", args); 329 | } 330 | --------------------------------------------------------------------------------