mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
350 lines
9.5 KiB
Go
350 lines
9.5 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package icat
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/fs"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/kovidgoyal/imaging"
|
|
"github.com/kovidgoyal/kitty/tools/tty"
|
|
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
"github.com/kovidgoyal/kitty/tools/utils/images"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
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() {
|
|
if err = 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
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
results = append(results, input_arg{arg: arg, value: arg})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
type opened_input struct {
|
|
file io.Reader
|
|
bytes []byte
|
|
path string
|
|
}
|
|
|
|
type image_frame struct {
|
|
filename string
|
|
in_memory_bytes []byte
|
|
width, height, left, top int
|
|
transmission_format graphics.GRT_f
|
|
compose_onto int
|
|
replace bool
|
|
number int
|
|
delay_ms int
|
|
}
|
|
|
|
type image_data struct {
|
|
canvas_width, canvas_height int
|
|
format_uppercase string
|
|
available_width, available_height int
|
|
needs_scaling bool
|
|
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
|
|
}
|
|
|
|
const inf = math.MaxInt
|
|
|
|
func set_basic_metadata(imgd *image_data) {
|
|
if imgd.frames == nil {
|
|
imgd.frames = make([]*image_frame, 0, 32)
|
|
}
|
|
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)
|
|
} else {
|
|
switch fit_mode {
|
|
case fit_none:
|
|
imgd.available_width, imgd.available_height = inf, inf
|
|
case fit_both:
|
|
imgd.available_width = int(screen_size.Xpixel)
|
|
imgd.available_height = int(screen_size.Ypixel)
|
|
case fit_width:
|
|
imgd.available_width = int(screen_size.Xpixel)
|
|
imgd.available_height = inf
|
|
case fit_height:
|
|
imgd.available_width = inf
|
|
imgd.available_height = int(screen_size.Ypixel)
|
|
}
|
|
}
|
|
imgd.needs_scaling = imgd.canvas_width > imgd.available_width || imgd.canvas_height > imgd.available_height || opts.ScaleUp
|
|
}
|
|
|
|
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) {
|
|
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 f.bytes != nil {
|
|
frame.in_memory_bytes = f.bytes
|
|
} else if f.path != "" {
|
|
frame.filename = f.path
|
|
} else {
|
|
var err error
|
|
if frame.in_memory_bytes, err = io.ReadAll(f.file); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func scale_up(width, height, maxWidth, maxHeight int) (newWidth, newHeight int) {
|
|
if width == 0 || height == 0 {
|
|
return 0, 0
|
|
}
|
|
// Calculate the ratio to scale the width and the ratio to scale the height.
|
|
// We use floating-point division for precision.
|
|
widthRatio := float64(maxWidth) / float64(width)
|
|
heightRatio := float64(maxHeight) / float64(height)
|
|
|
|
// To preserve the aspect ratio and fit within the limits, we must use the
|
|
// smaller of the two scaling ratios.
|
|
var ratio float64
|
|
if widthRatio < heightRatio {
|
|
ratio = widthRatio
|
|
} else {
|
|
ratio = heightRatio
|
|
}
|
|
|
|
// Calculate the new dimensions and convert them back to uints.
|
|
newWidth = int(float64(width) * ratio)
|
|
newHeight = int(float64(height) * ratio)
|
|
|
|
return newWidth, newHeight
|
|
}
|
|
|
|
func scale_image(imgd *image_data) bool {
|
|
if imgd.needs_scaling {
|
|
width, height := imgd.canvas_width, imgd.canvas_height
|
|
if opts.ScaleUp && (imgd.canvas_width < imgd.available_width || imgd.canvas_height < imgd.available_height) && (imgd.available_height != inf || imgd.available_width != inf) {
|
|
imgd.canvas_width, imgd.canvas_height = scale_up(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
|
|
}
|
|
neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height)
|
|
imgd.needs_scaling = false
|
|
x := float64(neww) / float64(width)
|
|
y := float64(newh) / float64(height)
|
|
imgd.canvas_width = int(x * float64(width))
|
|
imgd.canvas_height = int(y * float64(height))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func add_frame(imgd *image_data, img image.Image, left, top int) *image_frame {
|
|
const shm_template = "kitty-icat-*"
|
|
num_channels := 4
|
|
var pix []byte
|
|
if imaging.IsOpaque(img) {
|
|
num_channels, pix = 3, imaging.AsRGBData8(img)
|
|
} else {
|
|
pix = imaging.AsRGBAData8(img)
|
|
}
|
|
b := img.Bounds()
|
|
f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: left, top: top}
|
|
f.transmission_format = utils.IfElse(num_channels == 3, graphics.GRT_format_rgb, graphics.GRT_format_rgba)
|
|
f.in_memory_bytes = pix
|
|
imgd.frames = append(imgd.frames, &f)
|
|
return &f
|
|
}
|
|
|
|
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.bytes = dest.Bytes()
|
|
f.file = bytes.NewReader(f.bytes)
|
|
} else if arg.value == "" {
|
|
stdin, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
report_error("<stdin>", "Could not read from", err)
|
|
return
|
|
}
|
|
f.bytes = stdin
|
|
f.file = bytes.NewReader(f.bytes)
|
|
} else {
|
|
q, err := os.Open(arg.value)
|
|
if err != nil {
|
|
report_error(arg.value, "Could not open", err)
|
|
return
|
|
}
|
|
f.file = q
|
|
f.path = q.Name()
|
|
defer q.Close()
|
|
}
|
|
|
|
var img *images.ImageData
|
|
var dopts []imaging.DecodeOption
|
|
needs_conversion := false
|
|
if flip {
|
|
dopts = append(dopts, imaging.Transform(imaging.FlipVTransform))
|
|
needs_conversion = true
|
|
}
|
|
if flop {
|
|
dopts = append(dopts, imaging.Transform(imaging.FlipHTransform))
|
|
needs_conversion = true
|
|
}
|
|
if remove_alpha != nil {
|
|
dopts = append(dopts, imaging.Background(*remove_alpha))
|
|
needs_conversion = true
|
|
}
|
|
switch opts.Engine {
|
|
case "native", "builtin":
|
|
dopts = append(dopts, imaging.Backends(imaging.GO_IMAGE))
|
|
case "magick":
|
|
dopts = append(dopts, imaging.Backends(imaging.MAGICK_IMAGE))
|
|
}
|
|
imgd := image_data{source_name: arg.value}
|
|
dopts = append(dopts, imaging.ResizeCallback(func(w, h int) (int, int) {
|
|
imgd.canvas_width, imgd.canvas_height = w, h
|
|
set_basic_metadata(&imgd)
|
|
if scale_image(&imgd) {
|
|
needs_conversion = true
|
|
w, h = imgd.canvas_width, imgd.canvas_height
|
|
}
|
|
return w, h
|
|
}))
|
|
var err error
|
|
if f.path != "" {
|
|
img, err = images.OpenImageFromPath(f.path, dopts...)
|
|
} else {
|
|
img, f.file, err = images.OpenImageFromReader(f.file, dopts...)
|
|
}
|
|
if err != nil {
|
|
report_error(arg.value, "Could not render image to RGB", err)
|
|
return
|
|
}
|
|
if !keep_going.Load() {
|
|
return
|
|
}
|
|
imgd.format_uppercase = img.Format_uppercase
|
|
imgd.canvas_width, imgd.canvas_height = img.Width, img.Height
|
|
if !needs_conversion && imgd.format_uppercase == "PNG" && len(img.Frames) == 1 {
|
|
make_output_from_input(&imgd, &f)
|
|
} else {
|
|
for _, f := range img.Frames {
|
|
frame := add_frame(&imgd, f.Img, f.Left, f.Top)
|
|
frame.number, frame.compose_onto = int(f.Number), int(f.Compose_onto)
|
|
frame.replace = f.Replace
|
|
frame.delay_ms = int(f.Delay_ms)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|