diff --git a/kitty_graphics_test.go b/kitty_graphics_test.go index 96c0e8c..220132a 100644 --- a/kitty_graphics_test.go +++ b/kitty_graphics_test.go @@ -1,9 +1,35 @@ package libghostty import ( + "bytes" + "fmt" + "image" + "image/png" "testing" ) +// testDecodePng is a minimal SysDecodePngFn for tests. It avoids +// importing the syspng subpackage (which would create an import cycle +// in internal tests) by inlining the same logic. +func testDecodePng(data []byte) (*SysImage, error) { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("png decode: %w", err) + } + bounds := img.Bounds() + w, h := bounds.Dx(), bounds.Dy() + if nrgba, ok := img.(*image.NRGBA); ok { + return &SysImage{Width: uint32(w), Height: uint32(h), Data: nrgba.Pix}, nil + } + dst := image.NewNRGBA(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + dst.Set(x, y, img.At(x, y)) + } + } + return &SysImage{Width: uint32(w), Height: uint32(h), Data: dst.Pix}, nil +} + // newKittyTerminal creates a terminal with Kitty graphics enabled // (PNG decode callback, WritePty handler, storage limit, and cell // pixel dimensions), ready for Kitty graphics protocol testing. @@ -11,7 +37,7 @@ func newKittyTerminal(t *testing.T) *Terminal { t.Helper() // Install the PNG decoder. - if err := SysSetDecodePng(SysDecodePng); err != nil { + if err := SysSetDecodePng(testDecodePng); err != nil { t.Fatal(err) } diff --git a/sys/png/decode.go b/sys/png/decode.go new file mode 100644 index 0000000..da3ac87 --- /dev/null +++ b/sys/png/decode.go @@ -0,0 +1,67 @@ +// Package png provides a ready-to-use PNG decoder for libghostty +// using Go's standard [image/png] package. +// +// This package is separate from the root libghostty package so that +// importing libghostty does not unconditionally pull in image/png +// (and its init-time image format registration). Import this package +// only when you need PNG decoding: +// +// import ( +// libghostty "github.com/mitchellh/go-libghostty" +// syspng "github.com/mitchellh/go-libghostty/sys/png" +// ) +// +// libghostty.SysSetDecodePng(syspng.Decode) +package png + +import ( + "bytes" + "fmt" + "image" + goimg "image/png" + + libghostty "github.com/mitchellh/go-libghostty" +) + +// Decode is a ready-to-use [libghostty.SysDecodePngFn] implementation +// that decodes PNG data using Go's standard [image/png] package. It +// converts any decoded image format to NRGBA (non-premultiplied alpha) +// before returning the raw pixel bytes. +// +// Usage: +// +// libghostty.SysSetDecodePng(syspng.Decode) +func Decode(data []byte) (*libghostty.SysImage, error) { + img, err := goimg.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("png decode: %w", err) + } + + bounds := img.Bounds() + w := bounds.Dx() + h := bounds.Dy() + + // Fast path: if the image is already NRGBA we can use the pixels + // directly without a per-pixel conversion. + if nrgba, ok := img.(*image.NRGBA); ok { + return &libghostty.SysImage{ + Width: uint32(w), + Height: uint32(h), + Data: nrgba.Pix, + }, nil + } + + // Slow path: convert arbitrary image types to NRGBA. + dst := image.NewNRGBA(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + dst.Set(x, y, img.At(x, y)) + } + } + + return &libghostty.SysImage{ + Width: uint32(w), + Height: uint32(h), + Data: dst.Pix, + }, nil +} diff --git a/sys_builtin_test.go b/sys/png/decode_test.go similarity index 75% rename from sys_builtin_test.go rename to sys/png/decode_test.go index 11ec33a..493b68a 100644 --- a/sys_builtin_test.go +++ b/sys/png/decode_test.go @@ -1,14 +1,14 @@ -package libghostty +package png import ( "bytes" "image" "image/color" - "image/png" + goimg "image/png" "testing" ) -func TestSysDecodePng(t *testing.T) { +func TestDecode(t *testing.T) { // Encode a small 2x2 NRGBA PNG in-memory. src := image.NewNRGBA(image.Rect(0, 0, 2, 2)) src.SetNRGBA(0, 0, color.NRGBA{R: 255, A: 255}) @@ -17,13 +17,13 @@ func TestSysDecodePng(t *testing.T) { src.SetNRGBA(1, 1, color.NRGBA{R: 255, G: 255, B: 255, A: 255}) var buf bytes.Buffer - if err := png.Encode(&buf, src); err != nil { + if err := goimg.Encode(&buf, src); err != nil { t.Fatalf("png.Encode: %v", err) } - img, err := SysDecodePng(buf.Bytes()) + img, err := Decode(buf.Bytes()) if err != nil { - t.Fatalf("SysDecodePng: %v", err) + t.Fatalf("Decode: %v", err) } if img.Width != 2 || img.Height != 2 { @@ -41,27 +41,27 @@ func TestSysDecodePng(t *testing.T) { } } -func TestSysDecodePngInvalid(t *testing.T) { - _, err := SysDecodePng([]byte("not a png")) +func TestDecodeInvalid(t *testing.T) { + _, err := Decode([]byte("not a png")) if err == nil { - t.Fatal("SysDecodePng(invalid) = nil error, want error") + t.Fatal("Decode(invalid) = nil error, want error") } } -func TestSysDecodePngRGBA(t *testing.T) { +func TestDecodeRGBA(t *testing.T) { // Use an RGBA image (premultiplied alpha) to exercise the slow path // conversion to NRGBA. src := image.NewRGBA(image.Rect(0, 0, 1, 1)) src.SetRGBA(0, 0, color.RGBA{R: 128, G: 0, B: 0, A: 128}) var buf bytes.Buffer - if err := png.Encode(&buf, src); err != nil { + if err := goimg.Encode(&buf, src); err != nil { t.Fatalf("png.Encode: %v", err) } - img, err := SysDecodePng(buf.Bytes()) + img, err := Decode(buf.Bytes()) if err != nil { - t.Fatalf("SysDecodePng: %v", err) + t.Fatalf("Decode: %v", err) } if img.Width != 1 || img.Height != 1 { diff --git a/sys_builtin.go b/sys_builtin.go deleted file mode 100644 index d8446ba..0000000 --- a/sys_builtin.go +++ /dev/null @@ -1,55 +0,0 @@ -package libghostty - -// Built-in implementations for system callbacks using Go standard library -// packages. These are optional convenience functions that can be passed -// directly to their corresponding SysSet* installers. - -import ( - "bytes" - "fmt" - "image" - "image/png" -) - -// SysDecodePng is a ready-to-use [SysDecodePngFn] implementation that -// decodes PNG data using Go's standard [image/png] package. It converts -// any decoded image format to NRGBA (non-premultiplied alpha) before -// returning the raw pixel bytes. -// -// Usage: -// -// libghostty.SysSetDecodePng(libghostty.SysDecodePng) -func SysDecodePng(data []byte) (*SysImage, error) { - img, err := png.Decode(bytes.NewReader(data)) - if err != nil { - return nil, fmt.Errorf("png decode: %w", err) - } - - bounds := img.Bounds() - w := bounds.Dx() - h := bounds.Dy() - - // Fast path: if the image is already NRGBA we can use the pixels - // directly without a per-pixel conversion. - if nrgba, ok := img.(*image.NRGBA); ok { - return &SysImage{ - Width: uint32(w), - Height: uint32(h), - Data: nrgba.Pix, - }, nil - } - - // Slow path: convert arbitrary image types to NRGBA. - dst := image.NewNRGBA(bounds) - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - dst.Set(x, y, img.At(x, y)) - } - } - - return &SysImage{ - Width: uint32(w), - Height: uint32(h), - Data: dst.Pix, - }, nil -}