Composite Images with PHP and GD

Published Updated

Use PHP's GD extension when one image placed over another has to be saved as a merged file for storage, email, caching, or export.

Think of the background as a printed card and the transparent overlay as a clear sheet placed on top. CSS keeps those two layers separate in the browser, while GD presses them into one permanent sheet of pixels.

Choose CSS or GD based on whether you need to save the merged image. If the user is only seeing a badge, label, watermark preview, avatar stack, or product overlay in the browser, the HTML tutorials hub covers the browser-side tools. HTML and CSS can stack separate assets cleanly, and the design remains editable.

Use PHP/GD when the merged pixels need to exist after the request: a watermarked download, an exported certificate, a generated social image, a printable badge, or a product mockup.

These examples require the PHP GD extension with JPEG and PNG support. Check extension_loaded("gd") and gd_info() in the same PHP environment that runs the image job. The GdImage type hints require PHP 8.0 or newer, while the promoted readonly properties require PHP 8.1 or newer.

Keep the original files so you can regenerate the composite when the watermark or output size changes.

Read this after creating image thumbnails with PHP. The thumbnail lesson covers resizing and generated derivative files; this lesson adds compositing, opacity, alpha handling, and output storage.

Use the HTML tutorials hub when the browser preview and saved image need the same composition rules.

How Should PHP Choose Image File Paths?

PHP should choose image paths from trusted application records rather than accept a path from the request. Do not pass raw form fields, query strings, or uploaded filenames into imagecreatefromjpeg() or imagecreatefrompng().

Use application records and generated filenames like this:

<?php

final class ImageCompositeJob
{
    public function __construct(
        public readonly int $assetId,
        public readonly string $backgroundPath,
        public readonly string $overlayPath,
        public readonly string $targetPath,
    ) {
    }
}

The controller can look up the image asset by ID, confirm the current user can use it, then hand known server-side paths to the image job.

Validate the input before GD touches it. Check the stored file exists, keep private originals outside the public directory, cap dimensions during upload, generate your own target names, and decide whether the output should be public or private. GD is only the pixel tool in that chain.

How Do You Overlay a Transparent PNG with PHP?

Use imagecopy() with alpha blending enabled on the background to place a transparent PNG over a JPEG. In that case, imagecopy() is usually the clearer choice because the overlay already carries the alpha channel you want.

<?php

function compositePngOverlay(
    string $backgroundPath,
    string $overlayPath,
    string $targetPath,
    int $x,
    int $y,
    int $jpegQuality = 86,
): void {
    $backgroundInfo = getimagesize($backgroundPath);
    $overlayInfo = getimagesize($overlayPath);

    if ($backgroundInfo === false || $backgroundInfo[2] !== IMAGETYPE_JPEG) {
        throw new RuntimeException("The background must be a JPEG file.");
    }

    if ($overlayInfo === false || $overlayInfo[2] !== IMAGETYPE_PNG) {
        throw new RuntimeException("The overlay must be a PNG file.");
    }

    $background = imagecreatefromjpeg($backgroundPath);
    $overlay = imagecreatefrompng($overlayPath);

    if ($background === false || $overlay === false) {
        throw new RuntimeException("Could not load one of the source images.");
    }

    imagealphablending($background, true);

    imagecopy(
        $background,
        $overlay,
        $x,
        $y,
        0,
        0,
        imagesx($overlay),
        imagesy($overlay),
    );

    if (! imagejpeg($background, $targetPath, $jpegQuality)) {
        throw new RuntimeException("Could not write the composite image.");
    }

}

The function accepts a JPEG background and a PNG overlay, then writes a JPEG. Create the output directory first and ensure PHP can write to it. In PHP 8, GD images are objects that are released when no references remain; imagedestroy() has no effect and is deprecated as of PHP 8.5.

There is also a format decision in the output. Saving as JPEG flattens transparency, which is fine for most photo backgrounds. If the output itself needs transparency, save a PNG target instead and configure alpha on the destination image deliberately.

How Do You Position a GD Overlay?

Position a GD overlay by passing its top-left pixel coordinates to imagecopy(). If every exported card is 1200 by 630, placing a watermark 48 pixels from the bottom right is understandable and repeatable.

<?php

function bottomRightOverlayPoint(
    GdImage $background,
    GdImage $overlay,
    int $padding = 48,
): array {
    return [
        imagesx($background) - imagesx($overlay) - $padding,
        imagesy($background) - imagesy($overlay) - $padding,
    ];
}

Use that kind of helper when the product wants a consistent house mark. If the user can move the overlay in a browser preview, store the position as a percentage of the preview box, then translate it to pixels when generating the final image. Fixed preview pixels will betray you the first time the page renders on a different screen.

How Do You Resize a PNG Overlay?

Resize the overlay for the output dimensions. If someone uploads a 3000-pixel logo and the exported social image is 1200 pixels wide, the app should resize the overlay first.

<?php

function resizePngToWidth(GdImage $overlay, int $targetWidth): GdImage
{
    if ($targetWidth < 1) {
        throw new InvalidArgumentException("Target width must be positive.");
    }

    $sourceWidth = imagesx($overlay);
    $sourceHeight = imagesy($overlay);
    $targetHeight = max(1, (int) round($sourceHeight * ($targetWidth / $sourceWidth)));

    $resized = imagecreatetruecolor($targetWidth, $targetHeight);

    imagealphablending($resized, false);
    imagesavealpha($resized, true);

    $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
    imagefilledrectangle($resized, 0, 0, $targetWidth, $targetHeight, $transparent);

    imagecopyresampled(
        $resized,
        $overlay,
        0,
        0,
        0,
        0,
        $targetWidth,
        $targetHeight,
        $sourceWidth,
        $sourceHeight,
    );

    return $resized;
}

That helper keeps the transparent pixels around the PNG instead of filling the empty area with black. This is the sort of small GD detail that makes the difference between a usable export feature and an image that looks like it came through a fax machine.

When Should You Use imagecopymerge()?

The imagecopymerge() function still has a place. Use it when the whole overlay should be blended at one percentage, such as a simple washed-out watermark over a JPEG.

<?php

imagecopymerge(
    $background,
    $watermark,
    $x,
    $y,
    0,
    0,
    imagesx($watermark),
    imagesy($watermark),
    35,
);

The example assumes $background and $watermark are loaded GD images and $x and $y are destination coordinates. The final 35 applies a 35 percent blend. It is easy to read, and for old-school JPEG watermarking it does the job.

For modern transparent PNG overlays, prefer imagecopy() and preserve the overlay's alpha channel. Applying one global percentage to an asset that already has careful transparency can make the result look muddy. The clear-sheet analogy matters here. Keep the transparency painted into that layer instead of washing the whole sheet with one opacity value.

Save Once, Serve Normally

Compositing work should usually happen when the user uploads an asset, exports a file, publishes a product image, or changes a watermark setting. The public page should point at the generated file.

<?php

$job = new ImageCompositeJob(
    assetId: 1329,
    backgroundPath: __DIR__ . "/../storage/originals/product-1329.jpg",
    overlayPath: __DIR__ . "/../storage/overlays/verified-badge.png",
    targetPath: __DIR__ . "/../public/uploads/composites/product-1329-verified.jpg",
);

compositePngOverlay(
    $job->backgroundPath,
    $job->overlayPath,
    $job->targetPath,
    880,
    48,
);

Do not regenerate the same composite on every gallery request unless you have a measured reason. Generated files can be cached, backed up, reviewed, and invalidated when their inputs change. A dynamic image endpoint has to solve all of that per request, plus authorization and error handling.

If the output is private, store it outside the public directory and serve it through an authorized download route. If the output is public marketing media, give it a stable public path and let the CDN do the boring work.

How Do You Match a CSS Preview to a PHP Export?

The browser preview should use the same source image, overlay asset, anchor point, and sizing rule as the PHP export. Otherwise the user approves one image and downloads another, which is a fast way to lose trust in a feature.

A small composition model is enough here:

<?php

$composition = [
    "anchor" => "bottom-right",
    "padding" => 48,
    "overlayWidth" => 220,
    "opacity" => 1.0,
];

Use that model to render the preview and generate the file. The preview can still be HTML and CSS; the saved file can still be GD. The shared model is the contract between them.

Common Pitfalls & Debugging

Transparent Pixels Turn Black

The resized PNG gets a black rectangle around the logo. The new true-color image was copied before alpha saving and a transparent fill were configured. Disable alpha blending on the destination, call imagesavealpha(), fill it with a fully transparent color, then resample the source.

The Overlay Sits Outside the Canvas

The overlay is clipped or missing from the saved file. The destination coordinates were calculated from preview pixels or without subtracting the resized overlay dimensions. Calculate coordinates from the final background and overlay sizes, then reject negative or out-of-bounds positions.

Writing the Output File Fails

The in-memory composite looks valid and the target file is missing. The target directory is absent or unwritable by the PHP process, or the encoder returned false. Create the directory during deployment, verify its ownership, and treat a failed imagejpeg() call as an exception.

Frequently Asked Questions

Can GD composite more than two images at once?

GD combines more than two images by copying each source onto the same destination in separate calls. The copy functions take a single destination and a single source, so three or more layers means calling them repeatedly onto the same destination and saving once at the end.

Does imagecopy resize the overlay?

No. imagecopy copies the source rectangle at its current pixel dimensions. Resize the overlay with imagecopyresampled before compositing when its stored dimensions do not match the export. Keeping resize and copy as separate steps makes the final scale and position easier to test.

Does GD support SVG overlays?

No. GD is raster-only and has no SVG renderer, so a vector logo has to be rasterised to PNG at the size you need before GD can place it. Do that conversion outside GD.

Can a GD overlay be reused across many backgrounds?

A GD overlay can be reused across multiple backgrounds within one PHP process. Load it once with imagecreatefrompng and pass that same GdImage object into the composite call for every background in the batch rather than reopening the file each time.

Does the CSS position used to preview an overlay need to match the PHP compositing coordinates exactly?

A CSS preview and PHP export can use different pixel coordinates if they preserve the same overlay position and scale relative to the background. A CSS preview built with percentages or a scaled container can differ in pixels from the PHP coordinates as long as both express the overlay's position relative to the same corner and at the same relative scale.

Save the Image Your Page Needs

Generate the composite when its inputs change, then reuse the saved file. Keep private outputs outside the public directory and check that the encoder produced a readable, non-empty image before exposing the result.

Start with image thumbnails in PHP when you need derivative files, then use this lesson when the derivative is made from more than one source image. The HTML tutorials hub covers the browser-side preview.

Sources

  1. [1]
  2. [2]
    PHP imagecopy
    (php.net)
  3. [3]
  4. [4]
  5. [5]
  6. [6]
  7. [7]
    getimagesize
    (php.net)