# bmp2tile-mac

A native macOS (Apple Silicon) port of [maxim-zhao/bmp2tile](https://github.com/maxim-zhao/bmp2tile), written in C++20 + SDL3 + Dear ImGui.

This is a clean-room reimplementation, not a line-by-line port -- none of the original C# is reused.

## Status

Builds and runs on Apple Silicon. The GUI (all four tabs and save dialogs) and CLI are confirmed working. All compressed output formats have been verified byte-for-byte against bmp2tile 0.67 reference output (ZX7, PSGaiden, STC0, STC4, STM exact match; APLib produces a larger but fully correct bitstream -- see Compression formats below).

## What's implemented

- Indexed (1/4/8bpp) BMP loading, preserving the on-disk palette order exactly. PNG, GIF, and 24/32bpp BMP loading via stb_image, quantized to a palette by first-seen unique colour.
- 8x8 tile extraction with duplicate detection and h/v/both-axis mirroring detection.
- Saving to all the formats devkitSMS/SMSlib can load:
  - **Tiles**: uncompressed, ZX7, PSGaiden, STC0, STC4, APLib, 1bpp raw, 2bpp raw.
  - **Tilemap**: uncompressed, ZX7, STM, APLib. Supports a tile index offset (for tiles not loaded at VRAM tile 0).
  - **Palette**: SMS (`%00BBGGRR`), GG (12-bit little-endian), or `cl123` symbolic constants. "Always emit 16 colours" pads with black.
- GUI with Source / Tiles / Tilemap / Palette tabs.
- CLI (`bmp2tile_cli`) as a separate binary with zero SDL/ImGui dependency -- see "Command-line mode" below.

## Compression formats

All formats devkitSMS can load are implemented:

- **ZX7**: faithful port of Einar Saukas's public-domain reference encoder.
- **PSGaiden**: faithful port of the Phantasy Star Gaiden bitplane-delta encoder.
- **STC0 / STC4**: faithful ports of sverx's Simple Tile Compressor encoders. STC4 shares STC0's per-group encoder rather than duplicating it. **One bug in the original STC4 source is deliberately preserved**: C integer promotion makes the "inverted group match" branch permanently dead code in the real tool, meaning real STC4 output never contains a `0x30`-`0x3F` byte. This is reproduced exactly rather than fixed, since the goal is a compatible compressor. It triggers a `-Wsign-compare` warning on build; that warning is correct, and silencing it would obscure the deliberate preservation.
- **STM**: faithful port of sverx's ShrunkTileMap encoder. Run-length-encodes the flat 16-bit-word tilemap stream with a "sticky high byte" tracker.
- **APLib**: a hybrid port. Jorgen Ibsen's own aPLib isn't permissively licensed; `aplibsharp` (MIT) and `apultra` (zlib/CC0) are, but both use suffix-array optimal parsers (~5000+ lines combined) which is more than this project needs. The bitstream format is faithfully ported from `aplibsharp`'s model/state/tobinary, cross-checked against `apultra`'s format constants (they independently agree on the offset thresholds 128/1280/32000). The match finder is a from-scratch hash-chain + greedy parser -- valid, just not optimal. Verified with 2000+ round-trip fuzz tests against a ported decoder, and cross-checked against bmp2tile 0.67 reference output (both decompress to identical plaintext; the reference is smaller because it uses optimal parsing). `oapack` was ruled out: its license excludes commercial use, and physical cart sales are common in the SMS homebrew scene.
- **1bpp / 2bpp raw**: not compression -- keeps only the first 1 or 2 bitplane bytes of each tile row, dropping the rest. For tiles that only need 2 or 4 colours.

## Not implemented

- PCX support -- not supported by stb_image and I didn't think there was much call for it.
- Compressor plugins (the original's DLL/dylib architecture) -- replaced by statically linking the compressors directly.
- Tilemap area cropping, spritesheet rearrangement, palette overrides.

## New features

- Drag and drop tiles and palette colours to rearrange. You probably want to do this in the source image so it can be reconverted without messing around, but it might be useful for a quick test.

## Building

Two binaries:

- **`bmp2tile`** -- GUI app, also supports CLI when given arguments. Requires Xcode command line tools, SDL3, and `pkg-config`.
- **`bmp2tile_cli`** -- CLI only, zero SDL/ImGui dependency. Requires only Xcode command line tools.

stb_image is vendored in `third_party/stb/`. ImGui is fetched by `make gui` on first run.

```bash
xcode-select --install        # if you haven't already
cd bmp2tile-mac

make cli                      # no network access needed
./build/bmp2tile_cli --help

brew install sdl3 pkg-config
make gui                      # fetches ImGui on first run
./build/bmp2tile

make                          # both
```

If `pkg-config` can't find SDL3:

```bash
export PKG_CONFIG_PATH="$(brew --prefix sdl3)/lib/pkgconfig:$PKG_CONFIG_PATH"
```

`make bundle` wraps `bmp2tile` in a `bmp2tile.app` for double-click launching. `make clean` removes `build/` but leaves `third_party/` alone.

## Command-line mode

`bmp2tile_cli` always runs headlessly; `bmp2tile` does when given arguments, launches the GUI with none.

```bash
./build/bmp2tile_cli source.bmp \
    -savetiles tiles.bin zx7 \
    -savetilemap tilemap.bin stm \
    -savepalette palette.bin gg
```

```
Usage: bmp2tile <source-image> [options]

  -savetiles <path> [format]     format: uncompressed (default), zx7,
                                  psgaiden, stc0, stc4, aplib, 1bpp, 2bpp
  -savetilemap <path> [format]   format: uncompressed (default), zx7, stm, aplib
  -savepalette <path> [format]   format: sms (default), gg
  -palettetext <format>          sms / gg / cl123 -- prints instead of/as well as saving
  -removedupes / -noremovedupes  default: on
  -mirror / -nomirror            default: on
  -emit16colours                 pad palette to 16 entries with black
  -tileoffset <N>                add N to every tilemap tile index before saving
  -h, -help, --help
```

Formats are case-insensitive. Running with just a source image and no save flags prints the tile-dedup summary and exits.

## Project layout

```
Makefile
Info.plist
src/
  main.cpp              bmp2tile entry point (branches CLI/GUI on argc)
  main_cli.cpp          bmp2tile_cli entry point (CLI only, never includes App.h)
  App.h / App.cpp       SDL3+ImGui shell, main loop, texture management
  Cli.h / Cli.cpp       command-line mode, no SDL/ImGui dependency
  core/                 no SDL/ImGui -- pure conversion logic
    Image.h/.cpp        BMP/PNG/GIF → IndexedImage
    TileSet.h/.cpp      tile extraction, dedup/mirror, Save*Bin, compression dispatch
    Palette.h/.cpp      palette formatting and saving
    compressors/
      Zx7.h/.cpp        Einar Saukas reference zx7 encoder (public domain)
      PsGaiden.h/.cpp   PSGaiden encoder
      Stc0.h/.cpp       sverx gfxcomp_stc0.c port
      Stc4.h/.cpp       sverx gfxcomp_stc4.c port (intentional bug preserved -- see above)
      Stm.h/.cpp        sverx gfxcomp_stm.c port
      ApLib.h/.cpp      hybrid: aplibsharp format + from-scratch match finder
      RawBpp.h/.cpp     1bpp/2bpp bitplane extraction
  ui/
    SourceTab.h/.cpp    image load (native open dialog + drag-and-drop), tile-grid overlay
    TilesTab.h/.cpp     dedup/mirror checkboxes, thumbnail grid, save dialog
    TilemapTab.h/.cpp   compression dropdown, tile-index-offset field, save dialog, text preview
    PaletteTab.h/.cpp   colour swatches, always-emit-16 checkbox, format selector, save dialog
    CompressionFormatCombo.h/.cpp  shared dropdown widget (extracted from Tiles/Tilemap tabs)
third_party/
  imgui/              cloned by `make gui` on first run -- not checked in
  stb/                vendored (stb_image.h v2.30, public domain) -- checked in
```

## Code style

Allman braces, PascalCase members/globals (`b`-prefix for booleans), camelCase locals, pre-increment.

## Suggested next steps

1. Tilemap area cropping, spritesheet rearrangement, palette overrides.
