···11package libghostty
2233import (
44+ "bytes"
55+ "fmt"
66+ "image"
77+ "image/png"
48 "testing"
59)
6101111+// testDecodePng is a minimal SysDecodePngFn for tests. It avoids
1212+// importing the syspng subpackage (which would create an import cycle
1313+// in internal tests) by inlining the same logic.
1414+func testDecodePng(data []byte) (*SysImage, error) {
1515+ img, err := png.Decode(bytes.NewReader(data))
1616+ if err != nil {
1717+ return nil, fmt.Errorf("png decode: %w", err)
1818+ }
1919+ bounds := img.Bounds()
2020+ w, h := bounds.Dx(), bounds.Dy()
2121+ if nrgba, ok := img.(*image.NRGBA); ok {
2222+ return &SysImage{Width: uint32(w), Height: uint32(h), Data: nrgba.Pix}, nil
2323+ }
2424+ dst := image.NewNRGBA(bounds)
2525+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
2626+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
2727+ dst.Set(x, y, img.At(x, y))
2828+ }
2929+ }
3030+ return &SysImage{Width: uint32(w), Height: uint32(h), Data: dst.Pix}, nil
3131+}
3232+733// newKittyTerminal creates a terminal with Kitty graphics enabled
834// (PNG decode callback, WritePty handler, storage limit, and cell
935// pixel dimensions), ready for Kitty graphics protocol testing.
···1137 t.Helper()
12381339 // Install the PNG decoder.
1414- if err := SysSetDecodePng(SysDecodePng); err != nil {
4040+ if err := SysSetDecodePng(testDecodePng); err != nil {
1541 t.Fatal(err)
1642 }
1743
+67
sys/png/decode.go
···11+// Package png provides a ready-to-use PNG decoder for libghostty
22+// using Go's standard [image/png] package.
33+//
44+// This package is separate from the root libghostty package so that
55+// importing libghostty does not unconditionally pull in image/png
66+// (and its init-time image format registration). Import this package
77+// only when you need PNG decoding:
88+//
99+// import (
1010+// libghostty "github.com/mitchellh/go-libghostty"
1111+// syspng "github.com/mitchellh/go-libghostty/sys/png"
1212+// )
1313+//
1414+// libghostty.SysSetDecodePng(syspng.Decode)
1515+package png
1616+1717+import (
1818+ "bytes"
1919+ "fmt"
2020+ "image"
2121+ goimg "image/png"
2222+2323+ libghostty "github.com/mitchellh/go-libghostty"
2424+)
2525+2626+// Decode is a ready-to-use [libghostty.SysDecodePngFn] implementation
2727+// that decodes PNG data using Go's standard [image/png] package. It
2828+// converts any decoded image format to NRGBA (non-premultiplied alpha)
2929+// before returning the raw pixel bytes.
3030+//
3131+// Usage:
3232+//
3333+// libghostty.SysSetDecodePng(syspng.Decode)
3434+func Decode(data []byte) (*libghostty.SysImage, error) {
3535+ img, err := goimg.Decode(bytes.NewReader(data))
3636+ if err != nil {
3737+ return nil, fmt.Errorf("png decode: %w", err)
3838+ }
3939+4040+ bounds := img.Bounds()
4141+ w := bounds.Dx()
4242+ h := bounds.Dy()
4343+4444+ // Fast path: if the image is already NRGBA we can use the pixels
4545+ // directly without a per-pixel conversion.
4646+ if nrgba, ok := img.(*image.NRGBA); ok {
4747+ return &libghostty.SysImage{
4848+ Width: uint32(w),
4949+ Height: uint32(h),
5050+ Data: nrgba.Pix,
5151+ }, nil
5252+ }
5353+5454+ // Slow path: convert arbitrary image types to NRGBA.
5555+ dst := image.NewNRGBA(bounds)
5656+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
5757+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
5858+ dst.Set(x, y, img.At(x, y))
5959+ }
6060+ }
6161+6262+ return &libghostty.SysImage{
6363+ Width: uint32(w),
6464+ Height: uint32(h),
6565+ Data: dst.Pix,
6666+ }, nil
6767+}
-55
sys_builtin.go
···11-package libghostty
22-33-// Built-in implementations for system callbacks using Go standard library
44-// packages. These are optional convenience functions that can be passed
55-// directly to their corresponding SysSet* installers.
66-77-import (
88- "bytes"
99- "fmt"
1010- "image"
1111- "image/png"
1212-)
1313-1414-// SysDecodePng is a ready-to-use [SysDecodePngFn] implementation that
1515-// decodes PNG data using Go's standard [image/png] package. It converts
1616-// any decoded image format to NRGBA (non-premultiplied alpha) before
1717-// returning the raw pixel bytes.
1818-//
1919-// Usage:
2020-//
2121-// libghostty.SysSetDecodePng(libghostty.SysDecodePng)
2222-func SysDecodePng(data []byte) (*SysImage, error) {
2323- img, err := png.Decode(bytes.NewReader(data))
2424- if err != nil {
2525- return nil, fmt.Errorf("png decode: %w", err)
2626- }
2727-2828- bounds := img.Bounds()
2929- w := bounds.Dx()
3030- h := bounds.Dy()
3131-3232- // Fast path: if the image is already NRGBA we can use the pixels
3333- // directly without a per-pixel conversion.
3434- if nrgba, ok := img.(*image.NRGBA); ok {
3535- return &SysImage{
3636- Width: uint32(w),
3737- Height: uint32(h),
3838- Data: nrgba.Pix,
3939- }, nil
4040- }
4141-4242- // Slow path: convert arbitrary image types to NRGBA.
4343- dst := image.NewNRGBA(bounds)
4444- for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
4545- for x := bounds.Min.X; x < bounds.Max.X; x++ {
4646- dst.Set(x, y, img.At(x, y))
4747- }
4848- }
4949-5050- return &SysImage{
5151- Width: uint32(w),
5252- Height: uint32(h),
5353- Data: dst.Pix,
5454- }, nil
5555-}