Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Created March 16, 2026 22:02
Show Gist options
  • Select an option

  • Save peterhellberg/5f881735c6e91eadeefd8e0a7e6b3bab to your computer and use it in GitHub Desktop.

Select an option

Save peterhellberg/5f881735c6e91eadeefd8e0a7e6b3bab to your computer and use it in GitHub Desktop.
Conversion between .4bpp to .png and vice versa
package main
import (
"fmt"
"image"
"image/color"
"image/png"
"io"
"os"
)
const (
width = 128
height = 128
)
func main() {
if len(os.Args) != 3 {
fmt.Println("usage: decode input.4bpp output.png")
return
}
in, err := os.Open(os.Args[1])
if err != nil {
panic(err)
}
defer in.Close()
// ---- PICO-8 Palette ----
palette := []color.RGBA{
{0, 0, 0, 255},
{29, 43, 83, 255},
{126, 37, 83, 255},
{0, 135, 81, 255},
{171, 82, 54, 255},
{95, 87, 79, 255},
{194, 195, 199, 255},
{255, 241, 232, 255},
{255, 0, 77, 255},
{255, 163, 0, 255},
{255, 236, 39, 255},
{0, 228, 54, 255},
{41, 173, 255, 255},
{131, 118, 156, 255},
{255, 119, 168, 255},
{255, 204, 170, 255},
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
// ---- Decode Tiles (16x16) ----
for ty := range 16 {
for tx := range 16 {
for row := range 8 {
for col := 0; col < 8; col += 2 {
var b [1]byte
if _, readErr := io.ReadFull(in, b[:]); readErr != nil {
panic("unexpected EOF")
}
// Packed 4bpp:
// low nibble = first pixel
// high nibble = second pixel
p1 := b[0] & 0x0F
p2 := b[0] >> 4
x := tx*8 + col
y := ty*8 + row
img.Set(x, y, palette[p1])
img.Set(x+1, y, palette[p2])
}
}
}
}
out, err := os.Create(os.Args[2])
if err != nil {
panic(err)
}
defer out.Close()
if err := png.Encode(out, img); err != nil {
panic(err)
}
fmt.Println("Decoded successfully.")
}
package main
import (
"fmt"
"image/color"
"image/png"
"os"
)
const (
width = 128
height = 128
tile = 8
tilesX = width / tile
tilesY = height / tile
)
func main() {
if len(os.Args) != 3 {
fmt.Println("usage: png-to-4bpp input.png output.4bpp")
return
}
// ---- Open PNG ----
f, err := os.Open(os.Args[1])
if err != nil {
panic(err)
}
defer f.Close()
img, err := png.Decode(f)
if err != nil {
panic(err)
}
bounds := img.Bounds()
if bounds.Dx() != width || bounds.Dy() != height {
panic("image must be exactly 128x128")
}
// ---- PICO-8 Palette ----
palette := []color.RGBA{
{0, 0, 0, 255},
{29, 43, 83, 255},
{126, 37, 83, 255},
{0, 135, 81, 255},
{171, 82, 54, 255},
{95, 87, 79, 255},
{194, 195, 199, 255},
{255, 241, 232, 255},
{255, 0, 77, 255},
{255, 163, 0, 255},
{255, 236, 39, 255},
{0, 228, 54, 255},
{41, 173, 255, 255},
{131, 118, 156, 255},
{255, 119, 168, 255},
{255, 204, 170, 255},
}
// Reverse palette lookup (RGBA → index)
colorToIndex := make(map[color.RGBA]uint8)
for i, c := range palette {
colorToIndex[c] = uint8(i)
}
out, err := os.Create(os.Args[2])
if err != nil {
panic(err)
}
defer out.Close()
// ---- Encode Tiles in Correct Order ----
for ty := range tilesY {
for tx := range tilesX {
for row := range tile {
for col := 0; col < tile; col += 2 {
x1 := tx*tile + col
y := ty*tile + row
x2 := x1 + 1
c1 := colorToIndex[img.At(x1, y).(color.RGBA)]
c2 := colorToIndex[img.At(x2, y).(color.RGBA)]
// Packed format:
// low nibble = first pixel
// high nibble = second pixel
byteVal := (c2 << 4) | c1
if _, err := out.Write([]byte{byteVal}); err != nil {
panic(err)
}
}
}
}
}
fmt.Println("Encoded successfully.")
}
@peterhellberg
Copy link
Copy Markdown
Author

1x

spr

10x

spr_scaled_10x_pngcrushed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment