├── img2gb ├── version.py ├── c_export.py ├── helpers.py ├── __main__.py ├── gbtile.py ├── cli.py ├── gbtilemap.py ├── gbtileset.py └── __init__.py ├── .flake8 ├── img.png ├── doc ├── _static │ ├── img.png │ ├── banner.png │ └── tileset.png ├── api │ ├── gbtile.rst │ ├── gbtilemap.rst │ ├── gbtileset.rst │ ├── c_export.rst │ ├── index.rst │ └── high-level-functions.rst ├── index.rst ├── install.rst ├── cli.rst ├── conf.py └── howto.rst ├── example ├── tileset.png ├── README.rst ├── tileset.h ├── tilemap.h ├── tilemap.c └── tileset.c ├── .gitignore ├── scripts ├── img2gb_win.py └── build-windows.sh ├── test ├── assets │ ├── tilemap.png │ └── tileset.png ├── test_gbtile.py ├── test_gbtilemap.py └── test_gbtileset.py ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── python-ci.yml │ ├── winbuild.yml │ ├── gh-pages.yml │ └── python-packages.yml ├── RELEASE.rst ├── noxfile.py ├── requirements.txt ├── setup.py ├── LICENSE └── README.rst /img2gb/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.3.0" 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E241, W503, E501 3 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/img.png -------------------------------------------------------------------------------- /doc/_static/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/doc/_static/img.png -------------------------------------------------------------------------------- /example/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/example/tileset.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __env__ 2 | *.pyc 3 | *.egg* 4 | __pycache__ 5 | dist 6 | build 7 | *.spec 8 | -------------------------------------------------------------------------------- /doc/_static/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/doc/_static/banner.png -------------------------------------------------------------------------------- /doc/_static/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/doc/_static/tileset.png -------------------------------------------------------------------------------- /doc/api/gbtile.rst: -------------------------------------------------------------------------------- 1 | GBTile 2 | ====== 3 | 4 | .. automodule:: img2gb.gbtile 5 | :members: 6 | -------------------------------------------------------------------------------- /scripts/img2gb_win.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from img2gb.__main__ import main 3 | main() 4 | -------------------------------------------------------------------------------- /test/assets/tilemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/test/assets/tilemap.png -------------------------------------------------------------------------------- /test/assets/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flozz/img2gb/HEAD/test/assets/tileset.png -------------------------------------------------------------------------------- /doc/api/gbtilemap.rst: -------------------------------------------------------------------------------- 1 | GBTilemap 2 | ========= 3 | 4 | .. automodule:: img2gb.gbtilemap 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/api/gbtileset.rst: -------------------------------------------------------------------------------- 1 | GBTileset 2 | ========= 3 | 4 | .. automodule:: img2gb.gbtileset 5 | :members: 6 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Example Files 2 | ============= 3 | 4 | This folder contains example of files generated by img2gb. 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: flozz 2 | custom: 3 | - https://www.paypal.me/0xflozz 4 | - https://www.buymeacoffee.com/flozz 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /doc/api/c_export.rst: -------------------------------------------------------------------------------- 1 | C Export Functions 2 | ================== 3 | 4 | This module contains function to wrap C code into C file and C header file. 5 | 6 | .. automodule:: img2gb.c_export 7 | :members: 8 | -------------------------------------------------------------------------------- /example/tileset.h: -------------------------------------------------------------------------------- 1 | // This file was generated by img2gb, DO NOT EDIT 2 | 3 | #ifndef _TILESET_H 4 | #define _TILESET_H 5 | 6 | extern const UINT8 TILESET[]; 7 | #define TILESET_TILE_COUNT 97 8 | 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /example/tilemap.h: -------------------------------------------------------------------------------- 1 | // This file was generated by img2gb, DO NOT EDIT 2 | 3 | #ifndef _TILEMAP_H 4 | #define _TILEMAP_H 5 | 6 | extern const UINT8 TILEMAP[]; 7 | #define TILEMAP_WIDTH 20 8 | #define TILEMAP_HEIGHT 18 9 | 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /doc/api/index.rst: -------------------------------------------------------------------------------- 1 | Python API 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | ./high-level-functions.rst 9 | ./gbtile.rst 10 | ./gbtileset.rst 11 | ./gbtilemap.rst 12 | ./c_export.rst 13 | -------------------------------------------------------------------------------- /scripts/build-windows.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d __env__ ] ; then 4 | python -m venv __env__ 5 | . __env__/Scripts/activate 6 | pip install -e . 7 | pip install pyinstaller 8 | else 9 | . __env__/Scripts/activate 10 | fi 11 | 12 | pyinstaller \ 13 | --onefile \ 14 | --name img2gb-$(python setup.py --version) \ 15 | scripts/img2gb_win.py 16 | -------------------------------------------------------------------------------- /doc/api/high-level-functions.rst: -------------------------------------------------------------------------------- 1 | High-Level Functions 2 | ==================== 3 | 4 | You will find bellow the high-level function to easily generate tilesets and 5 | tilemaps. 6 | 7 | 8 | Generating Tilesets 9 | ------------------- 10 | 11 | .. autofunction:: img2gb.generate_tileset 12 | 13 | 14 | Generating Tilemaps 15 | ------------------- 16 | 17 | .. autofunction:: img2gb.generate_tilemap 18 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to img2gb's documentation! 2 | ================================== 3 | 4 | img2gb generates GameBoy Tilesets and Tilemaps from standard image (PNG, 5 | JPEG,...). It converts the images into the GameBoy image format and generates 6 | C code (``.c`` and ``.h`` files) that can be used in GameBoy projects. 7 | 8 | .. figure:: ./_static/banner.png 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :caption: Contents: 13 | 14 | ./install.rst 15 | ./howto.rst 16 | ./cli.rst 17 | ./api/index.rst 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installing img2gb 2 | ================= 3 | 4 | From PYPI 5 | --------- 6 | 7 | Run the following command (as root):: 8 | 9 | pip install img2gb 10 | 11 | 12 | From Sources 13 | ------------ 14 | 15 | Clone this repository:: 16 | 17 | git clone https://github.com/flozz/img2gb.git 18 | 19 | Go the the project repository:: 20 | 21 | cd img2gb 22 | 23 | Then install the software (as root):: 24 | 25 | python setup.py install 26 | 27 | 28 | Windows Binary 29 | -------------- 30 | 31 | Download the latest ``img2gb.exe`` binary here: 32 | 33 | * https://github.com/flozz/img2gb/releases 34 | 35 | and use it from command line:: 36 | 37 | img2gb.exe -h 38 | 39 | -------------------------------------------------------------------------------- /RELEASE.rst: -------------------------------------------------------------------------------- 1 | Things to do while releasing a new version 2 | ========================================== 3 | 4 | This file is a memo for the maintainer. 5 | 6 | 7 | 1. Release 8 | ---------- 9 | 10 | * Update version number in ``setup.py`` 11 | * Update version number in ``img2gb/version.py`` 12 | * Edit / update changelog in ``README.rst`` 13 | * Commit / tag (``git commit -m vX.Y.Z && git tag vX.Y.Z && git push && git push --tags``) 14 | 15 | 16 | 2. Publish PyPI package 17 | ----------------------- 18 | 19 | Automated :) 20 | 21 | 22 | 3. Windows standalone version 23 | ----------------------------- 24 | 25 | Automated :) 26 | 27 | → Just download and unzip the artifact of the ``winbuild`` CI workflow. 28 | 29 | 30 | 4. Publish Github Release 31 | ------------------------- 32 | 33 | * Make a release on Github 34 | * Add changelog 35 | * Add Windows standalone executable 36 | -------------------------------------------------------------------------------- /.github/workflows/python-ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 14 | 15 | steps: 16 | 17 | - name: "Pull the repository" 18 | uses: actions/checkout@v6 19 | 20 | - name: "Set up Python ${{ matrix.python-version }}" 21 | uses: actions/setup-python@v6 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: "Install Nox" 26 | run: | 27 | pip3 install setuptools 28 | pip3 install nox 29 | 30 | - name: "Lint with Flake8 and Black" 31 | run: | 32 | python3 -m nox --session lint 33 | 34 | - name: "Test with pytest" 35 | run: | 36 | python3 -m nox --session test-${{ matrix.python-version }} 37 | -------------------------------------------------------------------------------- /.github/workflows/winbuild.yml: -------------------------------------------------------------------------------- 1 | name: "Windows Standalone Build" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+\\.[0-9]+\\.[0-9]+" 7 | - "v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+" 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | build: 14 | 15 | name: "Build Windows standalone version" 16 | runs-on: windows-latest 17 | 18 | steps: 19 | 20 | - name: "Checkout the repository" 21 | uses: actions/checkout@v6 22 | with: 23 | submodules: true 24 | 25 | - name: "Set up Python" 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: "3.14" 29 | 30 | - name: "Build img2gb Windows Standelone Version" 31 | shell: bash 32 | run: | 33 | ./scripts/build-windows.sh 34 | 35 | - name: "Archive Windows Build" 36 | uses: actions/upload-artifact@v6 37 | with: 38 | path: dist/img2gb-*.exe 39 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | PYTHON_FILES = [ 5 | "img2gb", 6 | "setup.py", 7 | "noxfile.py", 8 | "test", 9 | ] 10 | 11 | 12 | @nox.session(reuse_venv=True) 13 | def lint(session): 14 | session.install("flake8", "black") 15 | session.run("flake8", *PYTHON_FILES) 16 | session.run("black", "--check", "--diff", "--color", *PYTHON_FILES) 17 | 18 | 19 | @nox.session(reuse_venv=True) 20 | def black_fix(session): 21 | session.install("black") 22 | session.run("black", *PYTHON_FILES) 23 | 24 | 25 | @nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"], reuse_venv=True) 26 | def test(session): 27 | session.install("pytest") 28 | session.install("-e", ".") 29 | session.run("pytest", "-v", "test") 30 | 31 | 32 | @nox.session(reuse_venv=True) 33 | def gendoc(session): 34 | session.install("sphinx", "sphinx-rtd-theme") 35 | session.install("-e", ".") 36 | session.run("sphinx-build", "-M", "html", "doc", "build") 37 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: "Build and deploy Github pages" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | build-and-deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: "Checkout" 15 | uses: actions/checkout@v6 16 | with: 17 | persist-credentials: false 18 | 19 | - name: "Set up Python" 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: "3.14" 23 | 24 | - name: "Install Python dependencies" 25 | run: | 26 | pip3 install setuptools 27 | pip3 install nox 28 | 29 | - name: "Build Sphinx Doc" 30 | run: | 31 | nox --session gendoc 32 | 33 | - name: "Deploy Github Pages" 34 | uses: JamesIves/github-pages-deploy-action@v4.7.6 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BRANCH: gh-pages 38 | FOLDER: build/html 39 | -------------------------------------------------------------------------------- /img2gb/c_export.py: -------------------------------------------------------------------------------- 1 | _COMMENT = "// This file was generated by img2gb, DO NOT EDIT\n\n" 2 | 3 | 4 | def generate_c_file(code): 5 | """Generate the content of a C file that contains the given code. 6 | 7 | :param str code: The C code to embed in the C file. 8 | 9 | :rtype: str 10 | """ 11 | c = _COMMENT 12 | c += "#include \n\n" 13 | c += code 14 | c += "\n" 15 | return c 16 | 17 | 18 | def generate_c_header_file(code, filename="header.h"): 19 | """Generate the content of a C header file that contains the given code. 20 | 21 | :param str code: The C code to embed in the C header file. 22 | :param str filename: The header file name (default = ``"header.h"``) 23 | 24 | :rtype: str 25 | """ 26 | name = filename.replace(".", "_").upper() 27 | h = _COMMENT 28 | h += "#ifndef _%s\n" % name 29 | h += "#define _%s\n\n" % name 30 | h += code 31 | h += "\n\n" 32 | h += "\n#endif\n" 33 | return h 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 2 | argcomplete==3.1.2 3 | Babel==2.13.0 4 | black==24.3.0 5 | certifi==2024.7.4 6 | charset-normalizer==3.3.0 7 | click==8.1.7 8 | colorlog==6.7.0 9 | distlib==0.3.7 10 | docutils==0.18.1 11 | filelock==3.20.1 12 | flake8==6.1.0 13 | idna==3.7 14 | imagesize==1.4.1 15 | iniconfig==2.0.0 16 | Jinja2==3.1.6 17 | MarkupSafe==2.1.3 18 | mccabe==0.7.0 19 | mypy-extensions==1.0.0 20 | nox==2023.4.22 21 | packaging==23.2 22 | pathspec==0.11.2 23 | Pillow==10.3.0 24 | platformdirs==3.11.0 25 | pluggy==1.3.0 26 | pycodestyle==2.11.1 27 | pyflakes==3.1.0 28 | Pygments==2.16.1 29 | pytest==7.4.2 30 | requests==2.32.4 31 | snowballstemmer==2.2.0 32 | Sphinx==7.2.6 33 | sphinx-rtd-theme==1.3.0 34 | sphinxcontrib-applehelp==1.0.7 35 | sphinxcontrib-devhelp==1.0.5 36 | sphinxcontrib-htmlhelp==2.0.4 37 | sphinxcontrib-jquery==4.1 38 | sphinxcontrib-jsmath==1.0.1 39 | sphinxcontrib-qthelp==1.0.6 40 | sphinxcontrib-serializinghtml==1.1.9 41 | urllib3==2.6.0 42 | virtualenv==20.26.6 43 | -------------------------------------------------------------------------------- /img2gb/helpers.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | 3 | 4 | def rgba_brightness(r, g, b, a=None): 5 | if a is not None and a < 128: 6 | return 255 7 | return max(r, g, b) 8 | 9 | 10 | def brightness_to_color_id(brightness, invert=False): 11 | if brightness > 240: 12 | return 0 if not invert else 3 13 | if brightness < 15: 14 | return 3 if not invert else 0 15 | if brightness < 128: 16 | return 2 if not invert else 1 17 | return 1 if not invert else 2 18 | 19 | 20 | def to_pil_rgb_image(image): 21 | if image.mode == "RGB": 22 | return image 23 | 24 | image.load() 25 | rgb_image = Image.new("RGB", image.size, (0x00, 0x00, 0x00)) 26 | mask = None 27 | 28 | if image.mode == "RGBA": 29 | mask = image.split()[3] # bands: R=0, G=1, B=2, 1=3 30 | 31 | rgb_image.paste(image, mask=mask) 32 | 33 | return rgb_image 34 | 35 | 36 | def tileset_iterator(width, height, sprite8x16=False): 37 | for y in range(0, height, 16 if sprite8x16 else 8): 38 | for x in range(0, width, 8): 39 | for d in (0, 8) if sprite8x16 else (0,): 40 | yield x, y + d 41 | -------------------------------------------------------------------------------- /img2gb/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PIL import Image 4 | 5 | from .cli import parse_cli 6 | from . import generate_tileset, generate_tilemap 7 | 8 | 9 | def main(argv=sys.argv): 10 | args = parse_cli(argv[1:]) 11 | 12 | if args.subcommand == "tileset": 13 | images = [Image.open(image) for image in args.image] 14 | generate_tileset( 15 | images, 16 | output_c=args.output_c_file, 17 | output_h=args.output_header_file, 18 | output_image=args.output_image, 19 | output_binary=args.output_binary, 20 | name=args.name, 21 | dedup=args.deduplicate, 22 | alternative_palette=args.alternative_palette, 23 | sprite8x16=args.sprite8x16, 24 | ) 25 | elif args.subcommand == "tilemap": 26 | tileset = Image.open(args.tileset) 27 | tilemap = Image.open(args.tilemap) 28 | generate_tilemap( 29 | tileset, 30 | tilemap, 31 | output_c=args.output_c_file, 32 | output_h=args.output_header_file, 33 | output_binary=args.output_binary, 34 | name=args.name, 35 | offset=args.offset, 36 | missing=args.missing, 37 | replace=args.replace, 38 | ) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from setuptools import setup, find_packages 6 | 7 | 8 | long_description = "" 9 | if os.path.isfile("README.rst"): 10 | long_description = open("README.rst", "r", encoding="UTF-8").read() 11 | 12 | 13 | setup( 14 | name="img2gb", 15 | version="1.3.0", 16 | description="Converts images to GameBoy tileset", 17 | url="https://github.com/flozz/img2gb", 18 | project_urls={ 19 | "Source Code": "https://github.com/flozz/img2gb", 20 | "Issues": "https://github.com/flozz/img2gb/issues", 21 | "Chat": "https://discord.gg/P77sWhuSs4", 22 | "Donate": "https://github.com/flozz/img2gb#support-this-project", 23 | }, 24 | license="BSD-3-Clause", 25 | long_description=long_description, 26 | keywords="gb gameboy image tile tileset tilemap", 27 | author="Fabien LOISON", 28 | packages=find_packages(), 29 | install_requires=[ 30 | "pillow>=5.0.0", 31 | ], 32 | extras_require={ 33 | "dev": [ 34 | "nox", 35 | "flake8", 36 | "pytest", 37 | "black", 38 | "sphinx", 39 | "sphinx-rtd-theme", 40 | ] 41 | }, 42 | entry_points={ 43 | "console_scripts": [ 44 | "img2gb = img2gb.__main__:main", 45 | ], 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /.github/workflows/python-packages.yml: -------------------------------------------------------------------------------- 1 | name: "Build and Publish Python Packages" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+\\.[0-9]+\\.[0-9]+" 7 | - "v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+" 8 | 9 | jobs: 10 | 11 | build_sdist_wheel: 12 | 13 | name: "Source and wheel distribution" 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | 18 | - name: "Checkout the repository" 19 | uses: actions/checkout@v6 20 | 21 | - name: "Set up Python" 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: "3.14" 25 | 26 | - name: "Install Python build dependencies" 27 | run: | 28 | pip install setuptools wheel nox 29 | 30 | - name: "Build source distribution" 31 | run: | 32 | python setup.py sdist 33 | 34 | - name: "Build wheel" 35 | run: | 36 | python setup.py bdist_wheel 37 | 38 | - name: "Upload artifacts" 39 | uses: actions/upload-artifact@v6 40 | with: 41 | name: dist 42 | path: dist/ 43 | retention-days: 1 44 | 45 | publish_pypi: 46 | 47 | name: "Publish packages on PyPI" 48 | runs-on: ubuntu-latest 49 | needs: 50 | - build_sdist_wheel 51 | 52 | steps: 53 | 54 | - name: "Download artifacts" 55 | uses: actions/download-artifact@v7 56 | 57 | - name: "Publish packages on PyPI" 58 | uses: pypa/gh-action-pypi-publish@release/v1 59 | with: 60 | password: ${{ secrets.PYPI_API_TOKEN }} 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2022, Fabien LOISON 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Fabien LOISON nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /example/tilemap.c: -------------------------------------------------------------------------------- 1 | // This file was generated by img2gb, DO NOT EDIT 2 | 3 | #include 4 | 5 | const UINT8 TILEMAP[] = { 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x00, 0x00, 0x00, 0x00, 8 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x09, 0x0A, 0x00, 0x00, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x0B, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0E, 0x0F, 0x0A, 0x00, 0x00, 0x00, 0x00, 10 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x0B, 0x10, 0x11, 0x12, 0x00, 0x00, 0x13, 0x14, 0x15, 0x0F, 0x0A, 0x00, 0x00, 0x00, 0x00, 11 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x16, 0x17, 0x18, 0x19, 0x00, 0x00, 0x1A, 0x1B, 0x15, 0x0F, 0x0A, 0x00, 0x00, 0x00, 0x00, 12 | 0x00, 0x00, 0x1C, 0x1D, 0x1E, 0x0B, 0x0C, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x15, 0x0F, 0x21, 0x22, 0x23, 0x00, 0x00, 13 | 0x00, 0x00, 0x24, 0x25, 0x26, 0x0B, 0x0C, 0x00, 0x18, 0x27, 0x28, 0x29, 0x00, 0x15, 0x0F, 0x2A, 0x2B, 0x2C, 0x00, 0x00, 14 | 0x00, 0x00, 0x2D, 0x2E, 0x06, 0x0B, 0x2F, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x32, 0x0A, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x33, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x35, 0x0A, 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x38, 0x0A, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x39, 0x3A, 0x3B, 0x36, 0x36, 0x36, 0x36, 0x36, 0x3C, 0x3D, 0x0A, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x3E, 0x3A, 0x3F, 0x36, 0x36, 0x36, 0x40, 0x41, 0x42, 0x43, 0x0A, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x36, 0x44, 0x36, 0x36, 0x36, 0x36, 0x45, 0x46, 0x47, 0x48, 0x0A, 0x00, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x36, 0x36, 0x36, 0x49, 0x4A, 0x4B, 0x4C, 0x36, 0x4D, 0x4E, 0x0A, 0x00, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x36, 0x36, 0x36, 0x4F, 0x50, 0x51, 0x52, 0x36, 0x53, 0x54, 0x55, 0x00, 0x00, 0x00, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x56, 0x57, 0x58, 0x59, 0x57, 0x57, 0x57, 0x5A, 0x5B, 0x57, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x5E, 0x00, 0x00, 0x00, 0x5F, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | }; 25 | -------------------------------------------------------------------------------- /test/test_gbtile.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PIL import Image 3 | from img2gb.gbtile import GBTile 4 | 5 | 6 | class Test_GBTile(object): 7 | @pytest.fixture 8 | def image(self): 9 | return Image.open("./test/assets/tileset.png") 10 | 11 | @pytest.mark.parametrize( 12 | "x,result", 13 | [ 14 | (0, "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), 15 | (8, "FF 01 81 7F BD 7F A5 7B A5 7B BD 63 81 7F FF FF"), 16 | (16, "7E 00 81 7F 81 7F 81 7F 81 7F 81 7F 81 7F 7E 7E"), 17 | (24, "3C 00 54 2A A3 5F C1 3F 83 7F C5 3F 2A 7E 3C 3C"), 18 | (32, "04 04 04 04 0A 0A 12 12 66 00 99 77 99 77 66 66"), 19 | ], 20 | ) 21 | def test_from_image(self, image, x, result): 22 | tile = GBTile.from_image(image, x) 23 | assert tile.to_hex_string() == result 24 | 25 | def test_put_pixel(self): 26 | tile = GBTile() 27 | 28 | for b in tile.data: 29 | assert b == 0 30 | 31 | tile.put_pixel(0, 0, 3) 32 | 33 | assert tile.data[0] == 0x80 34 | assert tile.data[1] == 0x80 35 | 36 | tile.put_pixel(4, 0, 2) 37 | 38 | assert tile.data[0] == 0x80 39 | assert tile.data[1] == 0x88 40 | 41 | def test_get_pixel(self, image): 42 | tile = GBTile.from_image(image, 32) 43 | assert tile.get_pixel(0, 0) == 0b00 44 | assert tile.get_pixel(0, 6) == 0b01 45 | assert tile.get_pixel(2, 6) == 0b10 46 | assert tile.get_pixel(5, 0) == 0b11 47 | 48 | def test_to_hex_string(self): 49 | tile = GBTile() 50 | 51 | assert tile.to_hex_string() == "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" 52 | 53 | tile.put_pixel(0, 0, 3) 54 | tile.put_pixel(1, 0, 3) 55 | 56 | assert tile.to_hex_string() == "C0 C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00" 57 | 58 | def test_to_image(self, image): 59 | tile = GBTile.from_image(image, 32) 60 | tile_image = tile.to_image() 61 | assert tile_image.getpixel((0, 0)) == 0b00 62 | assert tile_image.getpixel((0, 6)) == 0b01 63 | assert tile_image.getpixel((2, 6)) == 0b10 64 | assert tile_image.getpixel((5, 0)) == 0b11 65 | 66 | def test_gbtile_equality(self): 67 | tile1 = GBTile() 68 | tile2 = GBTile() 69 | 70 | assert tile1 == tile2 71 | 72 | tile1.put_pixel(0, 0, 3) 73 | 74 | assert tile1 != tile2 75 | 76 | tile2.put_pixel(0, 0, 3) 77 | 78 | assert tile1 == tile2 79 | 80 | def test_data(self): 81 | tile = GBTile() 82 | assert len(tile.data) == 16 83 | assert tile.data[0] == 0x00 84 | assert tile.data[1] == 0x00 85 | tile.put_pixel(0, 0, 3) 86 | assert tile.data[0] == 0x80 87 | assert tile.data[1] == 0x80 88 | -------------------------------------------------------------------------------- /doc/cli.rst: -------------------------------------------------------------------------------- 1 | CLI 2 | === 3 | 4 | Main CLI 5 | -------- 6 | 7 | :: 8 | 9 | usage: img2gb [-h] [-V] {tileset,tilemap} ... 10 | 11 | Converts images to GameBoy tilesets and tilemaps 12 | 13 | positional arguments: 14 | {tileset,tilemap} 15 | tileset Generates GameBoy tilesets 16 | tilemap Generates GameBoy tilemaps 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | -V, --version show program's version number and exit 21 | 22 | 23 | Tileset CLI 24 | ----------- 25 | 26 | :: 27 | 28 | usage: img2gb tileset [-h] [-c FILE] [-H FILE] [-i FILE] [-d] [-a] [-n NAME] 29 | image [image ...] 30 | 31 | positional arguments: 32 | image input image file 33 | 34 | optional arguments: 35 | -h, --help show this help message and exit 36 | -c FILE, --output-c-file FILE 37 | output C file 38 | -H FILE, --output-header-file FILE 39 | output C header file 40 | -i FILE, --output-image FILE 41 | output image file representing the tileset (required 42 | to generate a tilemap) 43 | -b FILE, --output-binary FILE 44 | output binary file 45 | -d, --deduplicate remove duplicated tiles from the tileset 46 | -a, --alternative-palette 47 | invert the colors to allow tiles to be used with the 48 | sprites alternative palette 49 | -s, --sprite8x16 Rearrange the tiles to be used in 8x16 sprites 50 | -n NAME, --name NAME name of the tileset (used for variable names in 51 | generated code, default=TILESET) 52 | 53 | Tilemap CLI 54 | ----------- 55 | 56 | :: 57 | 58 | usage: img2gb tilemap [-h] [-c FILE] [-H FILE] [-o OFFSET] 59 | [-m {error,replace}] [-r TILE_ID] [-n NAME] 60 | tileset tilemap 61 | 62 | positional arguments: 63 | tileset the tileset (generated by img2gb tileset -i) 64 | tilemap an image representing the tilemap 65 | 66 | optional arguments: 67 | -h, --help show this help message and exit 68 | -c FILE, --output-c-file FILE 69 | output C file 70 | -H FILE, --output-header-file FILE 71 | output C header file 72 | -b FILE, --output-binary FILE 73 | output binary file 74 | -o OFFSET, --offset OFFSET 75 | offset of the tileset in the video memory (default = 76 | 0) 77 | -m {error,replace}, --missing {error,replace} 78 | action to do when a tile of the tilemap is missing 79 | from the tileset (default = error) 80 | -r TILE_ID, --replace TILE_ID 81 | replace missing tiles by the given one when 82 | --missing=replace 83 | -n NAME, --name NAME name of the tileset (used for variable names in 84 | generated code, default=TILEMAP) 85 | -------------------------------------------------------------------------------- /img2gb/gbtile.py: -------------------------------------------------------------------------------- 1 | """ 2 | The :class:`GBTile` class represents a single GameBoy tile (8x8 pixels, 16 3 | Bytes). 4 | 5 | Creating a tile from scratch:: 6 | 7 | from img2gb import GBTile 8 | 9 | tile = GBTile() 10 | tile.put_pixel(0, 0, 3) # Put a black pixel at (0, 0) 11 | tile.data # -> [128, 128, 0, 0, 0, ...] 12 | tile.to_hex_string() # -> "80 80 00 00 00 ..." 13 | 14 | Creating a tile from a PIL image:: 15 | 16 | from img2gb import GBTile 17 | from PIL import Image 18 | 19 | image = Image.open("./my_tile.png") 20 | tile = GBTile.from_image(image) 21 | """ 22 | 23 | from PIL import Image 24 | 25 | from .helpers import to_pil_rgb_image, rgba_brightness, brightness_to_color_id 26 | 27 | 28 | class GBTile(object): 29 | """Stores and manipulates data of a single GameBoy tile (8x8 pixels).""" 30 | 31 | @classmethod 32 | def from_image(Cls, pil_image, tile_x=0, tile_y=0, alternative_palette=False): 33 | """Create a new GBTile from the given image. 34 | 35 | :param PIL.Image.Image pil_image: The input PIL (or Pillow) image. 36 | :param int tile_x: The x location of the tile in the image (default = 37 | ``0``). 38 | :param int tile_y: The y location of the tile in the image (default = 39 | ``0``). 40 | :param bool alternative_palette: Use the sprite's alternative palette 41 | (inverted colors, default = ``False``). 42 | 43 | :rtype: GBTile 44 | """ 45 | image = to_pil_rgb_image(pil_image) 46 | tile = Cls() 47 | 48 | for y in range(8): 49 | for x in range(8): 50 | pix_rgb = image.getpixel((tile_x + x, tile_y + y)) 51 | pix_brightness = rgba_brightness(*pix_rgb) 52 | color_id = brightness_to_color_id( 53 | pix_brightness, invert=alternative_palette 54 | ) 55 | tile.put_pixel(x, y, color_id) 56 | 57 | return tile 58 | 59 | def __init__(self): 60 | self._data = [0x00] * 16 61 | 62 | @property 63 | def data(self): 64 | """Raw data of the tile. 65 | 66 | :type: list of int 67 | """ 68 | return self._data 69 | 70 | def put_pixel(self, x, y, color_id): 71 | """Set the color of one of the tile's pixels. 72 | 73 | :param int x: The x coordinate of the pixel to change (0-7). 74 | :param int y: The y coordinate of the pixel to change (0-7). 75 | :param int color_id: the color of the pixel (0-3). 76 | """ 77 | mask = 0b00000001 << (7 - x) 78 | mask1 = mask if color_id & 0b01 else 0b00000000 79 | mask2 = mask if color_id & 0b10 else 0b00000000 80 | 81 | # Clear changing bits 82 | self._data[y * 2 + 0] &= ~mask 83 | self._data[y * 2 + 1] &= ~mask 84 | 85 | # Set bits 86 | self._data[y * 2 + 0] |= mask1 87 | self._data[y * 2 + 1] |= mask2 88 | 89 | def get_pixel(self, x, y): 90 | """Returns the color id of a pixel of the tile. 91 | 92 | :param int x: The x coordinate of the pixel (0-7). 93 | :param int y: The y coordinate of the pixel (0-7). 94 | :rtype: int 95 | """ 96 | mask = 0b00000001 << (7 - x) 97 | lbit = (self._data[y * 2 + 0] & mask) >> (7 - x) 98 | hbit = (self._data[y * 2 + 1] & mask) >> (7 - x) 99 | return hbit * 0b10 + lbit 100 | 101 | def to_hex_string(self): 102 | """Returns the tile as an hexadecimal-encoded string. 103 | 104 | :rtype: str 105 | 106 | e.g.:: 107 | 108 | "04 04 04 04 0A 0A 12 12 66 00 99 77 99 77 66 66" 109 | """ 110 | return " ".join(["%02X" % b for b in self._data]) 111 | 112 | def to_image(self): 113 | """Generates a PIL image from the tile. The generated image is an 114 | indexed image with a 4 shades of gray palette. 115 | 116 | :rtype: PIL.Image.Image 117 | """ 118 | image = Image.new("P", (8, 8), 0) 119 | image.putpalette( 120 | [ 121 | # fmt: off 122 | 0xFF, 0xFF, 0xFF, 123 | 0xBB, 0xBB, 0xBB, 124 | 0x55, 0x55, 0x55, 125 | 0x00, 0x00, 0x00, 126 | # fmt: on 127 | ] 128 | ) 129 | for y in range(8): 130 | for x in range(8): 131 | image.putpixel((x, y), self.get_pixel(x, y)) 132 | return image 133 | 134 | def __eq__(self, other): 135 | if not isinstance(other, GBTile): 136 | return False 137 | return self.to_hex_string() == other.to_hex_string() 138 | -------------------------------------------------------------------------------- /img2gb/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from .version import VERSION 4 | 5 | 6 | def generate_tileset_cli(parser): 7 | # Positional 8 | parser.add_argument( 9 | "image", type=argparse.FileType("rb"), nargs="+", help="input image file" 10 | ) 11 | # Options 12 | parser.add_argument( 13 | "-c", 14 | "--output-c-file", 15 | type=argparse.FileType("w"), 16 | metavar="FILE", 17 | help="output C file", 18 | ) 19 | parser.add_argument( 20 | "-H", 21 | "--output-header-file", 22 | type=argparse.FileType("w"), 23 | metavar="FILE", 24 | help="output C header file", 25 | ) 26 | parser.add_argument( 27 | "-i", 28 | "--output-image", 29 | type=argparse.FileType("wb"), 30 | metavar="FILE", 31 | help="output image file representing the tileset (required to generate a tilemap)", 32 | ) 33 | parser.add_argument( 34 | "-b", 35 | "--output-binary", 36 | type=argparse.FileType("wb"), 37 | metavar="FILE", 38 | help="output binary file", 39 | ) 40 | parser.add_argument( 41 | "-d", 42 | "--deduplicate", 43 | action="store_true", 44 | default=False, 45 | help="remove duplicated tiles from the tileset", 46 | ) 47 | parser.add_argument( 48 | "-a", 49 | "--alternative-palette", 50 | action="store_true", 51 | default=False, 52 | help="invert the colors", 53 | ) 54 | parser.add_argument( 55 | "-s", 56 | "--sprite8x16", 57 | action="store_true", 58 | default=False, 59 | help="Rearrange the tiles to be used in 8x16 sprites", 60 | ) 61 | parser.add_argument( 62 | "-n", 63 | "--name", 64 | type=str, 65 | default="TILESET", 66 | help="name of the tileset (used for variable names in generated code, default=TILESET)", 67 | ) 68 | 69 | 70 | def generate_tilemap_cli(parser): 71 | # Positional 72 | parser.add_argument( 73 | "tileset", 74 | type=argparse.FileType("rb"), 75 | help="the tileset (generated by img2gb tileset -i)", 76 | ) 77 | parser.add_argument( 78 | "tilemap", 79 | type=argparse.FileType("rb"), 80 | help="an image representing the tilemap", 81 | ) 82 | # Options 83 | parser.add_argument( 84 | "-c", 85 | "--output-c-file", 86 | type=argparse.FileType("w"), 87 | metavar="FILE", 88 | help="output C file", 89 | ) 90 | parser.add_argument( 91 | "-H", 92 | "--output-header-file", 93 | type=argparse.FileType("w"), 94 | metavar="FILE", 95 | help="output C header file", 96 | ) 97 | parser.add_argument( 98 | "-b", 99 | "--output-binary", 100 | type=argparse.FileType("wb"), 101 | metavar="FILE", 102 | help="output binary file", 103 | ) 104 | parser.add_argument( 105 | "-o", 106 | "--offset", 107 | type=int, 108 | default=0, 109 | help="offset of the tileset in the video memory (default = 0)", 110 | ) 111 | parser.add_argument( 112 | "-m", 113 | "--missing", 114 | choices=["error", "replace"], 115 | default="error", 116 | help="action to do when a tile of the tilemap is missing from the tileset (default = error)", 117 | ) 118 | parser.add_argument( 119 | "-r", 120 | "--replace", 121 | type=int, 122 | metavar="TILE_ID", 123 | default=0, 124 | help="replace missing tiles by the given one when --missing=replace", 125 | ) 126 | parser.add_argument( 127 | "-n", 128 | "--name", 129 | type=str, 130 | default="TILEMAP", 131 | help="name of the tileset (used for variable names in generated code, default=TILEMAP)", 132 | ) 133 | 134 | 135 | def generate_cli(): 136 | parser = argparse.ArgumentParser( 137 | prog="img2gb", description="Converts images to GameBoy tilesets and tilemaps" 138 | ) 139 | 140 | parser.add_argument("-V", "--version", action="version", version=VERSION) 141 | 142 | subparsers = parser.add_subparsers(dest="subcommand") 143 | 144 | tileset_parser = subparsers.add_parser("tileset", help="Generates GameBoy tilesets") 145 | generate_tileset_cli(tileset_parser) 146 | 147 | tilemap_parser = subparsers.add_parser("tilemap", help="Generates GameBoy tilemaps") 148 | generate_tilemap_cli(tilemap_parser) 149 | 150 | return parser 151 | 152 | 153 | def parse_cli(args): 154 | parser = generate_cli() 155 | return parser.parse_args(args) 156 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | img2gb - Converts Images to GameBoy Tileset and Tilemap 2 | ======================================================= 3 | 4 | |GitHub| |Lint and Tests| |PYPI Version| |License| |Discord| |Black| 5 | 6 | img2gb generates GameBoy Tilesets and Tilemaps from standard image (PNG, 7 | JPEG,...). It converts the images into the GameBoy image format and 8 | generates C code (``.c`` and ``.h`` files) that can be used in GameBoy 9 | projects. 10 | 11 | .. image:: ./doc/_static/banner.png 12 | 13 | * Documentation: https://flozz.github.io/img2gb/ 14 | * HowTo: https://flozz.github.io/img2gb/howto.html 15 | 16 | 17 | Dependencies 18 | ------------ 19 | 20 | * Python >= 3.10 21 | * Pillow >= 5.0 22 | 23 | 24 | Install 25 | ------- 26 | 27 | * See https://flozz.github.io/img2gb/install.html 28 | 29 | 30 | Usage 31 | ----- 32 | 33 | * See https://flozz.github.io/img2gb/cli.html 34 | 35 | 36 | Hacking 37 | ------- 38 | 39 | Setup 40 | ~~~~~ 41 | 42 | To work on img2gb first create a virtualenv:: 43 | 44 | python3 -m venv __env__ 45 | 46 | and activate it:: 47 | 48 | source __env__/bin/activate 49 | 50 | Then install the project with all dev dependencies:: 51 | 52 | pip install -e .[dev] 53 | 54 | 55 | Commands 56 | ~~~~~~~~ 57 | 58 | You can lint the code and check coding style with:: 59 | 60 | nox -s lint 61 | 62 | You can fix coding style using Black with:: 63 | 64 | nox -s black_fix 65 | 66 | You can run test on all supported Python versions or on a specific Python 67 | version with:: 68 | 69 | nox -s test # Run on all Python version 70 | 71 | nox -s test-3.10 # Run on Python 3.10 72 | nox -s test-3.11 # Run on Python 3.11 73 | nox -s test-3.12 # Run on Python 3.12 74 | nox -s test-3.12 # Run on Python 3.13 75 | nox -s test-3.12 # Run on Python 3.14 76 | 77 | And you can build the documentation with (result in ``build/html/``):: 78 | 79 | nox -s gendoc 80 | 81 | 82 | Links 83 | ----- 84 | 85 | * Examples of GameBoy programs that uses img2gb for graphics: 86 | * https://github.com/flozz/gameboy-examples/tree/master/05-graphics2 87 | * https://github.com/flozz/gameboy-examples/tree/master/06-graphics3-background 88 | * Article about the tile encoding and img2gb: https://blog.flozz.fr/2018/11/19/developpement-gameboy-5-creer-des-tilesets/ (French) 89 | 90 | 91 | Support this project 92 | -------------------- 93 | 94 | Want to support this project? 95 | 96 | * `☕️ Buy me a coffee `__ 97 | * `💵️ Give me a tip on PayPal `__ 98 | * `❤️ Sponsor me on GitHub `__ 99 | 100 | 101 | Changelog 102 | --------- 103 | 104 | * **[NEXT]** (changes on ``master``, but not released yet): 105 | 106 | * misc: Added Python 3.14 support (@flozz) 107 | * misc!: Removed Python 3.9 support (@flozz) 108 | 109 | * **v1.3.0:** 110 | 111 | * feat: Added binary export of tilesets and tilemaps (@duysqubix, #44) 112 | * misc: Added Python 3.13 support (@flozz) 113 | * misc!: Removed Python 3.8 support (@flozz) 114 | 115 | * **v1.2.0:** 116 | 117 | * fix: Fixed wrong version displayed (@flozz, #3) 118 | * chore: Added Python 3.11 and 3.12 support 119 | * chore!: Removed Python 2.7 and 3.7 support 120 | 121 | * **v1.1.0:** 122 | 123 | * Removes arbitrary size limit for tilmaps 124 | * Implements ``offset`` option (#2) 125 | 126 | * **v1.0.0:** 127 | 128 | * Refacto of the Python API, with new high-level fuction to be easier to use 129 | * Refacto of the CLI: now tileset and tilemap are generated separately, this allow more options for both and covers more usecases. 130 | * New option to handle alternative palette 131 | * New option to handle 8x16px sprites 132 | * Documentation 133 | * Unit test (everything is not coverd but it is better than nothing :)) 134 | 135 | * **v0.10.0:** Adds non-RGB image support (indexed images,...) 136 | * **v0.9.1:** Fixes an issue with Python 3 137 | * **v0.9.0:** Initial release (generates tiles, tilesets and tilemaps) 138 | 139 | 140 | .. |GitHub| image:: https://img.shields.io/github/stars/flozz/img2gb?label=GitHub&logo=github 141 | :target: https://github.com/flozz/img2gb 142 | 143 | .. |Lint and Tests| image:: https://github.com/flozz/img2gb/actions/workflows/python-ci.yml/badge.svg 144 | :target: https://github.com/flozz/img2gb/actions 145 | 146 | .. |PYPI Version| image:: https://img.shields.io/pypi/v/img2gb.svg 147 | :target: https://pypi.python.org/pypi/img2gb 148 | 149 | .. |License| image:: https://img.shields.io/pypi/l/img2gb.svg 150 | :target: https://github.com/flozz/img2gb/blob/master/LICENSE 151 | 152 | .. |Discord| image:: https://img.shields.io/badge/chat-Discord-8c9eff?logo=discord&logoColor=ffffff 153 | :target: https://discord.gg/P77sWhuSs4 154 | 155 | .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 156 | :target: https://black.readthedocs.io/en/stable 157 | -------------------------------------------------------------------------------- /test/test_gbtilemap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PIL import Image 3 | from img2gb.gbtile import GBTile 4 | from img2gb.gbtileset import GBTileset 5 | from img2gb.gbtilemap import GBTilemap 6 | 7 | 8 | class Test_GBTilemap(object): 9 | @pytest.fixture 10 | def image(self): 11 | return Image.open("./test/assets/tilemap.png") 12 | 13 | @pytest.fixture 14 | def tileset(self): 15 | image = Image.open("./test/assets/tileset.png") 16 | return GBTileset.from_image(image) 17 | 18 | @pytest.fixture 19 | def tile(self, image): 20 | return GBTile.from_image(image, 8, 0) 21 | 22 | def test_from_image(self, image): 23 | tilemap = GBTilemap.from_image(image) 24 | assert tilemap.tileset.length == 4 25 | assert tilemap.width == 8 26 | assert tilemap.height == 8 27 | 28 | def test_put_tile(self, tileset): 29 | tilemap = GBTilemap(gbtileset=tileset) 30 | assert tilemap.data[0] == 0 31 | tilemap.put_tile(0, 0, tileset.tiles[4]) 32 | assert tilemap.data[0] == 4 33 | 34 | def test_put_tile_with_offset(self, tileset): 35 | tileset.offset = 10 36 | tilemap = GBTilemap(gbtileset=tileset) 37 | assert tilemap.data[0] == 0 38 | tilemap.put_tile(0, 0, tileset.tiles[4]) 39 | assert tilemap.data[0] == 14 40 | 41 | @pytest.mark.parametrize( 42 | "x,y", 43 | [ 44 | (-1, 0), 45 | (0, -1), 46 | (8, 0), 47 | (0, 8), 48 | ], 49 | ) 50 | def test_put_tile_with_out_of_map_coord(self, tile, x, y): 51 | tilemap = GBTilemap(width=8, height=8) 52 | with pytest.raises(ValueError): 53 | tilemap.put_tile(x, y, tile) 54 | 55 | def test_put_tile_with_missing_append(self, tile): 56 | tilemap = GBTilemap() 57 | tilemap.put_tile(0, 0, tile, missing="append") 58 | assert tilemap.tileset.length == 1 59 | 60 | def test_put_tile_with_missing_error(self, tile): 61 | tilemap = GBTilemap() 62 | with pytest.raises(ValueError): 63 | tilemap.put_tile(0, 0, tile, missing="error") 64 | 65 | def test_put_tile_with_missing_replace(self, tile): 66 | tilemap = GBTilemap() 67 | tilemap.put_tile(0, 0, tile, missing="replace", replace=42) 68 | assert tilemap.data[0] == 42 69 | 70 | def test_put_tile_with_missing_set_to_wrong_value(self, tile): 71 | tilemap = GBTilemap() 72 | with pytest.raises(ValueError): 73 | tilemap.put_tile(0, 0, tile, missing="foo") 74 | 75 | def test_put_tile_with_missing_append_and_dedup_true(self, tile): 76 | tilemap = GBTilemap() 77 | tilemap.put_tile(0, 0, tile, missing="append", dedup=True) 78 | tilemap.put_tile(0, 0, tile, missing="append", dedup=True) 79 | assert tilemap.tileset.length == 1 80 | 81 | def test_put_tile_with_missing_append_and_dedup_false(self, tile): 82 | tilemap = GBTilemap() 83 | tilemap.put_tile(0, 0, tile, missing="append", dedup=False) 84 | tilemap.put_tile(0, 0, tile, missing="append", dedup=False) 85 | assert tilemap.tileset.length == 2 86 | 87 | def test_to_hex_string(self, tileset): 88 | tilemap = GBTilemap(width=2, height=2) 89 | tilemap.put_tile(0, 0, tileset.tiles[0]) 90 | tilemap.put_tile(1, 0, tileset.tiles[1]) 91 | tilemap.put_tile(0, 1, tileset.tiles[2]) 92 | tilemap.put_tile(1, 1, tileset.tiles[3]) 93 | result = "" 94 | result += "00 01\n" 95 | result += "02 03" 96 | assert tilemap.to_hex_string() == result 97 | 98 | def test_to_c_string(self): 99 | tilemap = GBTilemap(width=4, height=4) 100 | result = "const UINT8 TILEMAP[] = {\n" 101 | result += " 0x00, 0x00, 0x00, 0x00,\n" 102 | result += " 0x00, 0x00, 0x00, 0x00,\n" 103 | result += " 0x00, 0x00, 0x00, 0x00,\n" 104 | result += " 0x00, 0x00, 0x00, 0x00,\n" 105 | result += "};" 106 | assert tilemap.to_c_string() == result 107 | 108 | def test_to_c_string_with_custom_name(self): 109 | tilemap = GBTilemap(width=4, height=4) 110 | result = "const UINT8 FOO[] = {\n" 111 | result += " 0x00, 0x00, 0x00, 0x00,\n" 112 | result += " 0x00, 0x00, 0x00, 0x00,\n" 113 | result += " 0x00, 0x00, 0x00, 0x00,\n" 114 | result += " 0x00, 0x00, 0x00, 0x00,\n" 115 | result += "};" 116 | assert tilemap.to_c_string(name="Foo") == result 117 | 118 | def test_to_c_header_string(self): 119 | tilemap = GBTilemap(width=4, height=4) 120 | result = "extern const UINT8 TILEMAP[];\n" 121 | result += "#define TILEMAP_WIDTH 4\n" 122 | result += "#define TILEMAP_HEIGHT 4" 123 | assert tilemap.to_c_header_string() == result 124 | 125 | def test_to_c_header_string_with_custom_name(self): 126 | tilemap = GBTilemap(width=4, height=4) 127 | result = "extern const UINT8 FOO[];\n" 128 | result += "#define FOO_WIDTH 4\n" 129 | result += "#define FOO_HEIGHT 4" 130 | assert tilemap.to_c_header_string(name="Foo") == result 131 | 132 | def test_tileset(self): 133 | tilemap = GBTilemap() 134 | assert type(tilemap.tileset) is GBTileset 135 | 136 | def test_width(self): 137 | tilemap = GBTilemap(width=2, height=3) 138 | assert tilemap.width == 2 139 | 140 | def test_height(self): 141 | tilemap = GBTilemap(width=2, height=3) 142 | assert tilemap.height == 3 143 | 144 | def test_size(self): 145 | tilemap = GBTilemap(width=2, height=3) 146 | assert tilemap.size == (2, 3) 147 | 148 | def test_data(self): 149 | tilemap = GBTilemap(width=2, height=2) 150 | assert type(tilemap.data) is list 151 | assert tilemap.data[0] == 0 152 | assert tilemap.data[1] == 0 153 | assert tilemap.data[2] == 0 154 | assert tilemap.data[3] == 0 155 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'img2gb' 23 | copyright = u'2018, Fabien LOISON' 24 | author = u'Fabien LOISON' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.githubpages', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = [] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = None 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'sphinx_rtd_theme' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | # html_theme_options = {} 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | # Custom sidebar templates, must be a dictionary that maps document names 94 | # to template names. 95 | # 96 | # The default sidebars (for documents that don't match any pattern) are 97 | # defined by theme itself. Builtin themes are using these templates by 98 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 99 | # 'searchbox.html']``. 100 | # 101 | # html_sidebars = {} 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'img2gbdoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'img2gb.tex', u'img2gb Documentation', 135 | u'Fabien LOISON', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output ------------------------------------------ 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'img2gb', u'img2gb Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'img2gb', u'img2gb Documentation', 156 | author, 'img2gb', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | # -- Options for Epub output ------------------------------------------------- 162 | 163 | # Bibliographic Dublin Core info. 164 | epub_title = project 165 | 166 | # The unique identifier of the text. This can be a ISBN number 167 | # or the project homepage. 168 | # 169 | # epub_identifier = '' 170 | 171 | # A unique identification for the text. 172 | # 173 | # epub_uid = '' 174 | 175 | # A list of files that should not be packed into the epub file. 176 | epub_exclude_files = ['search.html'] 177 | 178 | 179 | # -- Extension configuration ------------------------------------------------- 180 | -------------------------------------------------------------------------------- /test/test_gbtileset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from PIL import Image 3 | from img2gb.gbtile import GBTile 4 | from img2gb.gbtileset import GBTileset 5 | 6 | 7 | class Test_GBTileset(object): 8 | @pytest.fixture 9 | def image(self): 10 | return Image.open("./test/assets/tileset.png") 11 | 12 | @pytest.fixture 13 | def image2(self): 14 | return Image.open("./test/assets/tilemap.png") 15 | 16 | @pytest.fixture 17 | def tile1(self): 18 | return GBTile() 19 | 20 | @pytest.fixture 21 | def tile2(self): 22 | tile = GBTile() 23 | tile.put_pixel(0, 0, 3) 24 | return tile 25 | 26 | def test_from_image(self, image): 27 | tileset = GBTileset.from_image(image) 28 | result = "" 29 | result += "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n" 30 | result += "FF 01 81 7F BD 7F A5 7B A5 7B BD 63 81 7F FF FF\n" 31 | result += "7E 00 81 7F 81 7F 81 7F 81 7F 81 7F 81 7F 7E 7E\n" 32 | result += "3C 00 54 2A A3 5F C1 3F 83 7F C5 3F 2A 7E 3C 3C\n" 33 | result += "04 04 04 04 0A 0A 12 12 66 00 99 77 99 77 66 66" 34 | assert tileset.to_hex_string() == result 35 | 36 | @pytest.mark.parametrize("dedup,count", [(False, 64), (True, 4)]) 37 | def test_from_image_dedup(self, image2, dedup, count): 38 | tileset = GBTileset.from_image(image2, dedup=dedup) 39 | assert tileset.length == count 40 | 41 | def test_add_tile(self, tile1): 42 | tileset = GBTileset() 43 | assert tileset.add_tile(tile1) == 0 44 | assert tileset.add_tile(tile1) == 1 45 | 46 | def test_add_tile_with_offset(self, tile1): 47 | tileset = GBTileset(offset=10) 48 | assert tileset.add_tile(tile1) == 10 49 | assert tileset.add_tile(tile1) == 11 50 | 51 | def test_add_tile_with_deduplication(self, tile1, tile2): 52 | tileset = GBTileset() 53 | assert tileset.add_tile(tile1, dedup=True) == 0 54 | assert tileset.add_tile(tile1, dedup=True) == 0 55 | assert tileset.add_tile(tile2, dedup=True) == 1 56 | 57 | def test_to_hex_string(self, tile2): 58 | tileset = GBTileset() 59 | assert tileset.to_hex_string() == "" 60 | tileset.add_tile(tile2) 61 | assert ( 62 | tileset.to_hex_string() == "80 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00" 63 | ) 64 | 65 | def test_length(self, tile1): 66 | tileset = GBTileset() 67 | assert tileset.length == 0 68 | tileset.add_tile(tile1) 69 | assert tileset.length == 1 70 | tileset.add_tile(tile1) 71 | assert tileset.length == 2 72 | 73 | def test_data(self, tile1): 74 | tileset = GBTileset() 75 | assert len(tileset.data) == 0 76 | tileset.add_tile(tile1) 77 | assert len(tileset.data) == 16 78 | tileset.add_tile(tile1) 79 | assert len(tileset.data) == 32 80 | 81 | def test_tiles(self, tile1): 82 | tileset = GBTileset() 83 | assert len(tileset.tiles) == 0 84 | tileset.add_tile(tile1) 85 | assert len(tileset.tiles) == 1 86 | assert tileset.tiles[0] == tile1 87 | tileset.add_tile(tile1) 88 | assert len(tileset.tiles) == 2 89 | assert tileset.tiles[1] == tile1 90 | 91 | def test_to_c_string(self, image): 92 | tileset = GBTileset.from_image(image) 93 | result = "const UINT8 TILESET[] = {\n" 94 | result += " 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n" 95 | result += " 0xFF, 0x01, 0x81, 0x7F, 0xBD, 0x7F, 0xA5, 0x7B, 0xA5, 0x7B, 0xBD, 0x63, 0x81, 0x7F, 0xFF, 0xFF,\n" 96 | result += " 0x7E, 0x00, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x7E, 0x7E,\n" 97 | result += " 0x3C, 0x00, 0x54, 0x2A, 0xA3, 0x5F, 0xC1, 0x3F, 0x83, 0x7F, 0xC5, 0x3F, 0x2A, 0x7E, 0x3C, 0x3C,\n" 98 | result += " 0x04, 0x04, 0x04, 0x04, 0x0A, 0x0A, 0x12, 0x12, 0x66, 0x00, 0x99, 0x77, 0x99, 0x77, 0x66, 0x66,\n" 99 | result += "};" 100 | assert tileset.to_c_string() == result 101 | 102 | def test_to_c_string_with_custom_name(self): 103 | tileset = GBTileset() 104 | assert tileset.to_c_string(name="Foo") == "const UINT8 FOO[] = {\n};" 105 | 106 | def test_to_c_header_string(self, image): 107 | tileset = GBTileset.from_image(image) 108 | result = "" 109 | result += "extern const UINT8 TILESET[];\n" 110 | result += "#define TILESET_TILE_COUNT 5" 111 | assert tileset.to_c_header_string() == result 112 | 113 | def test_to_image(self, image2): 114 | tileset = GBTileset.from_image(image2, dedup=True) 115 | tileset_image = tileset.to_image() 116 | assert tileset_image.width == 32 117 | assert tileset_image.height == 8 118 | 119 | def test_to_c_header_string_with_custom_name(self, image): 120 | tileset = GBTileset.from_image(image) 121 | result = "" 122 | result += "extern const UINT8 FOO[];\n" 123 | result += "#define FOO_TILE_COUNT 5" 124 | assert tileset.to_c_header_string(name="Foo") == result 125 | 126 | def test_merge(self, image): 127 | tileset1 = GBTileset.from_image(image) 128 | tileset2 = GBTileset.from_image(image) 129 | tileset1.merge(tileset2) 130 | assert tileset1.length == 10 131 | 132 | def test_merge_with_dedup(self, image): 133 | tileset1 = GBTileset.from_image(image) 134 | tileset2 = GBTileset.from_image(image) 135 | tileset1.merge(tileset2, dedup=True) 136 | assert tileset1.length == 5 137 | 138 | def test_index(self, tile1): 139 | tileset = GBTileset() 140 | tileset.add_tile(tile1) 141 | assert tileset.index(tile1) == 0 142 | 143 | def test_index_with_offset(self, tile1): 144 | tileset = GBTileset(offset=10) 145 | tileset.add_tile(tile1) 146 | assert tileset.index(tile1) == 10 147 | 148 | def test_offset(self, tile1): 149 | tileset = GBTileset() 150 | tileset.add_tile(tile1) 151 | assert tileset.index(tile1) == 0 152 | tileset.offset = 10 153 | assert tileset.index(tile1) == 10 154 | -------------------------------------------------------------------------------- /doc/howto.rst: -------------------------------------------------------------------------------- 1 | Howto Generate a Tileset and a Tilemap (CLI) 2 | ============================================ 3 | 4 | In this *howtow*, I will explain how to convert the image bellow to a GameBoy 5 | Tileset and a Tilemap that will be usable in a program to display the image on 6 | a real GameBoy (or on an emulator). 7 | 8 | The image we want to display in our GameBoy program: 9 | 10 | .. figure:: _static/img.png 11 | :alt: Example image to covert 12 | 13 | 14 | Generating a Tileset 15 | -------------------- 16 | 17 | First we have to convert the image into a **GameBoy tileset**... but what is 18 | a tileset ? To be displayed on a GameBoy, an image must be cut into 8x8 pixel 19 | pieces called **tiles**. A tileset is just... a set of tiles. 20 | 21 | To generate the tileset, the command to use is the following:: 22 | 23 | img2gb tileset \ 24 | --output-c-file=tileset.c \ 25 | --output-header-file=tileset.h \ 26 | --output-image=tileset.png \ 27 | --deduplicate \ 28 | img.png 29 | 30 | .. NOTE:: 31 | 32 | For readability reason, I wrote the command on multiple lines using 33 | backslashes... you can of course write it on one line :) 34 | 35 | Let's explain the above command line: 36 | 37 | * ``tileset`` tells img2gb that we whant to generates a tileset, 38 | * ``--output-c-file=tileset.c`` is the path of the output ``.c`` file that will 39 | contain the data of the tiles, 40 | * ``--output-header-file=tileset.h`` is the path of the output ``.h`` file that 41 | will contain information about the tileset (variable declaration and number 42 | of tiles), 43 | * ``--output-image=tileset.png`` is the path of the image that represents the 44 | tileset. It is useful to view what is in the ``.c`` file in a more readable 45 | way. This image is also mandatory yo generate a tilemap. 46 | * ``--deduplicate`` avoids img2gb to include duplicate tiles (in our example, 47 | this is mandatory as the input image is composed of 360 tiles and the video 48 | memory of the GameBoy can contain only 255 tiles...), 49 | * ``img.png`` is the path of the input image. 50 | 51 | Now let's see the result of the above command: 52 | 53 | * `tileset.c (truncated) `_: 54 | 55 | .. code-block:: c 56 | 57 | // This file was generated by img2gb, DO NOT EDIT 58 | 59 | #include 60 | 61 | const UINT8 TILESET[] = { 62 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, // ... 63 | }; 64 | 65 | * `tileset.h `_: 66 | 67 | .. code-block:: c 68 | 69 | // This file was generated by img2gb, DO NOT EDIT 70 | 71 | #ifndef _TILESET_H 72 | #define _TILESET_H 73 | 74 | extern const UINT8 TILESET[]; 75 | #define TILESET_TILE_COUNT 97 76 | 77 | 78 | #endif 79 | 80 | * `tileset.png `_: 81 | 82 | .. figure:: _static/tileset.png 83 | :alt: Result tileset image 84 | 85 | 86 | Generating a Tilemap 87 | -------------------- 88 | 89 | The tilemap is table that tells the GameBoy where to display each tile to 90 | compose an image. To generate a tilemap, we first have to generate a tileset 91 | (see previous section). 92 | 93 | To generate a tilemap you can use the following command:: 94 | 95 | img2gb tilemap \ 96 | --output-c-file=tilemap.c \ 97 | --output-header-file=tilemap.h \ 98 | tileset.png \ 99 | img.png 100 | 101 | Once again, let's explain this command: 102 | 103 | * ``--output-c-file=tileset.c`` is the path of the output ``.c`` file that will 104 | contain the data of the map, 105 | * ``--output-header-file=tileset.h`` is the path of the output ``.h`` file that 106 | will contain information about the tilemap (variable declaration, with and 107 | height), 108 | * ``tileset.png`` is the path of the tileset image generated in the previous 109 | section, 110 | * ``img.png`` is the path of the image that will be mapped using the tileset. 111 | 112 | Let's take a look of the generated files: 113 | 114 | * `tilemap.c (truncated) `_: 115 | 116 | .. code-block:: c 117 | 118 | // This file was generated by img2gb, DO NOT EDIT 119 | 120 | #include 121 | 122 | const UINT8 TILEMAP[] = { 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, // ... 124 | }; 125 | 126 | * `tilemap.h `_: 127 | 128 | .. code-block:: c 129 | 130 | // This file was generated by img2gb, DO NOT EDIT 131 | 132 | #ifndef _TILEMAP_H 133 | #define _TILEMAP_H 134 | 135 | extern const UINT8 TILEMAP[]; 136 | #define TILEMAP_WIDTH 20 137 | #define TILEMAP_HEIGHT 18 138 | 139 | 140 | #endif 141 | 142 | Using the Generated Code in a GameBoy Program 143 | --------------------------------------------- 144 | 145 | In this section I will only explain how to use the generated files in a GameBoy 146 | program that uses GBDK_. I will not explain how to make a complete GameBoy 147 | program or how to compile it: that is not the purpose of this *howto* (but 148 | I will put some links at the end of this document). 149 | 150 | Supposing you have a working folder that looks like this:: 151 | 152 | MyProject/ 153 | | 154 | +-- main.c 155 | | 156 | +-- tilemap.c 157 | | 158 | +-- tilemap.h 159 | | 160 | +-- tileset.c 161 | | 162 | +-- tileset.h 163 | 164 | Here is the minimal code to put in your ``main.c`` file to use the tileset and 165 | the tilemap: 166 | 167 | .. code-block:: c 168 | 169 | #include 170 | 171 | #include "tileset.h" 172 | #include "tilemap.h" 173 | 174 | void main(void) { 175 | // Load the tileset in the video memory of the GameBoy 176 | set_bkg_data(0, TILESET_TILE_COUNT, TILESET); 177 | // Load the tilemap in the Background layer of the GameBoy 178 | set_bkg_tiles(0, 0, TILEMAP_WIDTH, TILEMAP_HEIGHT, TILEMAP); 179 | // Make Background layer visible 180 | SHOW_BKG; 181 | } 182 | 183 | That's it :) 184 | 185 | 186 | Links 187 | ----- 188 | 189 | Some article I wrote about GameBoy development (in French, but Google Translate 190 | should help): 191 | 192 | * Article (the first one of a series) explaining how to write, compile and 193 | execute a simple "Hello World" program on a GameBoy: 194 | https://blog.flozz.fr/2018/10/01/developpement-gameboy-1-hello-world/ 195 | 196 | * A more complete article on the GameBoy graphics that use the same example 197 | that one of this document, but with more explanations: 198 | https://blog.flozz.fr/2018/11/19/developpement-gameboy-5-creer-des-tilesets/#convertir-des-images-avec-img2gb 199 | 200 | Some examples of GameBoy programs that use img2gb: 201 | 202 | * The full code of this *howto* example (with a Makefile and a ROM): 203 | https://github.com/flozz/gameboy-examples/tree/master/05-graphics2 204 | 205 | * An example of the Background layer use: 206 | https://github.com/flozz/gameboy-examples/tree/master/06-graphics3-background 207 | 208 | More links: 209 | 210 | * GameBoy Development Kit (GBDK): http://gbdk.sourceforge.net/ 211 | 212 | 213 | .. _GBDK: http://gbdk.sourceforge.net/ 214 | -------------------------------------------------------------------------------- /img2gb/gbtilemap.py: -------------------------------------------------------------------------------- 1 | """ 2 | The :class:`GBTilemap` class represents a GameBoy tilemap. It can map up to 3 | 32x32 tiles that are stored in a :class:`GBTileset` (if not given, a new empty 4 | tileset is created. 5 | 6 | Creating a new tilemap:: 7 | 8 | from img2gb import GBTilemap, GBTileset, GBTile 9 | 10 | tileset = GBTileset() 11 | tilemap = GBTilemap( 12 | width=32, 13 | height=32, 14 | gbtileset=tilset 15 | ) 16 | 17 | tile = GBTile() # blank tile 18 | 19 | tilemap.put_tile(0, 0, tile) # The tile will be added in the tileset 20 | 21 | 22 | Creating a tilemap from an image:: 23 | 24 | from img2gb import GBTilemap 25 | from PIL import Image 26 | 27 | image = Image.open("./my_tilemap.png") 28 | tilemap = tilemap.from_image(image) 29 | """ 30 | 31 | from .gbtile import GBTile 32 | from .gbtileset import GBTileset 33 | from .helpers import to_pil_rgb_image 34 | 35 | 36 | class GBTilemap(object): 37 | """Stores and manipulates GameBoy tilemaps. 38 | 39 | :param int width: With of the tilemap in tile (default = ``32``). 40 | :param int height: Height of the tilemap in tile (default = ``32``). 41 | :param GBTileset gbtileset: The tileset to use (a new empty one will be 42 | created if set to ``None``, default = 43 | ``None``). 44 | """ 45 | 46 | @classmethod 47 | def from_image( 48 | Cls, pil_image, gbtileset=None, missing="append", replace=0, dedup=True 49 | ): 50 | """Generates the tilemap from the given image. The tileset can also be 51 | generated at the same time. 52 | 53 | :param PIL.Image.Image pil_image: The image that represents the 54 | tilemap. 55 | :param GBTileset gbtileset: The tileset that contains the tiles used in 56 | the tilemap (a new empty one is created if not provided). 57 | :param str missing: What to do if a tile is missing from the tileset: 58 | 59 | * ``"append"`` (default): append the tile to the tileset, 60 | * ``"error"``: raise an error, 61 | * ``"replace"``: relpace by an other tile (see the ``replace`` 62 | argument). 63 | 64 | :param int replace: The id of the replacement tile when 65 | ``missing="replace"``. 66 | :param bool dedup: Deduplicate tiles when ``missing="append"`` (default 67 | = ``True``). 68 | """ 69 | image = to_pil_rgb_image(pil_image) 70 | width, height = image.size 71 | 72 | if width % 8 or height % 8: 73 | raise ValueError("The input image width and height must be a multiple of 8") 74 | 75 | tilemap = Cls(width / 8, height / 8, gbtileset=gbtileset) 76 | 77 | for tile_y in range(0, height, 8): 78 | for tile_x in range(0, width, 8): 79 | tile = GBTile.from_image(image, tile_x, tile_y) 80 | tilemap.put_tile( 81 | tile_x / 8, 82 | tile_y / 8, 83 | tile, 84 | missing=missing, 85 | replace=replace, 86 | dedup=dedup, 87 | ) 88 | 89 | return tilemap 90 | 91 | def __init__(self, width=32, height=32, gbtileset=None): 92 | self._tileset = gbtileset if gbtileset else GBTileset() 93 | self._map = [0x00] * int(width * height) 94 | self._width = width 95 | self._height = height 96 | 97 | @property 98 | def tileset(self): 99 | """The tileset that contains the tiles used by the tilemap. 100 | 101 | :type: GBTileset 102 | """ 103 | return self._tileset 104 | 105 | @property 106 | def width(self): 107 | """The width of the tilemap. 108 | 109 | :type: int 110 | """ 111 | return self._width 112 | 113 | @property 114 | def height(self): 115 | """The height of the tilemap. 116 | 117 | :type: int 118 | """ 119 | return self._height 120 | 121 | @property 122 | def size(self): 123 | """The with and height of the tilemap. 124 | 125 | :type: tuple of two int ``(width, height)`` 126 | """ 127 | return self._width, self._height 128 | 129 | @property 130 | def data(self): 131 | """Raw data of the tilemap. 132 | 133 | :type: list of int 134 | """ 135 | return self._map 136 | 137 | def put_tile(self, x, y, gbtile, missing="append", replace=0, dedup=True): 138 | """Put a tile at the given position in the tilemap. 139 | 140 | :param int x: The x coordinate where to put the tile in the tilemap. 141 | :param int y: The y coordinate where to put the tile in the tilemap. 142 | :param GBTile gbtile: The tile to put in the tilemap. 143 | :param str missing: What to do if a tile is missing from the tileset: 144 | 145 | * ``"append"`` (default): append the tile to the tileset, 146 | * ``"error"``: raise an error, 147 | * ``"replace"``: relpace by an other tile (see the ``replace`` 148 | argument). 149 | 150 | :param int replace: The id of the replacement tile when 151 | ``missing="replace"``. 152 | :param bool dedup: Deduplicate tiles when ``missing="append"`` (default 153 | = ``True``). 154 | """ 155 | if x < 0 or y < 0: 156 | raise ValueError("x and y coordinates cannot be negative") 157 | if x >= self._width: 158 | raise ValueError( 159 | "The x coordinate is greater than the width of the tilemap" 160 | ) 161 | if y >= self._height: 162 | raise ValueError( 163 | "The y coordinate is greater than the height of the tilemap" 164 | ) 165 | 166 | if missing not in ("append", "error", "replace"): 167 | raise ValueError( 168 | "Wrong value '%s' for the missing argument. Authorised values are 'append', 'error' and 'replace'." 169 | ) 170 | 171 | if gbtile in self._tileset.tiles and dedup: 172 | tile_id = self._tileset.index(gbtile) 173 | else: 174 | if missing == "append": 175 | tile_id = self._tileset.add_tile(gbtile, dedup=dedup) 176 | elif missing == "error": 177 | raise ValueError("The given tile is missing from the tileset.") 178 | elif missing == "replace": 179 | tile_id = replace 180 | 181 | index = int(y * self._width + x) 182 | self._map[index] = tile_id 183 | 184 | def to_hex_string(self): 185 | """Returns the tilemap as an hexadecimal-encoded string. 186 | 187 | :rtype: str 188 | 189 | e.g. (4x4 tiles):: 190 | 191 | 00 00 00 00 192 | 00 01 02 00 193 | 00 03 04 00 194 | 00 00 00 00 195 | """ 196 | result = "" 197 | for index in range(len(self._map)): 198 | tile_id = self._map[index] 199 | result += "%02X" % tile_id 200 | if (index + 1) % self._width == 0: 201 | result += "\n" 202 | else: 203 | result += " " 204 | return result.strip() 205 | 206 | def to_c_string(self, name="TILEMAP"): 207 | """Returns C code that represents the tilemap. 208 | 209 | :param str name: The name of the variable in the generated code (always 210 | converted to uppercase in the generated code, default = 211 | ``"TILEMAP"``) 212 | 213 | :rtype: str 214 | """ 215 | c = "const UINT8 %s[] = {\n" % name.upper() 216 | for index in range(len(self._map)): 217 | if index % self._width == 0: 218 | c += " " 219 | tile_id = self._map[index] 220 | c += "0x%02X," % tile_id 221 | if (index + 1) % self._width == 0: 222 | c += "\n" 223 | else: 224 | c += " " 225 | c += "};" 226 | return c 227 | 228 | def to_c_header_string(self, name="TILEMAP"): 229 | """Returns the C header (.h) code for the tilemap. 230 | 231 | :param str name: The name of the variable in the generated code (always 232 | converted to uppercase in the generated code, default = 233 | ``"TILEMAP"``) 234 | :rtype: str 235 | """ 236 | h = "extern const UINT8 %s[];\n" % name.upper() 237 | h += "#define %s_WIDTH %i\n" % (name.upper(), self._width) 238 | h += "#define %s_HEIGHT %i" % (name.upper(), self._height) 239 | return h 240 | -------------------------------------------------------------------------------- /img2gb/gbtileset.py: -------------------------------------------------------------------------------- 1 | """ 2 | The :class:`GBTileset` class represents a GameBoy tileset. It is composed of 3 | :class:`GBTile` (up to 255 tiles). 4 | 5 | Creating a tileset from scratch:: 6 | 7 | from img2gb import GBTile, GBTileset 8 | 9 | tileset = GBTileset() 10 | tile = GBTile() 11 | 12 | tileset.add_tile(tile) # -> 0 13 | tileset.length # -> 1 14 | 15 | Creating a tileset from a PIL image:: 16 | 17 | from img2gb import GBTileset 18 | from PIL import Image 19 | 20 | image = Image.open("./my_tileset.png") 21 | tileset = GBTileset.from_image(image) 22 | """ 23 | 24 | from PIL import Image 25 | 26 | from .gbtile import GBTile 27 | from .helpers import to_pil_rgb_image, tileset_iterator 28 | 29 | 30 | class GBTileset(object): 31 | """Stores and manipulate a GameBoy tileset (up to 255 tiles). 32 | 33 | :param int offset: An offset to apply to tile ids. 34 | """ 35 | 36 | @classmethod 37 | def from_image( 38 | Cls, 39 | pil_image, 40 | dedup=False, 41 | alternative_palette=False, 42 | sprite8x16=False, 43 | offset=0, 44 | ): 45 | """Create a new GBTileset from the given image. 46 | 47 | :param PIL.Image.Image pil_image: The input PIL (or Pillow) image. 48 | :param bool dedup: If ``True``, deduplicate the tiles (default = 49 | ``False``). 50 | :param bool alternative_palette: Use the sprite's alternative palette 51 | (inverted colors, default = ``False``). 52 | :param bool sprite8x16: Rearrange the tiles to be used in 8x16 sprites 53 | (default = ``False``). 54 | :param int offset: An offset to apply to tile ids. 55 | :rtype: GBTileset 56 | 57 | .. NOTE:: 58 | 59 | * The image width and height must be a multiple of 8. 60 | * The image can contain up to 255 different tiles. 61 | """ 62 | image = to_pil_rgb_image(pil_image) 63 | width, height = image.size 64 | 65 | if width % 8 or height % 8: 66 | raise ValueError("The input image width and height must be a multiple of 8") 67 | 68 | if height % 16 and sprite8x16: 69 | raise ValueError( 70 | "The input image height must be a multiple of 16 when sprite8x16=True" 71 | ) 72 | 73 | # TODO check tile count <= 255 74 | 75 | tileset = Cls(offset=offset) 76 | 77 | for tile_x, tile_y in tileset_iterator(width, height, sprite8x16): 78 | tile = GBTile.from_image( 79 | image, tile_x, tile_y, alternative_palette=alternative_palette 80 | ) 81 | tileset.add_tile(tile, dedup=dedup) 82 | 83 | return tileset 84 | 85 | def __init__(self, offset=0): 86 | self._offset = offset 87 | self._tiles = [] 88 | 89 | @property 90 | def offset(self): 91 | """An offset applied to each tiles. 92 | 93 | :type: int 94 | """ 95 | return self._offset 96 | 97 | @offset.setter 98 | def offset(self, offset): 99 | self._offset = offset 100 | 101 | @property 102 | def length(self): 103 | """Number of tiles in the tileset. 104 | 105 | :type: int 106 | """ 107 | return len(self._tiles) 108 | 109 | @property 110 | def data(self): 111 | """Raw data of the tiles in the tileset. 112 | 113 | :type: list of int 114 | """ 115 | data = [] 116 | for tile in self._tiles: 117 | data += tile.data 118 | return data 119 | 120 | @property 121 | def tiles(self): 122 | """Tiles of the tileset. 123 | 124 | :type: GBTile 125 | """ 126 | return self._tiles 127 | 128 | def add_tile(self, gbtile, dedup=False): 129 | """Adds a tile to the tileset. 130 | 131 | :param GBTile gbtile: The tile to add. 132 | :param bool dedup: If ``True``, the tile will be added only if there is 133 | no identical tile in the tileset (default = 134 | ``False``). 135 | 136 | :rtype: int 137 | :returns: The id of the tile in the tileset (including offset). 138 | """ 139 | if dedup and gbtile in self._tiles: 140 | return self._tiles.index(gbtile) + self._offset 141 | # TODO check tile count <= 255 142 | self._tiles.append(gbtile) 143 | return len(self._tiles) - 1 + self._offset 144 | 145 | def merge(self, gbtileset, dedup=False): 146 | """Merges the tiles of the given tileset in the current tileset. 147 | 148 | :param GBTileset gbtileset: The tileset to merge into the current one. 149 | :param bool dedup: Add only the tiles that are note already present in 150 | the current tileset (default = ``False``). 151 | """ 152 | for tile in gbtileset.tiles: 153 | self.add_tile(tile, dedup=dedup) 154 | 155 | def index(self, gbtile): 156 | """Get the id of the given tile in the tileset (including offset). 157 | 158 | :param GBTile gbtile: The tile. 159 | :rtype: int 160 | :returns: The id of the tile in the tileset (including offset). 161 | """ 162 | return self._tiles.index(gbtile) + self._offset 163 | 164 | def to_hex_string(self): 165 | """Returns the tileset as an hexadecimal-encoded string (one tile per 166 | line). 167 | 168 | :rtype: str 169 | 170 | e.g.:: 171 | 172 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 173 | FF 01 81 7F BD 7F A5 7B A5 7B BD 63 81 7F FF FF 174 | 7E 00 81 7F 81 7F 81 7F 81 7F 81 7F 81 7F 7E 7E 175 | 3C 00 54 2A A3 5F C1 3F 83 7F C5 3F 2A 7E 3C 3C 176 | 04 04 04 04 0A 0A 12 12 66 00 99 77 99 77 66 66 177 | 178 | """ 179 | return "\n".join([tile.to_hex_string() for tile in self._tiles]) 180 | 181 | def to_c_string(self, name="TILESET"): 182 | """Returns C code that represents the data of the tileset. 183 | 184 | :param str name: The name of the variable in the generated code (always 185 | converted to uppercase in the generated code, default = 186 | ``"TILESET"``) 187 | 188 | :rtype: str 189 | 190 | Example: 191 | 192 | .. code-block:: C 193 | 194 | const UINT8 TILESET[] = { 195 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 196 | 0xFF, 0x01, 0x81, 0x7F, 0xBD, 0x7F, 0xA5, 0x7B, 0xA5, 0x7B, 0xBD, 0x63, 0x81, 0x7F, 0xFF, 0xFF, 197 | 0x7E, 0x00, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x81, 0x7F, 0x7E, 0x7E, 198 | 0x3C, 0x00, 0x54, 0x2A, 0xA3, 0x5F, 0xC1, 0x3F, 0x83, 0x7F, 0xC5, 0x3F, 0x2A, 0x7E, 0x3C, 0x3C, 199 | 0x04, 0x04, 0x04, 0x04, 0x0A, 0x0A, 0x12, 0x12, 0x66, 0x00, 0x99, 0x77, 0x99, 0x77, 0x66, 0x66, 200 | }; 201 | """ 202 | c = "const UINT8 %s[] = {\n" % name.upper() 203 | for tile in self._tiles: 204 | c += " %s,\n" % ", ".join(["0x%02X" % b for b in tile.data]) 205 | c += "};" 206 | return c 207 | 208 | def to_c_header_string(self, name="TILESET"): 209 | """Returns the C header (.h) code for the tileset. 210 | 211 | :param str name: The name of the variable in the generated code (always 212 | converted to uppercase in the generated code, default = 213 | ``"TILESET"``) 214 | :rtype: str 215 | 216 | Example: 217 | 218 | .. code-block:: C 219 | 220 | extern const UINT8 TILESET[]; 221 | define TILESET_TILE_COUNT 5 222 | """ 223 | result = "extern const UINT8 %s[];\n" % name.upper() 224 | result += "#define %s_TILE_COUNT %i" % (name.upper(), self.length) 225 | return result 226 | 227 | def to_image(self): 228 | """Generates a PIL image from the tileset. The generated image is an 229 | indexed image with a 4 shades of gray palette. 230 | 231 | :rtype: PIL.Image.Image 232 | """ 233 | if self.length <= 16: 234 | width = self.length * 8 235 | height = 1 * 8 236 | else: 237 | width = 16 * 8 238 | height = (self.length // 16 + bool(self.length % 16)) * 8 239 | 240 | image = Image.new("P", (width, height)) 241 | image.putpalette( 242 | [ 243 | # fmt: off 244 | 0xFF, 0xFF, 0xFF, 245 | 0xBB, 0xBB, 0xBB, 246 | 0x55, 0x55, 0x55, 247 | 0x00, 0x00, 0x00, 248 | # fmt: on 249 | ] 250 | ) 251 | 252 | for i in range(self.length): 253 | tile_image = self._tiles[i].to_image() 254 | x = (i * 8) % width 255 | y = (i * 8) // width * 8 256 | image.paste(tile_image, (x, y)) 257 | 258 | return image 259 | -------------------------------------------------------------------------------- /img2gb/__init__.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from .gbtile import GBTile 4 | from .gbtileset import GBTileset 5 | from .gbtilemap import GBTilemap 6 | from .c_export import generate_c_file, generate_c_header_file 7 | from .version import VERSION 8 | 9 | 10 | def generate_tileset( 11 | input_images, 12 | output_c=None, 13 | output_h=None, 14 | output_image=None, 15 | output_binary=None, 16 | name="TILESET", 17 | dedup=False, 18 | alternative_palette=False, 19 | sprite8x16=False, 20 | ): 21 | """Function that generates tileset's C file, C header and image from an 22 | input image. 23 | 24 | :param PIL.Image.Image|list input_images: The input image to generate the 25 | tileset from. 26 | :param file output_c: A file-like object where the C code will be generated 27 | (``None`` to not generate C code). 28 | :param file output_h: A file-like object where the C header (.h) code will 29 | be generated (``None`` to not generate C header code). 30 | :param file output_image: A file-like object where the image representing 31 | the tileset will be generated (``None`` to not generate the image). 32 | 33 | .. NOTE:: 34 | 35 | The file must be openend in binary mode (``open("file", "wb")``) 36 | or you must be using a binary-compatible file-like object, like 37 | a :class:`io.BytesIO`. 38 | 39 | :param file output_binary: A file-like object where the binary version of 40 | the tileset will be generated (``None`` to not generate the binary 41 | version). 42 | 43 | .. NOTE:: 44 | 45 | The file must be openend in binary mode (``open("file", "wb")``) 46 | or you must be using a binary-compatible file-like object, like 47 | a :class:`io.BytesIO`. 48 | 49 | :param str name: The name of the tileset (will be used in the generated 50 | code, default = ``"TILESET"``) 51 | :param bool dedup: Deduplicate the tiles of the tileset (default = 52 | ``False``) 53 | :param bool alternative_palette: Use the sprite's alternative palette 54 | (inverted colors, default = ``False``) 55 | :param bool sprite8x16: Rearrange the tiles to be used in 8x16 sprites 56 | (default = ``False``). 57 | 58 | Example using files:: 59 | 60 | from PIL import Image 61 | import img2gb 62 | 63 | image = Image.open("./my_tileset.png") 64 | c_file = open("example.c", "w") 65 | h_file = open("example.h", "w") 66 | image_file = open("example.png", "wb") 67 | 68 | img2gb.generate_tileset( 69 | [image], 70 | output_c=c_file, 71 | output_h=h_file, 72 | output_image=image_file, 73 | dedup=True) 74 | 75 | c_file.close() 76 | h_file.close() 77 | image_file.close() 78 | 79 | Example using file-like objects:: 80 | 81 | from io import StringIO, BytesIO 82 | from PIL import Image 83 | import img2gb 84 | 85 | image = Image.open("./my_tileset.png") 86 | c_code_io = StringIO() 87 | h_code_io = StringIO() 88 | output_image = BytesIO() 89 | 90 | img2gb.generate_tileset( 91 | [image], 92 | output_c=c_code_io, 93 | output_h=h_code_io, 94 | output_image=output_image, 95 | dedup=True) 96 | 97 | # Print the C code for the example: 98 | c_code_io.seek(0) 99 | c_code = c_code_io.read() 100 | print(c_code) 101 | """ 102 | if type(input_images) is not list: 103 | tileset = GBTileset.from_image( 104 | input_images, 105 | dedup=dedup, 106 | alternative_palette=alternative_palette, 107 | sprite8x16=sprite8x16, 108 | ) 109 | else: 110 | tileset = GBTileset() 111 | for image in input_images: 112 | tileset.merge( 113 | GBTileset.from_image( 114 | image, 115 | alternative_palette=alternative_palette, 116 | sprite8x16=sprite8x16, 117 | ), 118 | dedup=dedup, 119 | ) 120 | 121 | if output_c: 122 | c_code = generate_c_file(tileset.to_c_string(name=name)) 123 | output_c.write(c_code) 124 | 125 | if output_h: 126 | filename = "%s.h" % name.lower() 127 | if hasattr(output_h, "name"): 128 | filename = os.path.basename(output_h.name) 129 | h_code = generate_c_header_file( 130 | tileset.to_c_header_string(name=name), filename=filename 131 | ) 132 | output_h.write(h_code) 133 | 134 | if output_binary: 135 | binary_data = bytearray(tileset.data) 136 | output_binary.write(binary_data) 137 | 138 | if output_image: 139 | image = tileset.to_image() 140 | image.save(output_image, "PNG") 141 | 142 | 143 | def generate_tilemap( 144 | input_tileset, 145 | input_tilemap_image, 146 | output_c=None, 147 | output_h=None, 148 | output_binary=None, 149 | name="TILEMAP", 150 | offset=0, 151 | missing="error", 152 | replace=0, 153 | ): 154 | """Function that generates tilemap's C file and C header from an input 155 | tileset and image. 156 | 157 | :param PIL.Image.Image input_tileset: The tileset that contains the tiles 158 | used in the tilemap. 159 | :param PIL.Image.Image input_tilemap_image: An image that represents the 160 | tilemap (its size must be a multiple of 8 and 256x256px maximum). 161 | :param file output_c: A file-like object where the C code will be generated 162 | (``None`` to not generate C code). 163 | :param file output_h: A file-like object where the C header (.h) code will 164 | be generated (``None`` to not generate C header code). 165 | :param file output_binary: A file-like object where the binary version of 166 | the tilemap will be generated (``None`` to not generate the binary 167 | version). 168 | 169 | .. NOTE:: 170 | 171 | The file must be openend in binary mode (``open("file", "wb")``) 172 | or you must be using a binary-compatible file-like object, like 173 | a :class:`io.BytesIO`. 174 | 175 | :param str name: The name of the tilemap (will be used in the generated 176 | code, default = ``"TILEMAP"``). 177 | :param int offset: Offset where the tileset starts (useful only of you will 178 | load the given tileset at a place different from ``0`` in the 179 | GameBoy video memeory). 180 | :param string missing: Action to do if a tile of the tilemap is missing 181 | from the tileset: 182 | 183 | * ``"error"`` (default): raise an error, 184 | * ``"replace"``: replace the missing tile by an other one (see 185 | ``replace`` option). 186 | 187 | :param int replace: The id of the replacement tile when 188 | ``missing="replace"``. 189 | 190 | Example: 191 | 192 | .. code-block:: C 193 | 194 | from io import BytesIO 195 | from PIL import Image 196 | import img2gb 197 | 198 | image = Image.open("./my_tilemap.png") 199 | 200 | # Generate the tileset image from the tilemap image 201 | tileset_io = BytesIO() 202 | img2gb.generate_tileset( 203 | [image], 204 | output_image=tileset_io, 205 | dedup=True) 206 | tileset_io.seek(0) 207 | 208 | # Generate the tilemap 209 | tileset_image = Image.open(tileset_io) 210 | img2gb.generate_tilemap( 211 | tileset_image, 212 | image, 213 | output_c=open("tilemap.c", "w"), 214 | output_h=open("tilemap.h", "w")) 215 | 216 | """ 217 | if missing == "append": 218 | raise ValueError("missing=append is not available from high level functions") 219 | 220 | tileset = GBTileset.from_image(input_tileset, dedup=False, offset=offset) 221 | tilemap = GBTilemap.from_image( 222 | input_tilemap_image, 223 | gbtileset=tileset, 224 | missing=missing, 225 | replace=replace, 226 | ) 227 | 228 | if output_c: 229 | c_code = generate_c_file(tilemap.to_c_string(name=name)) 230 | output_c.write(c_code) 231 | 232 | if output_h: 233 | filename = "%s.h" % name.lower() 234 | if hasattr(output_h, "name"): 235 | filename = os.path.basename(output_h.name) 236 | h_code = generate_c_header_file( 237 | tilemap.to_c_header_string(name=name), filename=filename 238 | ) 239 | output_h.write(h_code) 240 | 241 | if output_binary: 242 | binary_data = bytearray(tilemap.data) 243 | output_binary.write(binary_data) 244 | 245 | 246 | __all__ = [ 247 | "GBTile", 248 | "GBTileset", 249 | "GBTilemap", 250 | "generate_tileset", 251 | "generate_tilemap", 252 | "VERSION", 253 | ] 254 | -------------------------------------------------------------------------------- /example/tileset.c: -------------------------------------------------------------------------------- 1 | // This file was generated by img2gb, DO NOT EDIT 2 | 3 | #include 4 | 5 | const UINT8 TILESET[] = { 6 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 7 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x3F, 0xC0, 0x7F, 8 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 9 | 0xFF, 0x1F, 0xFF, 0x3F, 0xF0, 0x70, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 10 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 11 | 0xFF, 0xF0, 0xFF, 0xF8, 0x1F, 0x1C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 12 | 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 13 | 0x00, 0x00, 0x0F, 0x0F, 0x10, 0x1F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 14 | 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 15 | 0x00, 0x00, 0xE0, 0xE0, 0x10, 0xF0, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 16 | 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 17 | 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 18 | 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 19 | 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 20 | 0xE0, 0xFF, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 21 | 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 22 | 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0xC1, 0xFF, 23 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 24 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x78, 0xFF, 0xFC, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 25 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x07, 0xFF, 0x0F, 0xFF, 0x1F, 0xFF, 0x1F, 0xFF, 0x1F, 26 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0xC0, 0xFF, 0xE0, 0xFF, 0xE0, 0xFF, 0xE0, 27 | 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 28 | 0x21, 0x3F, 0x21, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 0x20, 0x3F, 29 | 0xE1, 0xFF, 0xE1, 0xFF, 0xC1, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 30 | 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 31 | 0xFF, 0xFE, 0xFF, 0xFC, 0xFF, 0x78, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 32 | 0xFF, 0x1F, 0xFF, 0x0F, 0xFF, 0x07, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 33 | 0xFF, 0xE0, 0xFF, 0xC0, 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 34 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x07, 35 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 36 | 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0x60, 0xE0, 0xE0, 37 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xFF, 0x0C, 0xFF, 0x06, 0xFF, 0x03, 38 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xFF, 0x18, 0xFF, 0x30, 0xFF, 0x60, 39 | 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0C, 0x0F, 0x0F, 40 | 0xFF, 0x0F, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xE0, 0xE0, 41 | 0xFF, 0xE0, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 42 | 0xF8, 0x08, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 43 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x1F, 0x10, 44 | 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0xE0, 0xE0, 0xE0, 0x60, 45 | 0xFF, 0xC0, 0xFF, 0xF8, 0xFF, 0x3F, 0xFF, 0x07, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 46 | 0xFF, 0x01, 0xFF, 0x0F, 0xFF, 0xFE, 0xFF, 0xF0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 47 | 0xFF, 0xC0, 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 48 | 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0F, 0x0F, 0x0F, 0x0C, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 50 | 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x3F, 0x20, 0xFF, 0xC0, 0xFF, 0x00, 51 | 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x10, 0xFF, 0x0F, 0xFF, 0x00, 0xFF, 0x00, 52 | 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0x1F, 0x10, 0xFF, 0xE0, 0xFF, 0x00, 0xFF, 0x00, 53 | 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 54 | 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 55 | 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 56 | 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x10, 0xF0, 57 | 0x20, 0x3F, 0x20, 0x3F, 0x10, 0x1F, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x10, 0xF0, 0x20, 0xE0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x1F, 0x1F, 62 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 63 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x3F, 0x3F, 0x3F, 64 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 65 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFC, 0xFC, 0xFC, 66 | 0x3F, 0x3F, 0x7F, 0x7F, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x7F, 0x7F, 0x7F, 67 | 0xE0, 0xE0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF0, 0xF0, 0xF0, 0xF0, 68 | 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 69 | 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x00, 70 | 0x00, 0x00, 0x03, 0x03, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 71 | 0xE0, 0xE0, 0xF8, 0xF8, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 72 | 0x3F, 0x3F, 0x1F, 0x1F, 0x07, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0xE0, 0xE0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 75 | 0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x07, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0xFE, 0xFE, 0xFE, 0xFE, 0xFC, 0xFC, 0xF8, 0xF8, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 77 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x07, 0x07, 0x1F, 0x1F, 80 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, 0xF8, 0xF8, 0xF8, 0xF8, 0xE0, 0xE0, 81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x0F, 0x0F, 82 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0xF0, 0xF0, 0xF0, 0xF0, 83 | 0x01, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x63, 0x00, 0x71, 0x00, 0x38, 0x00, 84 | 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x8E, 0x00, 0xC7, 0x00, 0xE3, 0x00, 85 | 0x7F, 0x7F, 0x7E, 0x7E, 0x38, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x3F, 0x3F, 0xFF, 0xFF, 0xFC, 0xFC, 0x70, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | 0x70, 0x00, 0x38, 0x00, 0x18, 0x00, 0x80, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x03, 91 | 0x0F, 0x0C, 0x1F, 0x18, 0x1F, 0x18, 0x3F, 0x30, 0x3F, 0x30, 0x7F, 0x60, 0xFF, 0xC0, 0xFF, 0x80, 92 | 0xF0, 0x70, 0xFF, 0x3F, 0xFF, 0x1F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 93 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 94 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x04, 0xFC, 0x04, 0xFC, 0x04, 0xFC, 0x04, 0xFC, 0x04, 95 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x04, 0x07, 0x04, 0x07, 0x04, 0x07, 0x04, 0x07, 0x04, 96 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 0xF8, 0x08, 97 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x08, 0x0F, 0x08, 0x0F, 0x08, 0x0F, 0x08, 0x0F, 0x08, 98 | 0x0F, 0x0F, 0xFF, 0xFC, 0xFF, 0xF0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 99 | 0xFC, 0x04, 0xFF, 0x03, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 100 | 0x07, 0x04, 0xFF, 0xF8, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 101 | 0xF8, 0x08, 0xFF, 0x07, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 102 | 0x0F, 0x08, 0xFF, 0xF0, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 103 | }; 104 | --------------------------------------------------------------------------------