ScreenshotNeo

BlogHow-to

How to Optimize Images in PHP

Resize and re-encode images in PHP with GD, preserve transparency, verify format support, and handle large or untrusted uploads safely.

By the ScreenshotNeo team4 October 202612 min read

To optimize images in PHP, create derivatives at the dimensions your application actually displays, then encode them in a format your PHP build supports. GD is enough for many straightforward resize and re-encode jobs. Check the production build’s capabilities, choose an output quality by comparing representative images, and measure the output bytes and visual appearance. There is no single quality value or format that works best for every photo, screenshot, logo, and illustration.

This guide uses GD for a runnable command-line example. It also covers format support, transparency, animation, large images, upload safety, and an ImageMagick option. If you want to inspect a web page’s rendered appearance as part of an image workflow, ScreenshotNeo provides website screenshots through an API and MCP server; its call is included below.

1. Choose the right optimization approach

Optimization usually combines several separate decisions:

  • Dimensions: resize an oversized source to the largest derivative your application needs. A smaller pixel count generally means less image data to encode and deliver, but the reduction depends on the source and output.
  • Format: choose based on content, transparency, animation requirements, your PHP build, and the clients that need to display the result.
  • Encoding: for lossy formats such as JPEG and WebP, compare quality settings against both byte size and visible artifacts.
  • Delivery: serve the derivative with the correct MIME type and cache policy, and keep the original if you may need to regenerate it.
Image or requirement Starting point Check before shipping
Photographs JPEG or WebP; compare WebP quality settings against the JPEG already in use. Fine detail, gradients, artifacts, output bytes, and client support.
Logos and images with transparency PNG, or WebP/AVIF when your deployment and clients support the chosen format. Transparent edges, alpha channel, background appearance, and format support.
Screenshots, diagrams, and text-heavy graphics Compare PNG with lossy alternatives at the intended display size. Text sharpness, thin lines, color shifts, and file size.
Animated GIF or WebP Use a workflow that explicitly supports animation; do not assume a single-frame GD conversion preserves it. Frame count, timing, disposal behavior, and output playback.

These are decision points, not guaranteed savings or universal format rankings. The cited PHP documentation describes available functions and capabilities; it does not establish a best quality setting or comparative compression benchmark.

2. Check GD and format support

GD’s read/write capabilities depend on the PHP build and linked libraries. Check the actual runtime that will process images, not just a developer workstation. PHP provides gd_info() and imagetypes() to inspect the linked GD library’s capabilities. The GD requirements and installation documentation explain that formats depend on the build. WebP uses the --with-webp configure switch as of PHP 7.4; GD AVIF support is available from PHP 8.1 when built with AVIF support.

<?php
if (!extension_loaded('gd')) {
    fwrite(STDERR, "GD is not loaded in this PHP runtime.\n");
    exit(1);
}

var_export(gd_info());
echo "\nSupported image type flags: ";
var_export(imagetypes());
echo "\n";

if (function_exists('imagewebp')) {
    echo "imagewebp() is available.\n";
} else {
    echo "WebP encoding is unavailable.\n";
}

if (function_exists('imageavif')) {
    echo "imageavif() is available.\n";
} else {
    echo "AVIF encoding is unavailable.\n";
}

Run that with the same PHP binary and deployment image used by the web worker or queue that will process images. A CLI PHP installation and PHP-FPM may load different configuration or extensions.

3. Runnable GD example: resize and encode to WebP

The following PHP 8+ command-line script reads JPEG, PNG, or WebP input, proportionally scales it down to a maximum width and height, and writes WebP. It rejects unsupported formats, checks pixel dimensions before decoding, and reports the source and output byte counts. It does not upscale smaller images. For production uploads, add application-specific authentication, storage, rate limits, and isolation.

<?php
// optimize.php INPUT OUTPUT.webp [MAX_WIDTH] [MAX_HEIGHT] [QUALITY]
// Example: php optimize.php photo.jpg public/photo.webp 1600 1600 82

function fail(string $message, int $exitCode = 1): never {
    fwrite(STDERR, $message . "\n");
    exit($exitCode);
}

if ($argc < 3 || $argc > 6) {
    fail("Usage: php optimize.php INPUT OUTPUT.webp [MAX_WIDTH] [MAX_HEIGHT] [QUALITY]");
}

if (!extension_loaded('gd')) {
    fail("GD is not loaded.");
}
if (!function_exists('imagewebp')) {
    fail("This GD build cannot encode WebP. Check the deployed PHP build.");
}

$input = $argv[1];
$output = $argv[2];
$maxWidth = isset($argv[3]) ? filter_var($argv[3], FILTER_VALIDATE_INT) : 1600;
$maxHeight = isset($argv[4]) ? filter_var($argv[4], FILTER_VALIDATE_INT) : 1600;
$quality = isset($argv[5]) ? filter_var($argv[5], FILTER_VALIDATE_INT) : 82;

if (!is_int($maxWidth) || $maxWidth < 1 || !is_int($maxHeight) || $maxHeight < 1) {
    fail("Maximum dimensions must be positive integers.");
}
if (!is_int($quality) || $quality < 0 || $quality > 100) {
    fail("WebP quality must be an integer from 0 to 100.");
}
if (!is_file($input) || !is_readable($input)) {
    fail("Input does not exist or is not readable.");
}
if (strtolower(pathinfo($output, PATHINFO_EXTENSION)) !== 'webp') {
    fail("Output filename must end in .webp.");
}

// Limit input bytes before asking an image decoder to process it.
$maxInputBytes = 20 * 1024 * 1024;
$sourceBytes = filesize($input);
if ($sourceBytes === false || $sourceBytes > $maxInputBytes) {
    fail("Input is too large or its size could not be read.");
}

$info = getimagesize($input);
if ($info === false) {
    fail("Input is not a recognized image.");
}
[$width, $height, $type] = $info;
$allowedTypes = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP];
if (!in_array($type, $allowedTypes, true)) {
    fail("Supported input formats are JPEG, PNG, and WebP.");
}

// A pixel limit helps reject unexpectedly large decoded images. Set this for
// your own memory budget and workload; decoding uses more memory than file size.
$maxPixels = 24_000_000;
if ($width < 1 || $height < 1 || $width * $height > $maxPixels) {
    fail("Image dimensions are invalid or exceed the configured pixel limit.");
}

$loaders = [
    IMAGETYPE_JPEG => 'imagecreatefromjpeg',
    IMAGETYPE_PNG => 'imagecreatefrompng',
    IMAGETYPE_WEBP => 'imagecreatefromwebp',
];
$loader = $loaders[$type];
if (!function_exists($loader)) {
    fail("GD cannot decode this input format in the current build.");
}
$source = @$loader($input);
if ($source === false) {
    fail("GD could not decode the input.");
}

$scale = min(1, $maxWidth / $width, $maxHeight / $height);
$newWidth = max(1, (int) round($width * $scale));
$newHeight = max(1, (int) round($height * $scale));
$target = imagecreatetruecolor($newWidth, $newHeight);
if ($target === false) {
    imagedestroy($source);
    fail("Could not allocate the destination image.");
}

// Preserve alpha when the source has transparency. WebP stores its alpha
// channel; disabling blending preserves copied source alpha in the target.
imagealphablending($target, false);
imagesavealpha($target, true);
if (!imagecopyresampled($target, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height)) {
    imagedestroy($source);
    imagedestroy($target);
    fail("Could not resize the image.");
}

$outDir = dirname($output);
if (!is_dir($outDir) || !is_writable($outDir)) {
    imagedestroy($source);
    imagedestroy($target);
    fail("Output directory does not exist or is not writable.");
}

if (!imagewebp($target, $output, $quality) || !is_file($output) || filesize($output) === 0) {
    imagedestroy($source);
    imagedestroy($target);
    fail("WebP encoding failed or produced an empty file.");
}

$outputBytes = filesize($output);
printf("%dx%d -> %dx%d; %d bytes -> %d bytes; quality %d\n",
    $width, $height, $newWidth, $newHeight, $sourceBytes, $outputBytes, $quality);

imagedestroy($source);
imagedestroy($target);

The script chooses an example pixel and byte limit for demonstration; tune the limits to the memory and processing capacity of your service. PHP’s GD manual notes that memory allocated by the system GD library may not be governed by PHP’s memory_limit. A compressed upload’s file size therefore does not tell you how much memory decoding it will need.

Run it and compare the result

  1. Save the script as optimize.php.
  2. Run php optimize.php input.jpg output.webp 1600 1600 82, adjusting the bounds for the largest display size you need.
  3. Open the output at its intended display size. Inspect detailed edges, gradients, text, and transparent areas.
  4. Repeat with a few quality values and representative images. Record dimensions and byte counts for your own set; do not assume one image predicts the rest.
  5. Serve the result with Content-Type: image/webp and a filename/URL that matches its actual encoding.

PHP’s imagewebp() documentation defines quality from 0 (worst quality, smaller file) to 100 (best quality, larger file); -1 selects the encoder default of 80. The manual cautions that a successful return value does not always prove the underlying library successfully wrote the image, so the example also checks that a nonempty file exists. Choose a setting by evaluating your own image mix.

4. Resize and preserve transparency correctly

Resizing is useful when the stored original is much larger than the displayed image. Decide the required derivative sizes from actual display contexts, such as a thumbnail and a detail view. Generate each from the best available original rather than repeatedly resizing an already compressed derivative.

For transparent PNG input, use a true-color destination and preserve alpha during resampling. Disable alpha blending on the destination before copying; then call imagesavealpha($target, true). PHP documents that imagesavealpha() is meaningful for PNG, while WebP and AVIF store the full alpha channel. The manual also advises calling it deliberately for WebP and AVIF rather than relying on the current behavior. See PHP’s alpha-channel documentation.

Inspect transparent output against both light and dark backgrounds. A conversion can be technically valid yet still show halos or unwanted matte colors around edges. Do not flatten transparency onto a solid background unless that is the intended result.

5. JPEG, PNG, WebP, and AVIF in PHP

Format GD considerations Typical decision checks
JPEG GD provides JPEG read/write functions when built with JPEG support. Useful for many photographs; compare quality, detail, and bytes. JPEG does not provide transparency.
PNG Common for lossless output and transparency; PNG support requires the relevant build libraries. Check whether lossless output is worth the resulting bytes for the content. Preserve alpha when needed.
WebP imagewebp() encodes WebP and takes a quality argument. Confirm the runtime can decode and encode it. Compare quality and bytes for each content class; preserve alpha if needed.
AVIF GD AVIF support is available from PHP 8.1 when built with AVIF support; check for the required functions in production. Verify deployment support, delivery/client requirements, visual result, and processing behavior.

Do not select a format from an extension name alone. Confirm the actual file content and the output encoder used, then send a matching MIME type. If you must support clients with different format capabilities, generate and serve suitable variants according to your application’s delivery design. The research sources used here do not establish current browser support or quantify format-to-format savings.

6. When to use ImageMagick

GD is a practical choice for common resize and re-encode jobs. ImageMagick may be appropriate when your workload needs its processing features or your organization already operates it. Its security policy can restrict readable and writable formats and limit resource use; review the ImageMagick security policy for the installed version and configure only the formats and resources the service needs.

Format policy is one hardening control, not a complete upload-security design. Validate uploads, isolate processing from public application paths, limit inputs and work queues, keep libraries maintained, and decide how failed or suspicious inputs are handled. Use separate operational controls appropriate to the deployment.

7. Upload handling and resource limits

Image decoders process complex files. Treat uploads as untrusted even when a filename ends in .jpg or the request supplies an image MIME type. At minimum, a production workflow should:

  • Enforce request and file-size limits before reading the file into memory.
  • Use server-side image inspection and an explicit allowlist of formats your pipeline supports.
  • Check dimensions and pixel count before decoding, and set limits suitable for your workers.
  • Write outputs to controlled storage with generated names rather than trusting user paths.
  • Set worker time and concurrency limits; monitor memory and disk use.
  • Keep originals when recovery or later reprocessing matters, subject to your retention rules.
  • Return generic client errors while recording useful diagnostic details in server logs.
  • Consider isolated workers and restrictive ImageMagick policies when using ImageMagick.

These are operational recommendations, not a complete security checklist. Validate the design against your framework, storage model, threat model, and deployed decoder versions.

8. Performance, reliability, and cost

Resizing and encoding consume CPU and memory. Large decoded images can require far more memory than their compressed upload size suggests; GD’s system-library allocations may not be governed by PHP’s memory_limit. Apply bounds before decode, limit parallel jobs, and load-test with representative large inputs in the target environment.

For a user-facing upload endpoint, consider moving expensive processing to a queue so the request does not wait on every derivative. Make jobs retry-safe: write to a temporary path, validate that output was produced, and publish or rename the derivative only after a successful encode. Keep a record of source dimensions, output dimensions, format, encoder settings, and byte sizes if you need to reproduce or audit results.

Cost depends on your own compute, storage, and bandwidth use. Smaller derivatives can reduce the bytes you store or deliver, but the amount depends on the source content, chosen dimensions, and encoding settings. Measure your own before-and-after set instead of quoting a general percentage.

9. Troubleshooting

Symptom Likely cause Fix
Call to undefined function imagewebp() GD is missing or the deployed GD build lacks WebP encoding. Check gd_info(), imagetypes(), and the actual FPM/worker PHP configuration. Install or build PHP with WebP support, or select an available output format.
WebP input cannot be opened The runtime cannot decode WebP, or the file is invalid. Check imagecreatefromwebp() availability and verify the input bytes and actual format.
Output looks jagged, blurry, or has ringing Dimensions or lossy quality are too aggressive for the image content. Compare at the intended display size; increase quality or output dimensions, or use a lossless format for suitable artwork.
Transparent areas turn black or opaque Alpha was lost or blending was enabled during destination copying. Use a true-color destination, call imagealphablending($target, false) before copying, and call imagesavealpha($target, true). Inspect the output on contrasting backgrounds.
Output is larger than the source The source was already efficiently encoded, the selected format/settings do not suit it, or dimensions stayed large. Compare alternative settings and formats; keep the original when the derivative is not useful. Avoid assuming re-encoding always reduces bytes.
PHP reports out of memory or a worker is killed Decoded pixels, concurrent jobs, or system GD allocations exceed available resources. Reject excessive dimensions before decode, lower worker concurrency, use bounded queues, and measure worker memory in production-like conditions.
imagewebp() returns true but the file is missing or empty The underlying encoder may fail to write despite the return value. Check output existence and size, verify destination permissions and free disk space, and log the encoder/runtime details.
AVIF function is undefined PHP may be older than 8.1 or GD was built without AVIF support. Check the deployed PHP version, build configuration, and function_exists('imageavif'); choose a supported fallback.
Animated input becomes a still image The selected GD workflow handles a frame rather than preserving the animation sequence. Use a tool and pipeline designed to retain animation, and verify frames and timing after conversion.
CLI works, website fails CLI and web PHP use different binaries, configuration, or extension builds. Inspect GD from the actual web worker or job runtime rather than relying on local CLI output.

10. Or skip the browser setup

If the goal is a clean screenshot of a rendered website rather than processing an uploaded image file, ScreenshotNeo returns an image or PDF from one GET request. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. The API also supports PNG, JPEG, WebP, PDF, and options such as full-page capture and custom viewport sizing. Sign up for 1,000 free screenshots a month, with no card.

11. Frequently asked questions

Does re-encoding always make an image smaller?

No. An image may already be efficiently encoded, and the new format or settings may produce more bytes. Compare the actual files and keep the better result for your requirements.

What WebP quality should I use?

PHP documents the encoder’s range and default, but does not prescribe a universal best setting. Compare a representative sample at several values and judge both visible quality and bytes.

Can GD optimize an animated GIF?

Do not assume a basic GD load-and-save workflow preserves animation. Verify frame count and timing or choose a processing tool and workflow designed for animated images.

Should I overwrite the original?

Keep the original if you may need to create different sizes or formats later, or recover from an encoding choice. Apply your application’s retention policy to originals.

Does memory_limit cap all GD memory?

Not necessarily. PHP’s GD documentation notes that memory allocated by the system GD library may not be governed by PHP’s memory_limit. Bound input dimensions and observe the worker process’s actual resource use.

Primary references