More work on image preview rendering

This commit is contained in:
Kovid Goyal
2025-10-08 22:00:12 +05:30
parent 811b4fa127
commit 49d8b1a9d0
7 changed files with 503 additions and 59 deletions

View File

@@ -0,0 +1,294 @@
package choose_files
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/images"
)
var _ = fmt.Print
type GraphicsHandler struct {
running_in_tmux bool
image_id_counter, detection_file_id uint32
files_to_delete []string
files_supported atomic.Bool
last_rendered_image struct {
p *ImagePreview
width, height int
image_width, image_height int
}
image_transmitted uint32
has_placements bool
}
func (self *GraphicsHandler) Cleanup() {
for _, f := range self.files_to_delete {
_ = os.Remove(f)
}
}
func (self *GraphicsHandler) new_graphics_command() *graphics.GraphicsCommand {
gc := graphics.GraphicsCommand{}
if self.running_in_tmux {
gc.WrapPrefix = "\033Ptmux;"
gc.WrapSuffix = "\033\\"
gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
}
return &gc
}
func (self *GraphicsHandler) Initialize(lp *loop.Loop) error {
tmux := tui.TmuxSocketAddress()
if tmux != "" && tui.TmuxAllowPassthrough() == nil {
self.running_in_tmux = true
}
if !self.running_in_tmux {
g := func(t graphics.GRT_t, payload string) uint32 {
self.image_id_counter++
g1 := self.new_graphics_command()
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(self.image_id_counter).SetDataWidth(1).SetDataHeight(1).SetFormat(
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
_ = g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
return self.image_id_counter
}
tf, err := images.CreateTempInRAM()
if err == nil {
if _, err = tf.Write([]byte{1, 2, 3}); err == nil {
self.detection_file_id = g(graphics.GRT_transmission_tempfile, tf.Name())
self.files_to_delete = append(self.files_to_delete, tf.Name())
}
tf.Close()
}
}
self.image_id_counter++
return nil
}
func (self *GraphicsHandler) Finalize(lp *loop.Loop) {
if self.image_transmitted > 0 {
g := self.new_graphics_command()
g.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_by_id).SetImageId(self.image_transmitted)
_ = g.WriteWithPayloadToLoop(lp, nil)
self.image_transmitted = 0
}
}
func (self *GraphicsHandler) ClearPlacements(lp *loop.Loop) {
if self.image_transmitted > 0 && self.has_placements {
g := self.new_graphics_command()
g.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_by_id).SetImageId(self.image_transmitted)
_ = g.WriteWithPayloadToLoop(lp, nil)
self.has_placements = false
}
}
func (self *GraphicsHandler) HandleGraphicsCommand(gc *graphics.GraphicsCommand) error {
switch gc.ImageId() {
case self.detection_file_id:
if gc.ResponseMessage() == "OK" {
self.files_supported.Store(true)
}
}
return nil
}
func (self *GraphicsHandler) cache_resized_image(cdir, cache_key string, img *images.ImageData) (m *images.SerializableImageMetadata, cached_data map[string]string, err error) {
s, frames := img.Serialize()
sd, err := json.Marshal(s)
if err != nil {
return nil, nil, err
}
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
if err = os.WriteFile(path, sd, 0o600); err != nil {
return nil, nil, fmt.Errorf("failed to write resized frame metadata to cache: %w", err)
}
cached_data = make(map[string]string, len(frames)+1)
for i, f := range frames {
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
if err = os.WriteFile(path, f, 0o600); err != nil {
return nil, nil, fmt.Errorf("failed to write resized frame %d data to cache: %w", i, err)
}
cached_data[key] = path
}
m = &s
return
}
func (self *GraphicsHandler) cached_resized_image(cdir, cache_key string) (m *images.SerializableImageMetadata, cached_data map[string]string) {
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
b, err := os.ReadFile(path)
if err != nil {
return
}
var s images.SerializableImageMetadata
if err = json.Unmarshal(b, &s); err != nil {
return
}
m = &s
cached_data = make(map[string]string, len(s.Frames)+1)
cached_data[IMAGE_METADATA_KEY] = path
for i := range len(s.Frames) {
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
cached_data[key] = path
}
return
}
func transmit_by_escape_code(lp *loop.Loop, frame []byte, gc *graphics.GraphicsCommand) {
atomic := lp.IsAtomicUpdateActive()
lp.EndAtomicUpdate()
gc.SetTransmission(graphics.GRT_transmission_direct)
_ = gc.WriteWithPayloadToLoop(lp, frame)
if atomic {
lp.StartAtomicUpdate()
}
}
func transmit_by_file(lp *loop.Loop, frame_path []byte, gc *graphics.GraphicsCommand) {
gc.SetTransmission(graphics.GRT_transmission_file)
_ = gc.WriteWithPayloadToLoop(lp, frame_path)
}
func (self *GraphicsHandler) transmit(lp *loop.Loop, img *images.ImageData, m *images.SerializableImageMetadata, cached_data map[string]string) {
if m == nil {
s := img.SerializeOnlyMetadata()
m = &s
}
self.last_rendered_image.image_width = m.Width
self.last_rendered_image.image_height = m.Height
is_animated := len(m.Frames) > 0
self.image_transmitted = self.image_id_counter
frame_control_cmd := self.new_graphics_command()
frame_control_cmd.SetAction(graphics.GRT_action_animate).SetImageId(self.image_transmitted)
for frame_num, frame := range m.Frames {
gc := self.new_graphics_command()
gc.SetImageId(self.image_transmitted)
gc.SetDataWidth(uint64(frame.Width)).SetDataHeight(uint64(frame.Height))
if frame.Is_opaque {
gc.SetFormat(graphics.GRT_format_rgb)
}
switch frame_num {
case 0:
gc.SetAction(graphics.GRT_action_transmit)
gc.SetCursorMovement(graphics.GRT_cursor_static)
default:
gc.SetAction(graphics.GRT_action_frame)
gc.SetGap(int32(frame.Delay_ms))
if frame.Compose_onto > 0 {
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
}
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
}
if cached_data == nil {
transmit_by_escape_code(lp, img.Frames[frame_num].Data(), gc)
} else {
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(frame_num)]
transmit_by_file(lp, utils.UnsafeStringToBytes(path), gc)
}
if is_animated {
switch frame_num {
case 0:
// set gap for the first frame and number of loops for the animation
c := frame_control_cmd
c.SetTargetFrame(uint64(frame.Number))
c.SetGap(int32(frame.Delay_ms))
c.SetNumberOfLoops(1)
_ = c.WriteWithPayloadToLoop(lp, nil)
case 1:
c := frame_control_cmd
c.SetAnimationControl(2) // set animation to loading mode
_ = c.WriteWithPayloadToLoop(lp, nil)
}
}
}
if is_animated {
c := frame_control_cmd
c.SetAnimationControl(3) // set animation to normal mode
_ = c.WriteWithPayloadToLoop(lp, nil)
}
}
func (self *GraphicsHandler) place_image(lp *loop.Loop, x, y, px_width int, sz ScreenSize) {
self.has_placements = true
gc := self.new_graphics_command()
gc.SetAction(graphics.GRT_action_display).SetImageId(self.image_transmitted).SetPlacementId(1).SetCursorMovement(graphics.GRT_cursor_static)
if extra := px_width - self.last_rendered_image.image_width; extra > 0 {
x += extra / sz.cell_width
gc.SetXOffset(uint64(extra % sz.cell_width))
}
lp.MoveCursorTo(x, y)
_ = gc.WriteWithPayloadToLoop(lp, nil)
}
func (self *GraphicsHandler) RenderImagePreview(h *Handler, p *ImagePreview, x, y, width, height int) {
sz := h.screen_size
px_width, px_height := width*sz.cell_width, height*sz.cell_height
var err error
defer func() {
self.last_rendered_image.p = p
self.last_rendered_image.width, self.last_rendered_image.height = width, height
if err != nil {
NewErrorPreview(fmt.Errorf("Failed to render image: %w", err)).Render(h, x, y, width, height)
} else if self.image_transmitted > 0 {
self.place_image(h.lp, x, y, px_width, sz)
}
}()
if self.last_rendered_image.p == p && self.last_rendered_image.width == width && self.last_rendered_image.height == height {
return
}
files_supported := self.files_supported.Load()
if p.img_metadata.Width <= px_width && p.img_metadata.Height <= px_height {
if files_supported {
self.transmit(h.lp, nil, p.img_metadata, p.cached_data)
} else {
if err = p.ensure_source_image(); err != nil {
return
}
self.transmit(h.lp, p.source_img, p.img_metadata, nil)
}
return
}
cache_key := fmt.Sprintf("%d-%d-%p", width, height, p)
img_metadata, cached_data := self.cached_resized_image(p.disk_cache.ResultsDir(), cache_key)
var img *images.ImageData
if len(cached_data) == 0 {
img = p.source_img
final_width, final_height := images.FitImage(img.Width, img.Height, px_width, px_height)
if final_width != img.Width || final_height != img.Height {
x_frac, y_frac := float64(final_width)/float64(img.Width), float64(final_height)/float64(img.Height)
img = img.Resize(x_frac, y_frac)
}
if img_metadata, cached_data, err = self.cache_resized_image(p.disk_cache.ResultsDir(), cache_key, img); err != nil {
err = fmt.Errorf("failed to cache resized image: %w", err)
return
}
}
if files_supported {
self.transmit(h.lp, img, img_metadata, p.cached_data)
} else {
if img == nil {
if img, err = load_image(cached_data); err != nil {
err = fmt.Errorf("failed to load resized image from cache: %w", err)
return
}
}
self.transmit(h.lp, img, nil, nil)
}
}

View File

@@ -1,19 +1,26 @@
package choose_files
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"sync"
"sync/atomic"
"github.com/kovidgoyal/kitty/tools/disk_cache"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/humanize"
"github.com/kovidgoyal/kitty/tools/utils/images"
)
var _ = fmt.Print
const IMAGE_METADATA_KEY = "image-metadata.json"
const IMAGE_DATA_PREFIX = "image-data-"
var dc_size atomic.Int64
var _ = fmt.Print
var preview_cache = sync.OnceValues(func() (*disk_cache.DiskCache, error) {
cdir := utils.CacheDir()
@@ -21,14 +28,24 @@ var preview_cache = sync.OnceValues(func() (*disk_cache.DiskCache, error) {
return disk_cache.NewDiskCache(cdir, dc_size.Load())
})
type ShowData struct {
abspath string
metadata fs.FileInfo
x, y, width, height int
cached_data map[string]string
img_metadata *images.SerializableImageMetadata
}
type PreviewRenderer interface {
Render(string) (map[string][]byte, error)
ShowMetadata(h *Handler, abspath string, metadata fs.FileInfo, x, y, width, height int, cached_data map[string]string) int
Render(string) (map[string][]byte, *images.ImageData, error)
ShowMetadata(h *Handler, s ShowData) int
}
type render_data struct {
cached_data map[string]string
err error
cached_data map[string]string
img *images.ImageData
img_metadata *images.SerializableImageMetadata
err error
}
type ImagePreview struct {
@@ -38,17 +55,72 @@ type ImagePreview struct {
cached_data map[string]string
render_err Preview
render_channel chan render_data
source_img *images.ImageData
img_metadata *images.SerializableImageMetadata
renderer PreviewRenderer
file_metadata_preview Preview
WakeupMainThread func() bool
}
func (p ImagePreview) IsValidForColorScheme(bool) bool { return true }
func (p *ImagePreview) IsValidForColorScheme(bool) bool { return true }
func (p ImagePreview) Render(h *Handler, x, y, width, height int) {
func (p *ImagePreview) Unload() {
p.source_img = nil
}
func load_image(cached_data map[string]string) (img *images.ImageData, err error) {
fp := cached_data[IMAGE_METADATA_KEY]
if fp == "" {
return nil, fmt.Errorf("missing cached image metadata")
}
b, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("failed to read cached image metadata: %w", err)
}
var m images.SerializableImageMetadata
if err = json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("failed to decode cached image metadata: %w", err)
}
frames := make([][]byte, len(m.Frames))
for i := range m.Frames {
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(i)]
if path == "" {
return nil, fmt.Errorf("missing cached data for frame: %d", i)
}
d, e := os.ReadFile(path)
if e != nil {
return nil, fmt.Errorf("failed to read cached image frame %d data: %w", i, e)
}
m.Frames[i].Size = len(d)
frames[i] = d
}
return images.ImageFromSerialized(m, frames)
}
func (p *ImagePreview) ensure_source_image() (err error) {
if p.source_img != nil {
return
}
defer func() {
if err != nil {
p.render_err = NewErrorPreview(err)
}
}()
p.source_img, err = load_image(p.cached_data)
return
}
func (p *ImagePreview) Render(h *Handler, x, y, width, height int) {
if p.render_channel == nil {
if p.render_err == nil {
y += p.renderer.ShowMetadata(h, p.abspath, p.metadata, x, y, width, height, p.cached_data)
offset := p.renderer.ShowMetadata(h, ShowData{
abspath: p.abspath, metadata: p.metadata, x: x, y: y, width: width, height: height, cached_data: p.cached_data,
img_metadata: p.img_metadata,
})
y += offset
height -= offset
h.graphics_handler.RenderImagePreview(h, p, x, y, width, height)
} else {
p.render_err.Render(h, x, y, width, height)
}
@@ -58,6 +130,8 @@ func (p ImagePreview) Render(h *Handler, x, y, width, height int) {
case hd := <-p.render_channel:
p.render_channel = nil
p.cached_data = hd.cached_data
p.source_img = hd.img
p.img_metadata = hd.img_metadata
p.render_err = NewErrorPreview(fmt.Errorf("Failed to render the preview with error: %w", hd.err))
p.Render(h, x, y, width, height)
return
@@ -75,28 +149,60 @@ func (p *ImagePreview) start_rendering() {
}()
key, ans, err := p.disk_cache.GetPath(p.abspath)
if err != nil {
p.render_channel <- render_data{nil, err}
}
if len(ans) > 0 {
p.render_channel <- render_data{ans, nil}
p.render_channel <- render_data{err: err}
return
}
rdata, err := p.renderer.Render(p.abspath)
if len(ans) > 0 {
if d := ans[IMAGE_METADATA_KEY]; d != "" {
if b, err := os.ReadFile(d); err == nil {
var m images.SerializableImageMetadata
if err = json.Unmarshal(b, &m); err == nil {
p.render_channel <- render_data{cached_data: ans, img_metadata: &m}
return
}
}
}
}
rdata, img, err := p.renderer.Render(p.abspath)
if err != nil {
p.render_channel <- render_data{nil, err}
p.render_channel <- render_data{err: err}
} else {
ans, err = p.disk_cache.AddPath(p.abspath, key, rdata)
p.render_channel <- render_data{utils.IfElse(err == nil, ans, nil), err}
if err == nil {
m := img.SerializeOnlyMetadata()
p.render_channel <- render_data{cached_data: ans, img_metadata: &m, img: img}
} else {
p.render_channel <- render_data{err: err}
}
}
}
type ImagePreviewRenderer uint
func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, err error) {
func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, img *images.ImageData, err error) {
if img, err = images.OpenImageFromPath(abspath); err != nil {
return nil, nil, err
}
m, data := img.Serialize()
ans = make(map[string][]byte, len(data)+1)
metadata, err := json.Marshal(m)
if err != nil {
return nil, nil, err
}
ans[IMAGE_METADATA_KEY] = metadata
for i, d := range data {
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
ans[key] = d
}
return
}
func (p ImagePreviewRenderer) ShowMetadata(h *Handler, abspath string, metadata fs.FileInfo, x, y, width, height int, cached_data map[string]string) int {
func (p ImagePreviewRenderer) ShowMetadata(h *Handler, s ShowData) int {
text := ""
if s.img_metadata != nil {
text = fmt.Sprintf("%s: %dx%d %s", s.img_metadata.Format_uppercase, s.img_metadata.Width, s.img_metadata.Height, humanize.Bytes(uint64(s.metadata.Size())))
}
h.render_wrapped_text_in_region(text, s.x, s.y, s.width, s.height, false)
return 0
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/kovidgoyal/kitty/tools/ignorefiles"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/graphics"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
@@ -206,16 +207,29 @@ type ScreenSize struct {
}
type Handler struct {
state State
screen_size ScreenSize
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
spinner *tui.Spinner
preview_manager *PreviewManager
state State
screen_size ScreenSize
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
spinner *tui.Spinner
preview_manager *PreviewManager
last_rendered_preview Preview
graphics_handler GraphicsHandler
}
func (self *Handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error {
switch etype {
case loop.APC:
gc := graphics.GraphicsCommandFromAPC(payload)
if gc != nil {
return self.graphics_handler.HandleGraphicsCommand(gc)
}
}
return nil
}
func (h *Handler) draw_screen() (err error) {
@@ -226,7 +240,8 @@ func (h *Handler) draw_screen() (err error) {
h.state.mouse_state.ApplyHoverStyles(h.lp)
h.lp.EndAtomicUpdate()
}()
h.lp.ClearScreen()
h.lp.ClearScreenButNotGraphics()
h.graphics_handler.ClearPlacements(h.lp)
h.state.mouse_state.ClearCellRegions()
switch h.state.screen {
case NORMAL:
@@ -294,6 +309,7 @@ func (h *Handler) OnInitialize() (ans string, err error) {
}
}
}
err = h.graphics_handler.Initialize(h.lp)
h.result_manager.set_root_dir()
h.draw_screen()
return
@@ -778,6 +794,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
lp.ColorSchemeChangeNotifications()
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
defer handler.graphics_handler.Cleanup()
handler.rl = readline.New(lp, readline.RlInit{
Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(handler.state.CurrentDir),
})
@@ -813,6 +830,10 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
lp.RequestCurrentColorScheme()
return handler.OnInitialize()
}
lp.OnFinalize = func() string {
handler.graphics_handler.Finalize(lp)
return ""
}
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
handler.init_sizes(new_size)
return handler.draw_screen()
@@ -829,6 +850,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
lp.OnKeyEvent = handler.OnKeyEvent
lp.OnText = handler.OnText
lp.OnMouseEvent = handler.OnMouseEvent
lp.OnEscapeCode = handler.on_escape_code
lp.OnWakeup = func() (err error) {
select {
case err = <-handler.err_chan:

View File

@@ -25,6 +25,7 @@ var _ = fmt.Print
type Preview interface {
Render(h *Handler, x, y, width, height int)
IsValidForColorScheme(light bool) bool
Unload()
}
type PreviewManager struct {
@@ -80,6 +81,7 @@ type MessagePreview struct {
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
func (p MessagePreview) Unload() {}
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
offset := 0
if p.title != "" {
@@ -189,6 +191,8 @@ type TextFilePreview struct {
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
func (p *TextFilePreview) Unload() {}
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
if p.highlighted_chan != nil {
select {
@@ -316,9 +320,14 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
return
}
abspath := filepath.Join(h.state.CurrentDir(), r.text)
if h.last_rendered_preview != nil {
h.last_rendered_preview.Unload()
h.last_rendered_preview = nil
}
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
} else {
h.last_rendered_preview = p
p.Render(h, x, y, width, height)
}
}

View File

@@ -39,6 +39,10 @@ func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {
return new_disk_cache(path, max_size)
}
func (dc *DiskCache) ResultsDir() string {
return dc.get_dir
}
func (dc *DiskCache) Get(key string, items ...string) (map[string]string, error) {
dc.lock()
defer dc.unlock()

View File

@@ -157,11 +157,10 @@ func (dc *DiskCache) write_entries_if_dirty() (err error) {
removed = true
}
}()
if err := os.WriteFile(temp, d, 0o600); err != nil {
return err
}
if err = os.Rename(temp, path); err == nil {
removed = true
if err = os.WriteFile(temp, d, 0o600); err == nil {
if err = os.Rename(temp, path); err == nil {
removed = true
}
}
return err
}

View File

@@ -175,13 +175,20 @@ type SerializableImageMetadata struct {
const SERIALIZE_VERSION = 1
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 := SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase}
m := self.SerializeOnlyMetadata()
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])
m.Frames[i].Size = len(data[i])
}
return m, data
}
@@ -272,29 +279,7 @@ func MakeTempDir(template string) (ans string, err error) {
return os.MkdirTemp("", template)
}
func check_resize(frame *ImageFrame, filename string) error {
// ImageMagick sometimes generates RGBA images smaller than the specified
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
s, err := os.Stat(filename)
if err != nil {
return err
}
sz := int(s.Size())
bytes_per_pixel := 4
if frame.Is_opaque {
bytes_per_pixel = 3
}
expected_size := bytes_per_pixel * frame.Width * frame.Height
if sz < expected_size {
missing := expected_size - sz
if missing%(bytes_per_pixel*frame.Width) != 0 {
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
}
frame.Height -= missing / (bytes_per_pixel * frame.Width)
}
return nil
}
// Native {{{
func (frame *ImageFrame) set_delay(min_gap, delay int) {
frame.Delay_ms = int32(max(min_gap, delay) * 10)
if frame.Delay_ms == 0 {
@@ -344,11 +329,36 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
return
}
// }}}
// ImageMagick {{{
var MagickExe = sync.OnceValue(func() string {
return utils.FindExe("magick")
})
func check_resize(frame *ImageFrame, filename string) error {
// ImageMagick sometimes generates RGBA images smaller than the specified
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
s, err := os.Stat(filename)
if err != nil {
return err
}
sz := int(s.Size())
bytes_per_pixel := 4
if frame.Is_opaque {
bytes_per_pixel = 3
}
expected_size := bytes_per_pixel * frame.Width * frame.Height
if sz < expected_size {
missing := expected_size - sz
if missing%(bytes_per_pixel*frame.Width) != 0 {
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
}
frame.Height -= missing / (bytes_per_pixel * frame.Width)
}
return nil
}
func RunMagick(path string, cmd []string) ([]byte, error) {
if MagickExe() != "magick" {
cmd = append([]string{MagickExe()}, cmd...)