How to Generate Dynamic Images with PHP
Generate dynamic PNG, JPEG, and WebP images in PHP with GD, Imagick, or Imagine, including runnable code, security checks, and troubleshooting.

Direct answer
Generate a dynamic image in PHP by creating or loading a canvas, drawing data-driven shapes and text, then encoding the result as PNG, JPEG, or WebP. PHP’s GD extension is enough for cards, badges, charts, thumbnails, and direct browser output. Imagick, which connects PHP to ImageMagick, fits projects that need broader formats or ImageMagick operations. Imagine provides a higher-level object-oriented API over GD2, Imagick, or Gmagick.
The basic sequence is:
- Validate the data that controls the image.
- Create a true-color canvas or load a controlled template.
- Allocate colors and draw shapes, images, and text.
- Encode the canvas in the requested format.
- Stream it with the correct content type or save it atomically.
- Destroy image resources and cache repeat requests.
Check the actual server build with gd_info(). PHP’s format table documents possible support, but the installed libraries and build flags determine what your runtime can really read and write. See the PHP GD documentation.
Choose GD, Imagick, or Imagine
| Option | Best fit | Checks and trade-offs |
|---|---|---|
| GD | Raster graphics, text overlays, thumbnails, and direct browser output | Format encoders and FreeType depend on the build; inspect gd_info(). |
| Imagick | ImageMagick operations and many formats | Requires the PHP extension, ImageMagick delegates, and a suitable security policy. PHP labels the extension experimental, so verify versions and deployment support in your environment. Read the Imagick manual. |
| Imagine | An object-oriented API with a selectable GD2, Imagick, or Gmagick driver | It still depends on the selected driver and its libraries. Check current release requirements in the Imagine repository. |
Inspect the running build
<?php
header('Content-Type: text/plain');
print_r(gd_info());
?>
Run this through the same PHP SAPI, container, and deployment path that serves production. A command-line PHP binary and PHP-FPM can have different extensions enabled. Confirm the result for JPEG, PNG, WebP, and FreeType before depending on those capabilities.

Build a dynamic image endpoint with GD
1. Install and verify GD
Enable your distribution’s GD package (often named php-gd or a versioned equivalent), restart PHP-FPM or Apache, and confirm that extension_loaded('gd') returns true. Enable FreeType if you plan to use imagefttext() and imageftbbox().
2. Create a reusable renderer
<?php
// card.php
$data = [
'name' => trim((string)($_GET['name'] ?? 'Ada Lovelace')),
'score' => filter_var($_GET['score'] ?? 87, FILTER_VALIDATE_INT),
];
if ($data['score'] === false) {
$data['score'] = 87;
}
$data['name'] = mb_substr($data['name'], 0, 80);
$data['score'] = max(0, min(100, $data['score']));
$width = 1200;
$height = 630;
$image = imagecreatetruecolor($width, $height);
imagealphablending($image, true);
imagesavealpha($image, true);
$background = imagecolorallocate($image, 245, 247, 250);
$ink = imagecolorallocate($image, 24, 31, 42);
$muted = imagecolorallocate($image, 90, 103, 120);
$accent = imagecolorallocate($image, 53, 104, 232);
$white = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $background);
imagefilledrectangle($image, 0, 0, 18, $height, $accent);
// Use a rectangle for maximum GD portability.
imagefilledrectangle($image, 70, 70, 1130, 560, $white);
$font = __DIR__ . '/fonts/Inter-Regular.ttf';
$fontBold = __DIR__ . '/fonts/Inter-Bold.ttf';
if (!is_readable($font) || !is_readable($fontBold)) {
imagedestroy($image);
http_response_code(500);
exit('Configured font file is missing');
}
imagefttext($image, 54, 0, 120, 210, $ink, $fontBold, $data['name']);
imagefttext($image, 28, 0, 120, 275, $muted, $font, 'Dynamic score');
imagefttext($image, 110, 0, 120, 430, $accent, $fontBold, (string)$data['score']);
imagefttext($image, 28, 0, 300, 430, $muted, $font, '/ 100');
header('Content-Type: image/png');
header('Cache-Control: public, max-age=300');
imagepng($image, null, 6);
imagedestroy($image);
?>
The endpoint validates and bounds the query values before they reach the renderer. Keep the response binary: PHP notices, debug output, whitespace before <?php, or HTML can corrupt the PNG. In a framework, return a binary response with the matching content type.
3. Run it
curl 'https://example.test/card.php?name=Grace%20Hopper&score=94' -o card.png
For production, authenticate image-generation endpoints, rate-limit requests, and avoid putting sensitive data in cacheable URLs.
Dynamic text that fits reliably
Use imageftbbox() to measure a TrueType string before positioning it. Wrap long input at word boundaries, cap the character count and line count, and reserve enough vertical space for the worst case. Without FreeType, imagestring() uses built-in bitmap fonts with limited typography.
<?php
function wrapForFont(string $text, string $font, int $size, int $maxWidth): array {
$words = preg_split('/\s+/', trim($text));
$lines = [];
$line = '';
foreach ($words as $word) {
$candidate = $line === '' ? $word : "$line $word";
$box = imageftbbox($size, 0, $font, $candidate);
$candidateWidth = $box[2] - $box[0];
if ($line !== '' && $candidateWidth > $maxWidth) {
$lines[] = $line;
$line = $word;
} else {
$line = $candidate;
}
}
if ($line !== '') {
$lines[] = $line;
}
return array_slice($lines, 0, 8);
}
?>
Draw each returned line at a controlled baseline increment. Normalize input encoding, provide a fallback font, and test long names, punctuation, emoji, and non-Latin scripts on the target FreeType build.
Load a template, resize, and compose
<?php
$source = imagecreatefrompng(__DIR__ . '/template.png');
$thumb = imagecreatetruecolor(320, 180);
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
imagecopyresampled(
$thumb,
$source,
0,
0,
0,
0,
320,
180,
imagesx($source),
imagesy($source)
);
imagepng($thumb, __DIR__ . '/thumb.png');
imagedestroy($thumb);
imagedestroy($source);
?>
Use format-specific loaders such as imagecreatefromjpeg() or imagecreatefromwebp() only when that codec is enabled. For user uploads, decode and validate the image rather than trusting its filename or MIME header.
Output formats and files
| Output | PHP call | Typical use |
|---|---|---|
| PNG | imagepng($image, $path, 6) |
Lossless graphics and transparency. |
| JPEG | imagejpeg($image, $path, 85) |
Photographic content and smaller files; no alpha channel. |
| WebP | imagewebp($image, $path, 80) |
Compact modern delivery when WebP support is present. |
When streaming, set Content-Type before encoding. When saving, pass only a controlled server path and write to a temporary file before renaming it into place. That prevents readers from seeing a partially encoded file.
Imagick and Imagine alternatives
Imagick exposes ImageMagick through PHP. A minimal conversion is:

<?php
$im = new Imagick(__DIR__ . '/input.tiff');
$im->setImageFormat('webp');
$im->setImageCompressionQuality(82);
$im->writeImage(__DIR__ . '/output.webp');
$im->clear();
$im->destroy();
?>
Confirm enabled delegates, resource limits, and ImageMagick security policy before accepting untrusted files. ImageMagick can process many formats, but a format being supported by the library does not mean it should be enabled for arbitrary uploads. Imagine is useful when you want an object-oriented drawing and manipulation layer while retaining the ability to select a driver.
Security and resource limits
- Validate upload size, decoded dimensions, and total pixel count before processing. Reject huge images early.
- Do not let request parameters select arbitrary local paths or remote URLs. Map identifiers to known templates.
- Set execution and processing timeouts. Free resources with
imagedestroy()or Imagick’sclear()anddestroy(). - PHP documents that system GD allocations may not be counted by
memory_limit. Leave operating-system headroom and enforce an application pixel limit; see the GD installation notes. - Store generated files outside executable directories and use unguessable names when they contain user data.
- For uploads, validate the decoded image and dimensions, isolate processing, and reject files that exceed your time or memory budget. PHP’s upload documentation describes the server-side handling model.
Or skip the browser setup
If your source is a web page rather than server-side drawing, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether it was billed. See the ScreenshotNeo API documentation for the complete option list.
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Options include full-page capture with lazy images loaded, CSS-selector element shots, dark mode, 12 device presets or custom viewports, retina scale, PDF paper size and margins, custom CSS and JavaScript, clicks, waits for selectors, delays or network idle, blocked ads, trackers, requests or resource types, custom headers, cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work for easier migration.
Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan.
Create a free ScreenshotNeo account and start with the no-card 1,000-shot allowance.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “Call to undefined function imagecreatetruecolor” | GD is not loaded for this SAPI. | Install or enable GD, restart the service, and verify with extension_loaded('gd'). |
| Blank or corrupted image | Warnings or HTML were sent before binary bytes, or the content type is wrong. | Disable display errors on the image endpoint, log instead, set the matching header, and inspect output buffering. |
| Text function missing | FreeType support is absent. | Check gd_info(); install a FreeType-enabled GD build or fall back to imagestring(). |
| Allowed memory size error or process killed | Pixel dimensions are too large; GD memory may bypass PHP accounting. | Reject oversized dimensions, downsample early, cap concurrency, and leave OS memory headroom. |
| JPEG or WebP loader fails | The encoder or decoder was not compiled in. | Check gd_info(); install a build with that codec or choose PNG. |
| Imagick security-policy error | ImageMagick policy or a delegate blocks the format. | Enable only required formats in a reviewed policy and avoid broadly allowing risky delegates. |
| ScreenshotNeo returns a bot or blank verdict | The target blocked automation or failed to load. | Inspect X-Page-Verdict and X-Billed; retry transient failures and use the verdict for accounting. |
Performance, reliability, and cost
- Cache by a normalized hash of the input data, template version, font version, and rendering options.
- Reuse loaded fonts and templates; avoid decoding the same source repeatedly in one request.
- Resize close to the final dimensions and choose JPEG or WebP quality according to the visual requirement.
- Queue expensive jobs, set worker timeouts, and return an identifier for asynchronous generation.
- Write generated files atomically and monitor failures, processing time, output size, and queue depth.
- For ScreenshotNeo, choose a TTL that matches page freshness, use asynchronous jobs and signed webhooks for batches, and use bulk capture for up to 100 URLs. Page verdict and billing headers let you distinguish clean captures from failed or unbilled attempts.
Production checklist
- Confirm required GD formats and FreeType with
gd_info()in the production runtime. - Bound every user-controlled string, number, image dimension, and processing duration.
- Use deterministic cache keys containing all visual inputs.
- Set accurate content types and prevent debug output on binary responses.
- Keep fonts, templates, and output directories controlled and non-executable.
- Log generation failures without returning internal paths or stack traces.
FAQ
Can GD create animated GIFs?
GD can read and write common still formats. Animation requires handling multiple frames yourself or using ImageMagick. Verify support in the deployed build before relying on a format.
Should generated images be stored or streamed?
Stream short-lived responses. Store images when URLs are reused, using controlled paths and atomic writes. Include every visual input in the cache key.
How do I prevent user text from breaking layout?
Normalize encoding, cap length, wrap with measured font bounds, and reserve a maximum number of lines. Keep a fallback font and test long names.
When is Imagick worth the dependency?
Use it when you need ImageMagick-specific transforms or formats and can operate its extension, delegates, and security policy safely. GD is simpler for ordinary cards and thumbnails.


