Create Image Thumbnails with PHP
A thumbnail needs its own smaller file instead of a CSS box around the full-size asset. A current PHP app should create thumbnail assets at upload time, import time, or in a background job, then serve those generated files through ordinary HTML.
Think of the original as a master negative and each thumbnail as a print made for one frame. Keep the master, name each print by its intended size, and regenerate those prints when the frame changes.
Use PHP for the file-generation step, then let the browser handle layout, srcset, lazy loading, and display rules. Mixing those jobs is how a small image feature becomes a slow gallery and, if the path handling is loose, a file-disclosure bug with nice square corners.
These examples require the PHP GD extension. Explicit GdImage type hints require PHP 8.0 or newer, while the promoted readonly properties require PHP 8.1 or newer.
Thumbnail code turns into an image pipeline as soon as the application owns more than one derivative size, so draw the storage and generation boundary early.
Guide Path
Read this after working with text files in PHP if your app now needs to create real image files. The HTML tutorials hub covers the stable markup and responsive display rules that generated files still need.
What to Generate
Generate the sizes your interface actually uses. A gallery might keep one original and two thumbnail files:
storage/
originals/
product-1329.jpg
thumbnails/
product-1329-320.jpg
product-1329-640.jpg The original file stays available for a detail view, download, or later regeneration. The thumbnail files are cheap to serve in the grid. If the design changes from 240-pixel cards to 320-pixel cards, regenerate the derivative files from the original. Do not keep resampling thumbnails from thumbnails until the image has been through the washing machine a few times.
For a small PHP app, GD is still a reasonable teaching tool. If the site is image-heavy, needs WebP/AVIF pipelines, or does a lot of editorial cropping, you eventually want a dedicated image library, a queue worker, or an image service. But the geometry is the same either way: load a source, calculate dimensions, resample into a destination, save the derivative.
Path Safety
Accepting an image path from the query string and stripping .. out of it is still a common move, and it is exactly the sort of thing that should make you uneasy. Removing one substring leaves the request in control of the path.
The safer move is to use an application identifier:
<?php
final class Photo
{
public function __construct(
public readonly int $id,
public readonly string $originalPath,
public readonly string $thumbnailBaseName,
) {
}
} The controller should look up Photo by ID, then pass a known server-side path into the thumbnail job. The user should never get to decide that the image path is ../../config.php, a private upload, or a file outside the upload root. That is the unglamorous part of image work, and it matters more than the resize formula.
Build a Proportional Thumbnail
This version keeps the full image visible and fits it inside a maximum box without cropping. That is the safer default for product photos, screenshots, diagrams, and anything where cutting off an edge loses information.
<?php
function createJpegThumbnail(
string $sourcePath,
string $targetPath,
int $maxWidth = 320,
int $maxHeight = 320,
): void {
$size = getimagesize($sourcePath);
if ($size === false) {
throw new RuntimeException("The source file is not a readable image.");
}
[$sourceWidth, $sourceHeight, $imageType] = $size;
if ($imageType !== IMAGETYPE_JPEG) {
throw new RuntimeException("This thumbnail job expects a JPEG source.");
}
$source = imagecreatefromjpeg($sourcePath);
if ($source === false) {
throw new RuntimeException("Could not load the source JPEG.");
}
$scale = min(
$maxWidth / $sourceWidth,
$maxHeight / $sourceHeight,
1,
);
$thumbWidth = max(1, (int) floor($sourceWidth * $scale));
$thumbHeight = max(1, (int) floor($sourceHeight * $scale));
$thumb = imagecreatetruecolor($thumbWidth, $thumbHeight);
imagecopyresampled(
$thumb,
$source,
0,
0,
0,
0,
$thumbWidth,
$thumbHeight,
imagesx($source),
imagesy($source),
);
if (! imagejpeg($thumb, $targetPath, 82)) {
throw new RuntimeException("Could not write the thumbnail file.");
}
imagedestroy($source);
imagedestroy($thumb);
} There are two quiet decisions in that function. First, it refuses non-JPEG input because a short tutorial should not pretend a full image pipeline is just a switch statement away. Add PNG, GIF, WebP, and AVIF support deliberately when your app needs them. Second, it refuses to upscale small images. Upscaling makes a small original look worse while doing nothing useful for performance.
The imagecopyresampled() call is the current GD workhorse here. It copies a rectangular region from the source into a destination image and interpolates pixels during the resize, which is the quality difference you care about when reducing a photo.
Fixed Crops
Square thumbnails look tidy in card grids, and the crop remains a product decision. If every gallery card must be square, crop from the center and be honest that the edges may disappear:
<?php
function createSquareJpegThumbnail(
string $sourcePath,
string $targetPath,
int $size = 320,
): void {
$source = imagecreatefromjpeg($sourcePath);
if ($source === false) {
throw new RuntimeException("Could not load the source JPEG.");
}
$sourceWidth = imagesx($source);
$sourceHeight = imagesy($source);
$side = min($sourceWidth, $sourceHeight);
$sourceX = intdiv($sourceWidth - $side, 2);
$sourceY = intdiv($sourceHeight - $side, 2);
$scale = min($size / $side, 1);
$targetSize = max(1, (int) floor($side * $scale));
$thumb = imagecreatetruecolor($targetSize, $targetSize);
imagecopyresampled(
$thumb,
$source,
0,
0,
$sourceX,
$sourceY,
$targetSize,
$targetSize,
$side,
$side,
);
if (! imagejpeg($thumb, $targetPath, 82)) {
throw new RuntimeException("Could not write the thumbnail file.");
}
imagedestroy($source);
imagedestroy($thumb);
} That is a clean crop, but it is still blind. Faces, logos, screenshots, and charts do not always sit in the center. In a real CMS, store a focal point or let an editor choose the crop. If that sounds like more product work than you wanted, that is the clue: use proportional thumbnails and let CSS handle the display box.
Generation Timing
Thumbnail generation should happen once per source image and per target size. The request that shows the gallery should not be resizing images for every visitor.
<?php
$photo = new Photo(
id: 1329,
originalPath: __DIR__ . "/../storage/originals/product-1329.jpg",
thumbnailBaseName: "product-1329",
);
createJpegThumbnail(
$photo->originalPath,
__DIR__ . "/../public/uploads/thumbs/{$photo->thumbnailBaseName}-320.jpg",
320,
320,
);
createJpegThumbnail(
$photo->originalPath,
__DIR__ . "/../public/uploads/thumbs/{$photo->thumbnailBaseName}-640.jpg",
640,
640,
); If the image came from an upload form, validate the upload before this step. Check the upload error code, cap the file size, store the original under a generated name, keep private originals out of the public directory when needed, and run image inspection on the server. The PHP manual's upload examples are deliberately low-level; your application still owns the storage policy.
The master-negative rule keeps this regeneration predictable. Every 320-pixel or 640-pixel derivative comes from the accepted original, never from another thumbnail that has already lost detail.
Serving Generated Files
Once PHP has generated the files, the page should point at the thumbnail files directly:
<img
src="/uploads/thumbs/product-1329-320.jpg"
srcset="/uploads/thumbs/product-1329-320.jpg 320w,
/uploads/thumbs/product-1329-640.jpg 640w"
sizes="(width < 700px) 45vw, 220px"
width="320"
height="320"
loading="lazy"
decoding="async"
alt="Walnut desk organizer with two notebooks"> That is where the HTML/CSS thumbnail guide takes over. PHP made the derivative files, HTML tells the browser which assets exist, and CSS decides whether the thumbnail is covered, contained, rounded, bordered, or lazy-loaded lower down the page.
Do not route every thumbnail request through thumbnail.php unless you have a measured reason. A generated file can be cached by the browser, CDN, or static server. A PHP thumbnail endpoint has to solve caching, validation, path safety, and error handling on every request, which is worth it in a few cases but usually overbuilt for a basic gallery.
Common Pitfalls & Debugging
Large Uploads Exhaust Memory
Symptom: a large photo passes the file-size check but thumbnail generation terminates. Cause: decoded pixel data needs far more memory than the compressed upload. Fix: inspect dimensions before decoding, reject images above the application's pixel limit, and move heavier jobs away from the request path.
PNG Transparency Becomes Black
Symptom: a transparent PNG gets a black background after resizing. Cause: the destination image was created without alpha preservation and a transparent fill. Fix: configure alpha on the destination before resampling, or keep the JPEG-only boundary shown in this tutorial until PNG output is implemented deliberately.
The Target File Never Appears
Symptom: no thumbnail is written even though the resize calculations finish. Cause: the destination directory is absent, the PHP process cannot write there, or imagejpeg() returned false. Fix: create and own the directory during deployment, then throw when the encoder reports failure.
Frequently Asked Questions
Is WebP always smaller than JPEG?
Usually, but not for every image. A well-optimised JPEG can occasionally beat a poorly configured WebP export, so measure the actual output rather than assuming the newer format wins by default.
Can a thumbnail use a different format from its source?
Yes, and it is common. GD can decode one format and encode another, though a transparent PNG converted to JPEG must be flattened onto a solid background first, because JPEG carries no alpha channel.
Does CSS resizing reduce the image download?
CSS resizing does not reduce the download because the browser still retrieves the selected image resource. Generate smaller files and describe them with srcset and sizes so the browser can choose an asset close to the space available.
Does GD rotate photos to match their EXIF orientation?
No. GD ignores the orientation tag, so a phone photo that looks upright in a viewer can come out sideways in the thumbnail. Read the EXIF orientation yourself and rotate before resizing.
Does cropping to a fixed size distort an image whose aspect ratio does not match?
Forcing a mismatched aspect ratio into a fixed width and height stretches the image instead of cropping it cleanly. Calculate a crop rectangle that matches the target ratio first, centering it on the source image, then resize that rectangle to the final dimensions so proportions stay correct.
Next Steps
Keep the core idea: read the source dimensions, calculate a scale ratio, and resize through GD so the browser receives a smaller image.
The parts to retire are just as important. Do not accept raw image paths from the URL. Do not resize the same image on every page load. Do not use CSS-only shrinking as a performance strategy. Do not turn every invalid source into a generated error image when a logged failure and a fallback asset would be clearer.
Build the file pipeline first, then make the thumbnail grid pleasant. For saved overlays or watermarks, read composite images with PHP and GD. The HTML tutorials hub covers the browser side of gallery markup.
This example has been updated from the original CodeWalkers thumbnail tutorial, which generated each thumbnail on request from a query-string path. That single detail is why this page starts with server-owned identifiers and pre-generated files rather than a resize endpoint.
Sources
-
[1]
PHP GD and Image Functions(php.net)
-
[2]
PHP getimagesize(php.net)
-
[3]
PHP imagecopyresampled(php.net)
-
[4]
PHP imagesx(php.net)
-
[5]
PHP imagesy(php.net)
-
[6]
PHP imagejpeg(php.net)
-
[7]
PHP Handling file uploads(php.net)
Read Next
Use PHP and GD to place a logo, watermark, badge, or foreground PNG over a source image and save the merged result as a real file.
Secure ordinary PHP surfaces with current password hashing, sessions, CSRF tokens, output escaping, prepared statements, and upload controls.
Compare database BLOBs with external image storage through transaction, delivery, backup, memory, security, and cleanup requirements.