From a76f32df2d6f0bdb9efb4197d5607d13a1026861 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Oct 2025 17:25:47 +0530 Subject: [PATCH] Code to serialize/unserialize loaded images --- embeds.go | 3 ++ tools/utils/images/loading.go | 81 ++++++++++++++++++++++++++++ tools/utils/images/serialize_test.go | 31 +++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tools/utils/images/serialize_test.go diff --git a/embeds.go b/embeds.go index d9e36a75f..e575c64f9 100644 --- a/embeds.go +++ b/embeds.go @@ -8,6 +8,9 @@ import ( var _ = fmt.Print +//go:embed logo/kitty.png +var KittyLogoAsPNGData []byte + //go:embed kitty_tests/GraphemeBreakTest.json var grapheme_break_test_data []byte diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go index 4554b082a..cb0e5d201 100644 --- a/tools/utils/images/loading.go +++ b/tools/utils/images/loading.go @@ -53,6 +53,23 @@ type ImageFrame struct { Img image.Image } +type SerializableImageFrame struct { + Width, Height, Left, Top int + Number int // 1-based number + Compose_onto int // number of frame to compose onto + Delay_ms int // negative for gapless frame, zero ignored, positive is number of ms + Is_opaque bool + Size int +} + +func (s *ImageFrame) Serialize() SerializableImageFrame { + return SerializableImageFrame{ + Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top, + Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int(s.Delay_ms), + Is_opaque: s.Is_opaque, + } +} + func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) { bytes_per_pixel := 4 if self.Is_opaque { @@ -122,12 +139,73 @@ func (self *ImageFrame) Data() (ans []byte) { return } +func ImageFrameFromSerialized(s SerializableImageFrame, data []byte) (*ImageFrame, error) { + ans := ImageFrame{ + Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top, + Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int32(s.Delay_ms), + Is_opaque: s.Is_opaque, + } + r := image.Rect(0, 0, s.Width, s.Height) + if s.Is_opaque { + if len(data) != 3*r.Dx()*r.Dy() { + return nil, fmt.Errorf("serialized image data has size: %d != %d", len(data), 3*r.Dy()*r.Dx()) + } + ans.Img = &NRGB{Pix: data, Stride: 3 * r.Dx(), Rect: r} + } else { + if len(data) != 4*r.Dx()*r.Dy() { + return nil, fmt.Errorf("serialized image data has size: %d != %d", len(data), 4*r.Dy()*r.Dx()) + } + ans.Img = &image.NRGBA{Pix: data, Stride: 4 * r.Dx(), Rect: r} + } + return &ans, nil +} + type ImageData struct { Width, Height int Format_uppercase string Frames []*ImageFrame } +type SerializableImageMetadata struct { + Version int + Width, Height int + Format_uppercase string + Frames []SerializableImageFrame +} + +const SERIALIZE_VERSION = 1 + +func (self *ImageData) Serialize() (SerializableImageMetadata, [][]byte) { + m := SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase} + data := make([][]byte, len(self.Frames)) + for i, f := range self.Frames { + m.Frames = append(m.Frames, f.Serialize()) + data[i] = f.Data() + m.Frames[len(m.Frames)-1].Size = len(data[i]) + } + return m, data +} + +func ImageFromSerialized(m SerializableImageMetadata, data [][]byte) (*ImageData, error) { + if m.Version > SERIALIZE_VERSION { + return nil, fmt.Errorf("serialized image data has unsupported version: %d", m.Version) + } + if len(m.Frames) != len(data) { + return nil, fmt.Errorf("serialized image data has %d frames in metadata but have data for: %d", len(m.Frames), len(data)) + } + ans := ImageData{ + Width: m.Width, Height: m.Height, Format_uppercase: m.Format_uppercase, + } + for i, f := range m.Frames { + if ff, err := ImageFrameFromSerialized(f, data[i]); err != nil { + return nil, err + } else { + ans.Frames = append(ans.Frames, ff) + } + } + return &ans, nil +} + func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame { b := self.Img.Bounds() left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy() @@ -266,6 +344,7 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) { return } +// ImageMagick {{{ var MagickExe = sync.OnceValue(func() string { return utils.FindExe("magick") }) @@ -610,6 +689,8 @@ func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) { return ans, nil } +// }}} + func OpenImageFromPath(path string) (ans *ImageData, err error) { mt := utils.GuessMimeType(path) if DecodableImageTypes[mt] { diff --git a/tools/utils/images/serialize_test.go b/tools/utils/images/serialize_test.go new file mode 100644 index 000000000..286008889 --- /dev/null +++ b/tools/utils/images/serialize_test.go @@ -0,0 +1,31 @@ +package images + +import ( + "bytes" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/kovidgoyal/kitty" +) + +var _ = fmt.Print + +func TestImageSerialize(t *testing.T) { + img, err := OpenNativeImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData)) + if err != nil { + t.Fatal(err) + } + m, data := img.Serialize() + img2, err := ImageFromSerialized(m, data) + if err != nil { + t.Fatal(err) + } + m2, data2 := img2.Serialize() + if diff := cmp.Diff(m, m2); diff != "" { + t.Fatalf("Image metadata failed to roundtrip:\n%s", diff) + } + if diff := cmp.Diff(data, data2); diff != "" { + t.Fatalf("Image data failed to roundtrip:\n%s", diff) + } +}