How to Resize Images in PHP with GD

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:

  1. Validate the upload and read its real image type with getimagesize().
  2. Load it with the matching GD function, such as imagecreatefromjpeg().
  3. Calculate one scale factor from the maximum width and height.
  4. Create a destination canvas with the calculated dimensions.
  5. Copy the source with imagecopyresampled().
  6. 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 is 0.5.
  • The height ratio is 1200 / 1600, which is 0.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.

Photo of Vincy, PHP developer
Written by Vincy Last updated: July 28, 2026
I'm a PHP developer with 20+ years of experience and a Master's degree in Computer Science. I build and improve production PHP systems for eCommerce, payments, webhooks, and integrations, including legacy upgrades (PHP 5/7 to PHP 8.x).

Continue Learning

These related tutorials may help you continue learning.

42 Comments on "How to Resize Images in PHP with GD"

Leave a Reply

Your email address will not be published. Required fields are marked *

Explore topics
Need PHP help?