fix(server): collage drawScaled uses center-crop instead of stretch
test-go / test (push) Successful in 27s
test-go / integration (push) Has been cancelled

This commit is contained in:
2026-06-04 08:22:59 -04:00
parent 8cd2383a42
commit edd198cdf5
+33 -6
View File
@@ -176,16 +176,43 @@ func fallbackGlyph() image.Image {
return img
}
// drawScaled copies src into dst.Rect, scaling with simple nearest-neighbor.
// stdlib lacks high-quality scaling; nearest-neighbor is fine for a
// 600x600 output where each cell is 300x300 — most album covers are
// already 300-1500 pixels and the visual loss is minor.
// drawScaled copies src into dst.Rect using a center-cropped "cover" fit
// (the same model as BoxFit.cover / object-fit: cover in the clients).
// Non-square sources are scaled so the *smaller* destination dimension is
// fully filled and the larger axis is center-cropped, preserving aspect
// ratio. Without this, banner-shaped or LP-shaped album art stretches in
// the cell -- the album-coherent system playlists (new_for_you,
// first_listens) make the stretching disproportionately visible because
// fewer unique covers contribute, so each warped cell is a quarter of
// the collage rather than diluted.
//
// Scaling itself stays nearest-neighbor -- stdlib lacks high-quality
// scaling and dependency cost is unjustified for this 600x600 output.
func drawScaled(dst draw.Image, r image.Rectangle, src image.Image) {
srcBounds := src.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return
}
dstW := r.Dx()
dstH := r.Dy()
// Cover-fit: the side of src that maps to dst at the larger scale
// fully fills its axis; the other axis is center-cropped.
scaleX := float64(dstW) / float64(srcW)
scaleY := float64(dstH) / float64(srcH)
scale := scaleX
if scaleY > scale {
scale = scaleY
}
cropW := float64(dstW) / scale
cropH := float64(dstH) / scale
cropOffX := float64(srcBounds.Min.X) + (float64(srcW)-cropW)/2
cropOffY := float64(srcBounds.Min.Y) + (float64(srcH)-cropH)/2
for y := r.Min.Y; y < r.Max.Y; y++ {
sy := int(cropOffY + float64(y-r.Min.Y)*cropH/float64(dstH))
for x := r.Min.X; x < r.Max.X; x++ {
sx := srcBounds.Min.X + (x-r.Min.X)*srcBounds.Dx()/r.Dx()
sy := srcBounds.Min.Y + (y-r.Min.Y)*srcBounds.Dy()/r.Dy()
sx := int(cropOffX + float64(x-r.Min.X)*cropW/float64(dstW))
dst.Set(x, y, src.At(sx, sy))
}
}