scaled

fun KiteBitmap.scaled(maxWidth: Int, maxHeight: Int): KiteBitmap

Downscale to fit inside maxWidth×maxHeight, preserving aspect ratio, using box (area-average) filtering: each destination pixel averages every source pixel that maps into its bin, so thumbnails don't shimmer the way nearest-neighbor ones do.

Color channels average alpha-weighted (premultiply, average, unpremultiply): a fully transparent pixel contributes nothing to a bin's RGB, so transparent padding (whose RGB is usually black) doesn't smear dark halos into the edges of GIF/PNG thumbnails. For fully opaque input this reduces exactly to the plain per-channel mean.

Never upscales: if the bitmap already fits, the same instance returns.

This is a post-decode scale: the decoder still materialises the full-size image transiently. What it saves is retained memory (the thumbnail you keep vs the 12MP original). Decode-time DCT-domain scaling is future work.


fun KiteAnimation.scaled(maxWidth: Int, maxHeight: Int): KiteAnimation

Downscale every frame of an animation to fit inside maxWidth×maxHeight (same box filter and aspect handling as KiteBitmap.scaled), preserving delays and the loop count. This is where animated memory goes: a full-screen GIF shown as an avatar keeps W×H×4 bytes per frame unless scaled down.

Never upscales: if the canvas already fits, the same instance returns.