From 4bbad2b01ae4408547f95b888195ac97b97f9cb8 Mon Sep 17 00:00:00 2001 From: Piotr Halama Date: Sun, 22 Dec 2024 22:00:34 +0100 Subject: [PATCH] feat: add zbm_packer (#26) * feat: add zbm_packer * simplify --- .goreleaser.yaml | 12 ++-- cmd/zbm_pack/main.go | 156 +++++++++++++++++++++++++++++++++++++++++ cmd/zbm_unpack/main.go | 3 + pkg/common/common.go | 7 ++ pkg/zbm/common.go | 18 +++++ pkg/zbm/reader.go | 20 +----- pkg/zbm/writer.go | 130 ++++++++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 23 deletions(-) create mode 100644 cmd/zbm_pack/main.go create mode 100644 pkg/zbm/writer.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 16c7979..9525970 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -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 diff --git a/cmd/zbm_pack/main.go b/cmd/zbm_pack/main.go new file mode 100644 index 0000000..7342512 --- /dev/null +++ b/cmd/zbm_pack/main.go @@ -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 +} diff --git a/cmd/zbm_unpack/main.go b/cmd/zbm_unpack/main.go index 52aa52c..59cce1a 100644 --- a/cmd/zbm_unpack/main.go +++ b/cmd/zbm_unpack/main.go @@ -51,6 +51,8 @@ func main() { os.Exit(1) } + zbm.RegisterFormat() + for _, inputName := range args { f, err := os.Stat(inputName) if err != nil { @@ -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) diff --git a/pkg/common/common.go b/pkg/common/common.go index 92daaad..e66da25 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -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 { diff --git a/pkg/zbm/common.go b/pkg/zbm/common.go index 39d3bcb..a2fcba3 100644 --- a/pkg/zbm/common.go +++ b/pkg/zbm/common.go @@ -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 diff --git a/pkg/zbm/reader.go b/pkg/zbm/reader.go index f5499e9..a59f493 100644 --- a/pkg/zbm/reader.go +++ b/pkg/zbm/reader.go @@ -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 @@ -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) } diff --git a/pkg/zbm/writer.go b/pkg/zbm/writer.go new file mode 100644 index 0000000..6d1ca58 --- /dev/null +++ b/pkg/zbm/writer.go @@ -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 +}