Tier 3 — Upper-Intermediate

Image Resizer

Build the aspect-ratio math that resizes an image to fit within a bounding box without distortion — the logic behind every thumbnail.

🐍 Python 🟨 JavaScript

1. The Problem

Build the sizing logic for an image resizer: given an image's width and height and a maximum dimension, compute the new size so the longest side equals that maximum while keeping the aspect ratio. Images already small enough are left unchanged.

Why this project? Every thumbnail, avatar, and responsive image relies on this fit-within-a-box math. The pixel encoding is a library call; the logic that prevents squashed, distorted images is what matters.

2. How to Think About This

Find the scale factor: maxSize / longestSide. If that factor is 1 or more, the image already fits — return it unchanged. Otherwise multiply both dimensions by the scale and round. Because both sides use the same factor, the aspect ratio is preserved, so nothing looks stretched. (Applying it to real pixels needs an image library like sharp; the sizing maths below is the whole decision.)

3. The Build

resize.mjs
// Compute the new size so the longest side == maxSize, keeping proportions.
export function fitSize(width, height, maxSize) {
  const scale = maxSize / Math.max(width, height);
  if (scale >= 1) return { width, height };   // already small enough
  return { width: Math.round(width * scale), height: Math.round(height * scale) };
}

// Aspect ratio is preserved because both sides use the same scale.
export function aspectRatio({ width, height }) {
  return width / height;
}

// Demo
console.log(fitSize(4000, 3000, 800));   // { width: 800, height: 600 }
console.log(fitSize(400, 300, 800));     // { width: 400, height: 300 } (unchanged)
console.log(fitSize(1000, 2000, 500));   // { width: 250, height: 500 } (portrait)

4. Test & Prove

resize.test.mjs
import { fitSize, aspectRatio } from "./resize.mjs";

let pass = 0, fail = 0;
function test(desc, fn) {
  try { fn(); console.log("  \u2713", desc); pass++; }
  catch (e) { console.log("  \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }

test("a landscape image scales by its width", () => {
  const s = fitSize(4000, 3000, 800);
  assert(s.width === 800 && s.height === 600);
});
test("a portrait image scales by its height", () => {
  const s = fitSize(1000, 2000, 500);
  assert(s.width === 250 && s.height === 500);
});
test("an already-small image is unchanged", () => {
  const s = fitSize(400, 300, 800);
  assert(s.width === 400 && s.height === 300);
});
test("aspect ratio is preserved after resizing", () => {
  const before = aspectRatio({ width: 4000, height: 3000 });
  const after = aspectRatio(fitSize(4000, 3000, 800));
  assert(Math.abs(before - after) < 0.01);
});

console.log(`\n${pass} passed, ${fail} failed`);

5. The Interface

node resize.mjs

{ width: 800, height: 600 }
{ width: 400, height: 300 }
{ width: 250, height: 500 }

6. Run It

node resize.mjs
node resize.test.mjs

# To resize real pixels, feed fitSize() into an image library, e.g.
# sharp(input).resize(newWidth, newHeight).toFile(output)

🎯 Try this next

  1. Apply the math with the sharp library to resize real .jpg files
  2. Add a "cover" mode that crops to fill the box instead of fitting inside it
  3. Generate multiple sizes at once (thumbnail, medium, large)
  4. Preserve EXIF orientation so phone photos are not sideways

What you practised

Aspect-ratio-preserving scale math, the fit-within-a-box decision, guarding the already-small case, and separating the sizing logic (portable, testable) from the pixel encoding (a library call).

Reference: Numbers & Math · Functions · Objects

Try It Yourself