Files
kitty/kittens/icat/process_images.go
Kovid Goyal 1c8e8e9530 Switch over to the new imaging backend for icat
Greatly simplifies a whole bunch of code. The new backend takes care of
falling back to ImageMagick efficiently itself.
2025-11-10 11:34:56 +05:30

344 lines
9.4 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, pix := imaging.AsRGBData8(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
}
}
}