From a814ab4c2e915da54389db1a0334a3fcd89becd3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 8 Nov 2025 11:51:11 +0530 Subject: [PATCH] icat: Allow controlling how images are fit Fixes #9201 --- docs/changelog.rst | 8 ++++++-- kittens/icat/main.go | 32 ++++++++++++++++++++++++++++++-- kittens/icat/main.py | 13 ++++++++++--- kittens/icat/native.go | 31 ++++++++++++++++++++++++++++--- kittens/icat/process_images.go | 19 +++++++++++++++++-- kittens/icat/scaling_test.go | 29 +++++++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 kittens/icat/scaling_test.go diff --git a/docs/changelog.rst b/docs/changelog.rst index 11a58e6d5..e36b4b56b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -139,13 +139,17 @@ Detailed list of changes - Add support for the `paste events protocol `__ (: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`) diff --git a/kittens/icat/main.go b/kittens/icat/main.go index 64d8bef52..6b84c070e 100644 --- a/kittens/icat/main.go +++ b/kittens/icat/main.go @@ -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() diff --git a/kittens/icat/main.py b/kittens/icat/main.py index b3ad4e885..c571a500a 100644 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -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 diff --git a/kittens/icat/native.go b/kittens/icat/native.go index a9457fe29..e387f15c1 100644 --- a/kittens/icat/native.go +++ b/kittens/icat/native.go @@ -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 diff --git a/kittens/icat/process_images.go b/kittens/icat/process_images.go index c9957f279..4f5834bc1 100644 --- a/kittens/icat/process_images.go +++ b/kittens/icat/process_images.go @@ -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" diff --git a/kittens/icat/scaling_test.go b/kittens/icat/scaling_test.go new file mode 100644 index 000000000..82b974b71 --- /dev/null +++ b/kittens/icat/scaling_test.go @@ -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) + } + } +}