mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
Move the kittens Go code into the kittens folder
This commit is contained in:
137
kittens/icat/detect.go
Normal file
137
kittens/icat/detect.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func DetectSupport(timeout time.Duration) (memory, files, direct bool, err error) {
|
||||
temp_files_to_delete := make([]string, 0, 8)
|
||||
shm_files_to_delete := make([]shm.MMap, 0, 8)
|
||||
var direct_query_id, file_query_id, memory_query_id uint32
|
||||
lp, e := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
|
||||
if e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
print_error := func(format string, args ...any) {
|
||||
lp.Println(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if len(temp_files_to_delete) > 0 && transfer_by_file != supported {
|
||||
for _, name := range temp_files_to_delete {
|
||||
os.Remove(name)
|
||||
}
|
||||
}
|
||||
if len(shm_files_to_delete) > 0 && transfer_by_memory != supported {
|
||||
for _, name := range shm_files_to_delete {
|
||||
name.Unlink()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
lp.OnInitialize = func() (string, error) {
|
||||
var iid uint32
|
||||
lp.AddTimer(timeout, false, func(loop.IdType) error {
|
||||
return fmt.Errorf("Timed out waiting for a response form the terminal: %w", os.ErrDeadlineExceeded)
|
||||
})
|
||||
|
||||
g := func(t graphics.GRT_t, payload string) uint32 {
|
||||
iid += 1
|
||||
g1 := &graphics.GraphicsCommand{}
|
||||
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(iid).SetDataWidth(1).SetDataHeight(1).SetFormat(
|
||||
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
|
||||
g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
|
||||
return iid
|
||||
}
|
||||
|
||||
direct_query_id = g(graphics.GRT_transmission_direct, "123")
|
||||
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())
|
||||
tf.Write([]byte{1, 2, 3})
|
||||
tf.Close()
|
||||
} else {
|
||||
print_error("Failed to create temporary file for data transfer, file based transfer is disabled. Error: %v", err)
|
||||
}
|
||||
sf, err := shm.CreateTemp("icat-", 3)
|
||||
if err == nil {
|
||||
memory_query_id = g(graphics.GRT_transmission_sharedmem, sf.Name())
|
||||
shm_files_to_delete = append(shm_files_to_delete, sf)
|
||||
copy(sf.Slice(), []byte{1, 2, 3})
|
||||
sf.Close()
|
||||
} else {
|
||||
var ens *shm.ErrNotSupported
|
||||
if !errors.As(err, &ens) {
|
||||
print_error("Failed to create SHM for data transfer, memory based transfer is disabled. Error: %v", err)
|
||||
}
|
||||
}
|
||||
lp.QueueWriteString("\x1b[c")
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
lp.OnEscapeCode = func(etype loop.EscapeCodeType, payload []byte) (err error) {
|
||||
switch etype {
|
||||
case loop.CSI:
|
||||
if len(payload) > 3 && payload[0] == '?' && payload[len(payload)-1] == 'c' {
|
||||
lp.Quit(0)
|
||||
return nil
|
||||
}
|
||||
case loop.APC:
|
||||
g := graphics.GraphicsCommandFromAPC(payload)
|
||||
if g != nil {
|
||||
if g.ResponseMessage() == "OK" {
|
||||
switch g.ImageId() {
|
||||
case direct_query_id:
|
||||
direct = true
|
||||
case file_query_id:
|
||||
files = true
|
||||
case memory_query_id:
|
||||
memory = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
|
||||
if event.MatchesPressOrRepeat("ctrl+c") {
|
||||
event.Handled = true
|
||||
print_error("Waiting for response from terminal, aborting now could lead to corruption")
|
||||
}
|
||||
if event.MatchesPressOrRepeat("ctrl+z") {
|
||||
event.Handled = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = lp.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ds := lp.DeathSignalName()
|
||||
if ds != "" {
|
||||
fmt.Println("Killed by signal: ", ds)
|
||||
lp.KillIfSignalled()
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
58
kittens/icat/magick.go
Normal file
58
kittens/icat/magick.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils/images"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func Render(path string, ro *images.RenderOptions, frames []images.IdentifyRecord) (ans []*image_frame, err error) {
|
||||
ro.TempfilenameTemplate = shm_template
|
||||
image_frames, filenames, err := images.RenderWithMagick(path, ro, frames)
|
||||
if err == nil {
|
||||
ans = make([]*image_frame, len(image_frames))
|
||||
for i, x := range image_frames {
|
||||
ans[i] = &image_frame{
|
||||
filename: filenames[x.Number], filename_is_temporary: true,
|
||||
number: x.Number, width: x.Width, height: x.Height, left: x.Left, top: x.Top,
|
||||
transmission_format: graphics.GRT_format_rgba, delay_ms: int(x.Delay_ms), compose_onto: x.Compose_onto,
|
||||
}
|
||||
if x.Is_opaque {
|
||||
ans[i].transmission_format = graphics.GRT_format_rgb
|
||||
}
|
||||
}
|
||||
}
|
||||
return ans, err
|
||||
}
|
||||
|
||||
func render_image_with_magick(imgd *image_data, src *opened_input) (err error) {
|
||||
err = src.PutOnFilesystem()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
frames, err := images.IdentifyWithMagick(src.FileSystemName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imgd.format_uppercase = frames[0].Fmt_uppercase
|
||||
imgd.canvas_width, imgd.canvas_height = frames[0].Canvas.Width, frames[0].Canvas.Height
|
||||
set_basic_metadata(imgd)
|
||||
if !imgd.needs_conversion {
|
||||
make_output_from_input(imgd, src)
|
||||
return nil
|
||||
}
|
||||
ro := images.RenderOptions{RemoveAlpha: remove_alpha, Flip: flip, Flop: flop}
|
||||
if scale_image(imgd) {
|
||||
ro.ResizeTo.X, ro.ResizeTo.Y = imgd.canvas_width, imgd.canvas_height
|
||||
}
|
||||
imgd.frames, err = Render(src.FileSystemName(), &ro, frames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
278
kittens/icat/main.go
Normal file
278
kittens/icat/main.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/style"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type Place struct {
|
||||
width, height, left, top int
|
||||
}
|
||||
|
||||
var opts *Options
|
||||
var place *Place
|
||||
var z_index int32
|
||||
var remove_alpha *images.NRGBColor
|
||||
var flip, flop bool
|
||||
|
||||
type transfer_mode int
|
||||
|
||||
const (
|
||||
unknown transfer_mode = iota
|
||||
unsupported
|
||||
supported
|
||||
)
|
||||
|
||||
var transfer_by_file, transfer_by_memory, transfer_by_stream transfer_mode
|
||||
|
||||
var files_channel chan input_arg
|
||||
var output_channel chan *image_data
|
||||
var num_of_items int
|
||||
var keep_going *atomic.Bool
|
||||
var screen_size *unix.Winsize
|
||||
|
||||
func send_output(imgd *image_data) {
|
||||
output_channel <- imgd
|
||||
}
|
||||
|
||||
func parse_mirror() (err error) {
|
||||
flip = opts.Mirror == "both" || opts.Mirror == "vertical"
|
||||
flop = opts.Mirror == "both" || opts.Mirror == "horizontal"
|
||||
return
|
||||
}
|
||||
|
||||
func parse_background() (err error) {
|
||||
if opts.Background == "" || opts.Background == "none" {
|
||||
return nil
|
||||
}
|
||||
col, err := style.ParseColor(opts.Background)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid value for --background: %w", err)
|
||||
}
|
||||
remove_alpha = &images.NRGBColor{R: col.Red, G: col.Green, B: col.Blue}
|
||||
return
|
||||
}
|
||||
|
||||
func parse_z_index() (err error) {
|
||||
val := opts.ZIndex
|
||||
var origin int32
|
||||
if strings.HasPrefix(val, "--") {
|
||||
origin = -1073741824
|
||||
val = val[1:]
|
||||
}
|
||||
i, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid value for --z-index with error: %w", err)
|
||||
}
|
||||
z_index = int32(i) + origin
|
||||
return
|
||||
}
|
||||
|
||||
func parse_place() (err error) {
|
||||
if opts.Place == "" {
|
||||
return nil
|
||||
}
|
||||
area, pos, found := strings.Cut(opts.Place, "@")
|
||||
if !found {
|
||||
return fmt.Errorf("Invalid --place specification: %s", opts.Place)
|
||||
}
|
||||
w, h, found := strings.Cut(area, "x")
|
||||
if !found {
|
||||
return fmt.Errorf("Invalid --place specification: %s", opts.Place)
|
||||
}
|
||||
l, t, found := strings.Cut(pos, "x")
|
||||
if !found {
|
||||
return fmt.Errorf("Invalid --place specification: %s", opts.Place)
|
||||
}
|
||||
place = &Place{}
|
||||
place.width, err = strconv.Atoi(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
place.height, err = strconv.Atoi(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
place.left, err = strconv.Atoi(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
place.top, err = strconv.Atoi(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func print_error(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
|
||||
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
|
||||
opts = o
|
||||
err = parse_place()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
err = parse_z_index()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
err = parse_background()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
err = parse_mirror()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
t, err := tty.OpenControllingTerm()
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", err)
|
||||
}
|
||||
screen_size, err = t.GetSize()
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("Failed to query terminal using TIOCGWINSZ with error: %w", err)
|
||||
}
|
||||
|
||||
if opts.PrintWindowSize {
|
||||
fmt.Printf("%dx%d", screen_size.Xpixel, screen_size.Ypixel)
|
||||
return 0, nil
|
||||
}
|
||||
if opts.Clear {
|
||||
cc := &graphics.GraphicsCommand{}
|
||||
cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_visible)
|
||||
cc.WriteWithPayloadTo(os.Stdout, nil)
|
||||
}
|
||||
if screen_size.Xpixel == 0 || screen_size.Ypixel == 0 {
|
||||
return 1, fmt.Errorf("Terminal does not support reporting screen sizes in pixels, use a terminal such as kitty, WezTerm, Konsole, etc. that does.")
|
||||
}
|
||||
|
||||
items, err := process_dirs(args...)
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
if opts.Place != "" && len(items) > 1 {
|
||||
return 1, fmt.Errorf("The --place option can only be used with a single image, not %d", len(items))
|
||||
}
|
||||
files_channel = make(chan input_arg, len(items))
|
||||
for _, ia := range items {
|
||||
files_channel <- ia
|
||||
}
|
||||
num_of_items = len(items)
|
||||
output_channel = make(chan *image_data, 1)
|
||||
keep_going = &atomic.Bool{}
|
||||
keep_going.Store(true)
|
||||
if !opts.DetectSupport && num_of_items > 0 {
|
||||
num_workers := utils.Max(1, utils.Min(num_of_items, runtime.NumCPU()))
|
||||
for i := 0; i < num_workers; i++ {
|
||||
go run_worker()
|
||||
}
|
||||
}
|
||||
|
||||
passthrough_mode := no_passthrough
|
||||
switch opts.Passthrough {
|
||||
case "tmux":
|
||||
passthrough_mode = tmux_passthrough
|
||||
case "detect":
|
||||
if tui.TmuxSocketAddress() != "" {
|
||||
passthrough_mode = tmux_passthrough
|
||||
}
|
||||
}
|
||||
|
||||
if passthrough_mode == no_passthrough && (opts.TransferMode == "detect" || opts.DetectSupport) {
|
||||
memory, files, direct, err := DetectSupport(time.Duration(opts.DetectionTimeout * float64(time.Second)))
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
if !direct {
|
||||
keep_going.Store(false)
|
||||
return 1, fmt.Errorf("This terminal does not support the graphics protocol use a terminal such as kitty, WezTerm or Konsole that does. If you are running inside a terminal multiplexer such as tmux or screen that might be interfering as well.")
|
||||
}
|
||||
if memory {
|
||||
transfer_by_memory = supported
|
||||
} else {
|
||||
transfer_by_memory = unsupported
|
||||
}
|
||||
if files {
|
||||
transfer_by_file = supported
|
||||
} else {
|
||||
transfer_by_file = unsupported
|
||||
}
|
||||
}
|
||||
if passthrough_mode != no_passthrough {
|
||||
// tmux doesnt allow responses from the terminal so we cant detect if memory or file based transferring is supported
|
||||
transfer_by_memory = unsupported
|
||||
transfer_by_file = unsupported
|
||||
transfer_by_stream = supported
|
||||
}
|
||||
if opts.DetectSupport {
|
||||
if transfer_by_memory == supported {
|
||||
print_error("memory")
|
||||
} else if transfer_by_file == supported {
|
||||
print_error("files")
|
||||
} else {
|
||||
print_error("stream")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
use_unicode_placeholder := opts.UnicodePlaceholder
|
||||
if passthrough_mode != no_passthrough {
|
||||
use_unicode_placeholder = true
|
||||
}
|
||||
base_id := uint32(opts.ImageId)
|
||||
for num_of_items > 0 {
|
||||
imgd := <-output_channel
|
||||
if base_id != 0 {
|
||||
imgd.image_id = base_id
|
||||
base_id++
|
||||
if base_id == 0 {
|
||||
base_id++
|
||||
}
|
||||
}
|
||||
imgd.use_unicode_placeholder = use_unicode_placeholder
|
||||
imgd.passthrough_mode = passthrough_mode
|
||||
num_of_items--
|
||||
if imgd.err != nil {
|
||||
print_error("Failed to process \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err)
|
||||
} else {
|
||||
transmit_image(imgd)
|
||||
if imgd.err != nil {
|
||||
print_error("Failed to transmit \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
keep_going.Store(false)
|
||||
if opts.Hold {
|
||||
fmt.Print("\r")
|
||||
if opts.Place != "" {
|
||||
fmt.Println()
|
||||
}
|
||||
tui.HoldTillEnter(false)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func EntryPoint(parent *cli.Command) {
|
||||
create_cmd(parent, main)
|
||||
}
|
||||
183
kittens/icat/native.go
Normal file
183
kittens/icat/native.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func resize_frame(imgd *image_data, img image.Image) (image.Image, image.Rectangle) {
|
||||
b := img.Bounds()
|
||||
left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
|
||||
new_width := int(imgd.scaled_frac.x * float64(width))
|
||||
new_height := int(imgd.scaled_frac.y * float64(height))
|
||||
img = imaging.Resize(img, new_width, new_height, imaging.Lanczos)
|
||||
newleft := int(imgd.scaled_frac.x * float64(left))
|
||||
newtop := int(imgd.scaled_frac.y * float64(top))
|
||||
return img, image.Rect(newleft, newtop, newleft+new_width, newtop+new_height)
|
||||
}
|
||||
|
||||
const shm_template = "kitty-icat-*"
|
||||
|
||||
func add_frame(ctx *images.Context, imgd *image_data, img image.Image) *image_frame {
|
||||
is_opaque := false
|
||||
if imgd.format_uppercase == "JPEG" {
|
||||
// special cased because EXIF orientation could have already changed this image to an NRGBA making IsOpaque() very slow
|
||||
is_opaque = true
|
||||
} else {
|
||||
is_opaque = images.IsOpaque(img)
|
||||
}
|
||||
b := img.Bounds()
|
||||
if imgd.scaled_frac.x != 0 {
|
||||
img, b = resize_frame(imgd, img)
|
||||
}
|
||||
f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: b.Min.X, top: b.Min.Y}
|
||||
dest_rect := image.Rect(0, 0, f.width, f.height)
|
||||
var final_img image.Image
|
||||
bytes_per_pixel := 4
|
||||
|
||||
if is_opaque || remove_alpha != nil {
|
||||
var rgb *images.NRGB
|
||||
bytes_per_pixel = 3
|
||||
m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel))
|
||||
if err != nil {
|
||||
rgb = images.NewNRGB(dest_rect)
|
||||
} else {
|
||||
rgb = &images.NRGB{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect}
|
||||
f.shm = m
|
||||
}
|
||||
f.transmission_format = graphics.GRT_format_rgb
|
||||
f.in_memory_bytes = rgb.Pix
|
||||
final_img = rgb
|
||||
} else {
|
||||
var rgba *image.NRGBA
|
||||
m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel))
|
||||
if err != nil {
|
||||
rgba = image.NewNRGBA(dest_rect)
|
||||
} else {
|
||||
rgba = &image.NRGBA{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect}
|
||||
f.shm = m
|
||||
}
|
||||
f.transmission_format = graphics.GRT_format_rgba
|
||||
f.in_memory_bytes = rgba.Pix
|
||||
final_img = rgba
|
||||
}
|
||||
ctx.PasteCenter(final_img, img, remove_alpha)
|
||||
imgd.frames = append(imgd.frames, &f)
|
||||
if flip {
|
||||
ctx.FlipPixelsV(bytes_per_pixel, f.width, f.height, f.in_memory_bytes)
|
||||
if f.height < imgd.canvas_height {
|
||||
f.top = (2*imgd.canvas_height - f.height - f.top) % imgd.canvas_height
|
||||
}
|
||||
}
|
||||
if flop {
|
||||
ctx.FlipPixelsH(bytes_per_pixel, f.width, f.height, f.in_memory_bytes)
|
||||
if f.width < imgd.canvas_width {
|
||||
f.left = (2*imgd.canvas_width - f.width - f.left) % imgd.canvas_width
|
||||
}
|
||||
}
|
||||
return &f
|
||||
}
|
||||
|
||||
func scale_image(imgd *image_data) bool {
|
||||
if imgd.needs_scaling {
|
||||
width, height := imgd.canvas_width, imgd.canvas_height
|
||||
if imgd.canvas_width < imgd.available_width && opts.ScaleUp && place != nil {
|
||||
r := float64(imgd.available_width) / float64(imgd.canvas_width)
|
||||
imgd.canvas_width, imgd.canvas_height = imgd.available_width, int(r*float64(imgd.canvas_height))
|
||||
}
|
||||
neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
|
||||
imgd.needs_scaling = false
|
||||
imgd.scaled_frac.x = float64(neww) / float64(width)
|
||||
imgd.scaled_frac.y = float64(newh) / float64(height)
|
||||
imgd.canvas_width = int(imgd.scaled_frac.x * float64(width))
|
||||
imgd.canvas_height = int(imgd.scaled_frac.y * float64(height))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func load_one_frame_image(ctx *images.Context, imgd *image_data, src *opened_input) (img image.Image, err error) {
|
||||
img, err = imaging.Decode(src.file, imaging.AutoOrientation(true))
|
||||
src.Rewind()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// reset the sizes as we read EXIF tags here which could have rotated the image
|
||||
imgd.canvas_width = img.Bounds().Dx()
|
||||
imgd.canvas_height = img.Bounds().Dy()
|
||||
set_basic_metadata(imgd)
|
||||
scale_image(imgd)
|
||||
return
|
||||
}
|
||||
|
||||
func calc_min_gap(gaps []int) int {
|
||||
// Some broken GIF images have all zero gaps, browsers with their usual
|
||||
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
|
||||
// Browsers actually force a 100ms gap at any zero gap frame, but that
|
||||
// just means it is impossible to deliberately use zero gap frames for
|
||||
// sophisticated blending, so we dont do that.
|
||||
max_gap := utils.Max(0, gaps...)
|
||||
min_gap := 0
|
||||
if max_gap <= 0 {
|
||||
min_gap = 10
|
||||
}
|
||||
return min_gap
|
||||
}
|
||||
|
||||
func (frame *image_frame) set_disposal(anchor_frame int, disposal byte) int {
|
||||
anchor_frame, frame.compose_onto = images.SetGIFFrameDisposal(frame.number, anchor_frame, disposal)
|
||||
return anchor_frame
|
||||
}
|
||||
|
||||
func (frame *image_frame) set_delay(gap, min_gap int) {
|
||||
frame.delay_ms = utils.Max(min_gap, gap) * 10
|
||||
if frame.delay_ms == 0 {
|
||||
frame.delay_ms = -1
|
||||
}
|
||||
}
|
||||
|
||||
func add_gif_frames(ctx *images.Context, imgd *image_data, gf *gif.GIF) error {
|
||||
min_gap := images.CalcMinimumGIFGap(gf.Delay)
|
||||
scale_image(imgd)
|
||||
anchor_frame := 1
|
||||
for i, paletted_img := range gf.Image {
|
||||
frame := add_frame(ctx, imgd, paletted_img)
|
||||
frame.set_delay(gf.Delay[i], min_gap)
|
||||
anchor_frame = frame.set_disposal(anchor_frame, gf.Disposal[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func render_image_with_go(imgd *image_data, src *opened_input) (err error) {
|
||||
ctx := images.Context{}
|
||||
switch {
|
||||
case imgd.format_uppercase == "GIF" && opts.Loop != 0:
|
||||
gif_frames, err := gif.DecodeAll(src.file)
|
||||
src.Rewind()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode GIF file with error: %w", err)
|
||||
}
|
||||
err = add_gif_frames(&ctx, imgd, gif_frames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
img, err := load_one_frame_image(&ctx, imgd, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
add_frame(&ctx, imgd, img)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
329
kittens/icat/process_images.go
Normal file
329
kittens/icat/process_images.go
Normal file
@@ -0,0 +1,329 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"kitty/tools/tty"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type BytesBuf struct {
|
||||
data []byte
|
||||
pos int64
|
||||
}
|
||||
|
||||
func (self *BytesBuf) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
self.pos = offset
|
||||
case io.SeekCurrent:
|
||||
self.pos += offset
|
||||
case io.SeekEnd:
|
||||
self.pos = int64(len(self.data)) + offset
|
||||
default:
|
||||
return self.pos, fmt.Errorf("Unknown value for whence: %#v", whence)
|
||||
}
|
||||
self.pos = utils.Max(0, utils.Min(self.pos, int64(len(self.data))))
|
||||
return self.pos, nil
|
||||
}
|
||||
|
||||
func (self *BytesBuf) Read(p []byte) (n int, err error) {
|
||||
nb := utils.Min(int64(len(p)), int64(len(self.data))-self.pos)
|
||||
if nb == 0 {
|
||||
err = io.EOF
|
||||
} else {
|
||||
n = copy(p, self.data[self.pos:self.pos+nb])
|
||||
self.pos += nb
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *BytesBuf) Close() error {
|
||||
self.data = nil
|
||||
self.pos = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
type input_arg struct {
|
||||
arg string
|
||||
value string
|
||||
is_http_url bool
|
||||
}
|
||||
|
||||
func is_http_url(arg string) bool {
|
||||
return strings.HasPrefix(arg, "https://") || strings.HasPrefix(arg, "http://")
|
||||
}
|
||||
|
||||
func process_dirs(args ...string) (results []input_arg, err error) {
|
||||
results = make([]input_arg, 0, 64)
|
||||
if opts.Stdin != "no" && (opts.Stdin == "yes" || !tty.IsTerminal(os.Stdin.Fd())) {
|
||||
results = append(results, input_arg{arg: "/dev/stdin"})
|
||||
}
|
||||
for _, arg := range args {
|
||||
if arg != "" {
|
||||
if is_http_url(arg) {
|
||||
results = append(results, input_arg{arg: arg, value: arg, is_http_url: true})
|
||||
} else {
|
||||
if strings.HasPrefix(arg, "file://") {
|
||||
u, err := url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{Op: "Parse", Path: arg, Err: err}
|
||||
}
|
||||
arg = u.Path
|
||||
}
|
||||
s, err := os.Stat(arg)
|
||||
if err != nil {
|
||||
return nil, &fs.PathError{Op: "Stat", Path: arg, Err: err}
|
||||
}
|
||||
if s.IsDir() {
|
||||
filepath.WalkDir(arg, func(path string, d fs.DirEntry, walk_err error) error {
|
||||
if walk_err != nil {
|
||||
if d == nil {
|
||||
err = &fs.PathError{Op: "Stat", Path: arg, Err: walk_err}
|
||||
}
|
||||
return walk_err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
mt := utils.GuessMimeType(path)
|
||||
if strings.HasPrefix(mt, "image/") {
|
||||
results = append(results, input_arg{arg: arg, value: path})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
results = append(results, input_arg{arg: arg, value: arg})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type opened_input struct {
|
||||
file io.ReadSeekCloser
|
||||
name_to_unlink string
|
||||
}
|
||||
|
||||
func (self *opened_input) Rewind() {
|
||||
if self.file != nil {
|
||||
self.file.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *opened_input) Release() {
|
||||
if self.file != nil {
|
||||
self.file.Close()
|
||||
self.file = nil
|
||||
}
|
||||
if self.name_to_unlink != "" {
|
||||
os.Remove(self.name_to_unlink)
|
||||
self.name_to_unlink = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (self *opened_input) PutOnFilesystem() (err error) {
|
||||
if self.name_to_unlink != "" {
|
||||
return
|
||||
}
|
||||
f, err := images.CreateTempInRAM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create a temporary file to store input data with error: %w", err)
|
||||
}
|
||||
self.Rewind()
|
||||
_, err = io.Copy(f, self.file)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("Failed to copy input data to temporary file with error: %w", err)
|
||||
}
|
||||
self.Release()
|
||||
self.file = f
|
||||
self.name_to_unlink = f.Name()
|
||||
return
|
||||
}
|
||||
|
||||
func (self *opened_input) FileSystemName() string { return self.name_to_unlink }
|
||||
|
||||
type image_frame struct {
|
||||
filename string
|
||||
shm shm.MMap
|
||||
in_memory_bytes []byte
|
||||
filename_is_temporary bool
|
||||
width, height, left, top int
|
||||
transmission_format graphics.GRT_f
|
||||
compose_onto int
|
||||
number int
|
||||
disposal_background color.NRGBA
|
||||
delay_ms int
|
||||
}
|
||||
|
||||
type image_data struct {
|
||||
canvas_width, canvas_height int
|
||||
format_uppercase string
|
||||
available_width, available_height int
|
||||
needs_scaling, needs_conversion bool
|
||||
scaled_frac struct{ x, y float64 }
|
||||
frames []*image_frame
|
||||
image_number uint32
|
||||
image_id uint32
|
||||
cell_x_offset int
|
||||
move_x_by int
|
||||
move_to struct{ x, y int }
|
||||
width_cells, height_cells int
|
||||
use_unicode_placeholder bool
|
||||
passthrough_mode passthrough_type
|
||||
|
||||
// for error reporting
|
||||
err error
|
||||
source_name string
|
||||
}
|
||||
|
||||
func set_basic_metadata(imgd *image_data) {
|
||||
if imgd.frames == nil {
|
||||
imgd.frames = make([]*image_frame, 0, 32)
|
||||
}
|
||||
imgd.available_width = int(screen_size.Xpixel)
|
||||
imgd.available_height = 10 * imgd.canvas_height
|
||||
if place != nil {
|
||||
imgd.available_width = place.width * int(screen_size.Xpixel) / int(screen_size.Col)
|
||||
imgd.available_height = place.height * int(screen_size.Ypixel) / int(screen_size.Row)
|
||||
}
|
||||
imgd.needs_scaling = imgd.canvas_width > imgd.available_width || imgd.canvas_height > imgd.available_height || opts.ScaleUp
|
||||
imgd.needs_conversion = imgd.needs_scaling || remove_alpha != nil || flip || flop || imgd.format_uppercase != "PNG"
|
||||
}
|
||||
|
||||
func report_error(source_name, msg string, err error) {
|
||||
imgd := image_data{source_name: source_name, err: fmt.Errorf("%s: %w", msg, err)}
|
||||
send_output(&imgd)
|
||||
}
|
||||
|
||||
func make_output_from_input(imgd *image_data, f *opened_input) {
|
||||
bb, ok := f.file.(*BytesBuf)
|
||||
frame := image_frame{}
|
||||
imgd.frames = append(imgd.frames, &frame)
|
||||
frame.width = imgd.canvas_width
|
||||
frame.height = imgd.canvas_height
|
||||
if imgd.format_uppercase != "PNG" {
|
||||
panic(fmt.Sprintf("Unknown transmission format: %s", imgd.format_uppercase))
|
||||
}
|
||||
frame.transmission_format = graphics.GRT_format_png
|
||||
if ok {
|
||||
frame.in_memory_bytes = bb.data
|
||||
} else {
|
||||
frame.filename = f.file.(*os.File).Name()
|
||||
if f.name_to_unlink != "" {
|
||||
frame.filename_is_temporary = true
|
||||
f.name_to_unlink = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process_arg(arg input_arg) {
|
||||
var f opened_input
|
||||
if arg.is_http_url {
|
||||
resp, err := http.Get(arg.value)
|
||||
if err != nil {
|
||||
report_error(arg.value, "Could not get", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
report_error(arg.value, "Could not get", fmt.Errorf("bad status: %v", resp.Status))
|
||||
return
|
||||
}
|
||||
dest := bytes.Buffer{}
|
||||
dest.Grow(64 * 1024)
|
||||
_, err = io.Copy(&dest, resp.Body)
|
||||
if err != nil {
|
||||
report_error(arg.value, "Could not download", err)
|
||||
return
|
||||
}
|
||||
f.file = &BytesBuf{data: dest.Bytes()}
|
||||
} else if arg.value == "" {
|
||||
stdin, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
report_error("<stdin>", "Could not read from", err)
|
||||
return
|
||||
}
|
||||
f.file = &BytesBuf{data: stdin}
|
||||
} else {
|
||||
q, err := os.Open(arg.value)
|
||||
if err != nil {
|
||||
report_error(arg.value, "Could not open", err)
|
||||
return
|
||||
}
|
||||
f.file = q
|
||||
}
|
||||
defer f.Release()
|
||||
can_use_go := false
|
||||
var c image.Config
|
||||
var format string
|
||||
var err error
|
||||
imgd := image_data{source_name: arg.value}
|
||||
if opts.Engine == "auto" || opts.Engine == "native" {
|
||||
c, format, err = image.DecodeConfig(f.file)
|
||||
f.Rewind()
|
||||
can_use_go = err == nil
|
||||
}
|
||||
if !keep_going.Load() {
|
||||
return
|
||||
}
|
||||
if can_use_go {
|
||||
imgd.canvas_width = c.Width
|
||||
imgd.canvas_height = c.Height
|
||||
imgd.format_uppercase = strings.ToUpper(format)
|
||||
set_basic_metadata(&imgd)
|
||||
if !imgd.needs_conversion {
|
||||
make_output_from_input(&imgd, &f)
|
||||
send_output(&imgd)
|
||||
return
|
||||
}
|
||||
err = render_image_with_go(&imgd, &f)
|
||||
if err != nil {
|
||||
report_error(arg.value, "Could not render image to RGB", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = render_image_with_magick(&imgd, &f)
|
||||
if err != nil {
|
||||
report_error(arg.value, "ImageMagick failed", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !keep_going.Load() {
|
||||
return
|
||||
}
|
||||
send_output(&imgd)
|
||||
|
||||
}
|
||||
|
||||
func run_worker() {
|
||||
for {
|
||||
select {
|
||||
case arg := <-files_channel:
|
||||
if !keep_going.Load() {
|
||||
return
|
||||
}
|
||||
process_arg(arg)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
408
kittens/icat/transmit.go
Normal file
408
kittens/icat/transmit.go
Normal file
@@ -0,0 +1,408 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package icat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"kitty"
|
||||
"math"
|
||||
not_rand "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"kitty/tools/tui"
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/images"
|
||||
"kitty/tools/utils/shm"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type passthrough_type int
|
||||
|
||||
const (
|
||||
no_passthrough passthrough_type = iota
|
||||
tmux_passthrough
|
||||
)
|
||||
|
||||
func new_graphics_command(imgd *image_data) *graphics.GraphicsCommand {
|
||||
gc := graphics.GraphicsCommand{}
|
||||
switch imgd.passthrough_mode {
|
||||
case tmux_passthrough:
|
||||
gc.WrapPrefix = "\033Ptmux;"
|
||||
gc.WrapSuffix = "\033\\"
|
||||
gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
|
||||
}
|
||||
return &gc
|
||||
}
|
||||
|
||||
func gc_for_image(imgd *image_data, frame_num int, frame *image_frame) *graphics.GraphicsCommand {
|
||||
gc := new_graphics_command(imgd)
|
||||
gc.SetDataWidth(uint64(frame.width)).SetDataHeight(uint64(frame.height))
|
||||
gc.SetQuiet(graphics.GRT_quiet_silent)
|
||||
gc.SetFormat(frame.transmission_format)
|
||||
if imgd.image_number != 0 {
|
||||
gc.SetImageNumber(imgd.image_number)
|
||||
}
|
||||
if imgd.image_id != 0 {
|
||||
gc.SetImageId(imgd.image_id)
|
||||
}
|
||||
if frame_num == 0 {
|
||||
gc.SetAction(graphics.GRT_action_transmit_and_display)
|
||||
if imgd.use_unicode_placeholder {
|
||||
gc.SetUnicodePlaceholder(graphics.GRT_create_unicode_placeholder)
|
||||
gc.SetColumns(uint64(imgd.width_cells))
|
||||
gc.SetRows(uint64(imgd.height_cells))
|
||||
}
|
||||
if imgd.cell_x_offset > 0 {
|
||||
gc.SetXOffset(uint64(imgd.cell_x_offset))
|
||||
}
|
||||
if z_index != 0 {
|
||||
gc.SetZIndex(z_index)
|
||||
}
|
||||
if place != nil {
|
||||
gc.SetCursorMovement(graphics.GRT_cursor_static)
|
||||
}
|
||||
} else {
|
||||
gc.SetAction(graphics.GRT_action_frame)
|
||||
gc.SetGap(int32(frame.delay_ms))
|
||||
if frame.compose_onto > 0 {
|
||||
gc.SetOverlaidFrame(uint64(frame.compose_onto))
|
||||
} else {
|
||||
bg := (uint32(frame.disposal_background.R) << 24) | (uint32(frame.disposal_background.G) << 16) | (uint32(frame.disposal_background.B) << 8) | uint32(frame.disposal_background.A)
|
||||
gc.SetBackgroundColor(bg)
|
||||
}
|
||||
gc.SetLeftEdge(uint64(frame.left)).SetTopEdge(uint64(frame.top))
|
||||
}
|
||||
return gc
|
||||
}
|
||||
|
||||
func transmit_shm(imgd *image_data, frame_num int, frame *image_frame) (err error) {
|
||||
var mmap shm.MMap
|
||||
var data_size int64
|
||||
if frame.in_memory_bytes == nil {
|
||||
f, err := os.Open(frame.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
data_size, _ = f.Seek(0, io.SeekEnd)
|
||||
f.Seek(0, io.SeekStart)
|
||||
mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
|
||||
}
|
||||
dest := mmap.Slice()
|
||||
for len(dest) > 0 {
|
||||
n, err := f.Read(dest)
|
||||
dest = dest[n:]
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
mmap.Unlink()
|
||||
return fmt.Errorf("Failed to read data from image output data file: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if frame.shm == nil {
|
||||
data_size = int64(len(frame.in_memory_bytes))
|
||||
mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
|
||||
}
|
||||
copy(mmap.Slice(), frame.in_memory_bytes)
|
||||
} else {
|
||||
mmap = frame.shm
|
||||
frame.shm = nil
|
||||
}
|
||||
}
|
||||
gc := gc_for_image(imgd, frame_num, frame)
|
||||
gc.SetTransmission(graphics.GRT_transmission_sharedmem)
|
||||
gc.SetDataSize(uint64(data_size))
|
||||
gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
|
||||
mmap.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err error) {
|
||||
is_temp := false
|
||||
fname := ""
|
||||
var data_size int
|
||||
if frame.in_memory_bytes == nil {
|
||||
is_temp = frame.filename_is_temporary
|
||||
fname, err = filepath.Abs(frame.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err)
|
||||
}
|
||||
frame.filename = "" // so it isnt deleted in cleanup
|
||||
} else {
|
||||
is_temp = true
|
||||
if frame.shm != nil && frame.shm.FileSystemName() != "" {
|
||||
fname = frame.shm.FileSystemName()
|
||||
frame.shm.Close()
|
||||
frame.shm = nil
|
||||
} else {
|
||||
f, err := images.CreateTempInRAM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
|
||||
}
|
||||
data_size = len(frame.in_memory_bytes)
|
||||
_, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
|
||||
}
|
||||
fname = f.Name()
|
||||
}
|
||||
}
|
||||
gc := gc_for_image(imgd, frame_num, frame)
|
||||
if is_temp {
|
||||
gc.SetTransmission(graphics.GRT_transmission_tempfile)
|
||||
} else {
|
||||
gc.SetTransmission(graphics.GRT_transmission_file)
|
||||
}
|
||||
if data_size > 0 {
|
||||
gc.SetDataSize(uint64(data_size))
|
||||
}
|
||||
gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(fname))
|
||||
return nil
|
||||
}
|
||||
|
||||
func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err error) {
|
||||
data := frame.in_memory_bytes
|
||||
if data == nil {
|
||||
f, err := os.Open(frame.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
|
||||
}
|
||||
data, err = io.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read data from image output data file: %w", err)
|
||||
}
|
||||
}
|
||||
gc := gc_for_image(imgd, frame_num, frame)
|
||||
gc.WriteWithPayloadTo(os.Stdout, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculate_in_cell_x_offset(width, cell_width int) int {
|
||||
extra_pixels := width % cell_width
|
||||
if extra_pixels == 0 {
|
||||
return 0
|
||||
}
|
||||
switch opts.Align {
|
||||
case "left":
|
||||
return 0
|
||||
case "right":
|
||||
return cell_width - extra_pixels
|
||||
default:
|
||||
return (cell_width - extra_pixels) / 2
|
||||
}
|
||||
}
|
||||
|
||||
func place_cursor(imgd *image_data) {
|
||||
cw := int(screen_size.Xpixel) / int(screen_size.Col)
|
||||
ch := int(screen_size.Ypixel) / int(screen_size.Row)
|
||||
imgd.cell_x_offset = calculate_in_cell_x_offset(imgd.canvas_width, cw)
|
||||
imgd.width_cells = int(math.Ceil(float64(imgd.canvas_width) / float64(cw)))
|
||||
imgd.height_cells = int(math.Ceil(float64(imgd.canvas_height) / float64(ch)))
|
||||
if place == nil {
|
||||
switch opts.Align {
|
||||
case "center":
|
||||
imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells) / 2
|
||||
case "right":
|
||||
imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells)
|
||||
}
|
||||
} else {
|
||||
imgd.move_to.x = place.left + 1
|
||||
imgd.move_to.y = place.top + 1
|
||||
switch opts.Align {
|
||||
case "center":
|
||||
imgd.move_to.x += (place.width - imgd.width_cells) / 2
|
||||
case "right":
|
||||
imgd.move_to.x += (place.width - imgd.width_cells)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func next_random() (ans uint32) {
|
||||
for ans == 0 {
|
||||
b := make([]byte, 4)
|
||||
_, err := rand.Read(b)
|
||||
if err == nil {
|
||||
ans = binary.LittleEndian.Uint32(b[:])
|
||||
} else {
|
||||
ans = not_rand.Uint32()
|
||||
}
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func write_unicode_placeholder(imgd *image_data) {
|
||||
prefix := ""
|
||||
foreground := fmt.Sprintf("\033[38:2:%d:%d:%dm", (imgd.image_id>>16)&255, (imgd.image_id>>8)&255, imgd.image_id&255)
|
||||
os.Stdout.WriteString(foreground)
|
||||
restore := "\033[39m"
|
||||
if imgd.move_to.y > 0 {
|
||||
os.Stdout.WriteString(loop.SAVE_CURSOR)
|
||||
restore += loop.RESTORE_CURSOR
|
||||
} else if imgd.move_x_by > 0 {
|
||||
prefix = strings.Repeat(" ", imgd.move_x_by)
|
||||
}
|
||||
defer func() { os.Stdout.WriteString(restore) }()
|
||||
if imgd.move_to.y > 0 {
|
||||
fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, 0)
|
||||
}
|
||||
id_char := string(images.NumberToDiacritic[(imgd.image_id>>24)&255])
|
||||
for r := 0; r < imgd.height_cells; r++ {
|
||||
if imgd.move_to.x > 0 {
|
||||
fmt.Printf("\x1b[%dC", imgd.move_to.x)
|
||||
} else {
|
||||
os.Stdout.WriteString(prefix)
|
||||
}
|
||||
for c := 0; c < imgd.width_cells; c++ {
|
||||
os.Stdout.WriteString(string(kitty.ImagePlaceholderChar) + string(images.NumberToDiacritic[r]) + string(images.NumberToDiacritic[c]) + id_char)
|
||||
}
|
||||
os.Stdout.WriteString("\n\r")
|
||||
}
|
||||
}
|
||||
|
||||
var seen_image_ids *utils.Set[uint32]
|
||||
|
||||
func transmit_image(imgd *image_data) {
|
||||
if seen_image_ids == nil {
|
||||
seen_image_ids = utils.NewSet[uint32](32)
|
||||
}
|
||||
defer func() {
|
||||
for _, frame := range imgd.frames {
|
||||
if frame.filename_is_temporary && frame.filename != "" {
|
||||
os.Remove(frame.filename)
|
||||
frame.filename = ""
|
||||
}
|
||||
if frame.shm != nil {
|
||||
frame.shm.Unlink()
|
||||
frame.shm.Close()
|
||||
frame.shm = nil
|
||||
}
|
||||
frame.in_memory_bytes = nil
|
||||
}
|
||||
}()
|
||||
var f func(*image_data, int, *image_frame) error
|
||||
if opts.TransferMode != "detect" {
|
||||
switch opts.TransferMode {
|
||||
case "file":
|
||||
f = transmit_file
|
||||
case "memory":
|
||||
f = transmit_shm
|
||||
case "stream":
|
||||
f = transmit_stream
|
||||
}
|
||||
}
|
||||
if f == nil && transfer_by_memory == supported && imgd.frames[0].in_memory_bytes != nil {
|
||||
f = transmit_shm
|
||||
}
|
||||
if f == nil && transfer_by_file == supported {
|
||||
f = transmit_file
|
||||
}
|
||||
if f == nil {
|
||||
f = transmit_stream
|
||||
}
|
||||
if imgd.image_id == 0 {
|
||||
if imgd.use_unicode_placeholder {
|
||||
for imgd.image_id&0xFF000000 == 0 || imgd.image_id&0x00FFFF00 == 0 || seen_image_ids.Has(imgd.image_id) {
|
||||
// Generate a 32-bit image id using rejection sampling such that the most
|
||||
// significant byte and the two bytes in the middle are non-zero to avoid
|
||||
// collisions with applications that cannot represent non-zero most
|
||||
// significant bytes (which is represented by the third combining character)
|
||||
// or two non-zero bytes in the middle (which requires 24-bit color mode).
|
||||
imgd.image_id = next_random()
|
||||
}
|
||||
seen_image_ids.Add(imgd.image_id)
|
||||
} else {
|
||||
if len(imgd.frames) > 1 {
|
||||
for imgd.image_number == 0 {
|
||||
imgd.image_number = next_random()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
place_cursor(imgd)
|
||||
if imgd.use_unicode_placeholder && utils.Max(imgd.width_cells, imgd.height_cells) >= len(images.NumberToDiacritic) {
|
||||
imgd.err = fmt.Errorf("Image too large to be displayed using Unicode placeholders. Maximum size is %dx%d cells", len(images.NumberToDiacritic), len(images.NumberToDiacritic))
|
||||
return
|
||||
}
|
||||
switch imgd.passthrough_mode {
|
||||
case tmux_passthrough:
|
||||
imgd.err = tui.TmuxAllowPassthrough()
|
||||
if imgd.err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Print("\r")
|
||||
if !imgd.use_unicode_placeholder {
|
||||
if imgd.move_x_by > 0 {
|
||||
fmt.Printf("\x1b[%dC", imgd.move_x_by)
|
||||
}
|
||||
if imgd.move_to.x > 0 {
|
||||
fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, imgd.move_to.x)
|
||||
}
|
||||
}
|
||||
frame_control_cmd := new_graphics_command(imgd)
|
||||
frame_control_cmd.SetAction(graphics.GRT_action_animate)
|
||||
if imgd.image_id != 0 {
|
||||
frame_control_cmd.SetImageId(imgd.image_id)
|
||||
} else {
|
||||
frame_control_cmd.SetImageNumber(imgd.image_number)
|
||||
}
|
||||
is_animated := len(imgd.frames) > 1
|
||||
|
||||
for frame_num, frame := range imgd.frames {
|
||||
err := f(imgd, frame_num, frame)
|
||||
if err != nil {
|
||||
imgd.err = err
|
||||
return
|
||||
}
|
||||
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))
|
||||
switch {
|
||||
case opts.Loop < 0:
|
||||
c.SetNumberOfLoops(1)
|
||||
case opts.Loop > 0:
|
||||
c.SetNumberOfLoops(uint64(opts.Loop) + 1)
|
||||
}
|
||||
c.WriteWithPayloadTo(os.Stdout, nil)
|
||||
case 1:
|
||||
c := frame_control_cmd
|
||||
c.SetAnimationControl(2) // set animation to loading mode
|
||||
c.WriteWithPayloadTo(os.Stdout, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
if imgd.use_unicode_placeholder {
|
||||
write_unicode_placeholder(imgd)
|
||||
}
|
||||
if is_animated {
|
||||
c := frame_control_cmd
|
||||
c.SetAnimationControl(3) // set animation to normal mode
|
||||
c.WriteWithPayloadTo(os.Stdout, nil)
|
||||
}
|
||||
if imgd.move_to.x == 0 {
|
||||
fmt.Println() // ensure cursor is on new line
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user