Real-image posters: match the box to the poster's shape and crop the remainder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 23:13:47 +02:00
co-authored by Claude Sonnet 5.5
parent b3275f6a81
commit dd6f862d61
5 changed files with 153 additions and 27 deletions
+56 -9
View File
@@ -294,17 +294,47 @@ func (c *coverView) aspect() (w, h int) {
return b.Dx(), b.Dy()
}
// coverCells returns how many columns and rows a poster of the given aspect
// coverFitTolerance is how far the shape of the cell box may differ from the
// poster's, as a fraction. The box is chosen to match the poster's shape
// closely; the small remainder is cropped away from the poster before it is
// shown as a real image, because a real image placed in a box of another shape
// is letterboxed (visible bars beside or above it).
const coverFitTolerance = 0.03
// coverCells returns how many columns and rows a poster of the given shape
// needs inside w×h cells. cellAspect is the height of a cell divided by its
// width, about 2 in most terminals.
// width, about 2 in most terminals. Cell counts are whole numbers, so the
// largest size whose box is within coverFitTolerance of the poster's shape is
// chosen (or the closest one if none is).
func coverCells(imgW, imgH, w, h int, cellAspect float64) (cols, rows int) {
cols = w
rows = int(float64(cols) * float64(imgH) / float64(imgW) / cellAspect)
if rows > h {
rows = h
cols = int(float64(rows) * cellAspect * float64(imgW) / float64(imgH))
if imgW <= 0 || imgH <= 0 || w <= 0 || h <= 0 || cellAspect <= 0 {
return 0, 0
}
return max(cols, 0), max(rows, 0)
shape := float64(imgW) / float64(imgH) // width / height of the poster
// A box of r rows is r*cellAspect*shape columns wide for the same shape.
maxRows := min(h, int(float64(w)/(cellAspect*shape)))
if maxRows < 1 {
return w, 0
}
bestErr := 2.0
for r := maxRows; r >= max(1, maxRows-6); r-- {
c := int(float64(r)*cellAspect*shape + 0.5)
if c < 1 || c > w {
continue
}
boxShape := float64(c) / (float64(r) * cellAspect)
err := boxShape/shape - 1
if err < 0 {
err = -err
}
if err <= coverFitTolerance {
return c, r
}
if err < bestErr {
bestErr, cols, rows = err, c, r
}
}
return cols, rows
}
func (c *coverView) Draw(screen tcell.Screen) {
@@ -341,6 +371,23 @@ func (c *coverView) Draw(screen tcell.Screen) {
}
}
// cropToBox returns the part of an image of the given bounds that has the same
// shape as a box of cols×rows cells: a few pixels are trimmed from both sides
// of the longer dimension (at most the fit tolerance), so the image fills the
// box exactly and the terminal does not letterbox it.
func cropToBox(b image.Rectangle, cols, rows int, cellAspect float64) image.Rectangle {
box := float64(cols) / (float64(rows) * cellAspect) // width / height of the box
shape := float64(b.Dx()) / float64(b.Dy())
if box < shape { // box is narrower: trim the sides
w := int(float64(b.Dy())*box + 0.5)
trim := (b.Dx() - w) / 2
return image.Rect(b.Min.X+trim, b.Min.Y, b.Min.X+trim+w, b.Max.Y)
}
h := int(float64(b.Dx())/box + 0.5) // box is wider: trim top and bottom
trim := (b.Dy() - h) / 2
return image.Rect(b.Min.X, b.Min.Y+trim, b.Max.X, b.Min.Y+trim+h)
}
// drawGraphics shows the poster as a real image through the Kitty graphics
// protocol: the image is uploaded once and the cells get placeholder
// characters. It reports false when that is not possible (no protocol, no true
@@ -350,7 +397,7 @@ func (c *coverView) drawGraphics(screen tcell.Screen, x, y, cols, rows int) bool
if c.gfx == nil || screen.Colors() < 1<<24 || cols > maxPlaceholders || rows > maxPlaceholders {
return false
}
id, err := c.gfx.imageID(c.img)
id, err := c.gfx.imageID(c.img, cropToBox(c.img.Bounds(), cols, rows, c.aspectOfCells()))
if err == nil {
err = c.gfx.place(id, cols, rows)
}