More work on diffing images
This commit is contained in:
parent
c745961f47
commit
508a61bd1c
@ -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/")
|
||||
}
|
||||
|
||||
@ -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)))
|
||||
text = place_in(text, available_cols)
|
||||
return margin_format(strings.Repeat(` `, margin_size)) + formatter(text)
|
||||
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
|
||||
}
|
||||
return ""
|
||||
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 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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
86
tools/tui/graphics/collection.go
Normal file
86
tools/tui/graphics/collection.go
Normal 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}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user