icat: Allow controlling how images are fit

Fixes #9201
This commit is contained in:
Kovid Goyal
2025-11-08 11:51:11 +05:30
parent 57f7c8f65e
commit a814ab4c2e
6 changed files with 120 additions and 12 deletions

View File

@@ -139,13 +139,17 @@ Detailed list of changes
- Add support for the `paste events protocol <https://rockorager.dev/misc/bracketed-paste-mime/>`__ (:iss:`9183`)
- icat kitten: Add support for animated PNG and animated WebP, netPBM images, ICC color profiles and CCIP metadata to the builtin engine
- icat kitten: Add a new flag :option:`kitty +kitten icat --fit` to control how images are scaled to fit the screen (:iss:`9201`)
- icat kitten: The :option:`kitty +kitten icat --scale-up` flag now takes effect when not using :option:`kitty +kitten icat --place` as well
- Add a mappable action :ac:`copy_last_command_output` to copy the output of the last
command to the clipboard (:pull:`9185`)
- ssh kitten: Fix a bug where automatic login was not working (:iss:`9187`)
- icat kitten: Add support for animated PNG and animated WebP, netPBM images, ICC color profiles and CCIP metadata to the builtin engine
- Graphics: Fix overwrite composition mode for animation frames not being honored
- Automatic color scheme switching: Fix title bar and scroll bar colors not being updated (:iss:`9167`)

View File

@@ -42,6 +42,15 @@ const (
supported
)
type fit_t int
const (
fit_none fit_t = iota
fit_width
fit_height
fit_both
)
var transfer_by_file, transfer_by_memory transfer_mode
var files_channel chan input_arg
@@ -49,6 +58,7 @@ var output_channel chan *image_data
var num_of_items int
var keep_going *atomic.Bool
var screen_size *unix.Winsize
var fit_mode fit_t
func send_output(imgd *image_data) {
output_channel <- imgd
@@ -87,6 +97,22 @@ func parse_z_index() (err error) {
return
}
func parse_fit() (err error) {
switch strings.ToLower(opts.Fit) {
case "width":
fit_mode = fit_width
case "height":
fit_mode = fit_height
case "none", "neither":
fit_mode = fit_none
case "both":
fit_mode = fit_both
default:
return fmt.Errorf("unknown fit specification: %#v", opts.Fit)
}
return nil
}
func parse_place() (err error) {
if opts.Place == "" {
return nil
@@ -130,8 +156,10 @@ func print_error(format string, args ...any) {
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
opts = o
err = parse_place()
if err != nil {
if err = parse_place(); err != nil {
return 1, err
}
if err = parse_fit(); err != nil {
return 1, err
}
err = parse_z_index()

View File

@@ -22,9 +22,16 @@ be positioned at the top left corner of the image, instead of on the line after
--scale-up
type=bool-set
When used in combination with :option:`--place` it will cause images that are
smaller than the specified area to be scaled up to use as much of the specified
area as possible.
Cause images that are smaller than the specified area to be scaled up to use as much
of the specified area as possible. The specified area depends on either the :option:`--place`
or the :option:`--fit` options.
--fit
choices=width,height,both,none
default=width
When not using :option:`--place`, control how the image is scaled relative to the screen.
You can have it fit in the screen width or height or both or neither.
--background

View File

@@ -91,12 +91,37 @@ func add_frame(ctx *images.Context, imgd *image_data, img image.Image, left, top
return &f
}
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 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))
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

View File

@@ -8,6 +8,7 @@ import (
"image"
"io"
"io/fs"
"math"
"net/http"
"net/url"
"os"
@@ -196,15 +197,29 @@ type image_data struct {
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)
}
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)
} 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
imgd.needs_conversion = imgd.needs_scaling || remove_alpha != nil || flip || flop || imgd.format_uppercase != "PNG"

View File

@@ -0,0 +1,29 @@
package icat
import (
"fmt"
"image"
"testing"
)
var _ = fmt.Print
func TestScaling(t *testing.T) {
for _, tc := range []struct {
w, h, pw, ph, ew, eh int
}{
{1000, 50, 800, 600, 800, 40},
{1000, 50, 800000, 600, 12000, 600},
{100, 50, 800, 600, 800, 400},
{1920, 1080, 800, 600, 800, 450},
{300, 900, 800, 600, 200, 600},
{400, 300, 800, 600, 800, 600},
} {
aw, ah := scale_up(tc.w, tc.h, tc.pw, tc.ph)
actual := image.Pt(aw, ah)
expected := image.Pt(tc.ew, tc.eh)
if actual != expected {
t.Fatalf("want: %v got: %v", expected, actual)
}
}
}