Skip to content

Commit

Permalink
feat: add zbm_packer (#26)
Browse files Browse the repository at this point in the history
* feat: add zbm_packer

* simplify
  • Loading branch information
halamix2 authored Dec 22, 2024
1 parent f1bb0eb commit 4bbad2b
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 23 deletions.
12 changes: 6 additions & 6 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ builds:
# main: ./cmd/zwf_pack
# binary: zbc_pack
# id: zbc_pack
# - env: *envs
# goos: *gooses
# goarch: *goarchs
# main: ./cmd/zbm_pack
# binary: zbm_pack
# id: zbm_pack
- env: *envs
goos: *gooses
goarch: *goarchs
main: ./cmd/zbm_pack
binary: zbm_pack
id: zbm_pack
# - env: *envs
# goos: *gooses
# goarch: *goarchs
Expand Down
156 changes: 156 additions & 0 deletions cmd/zbm_pack/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
zbm_pack converts popular image formats to Gamewave .zbm images.
*/
package main

import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/iafan/cwalk"
"github.com/namgo/GameWaveFans/pkg/zbm"
"github.com/spf13/pflag"
)

// flags
var (
outputName string
)

func parseFlags() {
pflag.StringVarP(&outputName, "output", "o", "", "name of the output file")
pflag.Parse()
}

func usage() {
fmt.Println("Packs image to .zbm texture format used by Gamewave console")
fmt.Println("Flags:")
pflag.PrintDefaults()
}

func main() {
failed := false
parseFlags()
args := pflag.Args()
if len(args) < 1 {
usage()
os.Exit(1)
}

if outputName != "" && len(args) > 1 {
fmt.Println("Output name can only be used with one input file")
usage()
os.Exit(1)
}

for _, inputName := range args {
f, err := os.Stat(inputName)
if err != nil {
fmt.Printf("Failed to get info about %s: %s", inputName, err)
failed = true
}
if f.IsDir() {
walkFunc := getWalkFunc(inputName)
err := cwalk.Walk(inputName, walkFunc)
if err != nil {
fmt.Printf("Failed to unpack dir %s: %s\n", inputName, err)
// for _, errors := range err.(cwalk.WalkerError).ErrorList {
// fmt.Println(errors)
// }
failed = true
}
} else {
if outputName == "" || len(args) > 1 {
outputName = strings.TrimSuffix(inputName, filepath.Ext(inputName)) + ".zbm"
}
err := packTexture(inputName, outputName)
if err != nil {
fmt.Printf("Failed to unpack %s: %s", inputName, err)
failed = true
}
}
}
if failed {
os.Exit(1)
}
}

func getWalkFunc(basePath string) filepath.WalkFunc {
return func(path string, info fs.FileInfo, _ error) error {
if !info.IsDir() {
// check if file is an image
// file deepcode ignore PT: This is CLI tool, this is intended to be traversable
file, err := os.Open(filepath.Join(basePath, path))
if err != nil {
return err
}
_, format, err := image.DecodeConfig(file)
if err != nil {
return err
}

err = file.Close()
if err != nil {
return err
}

if err != nil && format != zbm.FormatName {
outputName = filepath.Join(basePath, strings.TrimSuffix(path, filepath.Ext(path))+".zbm")
return packTexture(filepath.Join(basePath, path), outputName)
}
}
return nil
}
}

func packTexture(inputName, outputName string) error {
// file deepcode ignore PT: This is CLI tool, this is intended to be traversable
file, err := os.Open(inputName)
if err != nil {
return fmt.Errorf("couldn't open file %s: %s", inputName, err)
}
config, format, err := image.DecodeConfig(file)
if err != nil {
return fmt.Errorf("couldn't read image file config %s: %s", inputName, err)
}

fmt.Printf("Packing %s (detected %s): %dx%d\n", inputName, format, config.Width, config.Height)

_, err = file.Seek(0, 0)
if err != nil {
return fmt.Errorf("couldn't seek in image file %s: %s", inputName, err)
}

img, _, err := image.Decode(file)
if err != nil {
return fmt.Errorf("couldn't read image file %s: %s", inputName, err)
}

err = file.Close()
if err != nil {
return fmt.Errorf("couldn't close image file %s: %s", inputName, err)
}

outputFile, err := os.Create(outputName)
if err != nil {
return fmt.Errorf("couldn't create output image file %s: %s", outputName, err)
}

err = zbm.Encode(outputFile, img)
if err != nil {
return fmt.Errorf("couldn't pack output image %s: %s", outputName, err)
}

err = outputFile.Close()
if err != nil {
return fmt.Errorf("couldn't close output image file %s: %s", outputName, err)
}

return nil
}
3 changes: 3 additions & 0 deletions cmd/zbm_unpack/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ func main() {
os.Exit(1)
}

zbm.RegisterFormat()

for _, inputName := range args {
f, err := os.Stat(inputName)
if err != nil {
Expand Down Expand Up @@ -149,6 +151,7 @@ func unpackTexture(inputName, outputName string) error {
return fmt.Errorf("couldn't pack output image %s: %s", outputName, err)
}

// TODO: won't close file on error in unpacking
err = outputFile.Close()
if err != nil {
return fmt.Errorf("couldn't close image file %s: %s", outputName, err)
Expand Down
7 changes: 7 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ func ReadUint32(r io.ReadSeeker, offset int64) (uint32, error) {
return headerInt, nil
}

// WriteUint32 writes a Little Endian number to an io.Writer
func WriteUint32(w io.Writer, data uint32) (int, error) {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, data)
return w.Write(buf)
}

// ReadBytes reads length bytes from a specified location in a stream
func ReadBytes(r io.ReadSeeker, offset int64, length int) ([]byte, error) {
if _, err := r.Seek(offset, 0); err != nil {
Expand Down
18 changes: 18 additions & 0 deletions pkg/zbm/common.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
// Package zbm helps interfacing with zbm files, un unpack and repack them
package zbm

import "io"

type config struct {
r io.Reader
unknown1 uint32
unknown2 uint32
unknown3 uint32
unknown4 uint32
width uint32
height uint32
unknown5 uint32
unknown6 uint32
unknown7 uint32
sizePacked uint32
sizeUnpacked uint32
unknown8 uint32
}

// A FormatError reports that the input is not a valid Gamewave texture.
type FormatError string

Expand Down
20 changes: 3 additions & 17 deletions pkg/zbm/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,6 @@ import (
"github.com/namgo/GameWaveFans/pkg/common"
)

type config struct {
r io.Reader
unknown1 uint32
unknown2 uint32
unknown3 uint32
unknown4 uint32
width uint32
height uint32
unknown5 uint32
unknown6 uint32
unknown7 uint32
sizePacked uint32
sizeUnpacked uint32
unknown8 uint32
}

func getPixelValue(value uint16) (uint8, uint8, uint8, uint8) {
// most images store pixels in big endian, 4633

Expand Down Expand Up @@ -135,6 +119,8 @@ func DecodeConfig(r io.Reader) (image.Config, error) {
}, nil
}

func init() {
// RegisterFormat registers format to be used by image.DecodeConfig
// in normal circumstances we'd call image.RegisterFormat in init, but since there's no magic bytes to detect this file format, Go thinks all files are in zbm format
func RegisterFormat() {
image.RegisterFormat(FormatName, textureHeader, Decode, DecodeConfig)
}
130 changes: 130 additions & 0 deletions pkg/zbm/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package zbm

import (
"encoding/binary"
"image"
"image/color"
"io"
"math"

"github.com/namgo/GameWaveFans/pkg/common"
)

/*
Currently, writer only outputs images in 3364CrCbYA image format, which all official games use
*/

func convertColorToCrCbYA(c color.Color) uint16 {
r, g, b, a := c.RGBA()
col := uint16(0)

r8 := uint8((float64(r) / 65536.0) * 255.0)
g8 := uint8((float64(g) / 65536.0) * 255.0)
b8 := uint8((float64(b) / 65536.0) * 255.0)

y, cb, cr := color.RGBToYCbCr(r8, g8, b8)

y6 := uint16(y >> 2) //uint16(math.Round((float64(y) / 255.0) * 63.0))
cr3 := uint16(cr >> 5) //uint16(math.Round((float64(cr) / 255.0) * 7.0))
cb3 := uint16(cb >> 5) //uint16(math.Round((float64(cb) / 255.0) * 7.0))
a4 := uint16(math.Round((float64(a) / 65536.0) * 15.0))

col = a4<<12 | y6<<6 | cb3<<3 | cr3
return col
}

func convertImage(m image.Image) []byte {
pixelBuffer := make([]uint16, m.Bounds().Dx()*m.Bounds().Dy())

for y := 0; y < m.Bounds().Dy(); y++ {
for x := 0; x < m.Bounds().Dx(); x++ {
pixelBuffer[x+(y*m.Bounds().Dx())] = convertColorToCrCbYA(m.At(x, y))
}
}

data := make([]byte, len(pixelBuffer)*2)

// swap every two pixels, endianness changes a bit
for i := 0; i < len(pixelBuffer)-1; i += 2 {
pixelData := make([]byte, 2)
pixelData2 := make([]byte, 2)
binary.BigEndian.PutUint16(pixelData, pixelBuffer[i])
binary.BigEndian.PutUint16(pixelData2, pixelBuffer[i+1])

data[i*2] = pixelData2[0]
data[i*2+1] = pixelData2[1]
data[i*2+2] = pixelData[0]
data[i*2+3] = pixelData[1]
}
return data
}

// write header of zbm file, before packed data is written
func writeHeader(w io.Writer, im image.Image, unpackedSize, packedSize uint32) error {
if _, err := common.WriteUint32(w, 1); err != nil {
return err
}

// TEXTURE_OSD
if _, err := common.WriteUint32(w, 1); err != nil {
return err
}
// 3364 format
if _, err := common.WriteUint32(w, 4); err != nil {
return err
}
// BPP
if _, err := common.WriteUint32(w, 2); err != nil {
return err
}
// width
if _, err := common.WriteUint32(w, uint32(im.Bounds().Dx())); err != nil {
return err
}
// height
if _, err := common.WriteUint32(w, uint32(im.Bounds().Dy())); err != nil {
return err
}

if _, err := common.WriteUint32(w, 0); err != nil {
return err
}
if _, err := common.WriteUint32(w, 0); err != nil {
return err
}
if _, err := common.WriteUint32(w, 1); err != nil {
return err
}

if _, err := common.WriteUint32(w, packedSize); err != nil {
return err
}
if _, err := common.WriteUint32(w, unpackedSize); err != nil {
return err
}

_, err := common.WriteUint32(w, 0)
return err
}

// Encode encodes image.Image to .zbm file
func Encode(w io.Writer, m image.Image) error {
//convert data
convertedData := convertImage(m)

// pack data
packedData, err := common.WriteZlibToBuffer(convertedData)
if err != nil {
return err
}

// write header
err = writeHeader(w, m, uint32(len(convertedData)), uint32(len(packedData)))
if err != nil {
return err
}

// write data
_, err = w.Write(packedData)
return err
}

0 comments on commit 4bbad2b

Please sign in to comment.