How do you fit an image inside a frame such that every pixel is visible and the image isn’t distorted? In CSS, we write
object-fit: contain. In an Android
ImageView, we use
centerInside. But what if you are alone in the wild lands of custom drawing? We must determine the scale factors ourselves.
I’ve solved this problem many times, but I don’t find it intuitive and thus keep forgetting the answer. Time to write it down in LaTeX, am I right?
Our first step is to determine the image’s aspect ratio, which relates its width to its height:
For our image to not appear distorted, its resolution after scaling must maintain this same aspect ratio.
The frame has its own aspect ratio that generally isn’t the same as the image’s:
We have two choices for scaling the image: scale it to fill the frame horizontally or scale it to fill the frame vertically. Let’s examine the two possible resolutions of these approaches.
First, if we fill the frame horizontally, the width will be determined by the frame:
The height must be chosen to maintain the image’s aspect ratio. We can solve this constraint for the height:
So, our scaled resolution for horizontal filling is:
Second, if we fill the frame vertically, the height will be determined by the frame:
The width must be chosen to maintain the image’s aspect ratio. We can solve this constraint for the width:
So, our scaled resolution for vertical filling is:
We have two possible resolutions for our scaled version of the image. Which do we choose? We want the one that makes all pixels visible. That is, we want to ensure both of these criteria:
Let’s see how our two resolutions fare at meeting the criteria. Recall that our horizontal resolution is:
The width certainly meets the criteria. Does the height? It depends. Let’s suppose the height criterion to be true and rework it to a simpler expression:
We conclude that the horizontal resolution will contain the image inside the frame when the frame’s aspect ratio is less than the image’s.
Recall also that our vertical resolution is:
The height certainly meets the criteria. Does the width? It depends. Let’s suppose the width criterion to be true and rework it to a simpler expression:
We conclude that the vertical resolution will contain the image inside the frame when the image’s aspect ratio is less than the frame’s.
This is nice. The criteria for choosing the fill axis boils down to comparing the two aspect ratios. The two expressions are mostly mutually exclusive. When the aspect ratios happen to be the same, it doesn’t matter which scaled resolution we use, as they will both contain the image.
We can describe our algorithm in pseudocode:
frameAspect = frameWidth / frameHeight imageWidth = imageWidth / imageHeight if frameAspect < imageAspect // Fill horizontally scaledWidth = frameWidth scaledHeight = frameWidth / imageAspect else // Fill vertically scaledWidth = frameHeight * imageAspect scaledHeight = frameHeight end
We can draw the image in the corner of the frame:
draw(image, 0, 0, scaledWidth, scaledHeight)
Or in the center:
centerX = frameWidth / 2 centerY = frameHeight / 2 draw(image, centerX - scaledWidth / 2, centerY - scaledHeight / 2, centerX + scaledWidth / 2, centerY + scaledHeight / 2)