diff --git a/Makefile b/Makefile index 055451e..23c402f 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,10 @@ .PHONY: all -all: bin/script-decode bin/sbutil bin/just-stats +all: bin/script-decode bin/sbutil bin/just-stats bin/extract-imgs -bin/script-decode: cmd/script-decode.go script/*.go - go build -o $@ $< +bin/script-decode: script/*.go +bin/sbutil: rom/*.go +bin/just-stats: script/*.go -bin/sbutil: cmd/sbutil.go rom/*.go - go build -o $@ $< - -bin/just-stats: cmd/just-stats.go script/*.go +bin/%: cmd/%.go go build -o $@ $< diff --git a/cmd/extract-imgs.go b/cmd/extract-imgs.go new file mode 100644 index 0000000..b2b3e79 --- /dev/null +++ b/cmd/extract-imgs.go @@ -0,0 +1,575 @@ +package main + +import ( + "fmt" + "os" + "image" + "image/draw" + "image/color" + "image/png" + //"strings" + "encoding/binary" + + "github.com/alexflint/go-arg" + + nesimg "github.com/zorchenhimer/go-retroimg" + //"github.com/zorchenhimer/go-retroimg/palette" +) + +type Arguments struct { + Nametable string `arg:"--nt,required"` + Chr string `arg:"--chr,required"` + Output string `arg:"--output,required"` + IsSprite bool `arg:"--sprites"` + SpriteSheet bool `arg:"--sprite-sheet"` + NoBg bool `arg:"--no-bg"` // don't draw hot-pink background +} + +var ( + tileMissing *nesimg.Tile +) + +func main() { + args := &Arguments{} + arg.MustParse(args) + + var err error + tileMissing, err = nesimg.NewTileFromPlanes([][]byte{ + {0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33}, + {0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F} }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err = run(args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args *Arguments) error { + fmt.Println("--") + fmt.Println("Nametable:", args.Nametable) + fmt.Println("CHR:", args.Chr) + fmt.Println("Output:", args.Output) + fmt.Println("IsSprite:", args.IsSprite) + fmt.Println("SpriteSheet:", args.SpriteSheet) + fmt.Println("--") + + chrFile, err := os.Open(args.Chr) + if err != nil { + return err + } + defer chrFile.Close() + + raw := nesimg.NewRawChr(chrFile) + tiles, err := raw.ReadAllTiles(nesimg.BD_2bpp) + if err != nil { + return err + } + + //fmt.Printf("first tile: %#v\n", tiles[0]) + + //tileUniform_00 := image.NewUniformPaletted(nesimg.DefaultPal_2bpp, 0) + //tileUniform_F0 := image.NewUniformPaletted(nesimg.DefaultPal_2bpp, 1) + //tileUniform_0F := image.NewUniformPaletted(nesimg.DefaultPal_2bpp, 2) + //tileUniform_FF := image.NewUniformPaletted(nesimg.DefaultPal_2bpp, 3) + + tileUniform_00, err := nesimg.NewTileFromPlanes([][]byte{ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} }) + if err != nil { + return err + } + + tileUniform_F0, err := nesimg.NewTileFromPlanes([][]byte{ + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} }) + if err != nil { + return err + } + + tileUniform_0F, err := nesimg.NewTileFromPlanes([][]byte{ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} }) + if err != nil { + return err + } + + tileUniform_FF, err := nesimg.NewTileFromPlanes([][]byte{ + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} }) + if err != nil { + return err + } + + tiles = append([]*nesimg.Tile{ + tileUniform_00, + tileUniform_F0, + tileUniform_0F, + tileUniform_FF, + }, tiles...) + + ntData, err := os.ReadFile(args.Nametable) + if err != nil { + return err + } + + layers, err := ReadData(ntData, tiles, args.IsSprite) + if err != nil { + return fmt.Errorf("ReadData() error: %w", err) + } + + uni := image.NewUniform(color.RGBA{0xFF, 0x00, 0xFF, 0xFF}) + var screen *image.RGBA + if args.SpriteSheet { + maxWidth := 0 + maxHeight := 0 + for _, l := range layers { + maxWidth += l.Bounds().Dx() + if l.Bounds().Dy() > maxHeight { + maxHeight = l.Bounds().Dy() + } + } + screen = image.NewRGBA(image.Rect(0, 0, maxWidth, maxHeight)) + if !args.NoBg { + draw.Draw(screen, screen.Bounds(), uni, image.Pt(0, 0), draw.Over) + } + + loc := image.Pt(0, 0) + for _, l := range layers { + draw.Draw(screen, l.Bounds().Add(loc), l, image.Pt(0, 0), draw.Over) + loc = loc.Add(image.Pt(l.Bounds().Dx(), 0)) + } + + } else { + screen = image.NewRGBA(image.Rect(0, 0, 32*8, 30*8)) + if !args.NoBg { + draw.Draw(screen, screen.Bounds(), uni, image.Pt(0, 0), draw.Over) + } + + for _, l := range layers { + if args.IsSprite { + draw.Draw(screen, l.Bounds().Add(l.Location), l, image.Pt(0, 0), draw.Over) + } else { + draw.Draw(screen, screen.Bounds(), l, image.Pt(0, 0), draw.Over) + } + } + } + + output, err := os.Create(args.Output) + if err != nil { + return err + } + defer output.Close() + + err = png.Encode(output, screen) + if err != nil { + return err + } + + //img := image.NewRGBA(image.Rect(0, 0, 32*8, 30*8)) + return nil +} + +//type UniformPaletted struct { +// Palette color.Palette +// Index int +//} +// +//func NewUniformPaletted(palette color.Palette, idx int) *UniformPaletted { +// if idx >= len(palette) { +// panic("Index out of palette range") +// } +// +// return &UniformPaletted{ +// Palette: palette, +// Index: idx, +// } +//} +// +//func (u *UniformPaletted) At(x, y int) color.Color { +// if u.Index >= len(u.Palette) { +// panic("Index out of palette range") +// } +// +// return u.Palette[u.Index] +//} +// +//func (u *UniformPaletted) ColorModel() color.Model { +// return u.Palette +//} +// +//func (u *UniformPaletted) Bounds() image.Rectangle { +// // Copied from image.Uniform.Bounds() +// return image.Rectangle{image.Point{-1e9, -1e9}, image.Point{1e9, 1e9}} +//} + +func BytesToInt(raw []byte) int { + if len(raw) > 2 { + panic("only 8 and 16 bit numbers for now") + } + + if len(raw) == 1 { + return int(raw[0]) + } + + return int(raw[1])<<8 | int(raw[0]) +} + +type DataHeader struct { + PaletteOffset uint16 + ArgB uint16 + ImageCount uint8 +} + +func (h DataHeader) String() string { + return fmt.Sprintf("{DataHeader PaletteOffset:$%04X ArgB:$%04X ImageCount:%d}", + h.PaletteOffset, + h.ArgB, + h.ImageCount, + ) +} + +type ImageHeader struct { + Width uint8 + Height uint8 + AttrLength uint16 + + // in pixels + X uint8 + Y uint8 +} + +func (h ImageHeader) String() string { + return fmt.Sprintf("{ImageHeader Width:%d[%02X] Height:%d[%02X] AttrLength:$%04X XCoord:%d[%02X] YCoord:%d[%02X]}", + h.Width, h.Width, + h.Height, h.Height, + h.AttrLength, + h.X, h.X, + h.Y, h.Y, + ) +} + +func ReadData(raw []byte, tiles []*nesimg.Tile, isSprites bool) ([]*Layer, error) { + //raw, err := io.ReadAll(r) + //if err != nil { + // return nil, err + //} + + if isSprites { + tiles = tiles[4:] + } + + dataHeader := &DataHeader{} + _, err := binary.Decode(raw, binary.LittleEndian, dataHeader) + if err != nil { + return nil, err + } + dataHeader.PaletteOffset += 4 + + fmt.Println(dataHeader) + + imgHeaders := []*ImageHeader{} + for i := 0; i < int(dataHeader.ImageCount); i++ { + head := &ImageHeader{} + _, err = binary.Decode(raw[4+(i*6)+1:], binary.LittleEndian, head) + if err != nil { + return nil, err + } + fmt.Println(head) + imgHeaders = append(imgHeaders, head) + } + + palettes := []color.Palette{} + colorIds := [][]string{} + for i := 0; i < 4; i++ { + p := color.Palette{} + idlist := []string{} + for j := 0; j < 4; j++ { + v := raw[int(dataHeader.PaletteOffset)+(i*4)+j] + if v == 0x3D { + v = 0x0F + } + c, ok := NesColors[v] + if !ok { + return nil, fmt.Errorf("Color value $%02X invalid", v) + } + p = append(p, c) + idlist = append(idlist, fmt.Sprintf("%02X", v)) + } + palettes = append(palettes, p) + colorIds = append(colorIds, idlist) + } + + fmt.Println("Palettes:") + for i := 0; i < len(palettes); i++ { + fmt.Printf("%s: %v\n", colorIds[i], palettes[i]) + } + + nesTileSize := image.Rect(0, 0, 8, 8) + + offset := 5 + len(imgHeaders) * 6 + layers := []*Layer{} + for _, head := range imgHeaders { + rect := image.Rect(0, 0, 32, 30) + if isSprites { + rect = image.Rect(0, 0, int(head.Width), int(head.Height)) + } + + l := NewLayer( + rect, + nesTileSize, + palettes, + ) + l.Transparency = isSprites + l.Location = image.Pt(int(head.X), int(head.Y)) + l.IsSprite = isSprites + //tileIds := []int{} + + if isSprites { + for idx, b := range raw[offset:offset+(int(head.Width)*int(head.Height))] { + id := int(uint(b)) // is this required to ignore negatives? + if id < len(tiles) { + l.Tiles[idx] = tiles[id] + } + } + } else { + for idx, b := range raw[offset:offset+(int(head.Width)*int(head.Height))] { + row := idx / int(head.Width) + col := idx % int(head.Width) + arrIdx := ((row+int(head.Y/8)) * 32) + (col+int(head.X/8)) + //fmt.Printf("%d ", arrIdx) + id := int(uint(b)) // is this required to ignore negatives? + if id < len(tiles) { + l.Tiles[arrIdx] = tiles[id] + //tileIds = append(tileIds, id) + } + } + } + + //for idx, t := range tileIds { + // fmt.Printf("[%d] %02X\n", idx, t) + //} + + offset += int(head.Width)*int(head.Height) + + if isSprites { + l.Attributes = raw[offset:offset+int(head.AttrLength)] + } else { + // FIXME: This will break on background chunks that aren't a full screen. + // Need to verify the anchor point in the firmware for this case (for + // when the tile anchor point isn't aligned to an attribute byte). + // Partial screen attributes will also be a different size than 8*8. + l.SetAttributes(raw[offset:offset+int(head.AttrLength)]) + } + offset += int(head.AttrLength) + + layers = append(layers, l) + } + + return layers, nil +} + +type Layer struct { + Tiles []*nesimg.Tile + Attributes []byte + Palettes []color.Palette + TileSize image.Rectangle + + Location image.Point + + Width int + Height int + Transparency bool // true for sprites + IsSprite bool + + Solid bool +} + +func NewLayer(layerSize image.Rectangle, tileSize image.Rectangle, palettes []color.Palette) *Layer { + return &Layer{ + Tiles: make([]*nesimg.Tile, layerSize.Dx()*layerSize.Dy()), + Attributes: make([]byte, layerSize.Dx()*layerSize.Dy()), + TileSize: tileSize, + Location: image.Pt(0, 0), + Width: layerSize.Dx(), + Height: layerSize.Dy(), + Palettes: palettes, + } +} + +func (l *Layer) At(x, y int) color.Color { + if l.Solid { + return color.RGBA{0x00, 0x00, 0x00, 0xFF} + } + + width, height := l.TileSize.Dx(), l.TileSize.Dy() + + row := y / height + col := x / width + tx := x % width + ty := y % height + + tileIdx := (row*l.Width)+col + + if x == 89 && y == 101 { + fmt.Printf("row:%d col:%d tx:%d ty:%d tileIdx:%d width:%d height:%d l.Width:%d l.Height:%d\n", + row, col, tx, ty, tileIdx, width, height, l.Width, l.Height, + ) + } + + if l.Tiles[tileIdx] == nil { + return color.RGBA{0x00, 0x00, 0x00, 0x00} + //return color.RGBA{0xFF, 0x00, 0xFF, 0xFF} + } + + colorIdx := l.Tiles[tileIdx].ColorIndexAt(tx, ty) + if l.Transparency && colorIdx == 0 { + return color.RGBA{0x00, 0x00, 0x00, 0x00} + } + + palIdx := l.Attributes[tileIdx] + return l.Palettes[palIdx][colorIdx] +} + +func (l *Layer) Bounds() image.Rectangle { + return image.Rect(0, 0, l.TileSize.Max.X*l.Width, l.TileSize.Max.Y*l.Height) +} + +func (l *Layer) ColorModel() color.Model { + return color.RGBAModel +} + +func (sc *Layer) SetAttributes(data []byte) error { + if len(data) != 64 { + return fmt.Errorf("Attribute data must be 64 bytes") + } + + sc.Attributes = make([]byte, 32*30) + for row := 0; row < 8; row++ { + for col := 0; col < 8; col++ { + src := row*8+col + start := (row*32)*4 + (col*4) + + raw := data[src] + + br := (raw >> 6) & 0x03 + bl := (raw >> 4) & 0x03 + tr := (raw >> 2) & 0x03 + tl := raw & 0x03 + + //if row == 0 && col == 0 { + // fmt.Printf("br:%02X bl:%02X tr:%02X tl:%02X\n", br, bl, tr, tl) + //} + + sc.Attributes[start+0+(32*0)] = tl + sc.Attributes[start+1+(32*0)] = tl + sc.Attributes[start+0+(32*1)] = tl + sc.Attributes[start+1+(32*1)] = tl + + sc.Attributes[start+2+(32*0)] = tr + sc.Attributes[start+3+(32*0)] = tr + sc.Attributes[start+2+(32*1)] = tr + sc.Attributes[start+3+(32*1)] = tr + + if row < 7 { + sc.Attributes[start+0+(32*2)] = bl + sc.Attributes[start+1+(32*2)] = bl + sc.Attributes[start+0+(32*3)] = bl + sc.Attributes[start+1+(32*3)] = bl + + sc.Attributes[start+2+(32*2)] = br + sc.Attributes[start+3+(32*2)] = br + sc.Attributes[start+2+(32*3)] = br + sc.Attributes[start+3+(32*3)] = br + } + } + } + + return nil +} + +var NesColors map[byte]color.Color = map[byte]color.Color{ + 0x00: color.RGBA{0x66, 0x66, 0x66, 0xFF}, + 0x10: color.RGBA{0xAD, 0xAD, 0xAD, 0xFF}, + 0x20: color.RGBA{0xFF, 0xFF, 0xEF, 0xFF}, + 0x30: color.RGBA{0xFF, 0xFF, 0xEF, 0xFF}, + + 0x01: color.RGBA{0x00, 0x2A, 0x88, 0xFF}, + 0x11: color.RGBA{0x15, 0x5F, 0xD9, 0xFF}, + 0x21: color.RGBA{0x64, 0xB0, 0xFF, 0xFF}, + 0x31: color.RGBA{0xC0, 0xDF, 0xFF, 0xFF}, + + 0x02: color.RGBA{0x14, 0x12, 0xA7, 0xFF}, + 0x12: color.RGBA{0x42, 0x40, 0xFF, 0xFF}, + 0x22: color.RGBA{0x92, 0x90, 0xFF, 0xFF}, + 0x32: color.RGBA{0xD3, 0xD2, 0xFF, 0xFF}, + + 0x03: color.RGBA{0x3B, 0x00, 0xA4, 0xFF}, + 0x13: color.RGBA{0x75, 0x27, 0xFE, 0xFF}, + 0x23: color.RGBA{0xC6, 0x76, 0xFF, 0xFF}, + 0x33: color.RGBA{0xE8, 0xC8, 0xFF, 0xFF}, + + 0x04: color.RGBA{0x5C, 0x00, 0x7E, 0xFF}, + 0x14: color.RGBA{0xA0, 0x1A, 0xCC, 0xFF}, + 0x24: color.RGBA{0xF3, 0x6A, 0xFF, 0xFF}, + 0x34: color.RGBA{0xFB, 0xC2, 0xFF, 0xFF}, + + 0x05: color.RGBA{0x6E, 0x00, 0x40, 0xFF}, + 0x15: color.RGBA{0xB7, 0x1E, 0x7B, 0xFF}, + 0x25: color.RGBA{0xFE, 0x6E, 0xCC, 0xFF}, + 0x35: color.RGBA{0xFE, 0xC4, 0xEA, 0xFF}, + + 0x06: color.RGBA{0x6C, 0x06, 0x00, 0xFF}, + 0x16: color.RGBA{0xB5, 0x31, 0x20, 0xFF}, + 0x26: color.RGBA{0xFE, 0x81, 0x70, 0xFF}, + 0x36: color.RGBA{0xFE, 0xCC, 0xC5, 0xFF}, + + 0x07: color.RGBA{0x56, 0x1D, 0x00, 0xFF}, + 0x17: color.RGBA{0x99, 0x4E, 0x00, 0xFF}, + 0x27: color.RGBA{0xEA, 0x9E, 0x22, 0xFF}, + 0x37: color.RGBA{0xF7, 0xD8, 0xA5, 0xFF}, + + 0x08: color.RGBA{0x33, 0x35, 0x00, 0xFF}, + 0x18: color.RGBA{0x6B, 0x6D, 0x00, 0xFF}, + 0x28: color.RGBA{0xBC, 0xBE, 0x00, 0xFF}, + 0x38: color.RGBA{0xE4, 0xE5, 0x94, 0xFF}, + + 0x09: color.RGBA{0x0B, 0x48, 0x00, 0xFF}, + 0x19: color.RGBA{0x38, 0x87, 0x00, 0xFF}, + 0x29: color.RGBA{0x88, 0xD8, 0x00, 0xFF}, + 0x39: color.RGBA{0xCF, 0xEF, 0x96, 0xFF}, + + 0x0A: color.RGBA{0x00, 0x52, 0x00, 0xFF}, + 0x1A: color.RGBA{0x0C, 0x93, 0x00, 0xFF}, + 0x2A: color.RGBA{0x5C, 0xE4, 0x30, 0xFF}, + 0x3A: color.RGBA{0xBD, 0xF4, 0xAB, 0xFF}, + + 0x0B: color.RGBA{0x00, 0x4F, 0x08, 0xFF}, + 0x1B: color.RGBA{0x00, 0x8F, 0x32, 0xFF}, + 0x2B: color.RGBA{0x45, 0xE0, 0x82, 0xFF}, + 0x3B: color.RGBA{0xB3, 0xF3, 0xCC, 0xFF}, + + 0x0C: color.RGBA{0x00, 0x40, 0x4D, 0xFF}, + 0x1C: color.RGBA{0x00, 0x7C, 0x8D, 0xFF}, + 0x2C: color.RGBA{0x48, 0xCD, 0xDE, 0xFF}, + 0x3C: color.RGBA{0xB5, 0xEB, 0xF2, 0xFF}, + + 0x0D: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x1D: color.RGBA{0x00, 0x7C, 0x8D, 0xFF}, + 0x2D: color.RGBA{0x4F, 0x4F, 0x4F, 0xFF}, + 0x3D: color.RGBA{0xB8, 0xB8, 0xB8, 0xFF}, + + 0x0E: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x1E: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x2E: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x3E: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + + 0x0F: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x1F: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x2F: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 0x3F: color.RGBA{0x00, 0x00, 0x00, 0xFF}, +} + diff --git a/go.mod b/go.mod index dc82727..56bb205 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,10 @@ module git.zorchenhimer.com/Zorchenhimer/go-studybox -go 1.22.2 +go 1.24.1 -require github.com/alexflint/go-arg v1.4.3 +require ( + github.com/alexflint/go-arg v1.5.1 + github.com/zorchenhimer/go-retroimg v0.0.0-20250928212219-4e2260a7a700 +) -require github.com/alexflint/go-scalar v1.1.0 // indirect +require github.com/alexflint/go-scalar v1.2.0 // indirect diff --git a/go.sum b/go.sum index 9ef1991..a9ba63b 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,15 @@ -github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= -github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= -github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= -github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/zorchenhimer/go-retroimg v0.0.0-20250928212219-4e2260a7a700 h1:bsB3c08qVDW7tJ5lXdyvkTjCmHa+of5TmHL5Y9f7LJs= +github.com/zorchenhimer/go-retroimg v0.0.0-20250928212219-4e2260a7a700/go.mod h1:iQUJQkvvbgycl7TS2OWdSC0+kHYypOASX129xmnv+SE= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=