Real-image posters: scale to the box's pixel size

Ghostty shows an image smaller than its placement at its own size, so
library posters (Radarr's poster-500 is 333x500, not 500x750) came out
smaller than the 426x640 remote ones. Images are now scaled (bilinear)
to exactly cols*cellWidth x rows*cellHeight pixels before upload, and
library titles use the original poster for real images. The disk cache
stores posters as decoded (JPEG) instead of the possibly large originals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:25:45 +02:00
co-authored by Claude Opus 5.5
parent 786e783951
commit 15e97f4582
8 changed files with 100 additions and 25 deletions
+27 -2
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/base64"
"fmt"
xdraw "golang.org/x/image/draw"
"image"
"image/png"
"strings"
@@ -43,6 +44,11 @@ type kittyGraphics struct {
holdUntil time.Time // no real images until then (the window is being resized)
debug func(string) // reports what is uploaded, if set
// cellPixels returns the size of a cell in pixels (0 if unknown): images are
// scaled to exactly fill their cells, because terminals do not all enlarge
// an image that is smaller than its placement (Ghostty shows it at its own
// size, so a small poster came out smaller than the others).
cellPixels func() (w, h int)
}
// settleDelay is how long the window size must stay the same before real
@@ -92,9 +98,15 @@ func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows i
if id, ok := k.ids[key]; ok {
return id, nil
}
var src image.Image = img.SubImage(rect)
if k.cellPixels != nil {
if cw, ch := k.cellPixels(); cw > 0 && ch > 0 {
src = scaleImage(src, cols*cw, rows*ch)
}
}
var data bytes.Buffer
enc := png.Encoder{CompressionLevel: png.BestSpeed}
if err := enc.Encode(&data, img.SubImage(rect)); err != nil {
if err := enc.Encode(&data, src); err != nil {
return 0, err
}
encoded := base64.StdEncoding.EncodeToString(data.Bytes())
@@ -130,7 +142,8 @@ func (k *kittyGraphics) show(img *image.RGBA, rect image.Rectangle, cols, rows i
k.ids[key] = id
k.latest[img] = key
if k.debug != nil {
k.debug(fmt.Sprintf("graphics: id %d, poster %dx%d px cropped to %v, placed over %dx%d cells", id, img.Bounds().Dx(), img.Bounds().Dy(), rect, cols, rows))
k.debug(fmt.Sprintf("graphics: id %d, poster %dx%d px cropped to %v, sent as %dx%d px, placed over %dx%d cells",
id, img.Bounds().Dx(), img.Bounds().Dy(), rect, src.Bounds().Dx(), src.Bounds().Dy(), cols, rows))
}
return id, nil
}
@@ -175,3 +188,15 @@ func kittyTerminal(env func(string) string) bool {
}
return false
}
// scaleImage resizes src to w×h pixels, smoothly (bilinear) both up and down.
// Sizes beyond 4000 pixels are not made (a misreported cell size).
func scaleImage(src image.Image, w, h int) image.Image {
b := src.Bounds()
if w <= 0 || h <= 0 || w > 4000 || h > 4000 || (w == b.Dx() && h == b.Dy()) {
return src
}
dst := image.NewRGBA(image.Rect(0, 0, w, h))
xdraw.BiLinear.Scale(dst, dst.Bounds(), src, b, xdraw.Src, nil)
return dst
}