CodeWalkers is in beta

Composite Images with PHP and GD

Published Updated

The original "Overlapping Images with GD" task still matters: place one image over another and save the merged result 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.

The boundary controls the whole design of the feature. 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. The GdImage type hints require PHP 8.0 or newer, while the promoted readonly properties require PHP 8.1 or newer.

The GD call is usually the small part. Ownership of the permanent sheet controls caching, storage, authorization, replacement, and every cleanup job that follows.

Guide Path

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.

Start with Trusted File Paths

GD functions accept file paths, which means the security boundary comes before the image call. 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. That is less exciting than a one-file demo, but it is the difference between an image feature and a filesystem bug wearing a watermark.

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.

Preserve Alpha for PNG Overlays

The most common modern case is a PNG overlay with its own transparency: a logo, badge, signature, or watermark mark. 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.");
    }

    imagedestroy($overlay);
    imagedestroy($background);
}

That function is deliberately narrow: JPEG background, PNG overlay, JPEG output. A real image pipeline can support PNG, WebP, AVIF, and editorial crops, but a tutorial should not hide three product decisions inside one magic helper.

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.

Overlay Positioning

Pixel coordinates are reasonable when the generated output has a fixed size. 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.

Resize the Overlay Before Compositing

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
{
    $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.

Use Imagecopymerge for Flat Opacity

The old tutorial centered on imagecopymerge(), and the 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,
);

That final 35 means 35 percent opacity for the copied source. 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.

CSS and PHP Alignment

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

Symptom: the resized PNG gets a black rectangle around the logo. Cause: the new true-color image was copied before alpha saving and a transparent fill were configured. Fix: 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

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

Writing the Output File Fails

Symptom: the in-memory composite looks valid and the target file is missing. Cause: the target directory is absent or unwritable by the PHP process, or the encoder returned false. Fix: 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?

Not in one call. 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 one overlay be reused across many backgrounds?

Yes, within a single process. Load it once with imagecreatefrompng and pass that same resource 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?

Not exactly, but they must represent the same relationship between overlay and 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.

Next Steps

Keep the useful old idea: GD can combine images, and imagecopymerge() is still valid when a whole overlay needs one opacity percentage.

The parts to retire are the loose ones. URL paths need validation before they become file paths. Generated pixels should not be the default way to handle presentation. Avoid creating a fresh composite on every page load when the output could be generated once and cached.

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. The clean solution usually needs both halves, one for the preview and one for the saved file.

Sources

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