diff --git a/Makefile b/Makefile index 01f9162..f76563d 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ img_tag = local cory: clean go build -o ./cory ./_cmd/cory-bin +cory_win: clean + go build -o ./cory.exe ./_cmd/cory-bin + docker: DOCKER_BUILDKIT=1 docker build -f server.Dockerfile -t $(img_user)/$(img_name):$(img_tag) . @@ -12,4 +15,4 @@ server-run: docker docker run -p 3333:3333 $(img_user)/$(img_name):$(img_tag) --serve clean: - rm -rf cory + rm -rf cory cory.exe diff --git a/_cmd/cory-bin/main.go b/_cmd/cory-bin/main.go index 9fe436f..7643f3c 100644 --- a/_cmd/cory-bin/main.go +++ b/_cmd/cory-bin/main.go @@ -4,6 +4,7 @@ import ( "bytes" "flag" "fmt" + "git.tuxpa.in/a/nori/renderer" "net/http" "os" "time" @@ -57,7 +58,7 @@ func dump(filename string, output string) { } out := new(bytes.Buffer) printIf("rendering %d animation(s)\n", n.AnimationCount) - animations, err := nori.RenderAnimations(n) + animations, err := renderer.RenderAnimationsApng(n) if err != nil { log.Panicln("animation: %s", err) } @@ -93,7 +94,7 @@ func serve() { if err := n.Decode(r.Body); err != nil { http.Error(w, err.Error(), 500) } - frames, err := nori.RenderAnimations(n) + frames, err := renderer.RenderAnimationsApng(n) if err != nil { http.Error(w, err.Error(), 500) } diff --git a/frame.go b/frame.go index 8706e32..522aeb5 100644 --- a/frame.go +++ b/frame.go @@ -81,23 +81,25 @@ func (f *Frame) Decode(rd io.Reader) error { if err := discardN(rd, 18); err != nil { return err } - var cast uint32 - if err := binary.Read(rd, end, &cast); err != nil { - return err - } - // camp - f.HasCamp = (f.Version >= 303 && (cast > 0)) - if f.HasCamp { - if err := binary.Read(rd, end, f.Camp.Params[:]); err != nil { + if f.Version >= 303 { + var cast uint32 + if err := binary.Read(rd, end, &cast); err != nil { return err } - sz := int(f.Camp.Params[1]) * int(f.Camp.Params[2]) - f.Camp.Array = make([]byte, sz) - if _, err := io.ReadFull(rd, f.Camp.Array); err != nil { - return err - } - if err := discardN(rd, 20); err != nil { - return err + // camp + f.HasCamp = cast > 0 + if f.HasCamp { + if err := binary.Read(rd, end, f.Camp.Params[:]); err != nil { + return err + } + sz := int(f.Camp.Params[1]) * int(f.Camp.Params[2]) + f.Camp.Array = make([]byte, sz) + if _, err := io.ReadFull(rd, f.Camp.Array); err != nil { + return err + } + if err := discardN(rd, 20); err != nil { + return err + } } } diff --git a/go.mod b/go.mod index 67525da..63cc2a4 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,16 @@ go 1.18 require ( git.tuxpa.in/a/zlog v1.32.0 - github.com/go-chi/chi/v5 v5.0.7 + github.com/disintegration/imaging v1.6.2 github.com/phrozen/blend v0.0.0-20210220204729-f26b6cf7a28e gitlab.com/gfxlabs/gfximg v0.0.5 ) require ( + github.com/chai2010/webp v1.1.1 // indirect + github.com/go-chi/chi/v5 v5.0.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - gitlab.com/gfxlabs/goutil v1.5.0 // indirect + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect ) diff --git a/go.sum b/go.sum index 11456b0..0ff64b1 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ git.tuxpa.in/a/zlog v1.32.0 h1:KKXbRF1x8kJDSzUoGz/pivo+4TVY6xT5sVtdFZ6traY= git.tuxpa.in/a/zlog v1.32.0/go.mod h1:vUa2Qhu6DLPLqmfRy99FiPqaY2eb6/KQjtMekW3UNnA= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -14,8 +18,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= gitlab.com/gfxlabs/gfximg v0.0.5 h1:jtHE6In6axz0zGPy7YnLIZV9RFdcFEZzpdqoXb52K9s= gitlab.com/gfxlabs/gfximg v0.0.5/go.mod h1:IAYZwCoqy3JFKwkKafragpfecp5Up6FFIzI7VvK2Zzo= -gitlab.com/gfxlabs/goutil v1.5.0 h1:tYjMbDRtK93MPrWrZE4Ub67Ng8rYwTahtb+Bfpf0Spk= -gitlab.com/gfxlabs/goutil v1.5.0/go.mod h1:NGvdG6oTLV2cEX4HKtewdzx/ZIvC+eAS7EbjKy4sXsA= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/image.go b/image.go index 8efae96..97a9df8 100644 --- a/image.go +++ b/image.go @@ -103,17 +103,19 @@ func (i *Image) Decode(rd io.Reader, palette *PaletteSection) error { return err } alpha := uint8(0xff) + + if rgb[2] == 0x00 && rgb[1] == 0xff && rgb[0] == 0x00 { + alpha = 0x00 + } + if rgb[2] == 0xff && rgb[1] == 0x00 && rgb[0] == 0xff { + alpha = 0x00 + } + col := &color.RGBA{ - rgb[0], rgb[1], rgb[2], + rgb[2], rgb[1], rgb[0], alpha, } - if rgb[0] == 0x00 && rgb[1] == 0xff && rgb[2] == 0x00 { - alpha = 0x00 - } - if rgb[0] == 0xff && rgb[1] == 0x00 && rgb[2] == 0xff { - alpha = 0x00 - } cf.Set(idx%int(i.Width), idx/int(i.Width), col) } return nil diff --git a/readme.md b/readme.md index 6dff3d0..383cc58 100644 --- a/readme.md +++ b/readme.md @@ -18,7 +18,7 @@ func main() { log.Panicln("decode: %s", err) } // uses apng encoder/decoder from https://gitlab.com/gfxlabs/gfximg - animations, err := n.RenderAnimations(n) + animations, err := n.RenderAnimationsApng(n) if err != nil { log.Panicln("render: %s", err) } diff --git a/render.go b/render.go index d0c8176..cb9d6ee 100644 --- a/render.go +++ b/render.go @@ -2,47 +2,38 @@ package nori import ( "fmt" - "image" - "image/draw" - "strings" - "git.tuxpa.in/a/zlog/log" + "github.com/disintegration/imaging" "github.com/phrozen/blend" "gitlab.com/gfxlabs/gfximg/apng" + "image" + "image/color" + "image/draw" ) -func RenderAnimations(n *Nori) ([]*apng.APNG, error) { - apngs := make([]*apng.APNG, 0, len(n.Animations)) - for i := range n.Animations { - a, err := RenderAnimation(n, i) - if err != nil { - if strings.Contains(err.Error(), "no frame") { - continue - } - return nil, err - } - apngs = append(apngs, a) - } - return apngs, nil +func copyImage(img image.Image) image.Image { + copied := image.NewNRGBA(img.Bounds()) + draw.Draw(copied, img.Bounds(), img, img.Bounds().Min, draw.Over) + return copied } -func RenderAnimation(n *Nori, num int) (*apng.APNG, error) { +type RenderFunc = func(img image.Image, delay int) + +func RenderAnimation(n *Nori, num int, f RenderFunc) error { g := n.Gawi - a := &apng.APNG{ - Frames: make([]apng.Frame, 0, len(g.Images)), - } if len(n.Animations[num].Frames) == 0 { - return nil, fmt.Errorf("no frames found for animation") + return fmt.Errorf("no frames found for animation") } - images := make([]*image.NRGBA64, 0, len(n.Animations[num].Frames)) + images := make([]image.Image, 0, len(n.Animations[num].Frames)) + durations := make([]uint32, 0, len(n.Animations[num].Frames)) for _, frame := range n.Animations[num].Frames { planes := frame.Planes var canvasRect image.Rectangle for i, plane := range planes { - if int(plane.BitmapId) >= len(n.Gawi.Images) { - log.Printf("could not find bitmap %d, only have %d", plane.BitmapId, len(n.Gawi.Images)) + if int(plane.BitmapId) >= len(g.Images) { + log.Printf("could not find bitmap %d, only have %d", plane.BitmapId, len(g.Images)) } - bitmap := n.Gawi.Images[plane.BitmapId] + bitmap := g.Images[plane.BitmapId] pt := image.Pt(int(plane.PlaneX), int(plane.PlaneY)) // where to put the point rc := bitmap.Img.Bounds().Add(pt) // translate rectangle to put in the global canvas if i == 0 { @@ -51,15 +42,24 @@ func RenderAnimation(n *Nori, num int) (*apng.APNG, error) { canvasRect = canvasRect.Union(rc) } } - img := image.NewNRGBA64(canvasRect) + img := image.NewNRGBA(canvasRect) for _, plane := range planes { - bitmap := n.Gawi.Images[plane.BitmapId] + bitmap := g.Images[plane.BitmapId] + drawBitmap := bitmap.Img transparent := false //flipx if plane.RenderFlag&1 != 0 { + if bitmap.Img == drawBitmap { + drawBitmap = copyImage(bitmap.Img) + } + drawBitmap = imaging.FlipH(drawBitmap) } //flipy if plane.RenderFlag&2 != 0 { + if bitmap.Img == drawBitmap { + drawBitmap = copyImage(bitmap.Img) + } + drawBitmap = imaging.FlipV(drawBitmap) } // is transparent if plane.RenderFlag&0x20 != 0 { @@ -67,36 +67,46 @@ func RenderAnimation(n *Nori, num int) (*apng.APNG, error) { } _ = transparent pt := image.Pt(int(plane.PlaneX), int(plane.PlaneY)) // where to put the point - rc := bitmap.Img.Bounds().Add(pt) - src := bitmap.Img + rc := drawBitmap.Bounds().Add(pt) + src := drawBitmap switch plane.Blend { + case BlendMode_InvertMul, BlendMode_InvertMul5: + img = imaging.Invert(img) + fallthrough case BlendMode_Alpha: case BlendMode_Mul, BlendMode_Mul7: - blend.BlendImage(img, bitmap.Img, blend.Multiply) + blend.BlendImage(img, drawBitmap, blend.Multiply) case BlendMode_Add, BlendMode_Add8: - blend.BlendImage(img, bitmap.Img, blend.Add) - case BlendMode_InvertMul, BlendMode_InvertMul5: - blend.BlendImage(img, bitmap.Img, blend.Multiply) + blend.BlendImage(img, drawBitmap, blend.Add) case BlendMode_None: default: - return nil, fmt.Errorf("unknown blend mode: %d", plane.Blend) + return fmt.Errorf("unknown blend mode: %d", plane.Blend) } - draw.Draw( + + mask := (image.Image)(nil) + if transparent { + mask = &image.Uniform{C: color.RGBA{A: 160}} + } + + draw.DrawMask( img, rc, src, src.Bounds().Min, + mask, + src.Bounds().Min, draw.Over, ) } images = append(images, img) + durations = append(durations, frame.Duration) } or := images[0].Bounds() for _, realFrame := range images { or = realFrame.Bounds().Union(or) } if or.Size().X == 0 || or.Size().Y == 0 { - return nil, fmt.Errorf("no frames found for animation") + return fmt.Errorf("no frames found for animation") } for i, realFrame := range images { //log.Info().Int("frame", num).Interface("rect", or).Interface("frame", realFrame.Bounds()).Msg("") @@ -112,11 +122,10 @@ func RenderAnimation(n *Nori, num int) (*apng.APNG, error) { fr := apng.Frame{ Image: big, DelayDenominator: 1000, + DelayNumerator: uint16(durations[i]), } - if g.Images[i].Delay != 0 { - fr.DelayNumerator = uint16(g.Images[i].Delay) - } - a.Frames = append(a.Frames, fr) + f(big, int(fr.DelayNumerator)) } - return a, nil + return nil + } diff --git a/render_test.go b/render_test.go index 6db53a7..bd00ef2 100644 --- a/render_test.go +++ b/render_test.go @@ -1,8 +1,10 @@ -package nori +package nori_test import ( "bytes" "fmt" + "git.tuxpa.in/a/nori" + "git.tuxpa.in/a/nori/renderer" "os" "strings" "testing" @@ -11,7 +13,7 @@ import ( ) func TestParseFile1(t *testing.T) { - nori, err := FromFile("./render_test/test1.nri") + nori, err := nori.FromFile("./render_test/test1.nri") if err != nil { t.Errorf("decode: %s", err) } @@ -24,7 +26,7 @@ func TestParseFile1(t *testing.T) { } func TestParseFile2(t *testing.T) { - nori, err := FromFile("./render_test/test2.nri") + nori, err := nori.FromFile("./render_test/test2.nri") if err != nil { t.Errorf("decode: %s", err) } @@ -35,8 +37,34 @@ func TestParseFile2(t *testing.T) { } } +func TestParseFile3(t *testing.T) { + nori, err := nori.FromFile("./render_test/CharCreateNSelectUI.bac") + if err != nil { + t.Errorf("decode: %s", err) + } + t.Logf("\n nori: %+v\n gawi: %+v", nori, nori.Gawi) + + err = writeApng(nori, "charcreate") + if err != nil { + t.Errorf("export: %s", err) + } +} + +func TestParseFile4(t *testing.T) { + nori, err := nori.FromFile("./render_test/LoadingUI.bac") + if err != nil { + t.Errorf("decode: %s", err) + } + t.Logf("\n nori: %+v\n gawi: %+v", nori, nori.Gawi) + + err = writeApng(nori, "LoadingUI") + if err != nil { + t.Errorf("export: %s", err) + } +} + func TestParsePalette(t *testing.T) { - nori, err := FromFile("./render_test/palette.nri") + nori, err := nori.FromFile("./render_test/palette.nri") if err != nil { t.Errorf("decode: %s", err) } @@ -47,12 +75,12 @@ func TestParsePalette(t *testing.T) { } } -func writeApng(nori *Nori, name string) error { +func writeApng(n *nori.Nori, name string) error { postfix := ".example" out := new(bytes.Buffer) os.MkdirAll(fmt.Sprintf("./render_test/%s%s", name, postfix), 0740) - for i := range nori.Animations { - a, err := RenderAnimation(nori, i) + for i := range n.Animations { + a, err := renderer.RenderAnimationApng(n, i) if err != nil { if strings.Contains(err.Error(), "no frame") { continue diff --git a/render_test/CharCreateNSelectUI.bac b/render_test/CharCreateNSelectUI.bac new file mode 100644 index 0000000..a774d97 Binary files /dev/null and b/render_test/CharCreateNSelectUI.bac differ diff --git a/render_test/LoadingUI.bac b/render_test/LoadingUI.bac new file mode 100644 index 0000000..65c4b98 Binary files /dev/null and b/render_test/LoadingUI.bac differ diff --git a/renderer/apng.go b/renderer/apng.go new file mode 100644 index 0000000..9c648ad --- /dev/null +++ b/renderer/apng.go @@ -0,0 +1,47 @@ +package renderer + +import ( + "git.tuxpa.in/a/nori" + "gitlab.com/gfxlabs/gfximg/apng" + "image" + "strings" +) + +func newRenderApngFunc(a *apng.APNG) nori.RenderFunc { + return func(img image.Image, delay int) { + fr := apng.Frame{ + Image: img, + DelayDenominator: 1000, + } + if delay != 0 { + fr.DelayNumerator = uint16(delay) + } + a.Frames = append(a.Frames, fr) + } +} + +func RenderAnimationsApng(n *nori.Nori) ([]*apng.APNG, error) { + apngs := make([]*apng.APNG, 0, len(n.Animations)) + for i := range n.Animations { + a, err := RenderAnimationApng(n, i) + if err != nil { + if strings.Contains(err.Error(), "no frame") { + continue + } + return nil, err + } + apngs = append(apngs, a) + } + return apngs, nil +} + +func RenderAnimationApng(n *nori.Nori, i int) (*apng.APNG, error) { + a := &apng.APNG{ + Frames: make([]apng.Frame, 0, len(n.Gawi.Images)), + } + err := nori.RenderAnimation(n, i, newRenderApngFunc(a)) + if err != nil { + return nil, err + } + return a, nil +} diff --git a/renderer/gif.go b/renderer/gif.go new file mode 100644 index 0000000..e882496 --- /dev/null +++ b/renderer/gif.go @@ -0,0 +1,32 @@ +package renderer + +import ( + "git.tuxpa.in/a/nori" + "image" + "image/color/palette" + "image/draw" + "image/gif" +) + +func newRenderGifFunc(gif *gif.GIF) nori.RenderFunc { + return func(img image.Image, delay int) { + i := image.NewPaletted(img.Bounds(), palette.Plan9) + draw.Draw(i, img.Bounds(), img, img.Bounds().Min, draw.Over) + + gif.Image = append(gif.Image, i) + gif.Delay = append(gif.Delay, delay) + } +} + +func RenderAnimationGif(n *nori.Nori, i int) (*gif.GIF, error) { + gif := &gif.GIF{ + Image: make([]*image.Paletted, 0), + Delay: make([]int, 0), + LoopCount: 0, + } + err := nori.RenderAnimation(n, i, newRenderGifFunc(gif)) + if err != nil { + return nil, err + } + return gif, nil +} diff --git a/renderer/gif_test.go b/renderer/gif_test.go new file mode 100644 index 0000000..8bba5af --- /dev/null +++ b/renderer/gif_test.go @@ -0,0 +1,52 @@ +package renderer_test + +import ( + "bytes" + "fmt" + "git.tuxpa.in/a/nori" + "git.tuxpa.in/a/nori/renderer" + "git.tuxpa.in/a/zlog/log" + "image/gif" + "os" + "strings" + "testing" +) + +func TestMakeGif1(t *testing.T) { + nori, err := nori.FromFile("../render_test/CharCreateNSelectUI.bac") + if err != nil { + t.Errorf("decode: %s", err) + } + t.Logf("\n nori: %+v\n gawi: %+v", nori, nori.Gawi) + + err = writeGif(nori, "charcreate") + if err != nil { + t.Errorf("export: %s", err) + } +} + +func writeGif(n *nori.Nori, name string) error { + postfix := ".example" + out := new(bytes.Buffer) + os.MkdirAll(fmt.Sprintf("../render_test/%s%s", name, postfix), 0740) + for i := range n.Animations { + g, err := renderer.RenderAnimationGif(n, i) + if err != nil { + if strings.Contains(err.Error(), "no frame") { + continue + } + return err + } + err = gif.EncodeAll(out, g) + if err != nil { + log.Errorf("EncodeAll failed for frame %d, skipping: %s", i, err) + continue + } + err = os.WriteFile(fmt.Sprintf("../render_test/%s%s/animation_%d.gif", name, postfix, i), out.Bytes(), 0740) + if err != nil { + return err + } + out.Reset() + } + return nil +} diff --git a/utils/zip_test.go b/utils/zip_test.go index 28d5ea7..1684fa2 100644 --- a/utils/zip_test.go +++ b/utils/zip_test.go @@ -3,6 +3,7 @@ package utils import ( "bytes" "fmt" + "git.tuxpa.in/a/nori/renderer" "os" "testing" @@ -48,7 +49,7 @@ func TestParsePalette(t *testing.T) { func writeZip(n *nori.Nori, name string) error { out := new(bytes.Buffer) - frames, err := nori.RenderAnimations(n) + frames, err := renderer.RenderAnimationsApng(n) if err != nil { return err }