Lesson 32 • Advanced
Image Uploads & Processing 🖼️
By the end of this lesson you'll resize photos without squashing them, build square thumbnails, stamp watermarks, convert to WebP, and fix sideways phone uploads — the exact pipeline behind every avatar and gallery on the web.
What You'll Learn in This Lesson
- Tell GD and Imagick apart and check which is installed
- Load an existing image and create a blank canvas from scratch
- Resize and build thumbnails while keeping the aspect ratio
- Add watermarks and convert images to space-saving WebP
- Fix sideways phone photos and strip EXIF/GPS metadata
- Do it all in one line with the Intervention Image library
photo.jpg next to your script, and run php resize.php. The Output panel under each example shows exactly what to expect.1️⃣ GD vs Imagick — Know Your Tools
PHP can't process images on its own — it leans on a library (a bundle of ready-made functions). There are two. GD is bundled with almost every PHP install and covers JPEG, PNG, GIF and WebP — it's what you'll use 90% of the time. Imagick is a separate extension that wraps the powerful ImageMagick program: it supports 200+ formats and gives sharper results, but it has to be installed on the server first. Before you write a single resize, check what you actually have.
<?php
// Which image library do you actually have? Find out before you build anything.
// PHP has two: GD (built into most installs) and Imagick (a separate extension
// wrapping ImageMagick - handles 200+ formats and gives sharper results).
// gd_info() throws if GD is missing, so guard with extension_loaded() first.
if (extension_loaded('gd')) {
$gd = gd_info();
echo "GD is installed (version {$gd['GD Version']})\n";
echo "WebP support: " . ($gd['WebP Support'] ? "yes" : "no") . "\n";
} else {
echo "GD is NOT installed\n";
}
// Imagick is a class, so check the class exists rather than an extension flag.
echo "Imagick: " . (class_exists('Imagick') ? "installed" : "not installed") . "\n";
?>GD is installed (version bundled (2.1.0 compatible))
WebP support: yes
Imagick: not installedIf WebP Support says no or GD is missing, you'll get a fatal error later — so this is the first thing to confirm on any new server.
2️⃣ Loading & Creating Images
Every GD operation works on an image resource — the picture decompressed into memory. You get one two ways: load an existing file with imagecreatefromjpeg() (or ...png, ...webp), or create a blank canvas with imagecreatetruecolor(). Call getimagesize() first — it reads just the file header, so you learn the real type and dimensions cheaply, before loading megabytes into RAM.
<?php
// Two ways to get an image "resource" you can draw on or save.
// 1) LOAD an existing file. getimagesize() reads the header first so you know
// the real type and dimensions BEFORE loading the whole thing into memory.
[$w, $h, $type] = getimagesize('photo.jpg'); // e.g. [4000, 3000, 2]
echo "Source is {$w} x {$h}, type code {$type}\n"; // type 2 = IMAGETYPE_JPEG
$img = imagecreatefromjpeg('photo.jpg'); // a GD image resource in memory
// 2) CREATE a blank true-colour canvas from scratch (great for charts/badges).
$canvas = imagecreatetruecolor(200, 80); // 200 x 80 pixels
$blue = imagecolorallocate($canvas, 30, 90, 220); // pick an RGB colour
imagefill($canvas, 0, 0, $blue); // flood-fill it blue
imagejpeg($img, 'copy.jpg'); // save the loaded photo back out
imagepng($canvas, 'badge.png'); // save the blank canvas as PNG
// ALWAYS free the memory - GD images are not garbage-collected promptly.
imagedestroy($img);
imagedestroy($canvas);
echo "Saved copy.jpg and badge.png\n";
?>Source is 4000 x 3000, type code 2
Saved copy.jpg and badge.pngNotice the imagedestroy() calls at the end. GD resources are not cleaned up promptly by PHP, so on a busy server you must free each one the moment you're done or memory piles up.
3️⃣ Resizing & Thumbnails (the main event)
Serving a 4000×3000 original to a phone wastes bandwidth and slows your page. The fix is to resize. The one rule that matters: preserve the aspect ratio. Work out a single scale ratio and apply it to both width and height — apply two different ratios and the image squashes. To fit inside a box use min(); capping at 1.0 stops you upscaling a small image (which only blurs it).
<?php
// Resize while PRESERVING ASPECT RATIO. The trick is one scale ratio applied to
// BOTH width and height - never two different ratios, or the image squashes.
function resizeFit(string $src, string $dest, int $maxW, int $maxH, int $quality = 80): void
{
[$w, $h] = getimagesize($src); // original size, e.g. 4000 x 3000
// Scale to FIT inside the box. min() keeps both sides within bounds.
// The 1.0 cap means we shrink but never blow a small image up (which blurs it).
$ratio = min($maxW / $w, $maxH / $h, 1.0); // e.g. min(0.2, 0.2, 1) = 0.2
$newW = (int) round($w * $ratio); // 800
$newH = (int) round($h * $ratio); // 600
$source = imagecreatefromjpeg($src);
$canvas = imagecreatetruecolor($newW, $newH);
// imagecopyresampled does smooth bilinear resampling - far better quality
// than imagecopyresized, which just drops pixels and looks jagged.
imagecopyresampled($canvas, $source, 0, 0, 0, 0, $newW, $newH, $w, $h);
imagejpeg($canvas, $dest, $quality);
imagedestroy($source);
imagedestroy($canvas);
}
resizeFit('photo.jpg', 'photo-800.jpg', 800, 600);
echo "Resized 4000x3000 -> 800x600 (aspect ratio kept)\n";
?>Resized 4000x3000 -> 800x600 (aspect ratio kept)Always reach for imagecopyresampled(), not imagecopyresized() — the first smooths pixels for a clean result; the second just throws pixels away and looks jagged.
A thumbnail is usually a fixed square, which means you can't just fit — you'd get empty bars. Instead cover the box (use max() so it fills) and crop the overflow from the centre. That's "crop-to-fill", the look every avatar uses.
<?php
// A square thumbnail needs CROP-TO-FILL: scale so the image COVERS the box
// (use max(), not min()), then chop the overflow off centre.
function thumbnail(string $src, string $dest, int $size, int $quality = 80): void
{
[$w, $h] = getimagesize($src); // e.g. 4000 x 3000
$ratio = max($size / $w, $size / $h); // cover, so the box is filled
$scaledW = (int) round($w * $ratio);
$scaledH = (int) round($h * $ratio);
// Centre the crop: half the overflow on each side.
$cropX = (int) round(($scaledW - $size) / 2);
$cropY = (int) round(($scaledH - $size) / 2);
$source = imagecreatefromjpeg($src);
$scaled = imagecreatetruecolor($scaledW, $scaledH);
imagecopyresampled($scaled, $source, 0, 0, 0, 0, $scaledW, $scaledH, $w, $h);
$thumb = imagecreatetruecolor($size, $size);
// imagecopy lifts the centred $size x $size square out of the scaled image.
imagecopy($thumb, $scaled, 0, 0, $cropX, $cropY, $size, $size);
imagejpeg($thumb, $dest, $quality);
imagedestroy($source);
imagedestroy($scaled);
imagedestroy($thumb);
}
thumbnail('photo.jpg', 'thumb-150.jpg', 150);
echo "Created a 150x150 centre-cropped thumbnail\n";
?>Created a 150x150 centre-cropped thumbnail4️⃣ Watermarks
A watermark stamps text or a logo onto an image — useful for branding gallery photos. The key is the alpha (transparency) channel: imagecolorallocatealpha() takes a 4th value from 0 (solid) to 127 (invisible), so a value like 75 gives a soft, see-through mark that doesn't hide the photo.
<?php
// Stamp a semi-transparent text watermark onto the bottom-right corner.
$img = imagecreatefromjpeg('photo.jpg');
$w = imagesx($img); // width of the loaded image
$h = imagesy($img); // height of the loaded image
// The 4th argument (127) of imagecolorallocatealpha is the alpha: 0 = solid,
// 127 = fully transparent. 75 gives a soft, see-through white.
$white = imagecolorallocatealpha($img, 255, 255, 255, 75);
$text = '(c) My Site';
$font = 5; // built-in GD font (1-5)
$tx = $w - imagefontwidth($font) * strlen($text) - 12; // 12px from the right
$ty = $h - imagefontheight($font) - 10; // 10px from the bottom
imagestring($img, $font, $tx, $ty, $text, $white); // draw the watermark
imagejpeg($img, 'watermarked.jpg', 90);
imagedestroy($img);
echo "Watermarked photo saved\n";
?>Watermarked photo saved5️⃣ Format Conversion & Compression
Converting to WebP is the single biggest page-speed win you can make: it's typically 25-35% smaller than JPEG and 80%+ smaller than PNG at the same visual quality. The pattern is "load in any format, save in another". The save functions also take a quality value (0-100) — your compression dial. 80 is the sweet spot: near-perfect to the eye, a fraction of the bytes.
<?php
// Convert any supported image to WebP - typically 25-35% smaller than JPEG and
// 80%+ smaller than PNG, with the same visual quality. Huge for page speed.
// getimagesize() returns the type code so you can pick the right loader.
$type = getimagesize('photo.png')[2]; // e.g. IMAGETYPE_PNG (3)
$img = match ($type) {
IMAGETYPE_JPEG => imagecreatefromjpeg('photo.png'),
IMAGETYPE_PNG => imagecreatefrompng('photo.png'),
IMAGETYPE_GIF => imagecreatefromgif('photo.png'),
default => throw new RuntimeException("Unsupported type: {$type}"),
};
// The 3rd argument is quality, 0-100. 80 is a great default - near-lossless
// to the eye but a fraction of the file size.
imagewebp($img, 'photo.webp', 80);
imagedestroy($img);
$before = filesize('photo.png');
$after = filesize('photo.webp');
$saved = round((1 - $after / $before) * 100);
echo "PNG {$before} bytes -> WebP {$after} bytes ({$saved}% smaller)\n";
?>PNG 412840 bytes -> WebP 98214 bytes (76% smaller)6️⃣ EXIF Orientation & Metadata
EXIF is hidden data phones bake into a photo. Two parts matter. The Orientation flag records how the phone was held instead of rotating the pixels — so an untouched upload often appears sideways until you imagerotate() to compensate. The rest can include GPS coordinates and the camera model — a privacy leak if you publish the file as-is. The good news: re-saving through GD writes a clean file with no EXIF, so a load-process-save cycle fixes the rotation and strips the metadata in one go.
<?php
// Phone photos carry EXIF data: an "Orientation" flag, plus GPS coordinates and
// the camera model. Two jobs: ROTATE so the photo looks right, and STRIP the
// metadata so you do not leak where the user was standing.
$img = imagecreatefromjpeg('phone.jpg');
// exif_read_data needs the EXIF extension; @ silences the notice if there's none.
$exif = @exif_read_data('phone.jpg');
$orientation = $exif['Orientation'] ?? 1; // 1 = already upright
// The orientation flag tells you how the camera was held. Rotate to compensate.
$img = match ($orientation) {
3 => imagerotate($img, 180, 0), // upside down
6 => imagerotate($img, -90, 0), // rotated 90 clockwise
8 => imagerotate($img, 90, 0), // rotated 90 anticlockwise
default => $img, // 1, or unknown: leave it
};
// Re-saving through GD writes a CLEAN file with NO EXIF - the GPS/metadata is
// gone automatically, and the pixels are already rotated correctly.
imagejpeg($img, 'phone-fixed.jpg', 85);
imagedestroy($img);
echo "Rotated for orientation {$orientation} and stripped EXIF\n";
?>Rotated for orientation 6 and stripped EXIFNow you try. The script below has the aspect-ratio maths blanked out. Fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
<?php
// 🎯 YOUR TURN — finish the aspect-ratio maths, then run it.
// A 1600x900 source must fit inside a 400x400 box without distortion.
$w = 1600;
$h = 900;
$maxW = 400;
$maxH = 400;
// 1) Pick the SMALLER scale so both sides fit, and never upscale.
$ratio = ___; // 👉 use min($maxW / $w, $maxH / $h, 1.0)
// 2) Multiply both sides by the SAME ratio and round to whole pixels.
$newW = (int) round($w * ___); // 👉 multiply by $ratio
$newH = (int) round($h * ___); // 👉 multiply by $ratio (same ratio!)
echo "New size: {$newW} x {$newH}\n";
// ✅ Expected output:
// New size: 400 x 225
?>New size: 400 x 225___ blanks so the same $ratio scales both sides. A 1600×900 source should become 400×225.One more. This one converts a JPEG to WebP — fill in the two missing GD function names.
<?php
// 🎯 YOUR TURN — convert a JPEG to WebP at quality 80.
// The loader and the save call each need ONE function name filled in.
$img = ___('photo.jpg'); // 👉 load a JPEG: imagecreatefromjpeg
___($img, 'photo.webp', 80); // 👉 save as WebP: imagewebp (3rd arg = quality)
imagedestroy($img); // free the memory
echo "Converted photo.jpg to photo.webp\n";
// ✅ Expected output:
// Converted photo.jpg to photo.webp
?>Converted photo.jpg to photo.webpimagecreatefromjpeg and save with imagewebp (third argument is the quality).7️⃣ The Intervention Image Library
Writing raw GD by hand is verbose and easy to get wrong. Intervention Image is the most popular PHP image library: install it with composer require intervention/image and you get one fluent, chainable API that sits on top of either GD or Imagick. It reads EXIF orientation automatically, and methods like cover(), scaleDown() and toWebp() replace a dozen lines of GD each.
<?php
// Doing all of the above by hand is verbose. The Intervention Image library
// wraps GD/Imagick in one fluent API: install with composer require intervention/image
require 'vendor/autoload.php';
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
$manager = new ImageManager(new Driver()); // or Imagick\Driver for sharper output
$image = $manager->read('photo.jpg'); // reads EXIF orientation for you
// Chain operations - each returns the image so you can keep going.
$image->cover(300, 300) // crop-to-fill a 300x300 square
->toWebp(80) // encode as WebP, quality 80
->save('avatar.webp'); // write it to disk
// scaleDown() is the safe resize: fits inside the box and NEVER upscales.
$manager->read('photo.jpg')
->scaleDown(width: 800)
->save('photo-800.jpg');
echo "Intervention Image: avatar + 800px copy written\n";
?>Intervention Image: avatar + 800px copy writtenFor real projects, reach for Intervention first — you only drop down to raw GD when you need something the library doesn't expose.
Common Errors (and the fix)
- "Allowed memory size of … bytes exhausted" on a big photo — GD loads every pixel into RAM (~4 bytes each), so a 6000×4000 image needs ~96 MB before resizing. Check dimensions with
getimagesize()and reject oversized uploads, raisememory_limitfor the script, andimagedestroy()each resource the moment you're done. - Uploaded photos appear sideways — you didn't handle the EXIF Orientation flag. Read
exif_read_data()['Orientation']andimagerotate()to compensate. Re-saving the rotated image also strips the EXIF, which removes any GPS data the photo carried. - "Call to undefined function imagecreatefromjpeg()" — the GD extension isn't installed or enabled. Install it (
apt install php-gdon Debian/Ubuntu) and confirm withextension_loaded('gd'). For WebP you also need GD built with WebP support. - The image looks squashed or stretched — you set the new width and height independently instead of scaling by one shared ratio. Compute
$ratioonce withmin()(fit) ormax()(cover) and multiply both sides by it.
Pro Tips
- 💡 Process in the background. Resizing on the request that handles the upload makes the user wait — push the job to a queue and return immediately for anything heavy.
- 💡 Generate a responsive set once. Create 150 / 320 / 800 / 1200px WebP versions at upload time and serve them with the HTML
srcsetattribute so each device downloads only what it needs. - 💡 Use
imagecopyresampled, neverimagecopyresized. The quality difference is night and day for almost identical code.
📋 Quick Reference — Image Processing
| Function | Example | What It Does |
|---|---|---|
| getimagesize() | [$w,$h,$t] = getimagesize($f) | Read size/type from the header (cheap) |
| imagecreatefromjpeg() | $img = imagecreatefromjpeg($f) | Load a file into a GD resource |
| imagecreatetruecolor() | imagecreatetruecolor(200,80) | Make a blank canvas |
| imagecopyresampled() | imagecopyresampled($dst,$src,…) | Resize with smooth resampling |
| imagewebp() | imagewebp($img,$dest,80) | Save as WebP at a quality |
| imagerotate() | imagerotate($img,-90,0) | Rotate (for EXIF orientation) |
| imagedestroy() | imagedestroy($img) | Free the resource's memory |
Frequently Asked Questions
Q: Should I use GD or Imagick?
Use GD for the common case: it is bundled with almost every PHP install, needs no extra setup, and handles JPEG, PNG, GIF and WebP well. Reach for Imagick when you need sharper resampling, professional colour handling, formats GD doesn't support (TIFF, PSD, PDF, HEIC), or operations like multi-page handling. Imagick is a separate extension that wraps the ImageMagick binary, so it must be installed and enabled on the server. Many teams skip the choice entirely and use the Intervention Image library, which sits on top of either driver and gives one clean API.
Q: Why is my resized image squashed or stretched?
You applied two different scale factors to width and height. To preserve the aspect ratio you must compute one ratio and multiply BOTH sides by it. Use min($maxW/$w, $maxH/$h) to fit inside a box, or max(...) to fill (cover) a box, then crop the overflow. Never set the new width and height independently to your target numbers unless you genuinely want distortion.
Q: Why does PHP run out of memory on large images?
GD decompresses an image into raw pixels in RAM: every pixel costs roughly 4 bytes, so a 6000x4000 photo needs about 96 MB before you have even resized it, far more than the typical 128 MB memory_limit once you also hold the destination canvas. Check the dimensions with getimagesize() (which reads only the header) and reject images bigger than you support, raise memory_limit for the resize script, free each resource with imagedestroy() as soon as you are done, and consider Imagick, which can stream large files instead of loading them whole.
Q: Do I need to do anything about EXIF data?
Yes, for two reasons. First, phone photos store an Orientation flag instead of physically rotating the pixels, so an untouched upload often appears sideways; read $exif['Orientation'] and imagerotate() to compensate. Second, EXIF can contain GPS coordinates and the camera model, which is a privacy leak if you publish the file as-is. Re-saving through GD writes a clean file with no EXIF, which strips the metadata for free, so a load-process-save cycle solves both problems at once.
Q: What quality value should I use for WebP and JPEG?
Quality is a 0-100 dial trading file size against visual fidelity. 80 is the sweet spot for photos: nearly indistinguishable from the original to the eye while cutting the file size dramatically. Drop to 60-70 for thumbnails where small size matters more than detail, and push to 90+ only for hero images or when re-compression artefacts would show. WebP at quality 80 is typically 25-35% smaller than the equivalent JPEG, which is why it is the default modern format for the web.
Mini-Challenge: Make an Avatar
No code is filled in this time — just a brief and an outline. Write the function yourself, run it on a real photo with php avatar.php, then check the result against the expected output in the comments. This is the crop-to-fill plus WebP combo you'll use for every profile picture.
<?php
// 🎯 MINI-CHALLENGE: a "make an avatar" helper.
// No code is filled in — work from the steps, then run it on a real photo.
//
// Write a function makeAvatar(string $src, string $dest, int $size = 256) that:
// 1. Reads the source size with getimagesize($src).
// 2. Scales to COVER a $size x $size box: $ratio = max($size/$w, $size/$h).
// 3. Centre-crops the overflow with imagecopy() (see the thumbnail example).
// 4. Saves the result as WebP at quality 80 with imagewebp($thumb, $dest, 80).
// 5. Calls imagedestroy() on every GD resource you created.
// Then call it: makeAvatar('photo.jpg', 'avatar.webp');
//
// ✅ Expected output (example):
// Avatar written: avatar.webp (256x256)
// your code here
?>makeAvatar() function. It should write a 256×256 WebP and free every GD resource.🎉 Lesson Complete!
- ✅ GD ships with PHP and covers most needs; Imagick is sharper but a separate install
- ✅ Load files with
imagecreatefrom…()and create canvases withimagecreatetruecolor() - ✅ Resize by one shared ratio (
minto fit), and crop-to-fill (max) for square thumbnails - ✅ Watermark with an alpha colour, and convert to WebP at quality ~80 for big size savings
- ✅ Handle EXIF: rotate by the orientation flag, and re-save to strip GPS/metadata
- ✅ Always
imagedestroy()resources, and reach for Intervention Image in real projects - ✅ Next lesson: PDF Generation — build and export documents on the server
Sign up for free to track which lessons you've completed and get learning reminders.