teaching machines

Fittin’ Image

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?

Aspect Ratio

Our first step is to determine the image’s aspect ratio, which relates its width to its height:

$$\begin{array}{rcl}\textit{image aspect} &=& \frac{\textit{image width}}{\textit{image height}} \\\end{array}$$

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:

$$\begin{array}{rcl}\textit{frame aspect} &=& \frac{\textit{frame width}}{\textit{frame height}} \\\end{array}$$

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.

Fill Horizontally

First, if we fill the frame horizontally, the width will be determined by the frame:

$$\begin{array}{rcl}\textit{scaled width} &=& \textit{frame width} \\\end{array}$$

The height must be chosen to maintain the image’s aspect ratio. We can solve this constraint for the height:

$$\begin{array}{rcl}\textit{image aspect} &=& \frac{\textit{scaled width}}{\textit{scaled height}} \\\textit{scaled height} &=& \frac{\textit{scaled width}}{\textit{image aspect}} \\ &=& \frac{\textit{frame width}}{\textit{image aspect}} \\\end{array}$$

So, our scaled resolution for horizontal filling is:

$$\begin{array}{rcl}\textit{horizontal fill} = (\textit{frame width}, \frac{\textit{frame width}}{\textit{image aspect}})\end{array}$$

Fill Vertically

Second, if we fill the frame vertically, the height will be determined by the frame:

$$\begin{array}{rcl}\textit{scaled height} &=& \textit{frame height} \\\end{array}$$

The width must be chosen to maintain the image’s aspect ratio. We can solve this constraint for the width:

$$\begin{array}{rcl}\frac{\textit{scaled width}}{\textit{scaled height}} &=& \textit{image aspect} \\\textit{scaled width} &=& \textit{scaled height} \times \textit{image aspect} \\ &=& \textit{frame height} \times \textit{image aspect} \\\end{array}$$

So, our scaled resolution for vertical filling is:

$$\begin{array}{rcl}\textit{vertical fill} = (\textit{frame height} \times \textit{image aspect}, \textit{frame height})\end{array}$$

Choosing the Fill Axis

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:

$$\begin{array}{rcl}\textit{scaled width} &\leq& \textit{frame width} \\\textit{scaled height} &\leq& \textit{frame height} \\\end{array}$$

Let’s see how our two resolutions fare at meeting the criteria. Recall that our horizontal resolution is:

$$\begin{array}{rcl}\textit{horizontal fill} = (\textit{frame width}, \frac{\textit{frame width}}{\textit{image aspect}})\end{array}$$

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:

$$\begin{array}{rcl}\frac{\textit{frame width}}{\textit{image aspect}} &\leq& \textit{frame height} \\\frac{\textit{frame width}}{\textit{frame height}} &\leq& \textit{image aspect} \\\textit{frame aspect} &\leq& \textit{image aspect} \\\end{array}$$

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:

$$\begin{array}{rcl}\textit{vertical fill} = (\textit{frame height} \times \textit{image aspect}, \textit{frame height})\end{array}$$

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:

$$\begin{array}{rcl}\textit{frame height} \times \textit{image aspect} &\leq& \textit{frame width} \\\textit{image aspect} &\leq& \frac{\textit{frame width}}{\textit{frame height}} \\\textit{image aspect} &\leq& \textit{frame aspect} \\\end{array}$$

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)