Resizing an image in PHP is easy until a portrait becomes a pancake. The usual cause is forcing every source image into the same width and height without accounting for its aspect ratio.
This example uses the PHP GD extension to resize uploaded JPEG, PNG, and WebP images. It fits each image within a 1200 × 1200 pixel boundary, preserves its proportions, keeps PNG and WebP transparency, and does not enlarge images that are already smaller.
Quick answer
To resize an image in PHP without stretching it:
- Validate the upload and read its real image type with
getimagesize(). - Load it with the matching GD function, such as
imagecreatefromjpeg(). - Calculate one scale factor from the maximum width and height.
- Create a destination canvas with the calculated dimensions.
- Copy the source with
imagecopyresampled(). - Save the result with a generated filename.
The important part is using one scale factor for both dimensions. If the width and height are calculated independently, the output will be stretched.
What the example handles
- JPEG, PNG, and WebP uploads
- Aspect-ratio-preserving resize
- PNG and WebP alpha transparency
- JPEG EXIF orientation when the EXIF extension is available
- An 8 MB upload limit and a 24-megapixel processing limit
- Random server-generated filenames
- Clear upload and processing errors
The pixel limit deserves attention. A highly compressed image can occupy little disk space but require a large amount of memory after GD decodes it. Checking only the uploaded file size does not protect the application from that problem.
For a broader discussion of upload checks, see PHPpot’s secure file upload guide.
Requirements
The project requires PHP 8.2 or later and the GD extension with JPEG, PNG, and WebP support. The EXIF extension is optional, but it lets the example correct photos whose camera orientation is stored as JPEG metadata.
You can confirm that GD is enabled from the command line:
php -m | grep gd
php -r "print_r(gd_info());"
How the aspect-ratio calculation works
The target is a bounding box, not a forced canvas size. An image may use all the available width or all the available height, but it will not necessarily use both.
The resizer calculates the width ratio and height ratio, then uses the smaller value:
<?php
$scale = min($maxWidth / $sourceWidth, $maxHeight / $sourceHeight, 1);
$targetWidth = max(1, (int) round($sourceWidth * $scale));
$targetHeight = max(1, (int) round($sourceHeight * $scale));
Suppose the uploaded image is 2400 × 1600 pixels and the bounding box is 1200 × 1200:
- The width ratio is
1200 / 2400, which is0.5. - The height ratio is
1200 / 1600, which is0.75. - The smaller ratio is
0.5. - The resized image is therefore 1200 × 800 pixels.
The final 1 passed to min() prevents enlargement. For example, a 600 × 400 image remains 600 × 400 instead of being stretched to 1200 × 800. Removing that value allows the same calculation to enlarge smaller images.
Validate the uploaded image before resizing it
The original filename and extension come from the browser, so they are not reliable evidence of an image’s format. A file called holiday.jpg is still untrusted input.
The project first checks the PHP upload status and file size. It then calls getimagesize() on the temporary file. This verifies that PHP can read the image structure and provides its dimensions and detected MIME type.
<?php
$imageInfo = @getimagesize($file['tmp_name']);
if ($imageInfo === false) {
throw new RuntimeException('The uploaded file is not a readable image.');
}
[$sourceWidth, $sourceHeight] = $imageInfo;
$mimeType = $imageInfo['mime'] ?? '';
if ($sourceWidth < 1 || $sourceHeight < 1) {
throw new RuntimeException('The image dimensions are invalid.');
}
if ($sourceWidth * $sourceHeight > self::MAX_SOURCE_PIXELS) {
throw new RuntimeException('The image has too many pixels to process safely.');
}
$extension = match ($mimeType) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
default => throw new RuntimeException(
'Only JPEG, PNG, and WebP images are supported.'
),
};
The MIME type determines both the GD decoder and the output extension. The application never copies the user-supplied filename into the upload directory.
Why limit the source pixel count?
GD stores a decoded image in memory. A 24-megapixel photo can require far more memory than its compressed JPEG file size suggests, especially while both the source and destination images exist at the same time.
The example rejects images above 24 million pixels. This is separate from its 8 MB file-size limit because the two checks protect against different problems. Adjust both limits for your server’s memory allowance and expected uploads.
Load the source image with the correct GD function
GD has a separate decoder for each image format. After detecting the MIME type, the project selects the corresponding function with a match expression.
<?php
private function createSourceImage(string $path, string $mimeType): GdImage
{
$image = match ($mimeType) {
'image/jpeg' => @imagecreatefromjpeg($path),
'image/png' => @imagecreatefrompng($path),
'image/webp' => @imagecreatefromwebp($path),
};
if (!$image instanceof GdImage) {
throw new RuntimeException(
'The image data is corrupt or unsupported by GD.'
);
}
return $image;
}
In PHP 8, these functions return a GdImage object on success or false on failure. Checking the return value matters because a file can have a recognizable header while the remaining image data is damaged.
The example deliberately excludes GIF files. GD can resize a static GIF, but it does not preserve an animated GIF’s frames. Silently turning an animation into a still image is usually a surprising result, so it is better to reject GIF here and use an animation-aware library when that format is required.
Correct rotated JPEG photos
Phone cameras often store a photo’s orientation in EXIF metadata instead of rearranging its pixels. The image may look upright in a browser or photo viewer but appear sideways after GD creates a new file.
The project reads the orientation before calculating the target dimensions. It handles the common 90-degree, 180-degree, and 270-degree rotations:
<?php
private function applyExifOrientation(
GdImage $image,
string $path,
string $mimeType
): GdImage {
if ($mimeType !== 'image/jpeg' || !function_exists('exif_read_data')) {
return $image;
}
$exif = @exif_read_data($path);
$orientation = is_array($exif)
? (int) ($exif['Orientation'] ?? 1)
: 1;
$angle = match ($orientation) {
3 => 180,
6 => -90,
8 => 90,
default => 0,
};
if ($angle === 0) {
return $image;
}
$rotated = imagerotate($image, $angle, 0);
if (!$rotated instanceof GdImage) {
return $image;
}
imagedestroy($image);
return $rotated;
}
The EXIF extension is optional. If it is unavailable, the method returns the original GD image and resizing continues normally.
After rotation, read the dimensions from the GD object with imagesx() and imagesy(). A 90-degree turn swaps the effective width and height, so using the dimensions collected before rotation would produce the wrong target size.
<?php
$source = $this->createSourceImage($file['tmp_name'], $mimeType);
$source = $this->applyExifOrientation(
$source,
$file['tmp_name'],
$mimeType
);
$sourceWidth = imagesx($source);
$sourceHeight = imagesy($source);
Preserve transparency while resampling
A new true-color GD canvas starts with an opaque background. That is fine for JPEG, but it can turn transparent areas in PNG and WebP files black.
For formats with alpha transparency, disable alpha blending on the destination, enable saving of the alpha channel, and fill the canvas with a fully transparent color before copying the source image.
<?php
$target = imagecreatetruecolor($targetWidth, $targetHeight);
if ($target === false) {
imagedestroy($source);
throw new RuntimeException(
'PHP could not create the destination image.'
);
}
if ($mimeType === 'image/png' || $mimeType === 'image/webp') {
imagealphablending($target, false);
imagesavealpha($target, true);
$transparent = imagecolorallocatealpha(
$target,
0,
0,
0,
127
);
imagefill($target, 0, 0, $transparent);
}
The alpha value may look backward at first glance. In GD, 0 is fully opaque and 127 is fully transparent.
With the destination prepared, imagecopyresampled() copies the entire source into the calculated target dimensions:
<?php
$resampled = imagecopyresampled(
$target,
$source,
0,
0,
0,
0,
$targetWidth,
$targetHeight,
$sourceWidth,
$sourceHeight
);
if (!$resampled) {
imagedestroy($source);
imagedestroy($target);
throw new RuntimeException('PHP could not resize the image.');
}
Use imagecopyresampled() rather than imagecopyresized() for normal photographs and thumbnails. Resampling interpolates pixel values and produces a smoother result when reducing an image.
Handle PHP upload errors explicitly
Before inspecting image data, check the upload status supplied by PHP. A missing file, interrupted transfer, and server-side size rejection are different failures and should not all become the vague message “upload failed.”
The project keeps this logic in a small validation method:
<?php
private function validateUpload(array $file): void
{
if ($file['error'] !== UPLOAD_ERR_OK) {
$message = match ($file['error']) {
UPLOAD_ERR_INI_SIZE,
UPLOAD_ERR_FORM_SIZE => 'The uploaded file is too large.',
UPLOAD_ERR_NO_FILE => 'Choose an image to upload.',
UPLOAD_ERR_PARTIAL =>
'The image upload was interrupted. Please try again.',
default => 'The image upload failed.',
};
throw new RuntimeException($message);
}
if ($file['size'] < 1 || $file['size'] > self::MAX_FILE_SIZE) {
throw new RuntimeException(
'The image must be no larger than 8 MB.'
);
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('The upload could not be verified.');
}
}
is_uploaded_file() confirms that the temporary path came through PHP’s HTTP upload mechanism. This prevents a caller from passing an arbitrary local path into the upload-processing method.
The application’s 8 MB check does not replace PHP’s configuration limits. If upload_max_filesize or post_max_size is lower, PHP rejects the request before this code receives the complete file.
Save the resized image safely
Create the upload directory if it does not already exist, then generate the output name on the server. Do not reuse $_FILES['image']['name'] as a storage path.
<?php
if (
!is_dir($destinationDirectory)
&& !mkdir($destinationDirectory, 0755, true)
&& !is_dir($destinationDirectory)
) {
imagedestroy($source);
imagedestroy($target);
throw new RuntimeException(
'The upload directory could not be created.'
);
}
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = rtrim(
$destinationDirectory,
DIRECTORY_SEPARATOR
) . DIRECTORY_SEPARATOR . $filename;
random_bytes(16) provides enough randomness to make filename collisions impractical. The extension comes from the detected MIME type, not from the uploaded name.
Each output format has different quality or compression settings:
<?php
$saved = match ($mimeType) {
'image/jpeg' => imagejpeg($target, $destination, 85),
'image/png' => imagepng($target, $destination, 6),
'image/webp' => imagewebp($target, $destination, 82),
};
imagedestroy($source);
imagedestroy($target);
if (!$saved) {
throw new RuntimeException(
'The resized image could not be saved.'
);
}
return [
'path' => $destination,
'width' => $targetWidth,
'height' => $targetHeight,
];
JPEG and WebP accept a quality value, where a higher number generally means a larger file with less compression. PNG uses a compression level from 0 to 9. It is a lossless format, so this setting changes encoding effort and file size rather than visual quality.
The calls to imagedestroy() release the source and destination images as soon as they are no longer needed. PHP eventually cleans them up, but explicit cleanup is useful in upload endpoints that may process several large images during one request.
Prevent scripts from running in the upload directory
Image validation and generated filenames are important, but the upload directory should still be treated as untrusted storage. Disable script execution there and avoid sending uploaded files through the PHP handler.
The downloadable Apache example includes this uploads/.htaccess file:
Options -ExecCGI
<FilesMatch "\.(php|phtml|phar|cgi|pl|py|sh)$">
Require all denied
</FilesMatch>
For Nginx, add the equivalent restriction in the server configuration. A stronger production arrangement is to store uploads outside the document root or serve them from a separate static host.
Process the upload from the page controller
The page controller creates the resizer only for a POST request. It passes the temporary upload to resizeUpload() and converts expected failures into a message that can be shown beside the form.
<?php
declare(strict_types=1);
require __DIR__ . '/ImageResizer.php';
$message = '';
$messageType = '';
$result = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
if (!isset($_FILES['image']) || !is_array($_FILES['image'])) {
throw new RuntimeException('Choose an image to upload.');
}
$resizer = new ImageResizer();
$result = $resizer->resizeUpload(
$_FILES['image'],
__DIR__ . '/uploads',
1200,
1200
);
$message = sprintf(
'Image resized successfully to %d × %d pixels.',
$result['width'],
$result['height']
);
$messageType = 'success';
} catch (RuntimeException $exception) {
$message = $exception->getMessage();
$messageType = 'error';
}
}
The maximum width and height are passed as arguments rather than buried inside the resizer. This makes the same class usable for a different boundary, such as 400 × 400 thumbnails, without changing its internal code.
Create the image upload form
The form must use method="post" and enctype="multipart/form-data". Without the multipart encoding, the selected file will not appear in $_FILES.
<form method="post" enctype="multipart/form-data">
<label for="image">Image file</label>
<input
type="file"
id="image"
name="image"
accept="image/jpeg,image/png,image/webp"
required
>
<p class="help">Maximum file size: 8 MB</p>
<button type="submit">Upload and resize</button>
</form>
The accept attribute helps the browser present suitable files, but it is only a user-interface hint. A request can bypass the form, so the PHP MIME and image checks remain necessary.
Display the result and error messages
Every dynamic value printed into HTML is escaped. The current messages are written by the application, but consistently escaping output avoids future trouble if a message later includes a filename or another user-controlled value.
<?php if ($message !== ''): ?>
<div class="message <?=
htmlspecialchars($messageType, ENT_QUOTES, 'UTF-8')
?>">
<?= htmlspecialchars($message, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
After a successful resize, the project uses basename() to discard directory components from the saved path. rawurlencode() then prepares the generated filename for use in the image URL.
<?php if (is_array($result)): ?>
<?php
$imageUrl = 'uploads/'
. rawurlencode(basename($result['path']));
?>
<figure class="result">
<img
src="<?=
htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8')
?>"
alt="Resized uploaded image"
>
<figcaption>
<?= (int) $result['width'] ?>
×
<?= (int) $result['height'] ?>
pixels
</figcaption>
</figure>
<?php endif; ?>
The generated filename contains only hexadecimal characters and a server-selected extension. The escaping and URL encoding are still worth keeping because they make the output rule explicit instead of depending on today’s filename format.
Common errors and fixes
Call to undefined function imagecreatefromjpeg()
The GD extension is missing, disabled, or compiled without JPEG support. Confirm the active PHP installation and its supported formats:
php --ini
php -m | grep gd
php -r "print_r(gd_info());"
The command-line and web-server installations may load different php.ini files. If the command works but the browser request fails, check the configuration used by PHP-FPM or the web-server module.
WebP uploads are rejected or cannot be decoded
Having GD enabled does not guarantee that every image format is available. Check the WebP Support value returned by gd_info(). If it is disabled, rebuild or reinstall GD with WebP support, or remove WebP from the application’s allowed MIME types.
The resized PNG has a black background
Configure the destination canvas for alpha transparency before calling imagecopyresampled(). Calling imagesavealpha() after resampling is too late if the transparent pixels have already been blended onto an opaque background.
Uploaded phone photos appear sideways
The JPEG probably uses EXIF orientation metadata. Apply the orientation before reading the final source dimensions and resizing the image. The example handles the common rotation values 3, 6, and 8 when the EXIF extension is available.
PHP reports that the uploaded file is missing
Check upload_max_filesize and post_max_size. When the complete POST body exceeds post_max_size, PHP may leave both $_POST and $_FILES empty.
post_max_size should be larger than upload_max_filesize because the request also contains multipart form data.
Allowed memory size is exhausted
The compressed file size is not the amount of memory GD needs. Image dimensions, color depth, temporary canvases, and rotation all affect peak usage.
Reduce the accepted pixel count or the number of images processed per request before raising memory_limit. Increasing the limit without bounding image dimensions merely moves the failure point.
Developer FAQ
Why use imagecopyresampled() instead of imagecopyresized()?
imagecopyresampled() interpolates pixel values and normally produces smoother downscaled images. imagecopyresized() is faster but can create visibly jagged or pixelated results.
Can imagescale() preserve the aspect ratio?
Yes. imagescale() can calculate the height automatically when a negative height is supplied. This example uses imagecopyresampled() because the explicit source and destination dimensions also make it suitable for later crop or positioning changes.
How do I create an exact 400 × 400 thumbnail?
Resizing and cropping are different operations. Preserving the complete image inside a 400 × 400 boundary may produce an output such as 400 × 267. To create an exact square without distortion, scale the image until it covers the square and then crop the excess from the center or another chosen focal point.
Should small images be enlarged?
Usually not. Enlargement adds pixels but cannot restore missing detail, so the result often looks soft. The example limits the scale factor to 1. Remove that limit only when fixed minimum dimensions are more important than sharpness.
Does GD preserve image metadata?
No. A newly encoded image generally does not retain the source EXIF metadata. The example reads orientation before resizing, applies the required rotation, and saves the new pixels without copying the remaining metadata.
Why does the example not resize animated GIF files?
GD does not preserve all animation frames during this workflow. Use an animation-aware ImageMagick implementation or another suitable library when animated GIF support is required.
Download the PHP image resize example
The project contains the reusable ImageResizer class, upload form, result display, upload-directory protection, light CSS, and setup instructions.
Download the PHP image resize project
Run it with PHP 8.2 or later and a GD installation that supports JPEG, PNG, and WebP. No database or framework is required.
Excellent tutorial… Nicely explained
Thank you Pavan.
Thanks your are writing very nice
Welcome Devesh.
Transparent PNG Images not resized using this code. Can you help us on this.
Hi Mukesh,
I will check the script and update it.
Excellent script.
Would be nice if it would be possible to maintain aspect ratio while resizing..
Thank you Frin. Sure, I will update the script with the aspect ratio.
thanks
Welcome Sagar.
very nice explained
Thank you Narendra.
Thanks for all scripts including this
Welcome Midhun.
it doesn’t work for me….no image uploaded…why do you think ?
Pamela,
Send me details about the issue you are facing. Do you get any exception?
Simply excellent!!!
Thank you Yomi.
hi;
imagecopyresampled() is used for croping that image? or resize the image without cutting in same ratio?
It copies and resizes with resampling.
thank you for the post! Very helpfull!!
Welcome Renato.
Very helpful and nice tutorial
Thank you Martin.
Wow, detailed tutorial, thank you.
Welcome Syed
Can we add Watermark, thank you
Thank you for the suggestion! Yes, a watermark can be added with GD before saving the compressed image. The watermark is loaded as a transparent PNG and placed using `imagecopy()` or `imagecopymerge()`.
I will consider covering this in a separate tutorial because watermark positioning, transparency, margins, and scaling need more explanation than a small addition to this image-compression example.
simple but clear explanation
Thank you Tsaqib
I really like coding & Explanation, But can you please help me! I want to upload resize image in particular folder.
Thank you! Yes, you can save the resized image in any folder by changing the destination path in the code.
For example:
“`php
$outputDirectory = __DIR__ . ‘/uploads/resized/’;
if (!is_dir($outputDirectory)) {
mkdir($outputDirectory, 0755, true);
}
$outputPath = $outputDirectory . $outputFileName;
“`
Pass `$outputPath` to the image-saving function, such as `imagejpeg()`, `imagepng()`, or `imagewebp()`. Also ensure that PHP has permission to write to the destination folder. For security, generate the output filename on the server instead of using the uploaded filename directly.
Here are all the image types that can be handled:
IMAGETYPE_JPEG2000
IMAGETYPE_SWF
IMAGETYPE_PSD
IMAGETYPE_BMP
IMAGETYPE_WBMP
IMAGETYPE_XBM
IMAGETYPE_TIFF_II
IMAGETYPE_TIFF_MM
IMAGETYPE_IFF
IMAGETYPE_JB2
IMAGETYPE_JPC
IMAGETYPE_JP2
IMAGETYPE_JPX
IMAGETYPE_SWC
IMAGETYPE_ICO
IMAGETYPE_WEBP
Thank you John for a comprehensive list.
Thanks.. much better than other code
Welcome Sajeel.
Hello. I have implemented your code, it worked for me, Thank you.
Welcome Emir
Thank you
Welcome Esther.
I hope God keeps you alive for long to reap the fruit of your labor
Thank you Joel