{"id":48646618,"url":"https://github.com/tinic/png2amiga","last_synced_at":"2026-05-13T07:11:47.178Z","repository":{"id":349840870,"uuid":"1203476505","full_name":"tinic/png2amiga","owner":"tinic","description":"PNG/JPEG to Commodore Amiga graphics converter — OCS/AGA bitplane, HAM6/HAM8, EHB, copper palettes, 16 dithering modes","archived":false,"fork":false,"pushed_at":"2026-04-25T10:20:51.000Z","size":158674,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T10:29:04.052Z","etag":null,"topics":["aga","amiga","bitplane","commodore","copper","demoscene","dithering","ecs","iff","ilbm","image-converter","ocs","pixel-art","retro-computing"],"latest_commit_sha":null,"homepage":"https://png2amiga.app","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tinic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-07T04:29:07.000Z","updated_at":"2026-04-25T08:02:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tinic/png2amiga","commit_stats":null,"previous_names":["tinic/png2amiga"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/tinic/png2amiga","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinic%2Fpng2amiga","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinic%2Fpng2amiga/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinic%2Fpng2amiga/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinic%2Fpng2amiga/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tinic","download_url":"https://codeload.github.com/tinic/png2amiga/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tinic%2Fpng2amiga/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32356602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T20:07:02.737Z","status":"ssl_error","status_checked_at":"2026-04-27T20:07:00.910Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aga","amiga","bitplane","commodore","copper","demoscene","dithering","ecs","iff","ilbm","image-converter","ocs","pixel-art","retro-computing"],"created_at":"2026-04-10T05:13:34.924Z","updated_at":"2026-05-13T07:11:47.171Z","avatar_url":"https://github.com/tinic.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# png2amiga\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Web App](https://img.shields.io/badge/Try_it-png2amiga.app-brightgreen)](https://www.png2amiga.app)\n[![GitHub](https://img.shields.io/github/stars/tinic/png2amiga?style=social)](https://github.com/tinic/png2amiga)\n[![C++26](https://img.shields.io/badge/C%2B%2B-26-blue.svg)](https://en.cppreference.com/w/cpp/26)\n\n**Open-source** PNG/JPEG/WebP → Commodore Amiga, Atari ST/STE,\nIBM PC (CGA / EGA / VGA), Commodore 64, Sega Genesis, and SNES Mode 7\nimage converter. Supports OCS/AGA bitplane, HAM6/HAM8, EHB, and\n**sliced-HAM (SHAM) via copper palettes** with perceptual OKLab color\nmatching. Writes IFF ILBM, Degas `.PI1`/`.PI2`/`.PI3`, C64 `.prg` /\n`.koa` / `.hir`, Genesis SGDK headers, SNES tile/tilemap `.bin`, C\nheaders, raw bitplanes, and standalone AmigaOS viewer `.cpp` source.\nThe bundled `build-amiga.sh` wrapper runs the included\n`m68k-amiga-elf-gcc` + `exe2adf` toolchain to turn the `.cpp` into a\nrunnable `.exe` and bootable `.adf`. DOS-mode `.c` output compiles\nwith `ia16-elf-gcc` into a 16-bit real-mode viewer.\n\n**[Try it in your browser at png2amiga.app](https://www.png2amiga.app)** —\nlive preview via WebAssembly, server-side compile to Amiga executables.\n\n[![png2amiga.app web interface](docs/screenshot.png)](https://www.png2amiga.app)\n\nAimed at retro-platform asset pipelines (Amiga / Atari / IBM PC\ndemoscene, hobby AmigaOS games, MS-DOS coding). All color operations\nuse [OKLab](https://bottosson.github.io/posts/oklab/) perceptual color\nspace. Sister project to [png2c64](https://github.com/tinic/png2c64).\n\n## Features\n\n**Amiga modes**: Lores / Hires (+ interlace), HAM6 (OCS) + HAM8 (AGA)\nwith hires and/or interlace variants, EHB. 1–8 bitplanes within\nchipset limits. Optional **sliced palette** (per-line copper swaps —\nthe same technique behind [Sliced HAM / SHAM](https://en.wikipedia.org/wiki/Hold-And-Modify#Sliced_HAM),\nin use since 1989) and **strip palette** (additional mid-line swaps in\nthe active scanline, used in demoscene productions like Desire's\n*Shuffling Around the Christmas Tree*).\n\n**Atari modes**: STF Low/Medium/Hi, STE Low/Medium/Hi (9-bit palette\non STF, 12-bit on STE; ST-Hi is hardware-locked monochrome). Degas\nElite `.PI1`/`.PI2`/`.PI3` output.\n\n**IBM PC modes**: CGA 320×200 / 640×200 / composite (NTSC artifact\ncolors), CGA text-mode glyph matching at 80x{200,100,50,25} and\n40x{200,100} cell grids, EGA 320×200 / 640×200 / 640×350 (16 of the\n64-color IrgbIRGB gamut), VGA Mode 13h (320×200, 256-color chunky),\nMode 10h (640×350, 16-color planar), Mode 12h (640×480, 16-color\nplanar). 16-bit DOS viewer `.c` output for `ia16-elf-gcc` compilation.\n\n**Commodore 64**: VIC-II hires (320×200, 2 colors per 8×8 cell),\nmulticolor (160×200, 4 per 4×8), FLI / AFLI (per-row screen-RAM\nswaps for more colors per cell), PETSCII (40×25 text-mode glyph\nmatching against the C64 character ROM), and custom-charset modes\n(hires + multicolor) that build a per-image 256-glyph charset with\nHamming-distance pair merging when content overflows. Outputs\n`.prg` / `.koa` / `.hir` for direct loading on real hardware.\n\n**Sega Genesis / Mega Drive**: H32 (256×224) and H40 (320×224) with\noptional Shadow/Highlight extension. 4 palette lines × 16 BGR333,\n8×8 4bpp tiles + tilemap. SGDK `.h` / `.bin` output.\n\n**SNES Mode 7**: 256-color BGR555-palette and 2048-color Direct\nColor (BBGGGRRR) variants. Affine-transformable 8bpp BG1, ≤256\nunique 8×8 tiles via greedy distance-merging when content overflows.\n\n**Palette quantizers**: GPU-accelerated parallel-restart Lloyd\nk-means in OKLab on Apple GPU (default for AGA / VGA when Xcode's\nMetal toolchain is available; mean ΔS2 +2.6..+3.4 vs pngquant on\nDIV2K-100+Kodak-24 across K=8..256), OCS brute-force (histogram +\nk-means over all 4096 OCS colors), PNN agglomerative (Ward's\nlinkage in OKLab — default for HAM AGA), and median-cut + k-means\nrefinement in OKLab (CPU fallback when Metal isn't available).\n\n**Dithering**: 64 methods.\n\n- **Error Diffusion** (19) — Floyd–Steinberg, Sierra-Lite, Atkinson,\n  Jarvis, Stucki, Gilbert, Riemersma, DBS (slow); palette-aware\n  planning: Optimal Checker, Optimal Line, Optimal Line-Checker,\n  Tri-tone, Knoll, Yliluoma 1 (exhaustive) and Yliluoma 2 (greedy +\n  luma-weighted variant); structure-aware: Structure-FS,\n  Contrast-FS, Zhou–Fang.\n- **Bayer** (14) — Bayer 2×2 / 4×4 / 8×8 / 3×3 / 5×5 / 6×6 / 7×7,\n  non-square Bayer 4×2 and 2×4, plus the matrices Aseprite (old\n  4×4), libcaca (3×3 and 6×6), Pegasus 8×8 shipped, and\n  Cranley–Patterson rotated Bayer.\n- **Halftone** (4) — Halftone 8×8, Diagonal Newspaper, Spiral 5×5,\n  Clustered Dot.\n- **Hatching** (9) — horizontal Lines 2 / 4 / 8 + Line Checker,\n  vertical VLines 2 / 4 / 8 + VLine Checker, Crosshatch.\n- **Pattern** (8) — Checker, Wide 2×4, Tall 4×2, Hexagonal 8×8 and\n  5×5, Radial, Quasicrystal, Truchet.\n- **Noise** (10) — Blue Noise, Void \u0026 Cluster, Cluster Noise,\n  Niklasson 16×16 Fractal, IGN and IGN-triangle, R2 and\n  R2-triangle, Value Noise, White Noise.\n\n**HAM encoding**: DP beam search with a triple-pixel refinement pass\n(default on) that catches the fringe-lag artifacts 1-pixel DP misses.\n`--ham-fast` switches to the greedy encoder (~15× faster, ~0.04 dB\nquality cost) for live preview or batch video processing.\n\n**Output**: `.png` preview, `.iff` ILBM (Amiga), `.pi1`/`.pi2`/`.pi3`\nDegas (Atari ST/STE), `.prg`/`.koa`/`.hir` (C64), `.bin` + `.h` (SNES\ntile/tilemap, Genesis SGDK), `.h` C header, `.cpp` standalone viewer\nsource (Amiga) or `.c` (DOS, ia16-elf-gcc), `.raw` + `.pal` raw\nbitplanes with palette. The `build-amiga.sh` helper compiles\n`.cpp` → `.exe` → `.adf` via the bundled toolchain.\n\n## Build\n\n```bash\n# Native CLI (requires GCC 15 for C++26)\ncmake -B build -DCMAKE_C_COMPILER=gcc-15 -DCMAKE_CXX_COMPILER=g++-15 .\ncmake --build build\nctest --test-dir build --output-on-failure\n\n# WASM (requires Emscripten)\nemcmake cmake -B build-wasm -DCMAKE_BUILD_TYPE=Release .\ncmake --build build-wasm\n\n# Web frontend\ncd web \u0026\u0026 npm install \u0026\u0026 npm run dev\n\n# Production web bundle (writes to docs/)\n./tools/build-web.sh\n```\n\nPre-built Linux / macOS / Windows binaries are attached to each\n[GitHub release](https://github.com/tinic/png2amiga/releases).\n\n## Usage\n\n```bash\n# Basic conversion\n./build/png2amiga input.png output.iff\n./build/png2amiga input.jpg output.png\n\n# HAM8 on AGA (default — triple refinement, FS pre-dither, PNN palette)\n./build/png2amiga --mode ham8 --chipset aga input.png output.iff\n\n# HAM8 realtime / batch profile (greedy, ~15× faster)\n./build/png2amiga --mode ham8 --chipset aga --ham-fast input.png output.png\n\n# Sliced palette — per-line copper swaps (more colors per scanline).\n./build/png2amiga --mode lores --depth 5 --sliced input.png output.iff\n./build/png2amiga --mode ham6 --sliced input.png output.iff\n\n# Strip palette — mid-line swaps inside the active scanline. DPF or EHB only.\n# IFF has no chunk for mid-line MOVEs, so use .cpp (runnable AmigaOS viewer)\n# or .h (data-only) instead.\n./build/png2amiga --mode lores --dpf --strips input.png viewer.cpp\n./build/png2amiga --mode ehb --strips input.png data.h\n\n# HAM6 + sliced palette, multi-restart search (~4-5× slower, +0.5 to +2 dB PSNR)\n./build/png2amiga --mode ham6 --sliced --best input.png output.iff\n\n# Generate a bootable Amiga floppy that displays the image\n./build/png2amiga --mode ham6 input.png viewer.cpp\n./build-amiga.sh viewer.cpp viewer.adf\n\n# Launch in fs-uae (A1200 by default)\n./run-amiga.sh viewer.adf\n./run-amiga.sh viewer.adf A500 ntsc\n\n# Atari ST/STE\n./build/png2amiga --mode stf-low input.png output.pi1\n./build/png2amiga --mode ste-low input.png output.pi1\n\n# IBM PC (CGA / EGA / VGA)\n./build/png2amiga --mode vga-13h input.png output.png        # preview\n./build/png2amiga --mode ega-320 input.png viewer.c          # 16-bit DOS viewer\n./build/png2amiga --mode cga-320 --cga-palette p1-high \\\n    input.png output.png\n\n# Commodore 64\n./build/png2amiga --mode c64-multicolor input.png output.prg    # bootable .prg\n./build/png2amiga --mode c64-petscii input.png output.png       # text-mode preview\n\n# Sega Genesis (H40 + Shadow/Highlight, SGDK header)\n./build/png2amiga --mode genesis-h40-sh input.png output.h\n\n# SNES Mode 7 (256-color palette + tilemap + tile data)\n./build/png2amiga --mode snes-mode7-256 input.png output.bin\n```\n\nRun `./build/png2amiga --help` for the full flag reference.\n\n## Amiga Modes\n\n| Mode | Resolution | Max Depth | Colors | Notes |\n|------|-----------|-----------|--------|-------|\n| `lores` | 320px | OCS:5 AGA:8 | 2–256 | Square pixels |\n| `lores-lace` | 320px | OCS:5 AGA:8 | 2–256 | Interlaced (wide pixels) |\n| `hires` | 640px | OCS:4 AGA:8 | 2–256 | Tall pixels |\n| `hires-lace` | 640px | OCS:4 AGA:8 | 2–256 | Interlaced (square pixels) |\n| `ham6` (+ lace/hires variants) | 320/640px | 6 | 4096 | Hold-And-Modify (OCS) |\n| `ham8` (+ lace/hires variants) | 320/640px | 8 | 16M | Hold-And-Modify (AGA) |\n| `ehb` / `ehb-lace` | 320px | 6 | 64 | Extra Half-Brite |\n\n## Atari Modes\n\n| Mode | Resolution | Depth | Colors | Palette |\n|------|-----------|-------|--------|---------|\n| `stf-low` | 320×200 | 4 | 16 | 9-bit (512 colors) |\n| `stf-med` | 640×200 | 2 | 4 | 9-bit (512 colors) |\n| `stf-hi` / `ste-hi` | 640×400 | 1 | 2 (B/W) | hardware-locked monochrome |\n| `ste-low` | 320×200 | 4 | 16 | 12-bit (4096 colors) |\n| `ste-med` | 640×200 | 2 | 4 | 12-bit (4096 colors) |\n\n## IBM PC Modes\n\n| Mode | Resolution | Colors | Notes |\n|------|-----------|--------|-------|\n| `cga-320` | 320×200 | 4 | Fixed palettes (`--cga-palette p0-low/p0-high/p1-low/p1-high`) |\n| `cga-640` | 640×200 | 2 | Monochrome |\n| `cga-composite` | 160×200 effective | 16 | NTSC artifact colors from 320×200 2bpp |\n| `cga-text80x{200,100,50,25}` / `cga-text40x{200,100}` | 80×N or 40×N cells | 16 fg × 16 bg | Glyph + attribute matching against the IBM CGA 8×8 font; all variants except 80×200 fit in 16 KB CGA VRAM |\n| `ega-320` / `ega-640` / `ega-hi` | 320×200 / 640×200 / 640×350 | 16 of 64 | 4-plane IrgbIRGB gamut |\n| `vga-13h` | 320×200 | 256 | 8bpp chunky, 18-bit DAC |\n| `vga-10h` | 640×350 | 16 | 4-plane planar, 18-bit DAC |\n| `vga-12h` | 640×480 | 16 | 4-plane planar, square pixels |\n\n`--native-par` letterboxes/pillarboxes the source into the fixed DOS\nbuffer; the default is to stretch-fill.\n\n## Sliced palette (per-line copper swaps)\n\nAdd `--sliced` to any bitmap mode (lores, hires, EHB, HAM6, HAM8) to let\nthe Copper coprocessor rewrite palette registers in the horizontal blank\nbetween every scanline. Each line displays with its own palette state,\nand the planner picks per-line color swaps that minimize OKLab error\nagainst the source row.\n\nThis is the same technique that's been used in Amiga demos and HAM\nconverters since the late 1980s — the [Wikipedia article on\nHAM](https://en.wikipedia.org/wiki/Hold-And-Modify#Sliced_HAM) covers\nthe lineage as \"Sliced HAM\" / SHAM / dynamic HAM. On hires the same\ntrick is known as **Dynamic HiRes (DHIRES)** — copper-driven 16-color\npalette swaps per scanline; `--mode hires --sliced` is png2amiga's\nDHIRES path. The reference HAM encoder\n[ham_convert](http://mrsebe.bplaced.net/blog/wordpress/?page_id=374)\nand Leonard's [Brute Force Colors](https://arnaud-carre.github.io/2022-12-30-amiga-ham/)\nboth implement the per-line variant. png2amiga aims at the same target\nwith a perceptual error metric and applies the technique to indexed\nmodes too (lores, hires, EHB) rather than just HAM.\n\nThe encoder respects the real-hardware post-DDFSTOP DMA budget: **14\nMOVE instructions per line** (one of the 15 copper slots is the per-line\nWAIT). Safe static budget is 14 palette swaps on OCS (one MOVE per\nchange) and 3 on AGA (4 MOVEs per change worst-case under banked LOCT).\nAuto-mode tries K+3, K+2, K+1 and picks the highest K whose worst-case\ncost fits the budget — typically 6 swaps/line at depths 3–5 on AGA.\n\n`--slice-changes N` overrides the budget; use if you want to experiment\nwith configurations that may exceed real hardware limits but still\ndisplay correctly on emulators.\n\n`--best` runs a multi-restart sweep over jitter seeds, dither\nstrengths, and palette-diversity values, picking the trial with the\nbest result against `--best-metric` (SSIMULACRA2 by default). Available\non plain HAM6/HAM8, plain EHB, and any combination with sliced or\nstrip palette. Cost is ~20–30× the single-pass time on most modes\n(HAM-CAP / strips can land closer to ~5×); typical gain is\n+0.5 to +2 dB PSNR.\n\n## Strip palette (mid-line swaps inside the active scanline)\n\n`--strips` extends the sliced palette by issuing additional palette\nMOVEs at fixed **mid-line** copper slots — so a single scanline can\ndisplay multiple palette banks across its width. Where the per-line\nsliced palette gives \"this row's 64 colors\", strips gives \"this strip's\n64 colors\", with strips on a 16-pixel grid. Strips ride on top of the\nsliced base (each line opens with the sliced palette reload in hblank,\nthen mid-line swaps walk it through the visible region).\n\nMid-line copper register changes have been a demoscene staple for\ndecades — Shadow of the Beast (1989) used single-color bars, Spaceballs'\n[State of the Art](https://www.pouet.net/prod.php?which=99) (1992)\npushed full mid-line palette manipulation, and recent productions like\nDesire's [Shuffling Around the Christmas Tree](https://www.pouet.net/prod.php?which=90358)\n(2021, code by Platon42) and [Copper Chunky](https://www.powerprograms.nl/amiga/copper-chunky.html)\nby Jeroen Knoester (2021) showcase how dense the per-line copper traffic\ncan get. png2amiga's contribution is wiring this style of per-strip\npalette change into a still-image converter on top of an OKLab\nerror-diffused dither.\n\nTwo strip modes:\n\n* **DPF + strips** (`--mode lores --dpf --strips`) — OCS dual-playfield,\n  3-plane PF2 (8 base colors). The 8 PF2 registers are unconditionally\n  re-emitted in every line's hblank (~9 MOVEs, fixed) so mid-line swaps\n  cannot leak state across lines. Up to 19 useful mid-line swaps per\n  scanline; ~454 unique displayed colors per frame on a typical image.\n\n* **EHB + strips** (`--mode ehb --strips`) — OCS Extra Half-Brite, 32\n  base registers + 32 hardware-derived half-brites. Each base swap also\n  updates the matching half-brite slot via the hardware DAC. Adaptive\n  per-line hblank tracking keeps each line inside the 14-MOVE OCS hblank\n  budget. ~1100+ unique displayed colors per frame.\n\nBoth modes are OCS-only, lores, no interlace. The planner runs 6\niterative refinement passes alternating index dither and mid-line swap\nselection. Slot positions were calibrated empirically on real OCS\nhardware via `--strips-probe` (see `src/strips.hpp`); the published\nhardware budget is ~14 hblank MOVEs + ~20 visible-area MOVEs per line in\n6-plane modes, and the calibrated slot tables sit comfortably within\nthat.\n\n![strip palette copper-list density and bus usage in vAmiga's debug overlay](docs/scap.png)\n\nThe vAmiga debug overlay shows one frame's copper list and bus usage:\nevery visible scanline runs a near-saturated MOVE stream through the\ndisplayed area — each band of activity is one scanline's sliced-palette\nreload in hblank plus ~19 mid-line swaps inside the visible area.\n\n## Cross-fade between two images (`--fade-to`)\n\nEncode one bitmap and morph its palette toward a second image at\nruntime — joint k-means clusters every (source ⊕ target) slot together\nso the same index buffer reproduces both stops. The emitted `.cpp`\nviewer patches per-frame value tables on real hardware. Lores / hires\n/ EHB only.\n\n![source → target fade demo](docs/fade-demo.gif)\n\n```\npng2amiga --depth 5 --fade-to target.png source.png viewer.cpp\n```\n\n## How does it compare?\n\nSource: `examples/makena.jpg` resized to 320×213 (Lanczos),\nall encoders run with Floyd-Steinberg dither at their highest-quality\nsetting. Metrics: PSNR (sRGB byte distance) and SSIMULACRA2\n(Cloudinary 2022 — perceptual, calibrated against human ratings;\n30=low, 50=fair, 70=high quality).\n\n**Palette precision asymmetry** — read this before comparing PSNR\ncolumns. png2amiga's `lores d=5`, every `HAM6` row, and the\nham_convert / abc lores-d5 / ocs32 / HAM6 / SHAM6 entries all\noperate on real Amiga OCS hardware: a **12-bit palette** (4 bits\nper channel, 4096 colors total). The general-purpose quantizers\n(pngquant, ImageMagick, Netpbm, ffmpeg, gifsicle, pngnq, didder)\nquantize into **24-bit sRGB** (8 bits per channel, 16M colors).\nThat precision gap alone gives the 24-bit tools ~1 dB of \"free\"\nPSNR — they can land on the optimum-MSE centroid; the Amiga-mode\nencoders have to snap to the nearest 4-bit-per-channel grid point.\n\nThat's why some 32-color rows below show png2amiga's PSNR _behind_\npngquant's by ~1 dB while still leading perceptually (51.55 vs\n51.14 SSIMULACRA2 — the 12-bit handicap costs PSNR but the \nOKLab + ocs-bruteforce quantizer still wins the eyeball test). And it's\nwhy the 256-color tier is closer on PSNR: there both encoders\nwork in 24-bit (png2amiga's `--chipset aga` gates lift the OCS\nsnap).\n\npng2amiga, ham_convert, and abc additionally reserve palette index 0\nfor black via their respective lock flags (`--lock-color0` /\n`black_bkd` / `-forcecolor 0 000`). The general-purpose quantizers\ndon't expose a \"pin one slot, quantize the rest\" knob, so they're\nfree to spend the black-slot bit elsewhere — another small advantage\nthat doesn't move the rankings.\n\n| Encoder     | Mode                              | PSNR (dB) | SSIMULACRA2 | Time (s) |\n|-------------|-----------------------------------|----------:|------------:|---------:|\n| **png2amiga** | **lores d=8 AGA + best**       | 32.30     | **82.83**   |    37.64 |\n| png2amiga   | lores d=8 AGA                     | 32.38     | 82.52       |     1.54 |\n| pngquant    | libimagequant 256 (`--speed 1`)   | 33.61     | 80.13       |     0.06 |\n| ffmpeg      | 256 (`palettegen`+FS)†            | 31.18     | 78.61       |     0.06 |\n| png2amiga   | HAM6 + sliced + best              | 30.99     | 76.15       |    24.80 |\n| png2amiga   | HAM6 + sliced                     | 30.50     | 75.60       |     0.38 |\n| ham_convert | SHAM6 (`ham6_sliced`, `dither_fs`)| 31.81     | 74.82       |    16.15 |\n| png2amiga   | HAM6 (no copper)                  | 30.22     | 72.94       |     0.26 |\n| png2amiga   | HAM6 + best (no copper)           | 30.22     | 72.94       |    10.52 |\n| Netpbm      | pnmquant 256 (`-floyd`)†          | 31.59     | 72.20       |     0.11 |\n| png2amiga   | EHB + strips + best               | 29.39     | 71.60       |    20.84 |\n| ham_convert | HAM6 q1 (fastest, `dither_fs`)    | 29.45     | 70.41       |     4.07 |\n| ham_convert | HAM6 q7 (max quality, `dither_fs`)| 30.04     | 70.05       |    46.42 |\n| gifsicle    | 256 (`--dither floyd-steinberg`)† | 32.94     | 69.47       |     0.02 |\n| didder      | 256 (`mmcq`+FS edm serpentine)†   | 28.27     | 68.52       |     0.13 |\n| ImageMagick | 256 (`-dither FS`)†               | 30.69     | 66.85       |     0.05 |\n| pngnq       | 256 (NeuQuant + FS, `-s 1`)†      | 31.61     | 65.73       |     0.07 |\n| abc         | HAM6 (`-floyd`)                   | 28.31     | 63.24       |     0.75 |\n| png2amiga   | EHB + best (no copper)            | 24.09     | 61.16       |     7.13 |\n| abc         | SHAM6 (`-floyd`)                  | 26.66     | 60.59       |     1.25 |\n| png2amiga   | lores d=5 + best                  | 23.04     | 57.65       |    11.32 |\n| png2amiga   | EHB (no copper)                   | 25.03     | 52.38       |     0.11 |\n| png2amiga   | lores d=5                         | 24.96     | 51.55       |     0.13 |\n| pngquant    | libimagequant 32 (`--speed 1`)    | 26.08     | 51.14       |     0.06 |\n| ham_convert | EHB (`dither_fs`)                 | 25.82     | 49.68       |     6.07 |\n| ham_convert | ocs32 (`dither_fs`)               | 24.42     | 37.70       |     6.05 |\n| abc         | lores d=5 (`-floyd`, `-bpc 5`)    | 25.35     | 36.32       |     2.34 |\n| ffmpeg      | 32 (`palettegen`+FS)†             | 23.74     | 31.72       |     0.05 |\n| pngnq       | 32 (NeuQuant + FS, `-s 1`)†       | 25.76     | 30.81       |     0.04 |\n| Netpbm      | pnmquant 32 (`-floyd`)†           | 23.96     | 30.08       |     0.11 |\n| didder      | 32 (`mmcq`+FS edm serpentine)†    | 22.56     | 22.19       |     0.10 |\n| ImageMagick | 32 (`-dither FS`)†                | 23.41     | 21.90       |     0.05 |\n| gifsicle    | 32 (`--dither floyd-steinberg`)†  | 20.84     | 15.70       |     0.03 |\n\n† None of the general-purpose quantizers (pngquant, ImageMagick,\nNetpbm, ffmpeg, gifsicle, pngnq, didder) expose a \"force one slot,\nquantize the rest\" flag — `-remap` / `-mapfile` / their equivalents\naccept either no constraints or a fully-fixed palette. Their palette\nis unconstrained on these runs, which is a small advantage on\nphotographic input that doesn't move the rankings. At 256 colors\nlibimagequant has the highest PSNR (33.61 dB) but lands ~3\nSSIMULACRA2 below png2amiga — typical MSE-vs-perceptual split when\nthe quantizer runs in linear/sRGB rather than a perceptually uniform\nspace. Notable per-tool observations: **gifsicle** at 256 has very\nhigh PSNR (32.94 dB, beating most of the Amiga-mode encoders) but\nsits 13 SSIMULACRA2 below png2amiga; **pngnq** (NeuQuant) lands well\nbelow its perceptual-aware competitors despite the slowest-quality\nsetting; **didder**'s `mmcq:N` median-cut pairs a clean dither\nimplementation with an unsophisticated quantizer.\n\n(Tools not included: `pngnq-s9` — sources broken at all known\nmirrors; `exoquant` — Rust library, no CLI; `gurkandemir/Color-Quantizer`\n— interactive K-means classroom tool. None of the three is a fair\nbenchmark target right now.)\n\nThe harness lives at `tools/shootout/`:\n\n```bash\ncd tools/shootout\n./setup.sh   # downloads ham_convert.jar, clones + builds abc on macOS\n./run.sh     # encodes examples/makena.jpg (or pass your own)\n```\n\n`tools/shootout/README.md` has the full method, the rationale for the\nmetric, and notes on why amigagfxmangle / DPaint.js / AGAConv were\nexcluded.\n\n## Amiga Executable Generation\n\nThe project includes\n[vscode-amiga-debug](https://github.com/BartmanAbyss/vscode-amiga-debug)\nas a submodule, which provides the `m68k-amiga-elf-gcc` cross-compiler,\n`elf2hunk`, `exe2adf`, `fs-uae`, and AmigaOS SDK headers — everything\nneeded to produce bootable disk images locally.\n\n```bash\ngit submodule update --init\n\n./build/png2amiga --mode ham6 input.png viewer.cpp\n./build-amiga.sh viewer.cpp viewer.adf\n./run-amiga.sh viewer.adf\n```\n\nThe generated viewer takes the system, sets up the copper list\n(including per-line sliced-palette changes if `--sliced` was used and\nmid-line strip swaps if `--strips` was used), and waits for the left\nmouse button to exit.\n\n## Build-system integration (CMake / Make / Ninja)\n\npng2amiga is designed to slot into a CMake-driven asset pipeline (e.g.\nVSCode + vscode-amiga-debug + WinUAE). Relevant flags:\n\n| Flag | Purpose |\n|---|---|\n| `-q` / `--quiet` | Suppress stdout status; errors still go to stderr |\n| `--json` | Emit a JSON status object on success (implies `--quiet`) |\n| `--depfile \u003cpath\u003e` | Write a Make-format depfile so changes to `--palette` files trigger a rebuild |\n| `--list-modes` | Print supported modes and exit (pair with `--json` for machine-readable catalog) |\n\n**Exit codes** follow `sysexits.h` so `RESULT_VARIABLE` distinguishes\nfailure categories: `0` ok, `1` internal/encode error, `64` usage error\n(bad CLI args), `66` input file unreadable, `73` output write failed.\n\n**CMake helper module** (`cmake/Png2amiga.cmake`) provides\n`png2amiga_add_image()`:\n\n```cmake\ninclude(/path/to/png2amiga/cmake/Png2amiga.cmake)\n\npng2amiga_add_image(\n  TARGET   sprites\n  INPUT    ${CMAKE_CURRENT_SOURCE_DIR}/art/title.png\n  OUTPUT   ${CMAKE_CURRENT_BINARY_DIR}/title.h\n           ${CMAKE_CURRENT_BINARY_DIR}/title.iff\n  MODE     ham6\n  OPTIONS  --sliced --ham-beam 32\n  PALETTE  ${CMAKE_CURRENT_SOURCE_DIR}/palette.gpl   # optional\n)\n```\n\nEach `OUTPUT` becomes its own `add_custom_command` so `make -jN` /\n`ninja` build them in parallel. Each command writes a `.d` depfile next\nto its output for accurate dependency tracking.\n\n**Determinism**: encoding is deterministic — same input + same flags\nalways produces byte-identical output. Multithreading (HAM beam search,\nOCS palette quantization) uses lock-free per-row work distribution with\ndeterministic merge order. Safe to use under `ccache` / build cache\nhashing.\n\n## Full CLI reference\n\n\u003c!--\n  KEEP IN SYNC with `./build/png2amiga --help`.\n  Refresh this block (and bump --help text on the same edit) before\n  committing any change that adds, removes, renames, or re-defaults a\n  flag. One-liner to regenerate (note: --help prints to stderr):\n      ./build/png2amiga --help 2\u003e /tmp/help.txt\n  Then paste between the fences below.\n--\u003e\n\n```\npng2amiga 1.88.0.1066\n\nUsage: png2amiga [options] input.[png|jpg|webp] [-o output.[png|iff|h|raw|pal|pi1|pi2|pi3]]\n\n\nModes:\n  --mode \u003cmode\u003e                   Graphics mode (default: lores)\n    Amiga:  lores | lores-lace | hires | hires-lace |\n            ham6[-hires][-lace] | ham8[-hires][-lace] | ehb[-lace]\n    Atari:  stf-low | stf-med | stf-hi | ste-low | ste-med | ste-hi\n    DOS:    vga-13h | vga-10h | vga-12h | ega-320 | ega-640 | ega-hi |\n            cga-320 | cga-640 | cga-composite |\n            cga-text80x{200,100,50,25}\n    SNES:   snes-mode7-256 | snes-mode7-direct\n    Genesis: genesis-h32 | genesis-h40 | genesis-h32-sh | genesis-h40-sh\n    C64:    c64-multicolor | c64-hires | c64-fli | c64-afli |\n            c64-petscii | c64-charset-hires | c64-charset-multicolor\n  --depth \u003c1-8\u003e                   Bitplane depth (default: 5)\n  --chipset ocs|aga               Amiga chipset (default: auto)\n  --dual-playfield, --dpf         Encode into PF2 (depth 3 OCS / 4 AGA)\n  --width \u003cint\u003e                   Override output width\n  --height \u003cint\u003e                  Override output height\n  --no-scale                      Use source dimensions verbatim\n\nDithering:\n  --dither \u003cmethod\u003e               Dither method (default: floyd-steinberg;\n                                  --list-dithers for the full catalog)\n  --dither-strength \u003cfloat\u003e       Dither amount 0.0-2.0 (default: 1.0)\n  --error-clamp \u003cfloat\u003e           Max error per channel (default: 0.35)\n  --refine \u003c0-32\u003e                 Palette refinement iterations (default: 8)\n\nPalette:\n  --palette \u003cfile\u003e                Load palette (.gpl, IFF, hex text, .json)\n  --quantize-from \u003cfile\u003e          Train palette on file, lock onto input\n  --joint-input, --ji \u003cfile\u003e      Add input to joint-palette training set\n  --output-each, --oe \u003cpattern\u003e   Per-input output: '.ext' or path with {dir}/{stem}\n  --quantizer \u003cname\u003e              auto | median-cut | ocs-bruteforce | pnn | gpu-restart\n  --palette-diversity \u003c0-9\u003e       Drop near-duplicate palette entries\n  --print-palette                 Dump final CMAP to stderr (text)\n  --print-palette-json            Dump final CMAP to stdout (JSON)\n\nPalette index pinning (lores/hires/EHB/Atari):\n  --no-lock-color0                Allow palette index 0 to be image color\n  --lock-index, --li \u003cid\u003e \u003chex\u003e   Pin slot's color; image pixels CAN\n                                  still route to it (quantizer uses it)\n  --reserve-range, --rr \u003cr\u003e \u003chex\u003e Pin slot's color; image pixels CANNOT\n                                  route to it (quantizer skips it).\n                                  Range: 0,1,5-10 / -5 / 5- (open ends)\n  --pin-index-at, --pia \u003cid\u003e \u003cx\u003e \u003cy\u003e\n                                  Swap pixel (x,y)'s slot with \u003cid\u003e\n\nSearch quality:\n  --best                          Multi-restart search (~20–30× slower).\n                                  Works with plain HAM/EHB/lores/hires\n                                  and with --sliced / --strips.\n\nImage processing:\n  --brightness \u003cfloat\u003e            -1.0..1.0 (default: 0.0)\n  --contrast \u003cfloat\u003e              0.0..3.0 (default: 1.0)\n  --saturation \u003cfloat\u003e            0.0..3.0 (default: 1.0)\n  --gamma \u003cfloat\u003e                 0.1..8.0 (default: 1.0)\n  --hue-shift \u003cfloat\u003e             -180..180 degrees (default: 0)\n  --sharpen \u003cfloat\u003e               -1.0..2.0 (default: 0.0)\n  --black-point \u003cfloat\u003e           0.0..0.5 (default: 0.0)\n  --white-point \u003cfloat\u003e           0.0..0.5 (default: 0.0)\n  --match-range                   Stretch source chroma per-(L, hue) onto palette gamut\n  --crop \u003cx,y,w,h\u003e                Manual crop region (pixels)\n  --crop-auto                     Auto-crop to mode aspect ratio\n  --trim                          Auto-crop to non-transparent bbox\n                                  (pair with --transparent-color for\n                                  opaque sources)\n  --flip-x, --flip-y              Mirror over Y / X axis\n  --rotate \u003c0|1|2|3|0|90|180|270\u003e Rotate clockwise before crop/scale\n\nTransparency:\n  --alpha-threshold \u003c-0.5..0.5\u003e   Offset from 0.5 midpoint (default: 0)\n  --alpha-dither \u003cmethod\u003e         Dither alpha (default: none)\n  --alpha-dither-strength \u003cfloat\u003e Alpha dither strength (default: 1.0)\n  --transparent-output-slot \u003cN\u003e   Write slot N (not 0) for alpha=0 pixels in\n                                  .idx / --output-each output. Pair with\n                                  --reserve-range N \u003ccolor\u003e so no opaque\n                                  pixel ever routes there.\n  --transparent-color, --tc \u003chex\u003e Treat sentinel RGB as alpha=0\n                                  (repeatable, e.g. magenta atlases)\n  --mask \u003cfile\u003e                   Export transparency mask\n                                  (.png/.iff/.raw/.h by extension)\n  --mask-invert                   Invert mask polarity\n  --mask-layout \u003cwhich\u003e           Embed mask in .bpl/.raw/.bin output:\n                                  appended | replicated. Mask is drawn\n                                  from alpha; for opaque sources pair\n                                  with --transparent-color RRGGBB.\n\nSliced palette (Amiga, per-line swaps; aka SHAM / DHIRES):\n  --sliced                        Per-scanline palette swaps\n  --slice-changes \u003c0-16\u003e          Swaps per line (0 = auto)\n  --sliced-vertical-dither        Spread copper transitions across rows\n\nStrip palette (mid-line swaps, OCS lores):\n  --strips                        Mid-line swaps; pair with --dpf or ehb\n\nSeamless tile:\n  --tile                          Replicate input 3x3 before dither, export center\n                                  tile only. Lores/hires/EHB only.\n\nCross-fade (lores/hires/EHB; --preview animates):\n  --fade-to \u003ctarget.png\u003e          Target image to fade INPUT into (INPUT is the start).\n  --fade-frames \u003c2-256\u003e           Frames per segment (default: 16)\n  --fade-loop                     Loop forward (source→...→target→source); else\n                                  ping-pong (source→...→target→...→source).\n\nHAM:\n  --ham-beam \u003c1-256\u003e              DP search beam (default: 48)\n  --ham-triple \u003c0-256\u003e            Triple-pixel refinement (default: 16)\n  --ham-fast                      Greedy encoder (no DP search)\n  --ham-metric \u003coklab2|srgb-mse\u003e  Op-selection metric (default: oklab2)\n\nPlatform-specific:\n  --native-par                    Letterbox / pillarbox to preserve\n                                  source aspect on fixed-buffer hardware\n  --tile-budget \u003cN\u003e               Max unique tiles for charset / tile modes\n  --tile-reserve \u003cN\u003e              Reserve N tile slots from the budget\n  --cga-palette \u003cp\u003e               p0-low | p0-high | p1-low | p1-high\n  --cga-bg \u003c0..15\u003e                CGA background color\n  --cga-text-metric \u003cm\u003e           blur (default) | mse\n  --cga-text-kernel \u003ck\u003e           Blur kernel: auto | binomial | aniso53 |\n                                  aniso73 | aniso35 | aniso37 | wide55 | wide77\n  --c64-palette \u003cp\u003e               pepto | vice | colodore (default) |\n                                  deekay | godot | c64wiki | levy\n  --c64-metric \u003cm\u003e                blur (default) | mse\n  --c64-petscii-graphics          Restrict PETSCII to graphics glyphs\n\nOutput:\n  --symbol \u003cname\u003e                 Base symbol name (default: from filename)\n  --fade-in                       Fade in/out on viewer entry / exit\n  --layout \u003cwhich\u003e                auto | interleaved | standard |\n                                  word-interleaved\n  --non-interleaved, --planar     Alias for --layout standard\n  --interleaved                   Alias for --layout interleaved\n  --output-indexed \u003cfile\u003e         Raw chunky indices: 1 byte/pixel,\n                                  scan order, no header (post-pin)\n  --preview                       Inline preview (iTerm2, kitty, sixel)\n  --preview-scale \u003c1-8\u003e           Preview display scale\n  --preview-video                 Batch only: loop frames inline\n  --preview-video-fps \u003cfps\u003e       Playback rate (default 12.5)\n    Extensions:\n      .png                          Preview (24-bit)\n      .iff / .ilbm                  Amiga IFF ILBM\n      .h                            C header (Amiga UWORD bitplane arrays)\n      .cpp / .c                     Amiga cpp viewer (build-amiga.sh);\n                                    DOS C viewer with PC modes (ia16-elf-gcc)\n      .raw / .bin / .bpl            Raw bitplanes (writes .pal sibling;\n                                    embeds mask if --mask-layout set)\n      .pal                          OCS palette only (big-endian 0x0RGB words)\n      .idx                          Raw chunky indices (1 byte/pixel, scan order);\n                                    also via --output-indexed / --output-each .idx\n      .pi1 / .pi2 / .pi3            Atari Degas (STF/STE low / med / hi)\n      .prg                          C64 PRG (autostart)\n      .koa                          C64 Koala paint\n      .hir                          C64 hires bitmap\n\nBatch (multi-frame, shared palette / copper):\n  --batch \u003cdir\u003e                   Encode N inputs as a horizontal atlas;\n                                  emit per-frame outputs into \u003cdir\u003e\n  --batch-format \u003cext\u003e            h (default) | iff | png | raw | cpp\n\nBuild integration:\n  -q, --quiet                     Suppress stdout status (errors → stderr)\n  --json                          JSON status output (implies --quiet)\n  --depfile \u003cpath\u003e                Write a Make-format depfile\n  --list-modes                    Print supported modes and exit\n  --list-dithers                  Print supported dither methods and exit\n  --profile \u003cN\u003e                   Run encode N times for sampling profilers\n  --score-vs \u003cref\u003e                Score input against reference (no encoding).\n                                  Input may be a .png OR a .idx (raw chunky\n                                  bytes); .idx requires --palette and inherits\n                                  dims from \u003cref\u003e.\n\nExit codes (sysexits.h):\n  0 ok    1 internal    64 usage    66 no input    73 cannot create\n\nExit codes (sysexits.h):\n  0 ok    1 internal    64 usage    66 no input    73 cannot create\n\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftinic%2Fpng2amiga","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftinic%2Fpng2amiga","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftinic%2Fpng2amiga/lists"}