Files
kitty/tools/utils/images/loading.go
2025-11-17 12:59:04 +05:30

252 lines
7.4 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package images
import (
"fmt"
"image"
"image/png"
"io"
"os"
"strings"
"github.com/kovidgoyal/go-parallel"
"github.com/kovidgoyal/go-shm"
"github.com/kovidgoyal/imaging/nrgb"
"github.com/kovidgoyal/imaging"
)
var _ = fmt.Print
const TempTemplate = "kitty-tty-graphics-protocol-*"
func CreateTemp() (*os.File, error) {
return os.CreateTemp("", TempTemplate)
}
func CreateTempInRAM() (*os.File, error) {
if shm.SHM_DIR != "" {
f, err := os.CreateTemp(shm.SHM_DIR, TempTemplate)
if err == nil {
return f, err
}
}
return CreateTemp()
}
type ImageFrame struct {
Width, Height, Left, Top int
Number int // 1-based number
Compose_onto int // number of frame to compose onto
Delay_ms int32 // negative for gapless frame, zero ignored, positive is number of ms
Replace bool // do a replace rather than an alpha blend
Is_opaque bool
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
Replace bool // do a replace rather than an alpha blend
Is_opaque bool
Size int // size in bytes of the serialized data
Number_of_channels int
Bits_per_channel int
Has_alpha_channel bool
}
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, Replace: s.Replace,
}
}
func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
_, _, _, d := self.Data()
if ans, err = shm.CreateTemp(pattern, uint64(len(d))); err != nil {
return nil, err
}
copy(ans.Slice(), d)
return
}
func (self *ImageFrame) Data() (num_channels, bits_per_channel int, has_alpha_channel bool, ans []byte) {
if self.Is_opaque {
return 3, 8, false, imaging.AsRGBData8(self.Img)
}
return 4, 8, true, imaging.AsRGBAData8(self.Img)
}
func ImageFrameFromSerialized(s SerializableImageFrame, data []byte) (aa *ImageFrame, err 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 || !s.Has_alpha_channel, Replace: s.Replace,
}
bpc := s.Bits_per_channel
if bpc == 0 {
bpc = 8
}
if bpc != 8 {
return nil, fmt.Errorf("serialized image data has unsupported number of bits per channel: %d", bpc)
}
bytes_per_pixel := bpc * s.Number_of_channels / 8
if expected := bytes_per_pixel * s.Width * s.Height; len(data) != expected {
return nil, fmt.Errorf("serialized image data has size: %d != %d", len(data), expected)
}
switch s.Number_of_channels {
case 3, 0:
ans.Img, err = nrgb.NewNRGBWithContiguousRGBPixels(data, s.Left, s.Top, s.Width, s.Height)
case 4:
ans.Img, err = NewNRGBAWithContiguousRGBAPixels(data, s.Left, s.Top, s.Width, s.Height)
default:
return nil, fmt.Errorf("serialized image data has unsupported number of channels: %d", s.Number_of_channels)
}
return &ans, err
}
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 *ImageFrame) SaveAsUncompressedPNG(output io.Writer) error {
encoder := png.Encoder{CompressionLevel: png.NoCompression}
return encoder.Encode(output, self.Img)
}
func (self *ImageData) SerializeOnlyMetadata() SerializableImageMetadata {
f := make([]SerializableImageFrame, len(self.Frames))
for i, s := range self.Frames {
f[i] = s.Serialize()
}
return SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase, Frames: f}
}
func (self *ImageData) Serialize() (SerializableImageMetadata, [][]byte) {
m := self.SerializeOnlyMetadata()
data := make([][]byte, len(self.Frames))
for i, f := range self.Frames {
df := &m.Frames[i]
df.Number_of_channels, df.Bits_per_channel, df.Has_alpha_channel, data[i] = f.Data()
df.Size = len(data[i])
if !df.Has_alpha_channel {
df.Is_opaque = true
}
}
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 (ans ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
ans.Width = int(x_frac * float64(ans.Width))
ans.Height = int(y_frac * float64(ans.Height))
ans.Img = imaging.ResizeWithOpacity(ans.Img, ans.Width, ans.Height, imaging.Lanczos, ans.Is_opaque)
ans.Left = int(x_frac * float64(ans.Left))
ans.Top = int(y_frac * float64(ans.Top))
return &ans
}
func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
ans := *self
ans.Frames = make([]*ImageFrame, len(self.Frames))
if err := parallel.Run_in_parallel_over_range(1, func(start, limit int) {
for i := start; i < limit; i++ {
ans.Frames[i] = self.Frames[i].Resize(x_frac, y_frac)
}
}, 0, len(ans.Frames)); err != nil {
panic(err)
}
if len(ans.Frames) > 0 {
ans.Width, ans.Height = ans.Frames[0].Width, ans.Frames[0].Height
}
return &ans
}
func MakeTempDir(template string) (ans string, err error) {
if template == "" {
template = "kitty-img-*"
}
if shm.SHM_DIR != "" {
ans, err = os.MkdirTemp(shm.SHM_DIR, template)
if err == nil {
return
}
}
return os.MkdirTemp("", template)
}
func NewImageData(ic *imaging.Image) (ans *ImageData) {
b := ic.Bounds()
ans = &ImageData{
Width: b.Dx(), Height: b.Dy(),
}
if ic.Metadata != nil {
ans.Format_uppercase = strings.ToUpper(ic.Metadata.Format.String())
}
for _, f := range ic.Frames {
fr := ImageFrame{
Img: f.Image, Left: f.TopLeft.X, Top: f.TopLeft.Y, Width: f.Image.Bounds().Dx(), Height: f.Image.Bounds().Dy(),
Compose_onto: int(f.ComposeOnto), Number: int(f.Number), Delay_ms: int32(f.Delay.Milliseconds()),
Replace: f.Replace, Is_opaque: imaging.IsOpaque(f.Image),
}
if fr.Delay_ms <= 0 {
fr.Delay_ms = -1 // -1 is gapless in graphics protocol
}
ans.Frames = append(ans.Frames, &fr)
}
return
}
func OpenImageFromPath(path string, opts ...imaging.DecodeOption) (ans *ImageData, err error) {
ic, err := imaging.OpenAll(path, opts...)
if err != nil {
return nil, err
}
return NewImageData(ic), nil
}
func OpenImageFromReader(r io.Reader, opts ...imaging.DecodeOption) (ans *ImageData, s io.Reader, err error) {
ic, s, err := imaging.DecodeAll(r, opts...)
if err != nil {
return nil, nil, err
}
return NewImageData(ic), s, nil
}