More work on diffing images

This commit is contained in:
Kovid Goyal 2023-03-24 15:42:51 +05:30
parent c745961f47
commit 508a61bd1c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 221 additions and 57 deletions

View File

@ -18,6 +18,7 @@ var _ = fmt.Print
var path_name_map, remote_dirs map[string]string
var mimetypes_cache, data_cache, hash_cache *utils.LRUCache[string, string]
var size_cache *utils.LRUCache[string, int64]
var lines_cache *utils.LRUCache[string, []string]
var highlighted_lines_cache *utils.LRUCache[string, []string]
var is_text_cache *utils.LRUCache[string, bool]
@ -26,6 +27,7 @@ func init_caches() {
path_name_map = make(map[string]string, 32)
remote_dirs = make(map[string]string, 32)
const sz = 4096
size_cache = utils.NewLRUCache[string, int64](sz)
mimetypes_cache = utils.NewLRUCache[string, string](sz)
data_cache = utils.NewLRUCache[string, string](sz)
is_text_cache = utils.NewLRUCache[string, bool](sz)
@ -67,6 +69,16 @@ func data_for_path(path string) (string, error) {
})
}
func size_for_path(path string) (int64, error) {
return size_cache.GetOrCreate(path, func(path string) (int64, error) {
s, err := os.Stat(path)
if err != nil {
return 0, err
}
return s.Size(), nil
})
}
func is_image(path string) bool {
return strings.HasPrefix(mimetype_for_path(path), "image/")
}

View File

@ -210,12 +210,7 @@ func (self *LogicalLines) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta i
return
}
func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
// TODO: Implement this
return ans, nil
}
func human_readable(size int) string {
func human_readable(size int64) string {
divisor, suffix := 1, "B"
for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} {
if size < (1 << ((i + 1) * 10)) {
@ -253,33 +248,79 @@ func render_diff_line(number, text, ltype string, margin_size int, available_col
return margin + content
}
func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) {
func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
available_cols := columns/2 - margin_size
fl := func(path string, formatter func(...any) string) string {
if err == nil {
var data string
data, err = data_for_path(path)
text := fmt.Sprintf("Binary file: %s", human_readable(len(data)))
ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string, formatter func(...any) string) (string, error) {
sz, err := size_for_path(path)
if err != nil {
return "", err
}
text := fmt.Sprintf("Size: %s", human_readable(sz))
res := image_collection.ResolutionOf(path)
if res.X > -1 {
text = fmt.Sprintf("Dimensions: %dx%d %s", res.X, res.Y, text)
}
text = place_in(text, available_cols)
return margin_format(strings.Repeat(` `, margin_size)) + formatter(text)
}
return ""
return formatter(strings.Repeat(` `, margin_size) + text), err
})
if err != nil {
return nil, err
}
return append(ans, ll), nil
}
func first_binary_line(left_path, right_path string, columns, margin_size int, renderer func(path string, formatter func(...any) string) (string, error)) (*LogicalLine, error) {
available_cols := columns/2 - margin_size
line := ""
if left_path == "" {
filler := render_diff_line(``, ``, `filler`, margin_size, available_cols)
line = filler + fl(right_path, added_format)
r, err := renderer(right_path, added_format)
if err != nil {
return nil, err
}
line = filler + r
} else if right_path == "" {
filler := render_diff_line(``, ``, `filler`, margin_size, available_cols)
line = fl(left_path, removed_format) + filler
l, err := renderer(left_path, removed_format)
if err != nil {
return nil, err
}
line = l + filler
} else {
line = fl(left_path, removed_format) + fl(right_path, added_format)
l, err := renderer(left_path, removed_format)
if err != nil {
return nil, err
}
r, err := renderer(right_path, added_format)
if err != nil {
return nil, err
}
line = l + r
}
ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: left_path, linenum: 0}, screen_lines: []string{line}}
if left_path == "" {
ll.src.path = right_path
}
return append(ans, &ll), err
return &ll, nil
}
func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) {
available_cols := columns/2 - margin_size
ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string, formatter func(...any) string) (string, error) {
sz, err := size_for_path(path)
if err != nil {
return "", err
}
text := fmt.Sprintf("Binary file: %s", human_readable(sz))
text = place_in(text, available_cols)
return formatter(strings.Repeat(` `, margin_size) + text), err
})
if err != nil {
return nil, err
}
return append(ans, ll), nil
}
type DiffData struct {

View File

@ -25,6 +25,7 @@ const (
COLLECTION ResultType = iota
DIFF
HIGHLIGHT
IMAGE_LOAD
)
type ScrollPos struct {
@ -46,10 +47,11 @@ type AsyncResult struct {
diff_map map[string]*Patch
}
var image_collection *graphics.ImageCollection
type Handler struct {
async_results chan AsyncResult
shortcut_tracker config.ShortcutTracker
pending_keys []string
left, right string
collection *Collection
diff_map map[string]*Patch
@ -78,8 +80,8 @@ func (self *Handler) calculate_statistics() {
var DebugPrintln = tty.DebugPrintln
func (self *Handler) initialize() {
self.pending_keys = make([]string, 0, 4)
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
image_collection = graphics.NewImageCollection()
self.current_context_count = opts.Context
if self.current_context_count < 0 {
self.current_context_count = int(conf.Num_context_lines)
@ -154,12 +156,42 @@ func (self *Handler) highlight_all() {
}
func (self *Handler) load_all_images() {
self.collection.Apply(func(path, item_type, changed_path string) error {
if path != "" && is_image(path) {
image_collection.AddPaths(path)
}
if changed_path != "" && is_image(changed_path) {
image_collection.AddPaths(changed_path)
}
return nil
})
go func() {
r := AsyncResult{rtype: IMAGE_LOAD}
image_collection.LoadAll()
self.async_results <- r
self.lp.WakeupMainThread()
}()
}
func (self *Handler) rerender_diff() error {
if self.diff_map != nil && self.collection != nil {
err := self.render_diff()
if err != nil {
return err
}
self.draw_screen()
}
return nil
}
func (self *Handler) handle_async_result(r AsyncResult) error {
switch r.rtype {
case COLLECTION:
self.collection = r.collection
self.generate_diff()
self.highlight_all()
self.load_all_images()
case DIFF:
self.diff_map = r.diff_map
self.calculate_statistics()
@ -176,14 +208,8 @@ func (self *Handler) handle_async_result(r AsyncResult) error {
self.restore_position = nil
}
self.draw_screen()
case HIGHLIGHT:
if self.diff_map != nil && self.collection != nil {
err := self.render_diff()
if err != nil {
return err
}
self.draw_screen()
}
case HIGHLIGHT, IMAGE_LOAD:
return self.rerender_diff()
}
return nil
}

View File

@ -11,6 +11,7 @@ import (
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
)
@ -58,7 +59,7 @@ func DetectSupport(timeout time.Duration) (memory, files, direct bool, err error
}
direct_query_id = g(graphics.GRT_transmission_direct, "123")
tf, err := graphics.CreateTempInRAM()
tf, err := images.CreateTempInRAM()
if err == nil {
file_query_id = g(graphics.GRT_transmission_tempfile, tf.Name())
temp_files_to_delete = append(temp_files_to_delete, tf.Name())

View File

@ -18,6 +18,7 @@ import (
"kitty/tools/tty"
"kitty/tools/tui/graphics"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
)
@ -142,7 +143,7 @@ func (self *opened_input) PutOnFilesystem() (err error) {
if self.name_to_unlink != "" {
return
}
f, err := graphics.CreateTempInRAM()
f, err := images.CreateTempInRAM()
if err != nil {
return fmt.Errorf("Failed to create a temporary file to store input data with error: %w", err)
}

View File

@ -152,7 +152,7 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
frame.shm.Close()
frame.shm = nil
} else {
f, err := graphics.CreateTempInRAM()
f, err := images.CreateTempInRAM()
if err != nil {
return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
}

View File

@ -0,0 +1,86 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package graphics
import (
"fmt"
"image"
"sync"
"sync/atomic"
"kitty/tools/utils/images"
"golang.org/x/exp/maps"
)
var _ = fmt.Print
type Image struct {
src struct {
path string
data *images.ImageData
size image.Point
loaded bool
}
renderings map[image.Point]*images.ImageData
err error
}
type ImageCollection struct {
Shm_supported, Files_supported atomic.Bool
mutex sync.Mutex
images map[string]*Image
}
func (self *ImageCollection) ResolutionOf(key string) image.Point {
if !self.mutex.TryLock() {
return image.Point{-1, -1}
}
defer self.mutex.Unlock()
i := self.images[key]
if i == nil {
return image.Point{-2, -2}
}
return i.src.size
}
func (self *ImageCollection) AddPaths(paths ...string) {
self.mutex.Lock()
defer self.mutex.Unlock()
for _, path := range paths {
if self.images[path] == nil {
i := &Image{}
i.src.path = path
self.images[path] = i
}
}
}
func (self *ImageCollection) LoadAll() {
self.mutex.Lock()
defer self.mutex.Unlock()
ctx := images.Context{}
all := maps.Values(self.images)
ctx.Parallel(0, len(self.images), func(nums <-chan int) {
for i := range nums {
img := all[i]
if !img.src.loaded {
img.src.data, img.err = images.OpenImageFromPath(img.src.path)
if img.err == nil {
img.src.size.X, img.src.size.Y = img.src.data.Width, img.src.data.Height
}
}
}
})
}
func NewImageCollection(paths ...string) *ImageCollection {
items := make(map[string]*Image, len(paths))
for _, path := range paths {
i := &Image{}
i.src.path = path
items[path] = i
}
return &ImageCollection{images: items}
}

View File

@ -8,33 +8,15 @@ import (
"encoding/base64"
"fmt"
"io"
"os"
"strconv"
"strings"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/shm"
)
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()
}
// Enums {{{
type GRT_a int

View File

@ -17,7 +17,6 @@ import (
"strconv"
"strings"
"kitty/tools/tui/graphics"
"kitty/tools/utils"
"kitty/tools/utils/shm"
@ -27,6 +26,22 @@ import (
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
@ -189,7 +204,7 @@ type IdentifyRecord struct {
Width, Height int
Dpi struct{ X, Y float64 }
Index int
Mode graphics.GRT_f
Is_opaque bool
Needs_blend bool
Disposal int
Dimensions_swapped bool
@ -258,9 +273,9 @@ func parse_identify_record(ans *IdentifyRecord, raw *IdentifyOutput) (err error)
}
q := strings.ToLower(raw.Transparency)
if q == "blend" || q == "true" {
ans.Mode = graphics.GRT_format_rgba
ans.Is_opaque = false
} else {
ans.Mode = graphics.GRT_format_rgb
ans.Is_opaque = true
}
ans.Needs_blend = q == "blend"
switch strings.ToLower(raw.Dispose) {
@ -376,7 +391,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
}
defer os.RemoveAll(tdir)
mode := "rgba"
if frames[0].Mode == graphics.GRT_format_rgb {
if frames[0].Is_opaque {
mode = "rgb"
}
cmd = append(cmd, filepath.Join(tdir, "im-%[filename:f]."+mode))
@ -431,7 +446,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
continue
}
identify_data := frames[index]
df, cerr := os.CreateTemp(base_dir, graphics.TempTemplate+"."+mode)
df, cerr := os.CreateTemp(base_dir, TempTemplate+"."+mode)
if cerr != nil {
err = fmt.Errorf("Failed to create a temporary file in %s with error: %w", base_dir, cerr)
return
@ -444,7 +459,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
df.Close()
fmap[index+1] = df.Name()
frame := ImageFrame{
Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Mode == graphics.GRT_format_rgb,
Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque,
}
frame.set_delay(min_gap, identify_data.Gap)
err = check_resize(&frame, df.Name())