From 49d8b1a9d0f3676524ea62ab2d6c6eefdc8d093d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 8 Oct 2025 22:00:12 +0530 Subject: [PATCH] More work on image preview rendering --- kittens/choose_files/graphics.go | 294 ++++++++++++++++++++++++++ kittens/choose_files/image_preview.go | 140 ++++++++++-- kittens/choose_files/main.go | 44 +++- kittens/choose_files/preview.go | 9 + tools/disk_cache/api.go | 4 + tools/disk_cache/implementation.go | 9 +- tools/utils/images/loading.go | 62 +++--- 7 files changed, 503 insertions(+), 59 deletions(-) create mode 100644 kittens/choose_files/graphics.go diff --git a/kittens/choose_files/graphics.go b/kittens/choose_files/graphics.go new file mode 100644 index 000000000..6a2648f02 --- /dev/null +++ b/kittens/choose_files/graphics.go @@ -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) + } +} diff --git a/kittens/choose_files/image_preview.go b/kittens/choose_files/image_preview.go index 9b905aae9..4dccca34a 100644 --- a/kittens/choose_files/image_preview.go +++ b/kittens/choose_files/image_preview.go @@ -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 } diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index 57525654f..6124ea5b5 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -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: diff --git a/kittens/choose_files/preview.go b/kittens/choose_files/preview.go index a569b342f..da860803d 100644 --- a/kittens/choose_files/preview.go +++ b/kittens/choose_files/preview.go @@ -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) } } diff --git a/tools/disk_cache/api.go b/tools/disk_cache/api.go index 4edf10de5..d7d8cecb5 100644 --- a/tools/disk_cache/api.go +++ b/tools/disk_cache/api.go @@ -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() diff --git a/tools/disk_cache/implementation.go b/tools/disk_cache/implementation.go index 8ef8a32c6..e9ae1d053 100644 --- a/tools/disk_cache/implementation.go +++ b/tools/disk_cache/implementation.go @@ -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 } diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go index cb0e5d201..4610a9161 100644 --- a/tools/utils/images/loading.go +++ b/tools/utils/images/loading.go @@ -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...)