rfc:ext-gd-2.4

PHP RFC: Update ext/gd to latest features

Introduction

libgd has drifted significantly from the copy bundled in php-src: newer codecs, corrected color handling, and a much stronger compliance posture against upstream test corpora have accumulated upstream without a corresponding sync into ext/gd. This RFC brings that work into PHP.

  1. Codec completeness and correctness. Every bundled codec has been brought up against its upstream compliance/test corpus, with numerous correctness fixes landing as a result (see Backward Incompatible Changes). New codecs are added (QOI, JPEG XL, UltraHDR), and existing ones gain capabilities they previously lacked (GIF animation read/write, multi-page TIFF, full WebP animation support, PNG option exposure, JPEG reader/writer options). See the Codecs section
  2. Ability to extract metadata to be processed by callers, and save back. See the metadata section.
  3. A new object-oriented API surface, under the Gd\* namespace, exposing per-codec read/write options, metadata, and (where applicable) animation/multi-page access in a consistent, typed way.No existing procedural function (imagecreatetruecolor(), imagejpeg(), etc.) changes behavior or signatures. Legacy code continues to work unchanged, and can be freely mixed with the new API since both operate on the same GdImage object.
  4. A new 2D vector/canvas rendering API, built on FreeType's rasterizer, adding gradients, the full set of Cairo-style compositing operators (with correct unbounded-operator and premultiplied-alpha semantics), and a save/restore state stack. The canvas draws directly into an existing truecolor GdImage's pixel buffer -- there is no copy-on-wrap and no copy-back step, so canvas drawing and legacy drawing functions can be interleaved freely on the same image.See the 2D APIs section or the examples for some output.

A deliberate design goal across all three is extensibility without new procedural surface. Every codec exposes its read/write knobs through per-codec ReadOptions/WriteOptions value objects rather than positional arguments, and new capabilities (a new codec, a new encoder option, a new internal color model) are additions to those objects or new sibling classes, not new functions or growing parameter lists on existing ones. This is the same shape libgd itself is moving toward internally (see Future Scope for concrete cases: floating-point/HDR buffers, ICC profile support), and this RFC's API surface is built so those can land later as pure additions. imagepng($image) behavior require explicit STDOUT (or other streams) for toStream.

Similarly, Gd\Image::create( ) will remain and accept more formats as they are added.Gd\Image::loadFrom* automatically supports any format available in a given build.

Proposal

Expose the following new libgd 2.4 capabilities to userland:

  • New codecs: QOI (builtin), JPEG XL (libjxl, including animation), UltraHDR (libultrahdr).
  • Codec completeness: GIF (full animation read/write, builtin), BMP (all standard depths except OS/2 BMP, builtin), WebP (all formats and animation, libwebp), TIFF (single and multi-page read/write), PNG (full option exposure replacing the current single-filter-argument imagepng()), JPEG (full reader options -- scale, rotation, DCT method -- and full writer options).
  • Raw metadata read/write for formats that support it (EXIF, XMP, ICC, IPTC), returned/accepted as raw byte strings so userland can process them with any library (ext/exif, pure PHP, etc.) and write them back unchanged or modified.
  • A new 2D vector/canvas drawing API (Gd\Canvas / Gd\Context / Gd\Path, etc): gradients (linear/radial, Porter-Duff compositing via the two-circle model), the full Cairo compositing operator set, and a save/restore state stack. Truecolor images only -- see Backward Incompatible Changes / Open Issues for the palette-image rejection behavior.
  • gdTestImagePerceptualDiff-backed testing support, allowing PHP's own test suite (and userland test suites) to do architecture-resilient image comparison instead of brittle byte-identical comparison.

Each codec's new API follows the same shape: a Codec class with static fromFile()/fromString()/fromStream() and toFile()/toString()/toStream() methods operating on GdImage, an immutable Info value object describing what was read, and immutable ReadOptions/WriteOptions value objects for anything beyond the defaults. Formats with animation or multiple pages additionally expose a streaming Reader/Writer (or AnimReader/AnimWriter) pair rather than loading everything into memory at once.

For each codecs options, while they use the current values or options from a given underlying library, GD creates its own define and the extensions as enum or other types as it fits. The options class itself allows forward compatibility without BC in case of new options, changes of values etc. The result may indeed be affected, given the libraries may change their own behaviors. This was a main pain point with the current procedural APIs and arguments for each option.

Save/load entry points are deliberately split by destination and source kind (toFile(string $path), toStream($resource), toString(): string) rather than accepting one polymorphic string, the way imagejpeg($image, $filename) and similar legacy functions do today. That single-argument shape has repeatedly been a source of stream-wrapper and injection issues when the argument is built from user-influenced input; splitting by method makes the destination explicit at the call site rather than inferred from the shape or content of a string at runtime.

(External libgd 2.4+ support for these new APIs is being added ahead of feature freeze; bundled libgd already exposes them.)

There is also the libgd C (doxigen) docs here. It is also synchronized in ext/gd/libgd.

It is important to note that libgd 2.4 is already merged as part of the bundled gd sync.

Stubs

As it is surely friendly to link to them rather than having 10 pages of stubs/interface here, I added links to them. Some are still displayed in this RFC as they are specific to one area/codec. However common interfaces like load* save* are not.

Base Exception

Gd\Image

GdImage (added to existing)

Vector 2D

Codecs

Examples

Gd Image loading, scaling and other composition

<?php
use Gd\CompositeOperator;
 
use Gd\InterpolationMethod;
 
use Gd\Jpeg\Codec as JpegCodec;
 
use Gd\Png\Codec as PngCodec;
 
use Gd\ScaleOptions;
 
 
$image = JpegCodec::fromFile(__DIR__ . '/haves_inn.jpg');
$logo = PngCodec::fromFile(__DIR__ . '/logo.png');
{{ :rfc:watermarked.png?direct&400 |}}
$logoWidth = max(1, (int) round(imagesx($image) * 0.16));
$watermark = $logo->scale(
    width: $logoWidth,
    height: null,
    options: new ScaleOptions(interpolation: InterpolationMethod::CatmullRom),
);
 
$padding = max(12, (int) round(imagesx($image) * 0.02));
$x = imagesx($image) - imagesx($watermark) - $padding;
$y = imagesy($image) - imagesy($watermark) - $padding;
 
$image
    ->composeFrom($watermark, $x, $y, CompositeOperator::Over, 0.55)
    ->toFile('./watermarked.png');
 
<code php>
 
{{ :rfc:watermarked.png?direct&400 |}}
 
<code php>
<?php
use Gd\Jpeg\Codec;
use Gd\Jpeg\ReadOptions;
 
Codec::fromFile('/haves_inn.jpeg', 
    new ReadOptions(scaleNumerator: 3, scaleDenominator: 8))
    ->rotate(30)
    ->toFile('./scaled_on_load_rotate.png');

<?php
use Gd\Jpeg\Codec;
use Gd\Jpeg\ReadOptions;
use Gd\InterpolationMethod;
use Gd\Matrix;
use Gd\TransformOptions;
 
$image = Codec::fromFile(__DIR__ . '/haves_inn.jpg', new ReadOptions(scaleNumerator: 3, scaleDenominator: 8));
 
$centerX = imagesx($image) / 2;
$centerY = imagesy($image) / 2;
 
$matrix = (new Matrix())
    ->rotateAround(M_PI / 18, $centerX, $centerY)
    ->shear(0.08, 0.0)
    ->translate(18, -10);
 
$image
    ->transform($matrix, new TransformOptions(interpolation: InterpolationMethod::CatmullRom))
    ->toFile('./scaled_on_load_affine.png');

2D Vector APIs

Example with curves and gradients

Gradients and compositions

Gradient modes and compositions

const COLS = 5;
const TILE = 150;
const GAP = 8;
const GD_2D_PI = 3.14159265358979323846;
 
$names = [
    "CLEAR", "SOURCE", "OVER", "IN", "OUT", "ATOP",
    "DEST", "DEST_OVER", "DEST_IN", "DEST_OUT", "DEST_ATOP", "XOR",
    "ADD", "SATURATE", "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN",
    "LIGHTEN", "COLOR_DODGE", "COLOR_BURN", "HARD_LIGHT", "SOFT_LIGHT", "DIFFERENCE",
    "EXCLUSION", "HSL_HUE", "HSL_SATURATION", "HSL_COLOR", "HSL_LUMINOSITY",
];
 
$shortNames = [
    "CLEAR", "SOURCE", "OVER", "IN", "OUT", "ATOP",
    "DEST", "DOVER", "DIN", "DOUT", "DATOP", "XOR",
    "ADD", "SATURATE", "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN",
    "LIGHTEN", "DODGE", "BURN", "HARDLIGHT", "SOFTLIGHT", "DIFFERENCE",
    "EXCLUSION", "HUE", "SATURATION", "COLOR", "LUMINOSITY",
];
 
$operators = [
    Gd\CompositeOperator::Clear,
    Gd\CompositeOperator::Source,
    Gd\CompositeOperator::Over,
    Gd\CompositeOperator::In,
    Gd\CompositeOperator::Out,
    Gd\CompositeOperator::Atop,
    Gd\CompositeOperator::Dest,
    Gd\CompositeOperator::DestOver,
    Gd\CompositeOperator::DestIn,
    Gd\CompositeOperator::DestOut,
    Gd\CompositeOperator::DestAtop,
    Gd\CompositeOperator::Xor,
    Gd\CompositeOperator::Add,
    Gd\CompositeOperator::Saturate,
    Gd\CompositeOperator::Multiply,
    Gd\CompositeOperator::Screen,
    Gd\CompositeOperator::Overlay,
    Gd\CompositeOperator::Darken,
    Gd\CompositeOperator::Lighten,
    Gd\CompositeOperator::ColorDodge,
    Gd\CompositeOperator::ColorBurn,
    Gd\CompositeOperator::HardLight,
    Gd\CompositeOperator::SoftLight,
    Gd\CompositeOperator::Difference,
    Gd\CompositeOperator::Exclusion,
    Gd\CompositeOperator::HslHue,
    Gd\CompositeOperator::HslSaturation,
    Gd\CompositeOperator::HslColor,
    Gd\CompositeOperator::HslLuminosity,
];
 
$font = [
    [14, 17, 17, 31, 17, 17, 17], [30, 17, 17, 30, 17, 17, 30], [14, 17, 16, 16, 16, 17, 14],
    [30, 17, 17, 17, 17, 17, 30], [31, 16, 16, 30, 16, 16, 31], [31, 16, 16, 30, 16, 16, 16],
    [14, 17, 16, 23, 17, 17, 15], [17, 17, 17, 31, 17, 17, 17], [14, 4, 4, 4, 4, 4, 14],
    [7, 2, 2, 2, 18, 18, 12],     [17, 18, 20, 24, 20, 18, 17], [16, 16, 16, 16, 16, 16, 31],
    [17, 27, 21, 21, 17, 17, 17], [17, 25, 21, 19, 17, 17, 17], [14, 17, 17, 17, 17, 17, 14],
    [30, 17, 17, 30, 16, 16, 16], [14, 17, 17, 17, 21, 18, 13], [30, 17, 17, 30, 20, 18, 17],
    [15, 16, 16, 14, 1, 1, 30],   [31, 4, 4, 4, 4, 4, 4],       [17, 17, 17, 17, 17, 17, 14],
    [17, 17, 17, 17, 17, 10, 4],  [17, 17, 17, 21, 21, 21, 10], [17, 17, 10, 4, 10, 17, 17],
    [17, 17, 10, 4, 4, 4, 4],     [31, 1, 2, 4, 8, 16, 31],
];
 
function create_image(int $width, int $height): GdImage {
    $image = imagecreatetruecolor($width, $height);
    imagealphablending($image, false);
    imagesavealpha($image, true);
    imagefill($image, 0, 0, 0x7fffffff);
    imagealphablending($image, true);
    return $image;
}
 
function label(Gd\Context $ctx, array $font, string $text, float $x, float $y, float $scale): void {
    for ($i = 0, $length = strlen($text); $i < $length; $i++, $x += 6 * $scale) {
        $ord = ord($text[$i]);
        if ($ord < 65 || $ord > 90) {
            continue;
        }
        $glyph = $font[$ord - 65];
        for ($row = 0; $row < 7; $row++) {
            for ($col = 0; $col < 5; $col++) {
                if ($glyph[$row] & (16 >> $col)) {
                    $ctx->rect($x + $col * $scale, $y + $row * $scale, $scale, $scale);
                    $ctx->fill();
                }
            }
        }
    }
}
 
function render_tile(Gd\CompositeOperator $operator, string $shortName, array $font): GdImage {
    $image = create_image(TILE, TILE);
    $ctx = $image->getContext();
 
    $linear = (new Gd\LinearGradient(0, 0, TILE, TILE))
        ->addColorStopRgb(0, 0.05, 0.72, 0.86)
        ->addColorStopRgb(0.5, 0.17, 0.16, 0.48)
        ->addColorStopRgb(1, 0.96, 0.32, 0.18);
    $ctx
        ->setSource($linear)
        ->rect(12, 30, TILE * 0.68, TILE * 0.68)
        ->fill();
 
    $radial = (new Gd\RadialGradient(TILE * 0.66, TILE * 0.40, 2, TILE * 0.57, TILE * 0.52, TILE * 0.48))
        ->addColorStopRgba(0, 1, 0.94, 0.20, 0.96)
        ->addColorStopRgba(0.55, 0.96, 0.12, 0.63, 0.82)
        ->addColorStopRgba(1, 0.20, 0.02, 0.55, 0.12);
    $ctx
        ->setOperator($operator)
        ->setSource($radial)
        ->arc(TILE * 0.58, TILE * 0.52, TILE * 0.43, 0, 2 * GD_2D_PI)
        ->fill()
        ->setOperator(Gd\CompositeOperator::Over)
        ->setSourceRgba(0.05, 0.05, 0.08, 1);
    label($ctx, $font, $shortName, 6, 5, 1.8);
 
    $ctx->flushImage();
    $ctx->destroy(false);
    return $image;
}
 
$rows = intdiv(count($operators) + COLS - 1, COLS);
$width = COLS * TILE + (COLS + 1) * GAP;
$height = $rows * TILE + ($rows + 1) * GAP;
$gallery = create_image($width, $height);
imagealphablending($gallery, false);
 
foreach ($operators as $op => $operator) {
    $tile = render_tile($operator, $shortNames[$op], $font);
    $x = GAP + ($op % COLS) * (TILE + GAP);
    $y = GAP + intdiv($op, COLS) * (TILE + GAP);
    imagecopy($gallery, $tile, $x, $y, 0, 0, TILE, TILE);
}

Some drawings

function text_curve_create_tile(): GdImage {
    $tile = imagecreatetruecolor(48, 48);
    imagefilledrectangle($tile, 0, 0, 47, 47, 0x2084ff);
    imagefilledrectangle($tile, 0, 0, 47, 15, 0xff6aa6);
    imagefilledrectangle($tile, 0, 32, 47, 47, 0x48e0e6);
    imagefilledrectangle($tile, 0, 0, 11, 47, 0xffce48);
    imagefilledrectangle($tile, 28, 0, 35, 47, 0xfff5b9);
    return $tile;
}
 
function text_curve_create_canvas(int $width, int $height): GdImage {
    $image = imagecreatetruecolor($width, $height);
    imagealphablending($image, false);
    imagesavealpha($image, true);
    imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, 0x7f000000);
    imagealphablending($image, true);
    return $image;
}
 
function text_curve_draw_sine_text(
    Gd\Context $ctx,
    string $text,
    float $left,
    float $baseline,
    float $amplitude,
    float $cycles,
    Gd\Paint $fill,
): void {
    $total = $ctx->textExtents($text);
    $pen = 0.0;
 
    if ($total->xAdvance <= 0.0) {
        return;
    }
 
    $len = strlen($text);
    for ($i = 0; $i < $len; $i++) {
        $ch = $text[$i];
        $ext = $ctx->textExtents($ch);
        $center = $pen + $ext->xAdvance * 0.5;
        $phase = ($center / $total->xAdvance) * (2.0 * GD_TEXT_PI * $cycles);
        $x = $left + $center;
        $y = $baseline + $amplitude * sin($phase);
        $slope = $amplitude * (2.0 * GD_TEXT_PI * $cycles / $total->xAdvance) * cos($phase);
        $angle = atan($slope);
 
        if ($ch !== ' ') {
            $ctx
                ->save()
                ->translate($x, $y)
                ->rotate($angle)
                ->textPath($ch, -$ext->xAdvance * 0.5, 0.0)
                ->setSourceRgba(0.03, 0.05, 0.09, 0.98)
                ->setLineJoin(Gd\LineJoin::Round)
                ->setLineWidth(7.0)
                ->strokePreserve()
                ->setSource($fill)
                ->fillPreserve()
                ->setSourceRgba(0.03, 0.05, 0.09, 0.98)
                ->setLineJoin(Gd\LineJoin::Round)
                ->setLineWidth(2.2)
                ->stroke()
                ->restore();
        }
 
        $pen += $ext->xAdvance;
    }
}
 
$width = 1200;
$height = 320;
$text = "Text follows a curve with gd 2D paths";
 
$image = text_curve_create_canvas($width, $height);
$tile = text_curve_create_tile();
$ctx = $image->getContext();
$font = Gd\FontFace::fromFile(__DIR__ . "/DejaVuSans.ttf");
 
$ctx
    ->setFontFace($font)
    ->setFontSize(54);
 
$extents = $ctx->textExtents($text);
$left = ($width - $extents->xAdvance) * 0.5;
$pattern = $ctx
    ->createPattern($tile)
    ->setExtend(Gd\ExtendMode::Repeat);
 
text_curve_draw_sine_text($ctx, $text, $left, 178.0, 52.0, 1.35, $pattern);
$ctx->flushImage();

content aware resize

<?php
use Gd\Jpeg\Codec;
use Gd\ScaleFit;
use Gd\ScaleStrategy;
use Gd\ScaleOptions;
use Gd\Codec\Format;
Codec::fromFile('haves_inn.jpg')
   ->scale(120, 180, new ScaleOptions(fit: ScaleFit::Cover, strategy: ScaleStrategy::Entropy,))
   ->toFile('./scaled.jpeg'); // to a file
 
Codec::fromFile('haves_inn.jpg')
   ->scale(120, 180, new ScaleOptions(fit: ScaleFit::Cover, strategy: ScaleStrategy::Entropy,))
   ->toStream(STDOUT, Format::Jpeg); // Explicite to stdout
 
// Scale on load through libjpeg/libjpeg-turbo then rotate and save
Codec::fromFile(__DIR__ . '/haves_inn.jpg', new ReadOptions(scaleNumerator: 3, scaleDenominator: 8))->toFile('./scaled_on_load_rotate.jpeg');

Backward Incompatible Changes

  • Color matching fix. [describe the specific color-match bug and corrected behavior. Previous has a hard to use, and incorrect, square-like %. Fixed version uses L*a*b* + Delta-E uniform %.
  • FreeType text bounding box / baseline correctness. The previous FT2 integration computed bounding boxes incorrectly for angled and baseline-adjusted text. This is now corrected. Impact is minor for axis-aligned text (small bounding box shift) and can be significant for rotated text, where the old bounding box was substantially wrong. This is a deliberate correctness fix rather than a new feature, so it is called out here rather than gated behind an option.

Gd\Image

<?php
 
/**
 * @generate-class-entries
 * @generate-c-enums
 */
 
namespace Gd {
    enum ColorModel
    {
        case TrueColor;
        case Palette;
        // future internal color models won't require new functions or additional arguments
        // a separate method will be created to allow to create a buffer with custom internal model/stride/mask 
        // so the current ::create signature is stable
    }
 
    final class Image
    {
        private function __construct() {}
 
        /** @refcount 1 */
        public static function create(
            int $width,
            int $height,
            ColorModel $colorModel = ColorModel::TrueColor,
        ): \GdImage {}
 
 
        final class Image
        {
                private function __construct() {}
 
                /** @refcount 1 */
                public static function create(
                int $width,
                int $height,
                ColorModel $colorModel = ColorModel::TrueColor,
        ): \GdImage {}
 
        /** @refcount 1 */
        public static function loadFromFile(string $path): \GdImage {}
 
        /**
         * @param resource $stream
         * @refcount 1
         */
        public static function loadFromStream($stream): \GdImage {}
 
        /** @refcount 1 */
        public static function loadFromString(string $bytes): \GdImage {}
    }
    }
}

Codec

This document explains the common codec API introduced with the libgd 2.4 integration. It is written primarily for PHP users. The small internals section explains how the PHP API maps onto libgd without requiring applications to use the C API.

The goal is a common vocabulary for image formats while preserving the things that are genuinely format-specific. A PNG does not have the same settings as a JPEG, and an animated WebP does not have the same state as a still BMP. The common API standardizes the operation shape; each codec still owns its useful options and information fields.

The common codec shape

Every codec is exposed below its own namespace, for example Gd\Png, Gd\Jpeg, Gd\Webp, and Gd\Bmp. The central class is a static Codec facade:

final class Codec
{
    public static function fromFile(
        string $path,
        ReadOptions $options = new ReadOptions(),
    ): \GdImage {}
 
    /** @param resource $stream */
    public static function fromStream(
        $stream,
        ReadOptions $options = new ReadOptions(),
    ): \GdImage {}
 
    public static function fromString(
        string $bytes,
        ReadOptions $options = new ReadOptions(),
    ): \GdImage {}
 
    public static function toFile(
        \GdImage $image,
        string $path,
        WriteOptions $options = new WriteOptions(),
    ): void {}
 
    /** @param resource $stream */
    public static function toStream(
        \GdImage $image,
        $stream,
        WriteOptions $options = new WriteOptions(),
    ): void {}
 
    public static function toString(
        \GdImage $image,
        WriteOptions $options = new WriteOptions(),
    ): string {}
}

Every normalized non-animated codec exposes a format-specific ReadOptions class, and every Codec::from*() and regular Reader::from*() method accepts it. For formats that currently have no read settings, the class is intentionally empty and has no effect today. It is a reserved extension point for future decoder settings, such as selecting an output image format or controlling color-profile handling, without changing the public method signatures later. BMP, QOI, and the other no-option codecs therefore use the same no-op form as codecs with meaningful read settings.

Animation readers and animation-specific options are separate APIs. The uniform ReadOptions contract applies to normal image decoding and does not merge animation options into the common codec options model.

The three input forms have the same decoding behavior:

  • fromFile() opens and reads a path
  • fromStream() reads from an existing PHP stream
  • fromString() reads encoded bytes already held in PHP memory

The three output forms likewise have the same encoding behavior:

  • toFile() writes to a path
  • toStream() writes to an existing PHP stream resource
  • toString() returns the encoded byte

The codec facade is the convenient one-shot API. It returns a normal GdImage and is the right choice when an application only needs to decode or encode an image.

The shared support types are small:

namespace Gd\Codec;
 
interface WriteOptions {}
 
class CodecException extends \RuntimeException {}

Each format defines its own ReadOptions, WriteOptions, and optional Info classes in its namespace. Info and options objects are readonly value-style objects in the PHP API. Readers are stateful handles and are not serializable; they are intended to be used during the current request or operation.

ReadOptions and WriteOptions

ReadOptions controls decoder behavior when a format has meaningful read choices. Examples include JPEG scaling or DCT method and AVIF/HEIF transform handling. It is format-specific:

$options = new Gd\Jpeg\ReadOptions(scale: 2);
$image = Gd\Jpeg\Codec::fromFile($path, $options);

An empty ReadOptions does not mean that PHP is required to invent settings for the format. It provides a stable place for future settings and, where chosen by the codec API, keeps the fromFile/fromStream/fromString signatures uniform.

WriteOptions contains encoder settings for one still image. Every format implements the shared marker interface Gd\Codec\WriteOptions, but the properties differ by format:

$options = new Gd\Png\WriteOptions(
    compressionLevel: 6,
    progressive: false,
);
Gd\Png\Codec::toFile($image, $path, $options);

Options are applied while the new encoded image is created. They are not a second post-processing step over the encoded byte string.

Still-image options and animation or multipage options are separate. For example, an animated WebP writer has animation/frame configuration that does not belong on Gd\Webp\Codec::toString() for a single image.

Reader and Info

Some formats expose a Reader in addition to the one-shot Codec facade:

final class Reader
{
    public static function fromFile(
        string $path,
        ReadOptions $options = new ReadOptions(),
    ): self {}
 
    /** @param resource $stream */
    public static function fromStream(
        $stream,
        ReadOptions $options = new ReadOptions(),
    ): self {}
 
    public static function fromString(
        string $bytes,
        ReadOptions $options = new ReadOptions(),
    ): self {}
 
    public function info(): Info {}
    public function read(): \GdImage {}
}

Reader is useful when the application needs to inspect the encoded file before asking for pixels. Info describes facts about the encoded file or container, such as dimensions, bit depth, frame count, compression tags, orientation, color model, or format-specific header fields. The exact fields are intentionally format-specific because those facts are not interchangeable between formats.

Typical usage is:

$reader = Gd\Bmp\Reader::fromFile($path);
$info = $reader->info();
 
printf("BMP: %d x %d, %d bits/pixel\n",
    $info->width,
    $info->height,
    $info->bitsPerPixel,
);
 
$image = $reader->read();

Calling ::info() does not imply that decoded pixels have been allocated. A format may be inspected using its headers or container structure alone. For a buffered still-image reader, the encoded input is retained by the Reader and read() decodes it later. read() is a one-shot operation for still Readers; a second call throws Gd\Codec\CodecException. A failed Reader is also kept in a failed state and continues to report a codec exception.

For animated or multipage formats, the stateful API is different:

$reader = Gd\Webp\AnimReader::fromFile($path);
$fileInfo = $reader->info();
 
while (($frame = $reader->next()) !== null) {
    // $frame contains the decoded image and frame timing/state.
}

AnimReader and AnimWriter preserve frame/page behavior that cannot be represented by a single GdImage. TIFF similarly uses a Reader for pages.

Metadata

Metadata is separate from scalar Info. When a format has a supported metadata representation, the codec may expose a Gd\Metadata object through its Reader Info and accept metadata through WriteOptions:

$reader = Gd\Jpeg\Reader::fromFile($path);
$metadata = $reader->info()->metadata;
 
$options = new Gd\Jpeg\WriteOptions(metadata: $metadata);
Gd\Jpeg\Codec::toFile($image, $newPath, $options);

Gd\Metadata is an opaque collection of supported payloads identified by keys such as exif, xmp, or iptc. Applications can inspect, add, or remove values through the metadata object; they do not patch JPEG, PNG, or other container bytes themselves.

Formats without a metadata container do not gain an invented one. They expose no metadata property on Info and no metadata reader or injection function. For API consistency, a codec may retain a shared WriteOptions::$metadata property and explicitly ignore it. BMP and QOI follow this rule: metadata is accepted as an option value but is never read, written, retained, or appended to the encoded output.

ICC data is not treated as a general metadata pass-through during this phase. GD does not yet provide color-profile management, so codecs must not claim that embedding or returning ICC data makes the decoded pixels color-managed.

Errors and ownership

Invalid paths, invalid streams, malformed or truncated input, unsupported format features, allocation failures, and encoder failures are reported as Gd\Codec\CodecException where the operation has reached the codec layer. PHP argument type and value errors remain normal PHP TypeError or ValueError exceptions.

The PHP caller owns returned GdImage and Gd\Metadata objects according to normal PHP lifetime rules. Input streams are borrowed and are not closed by the codec. A Reader owns any buffered encoded input needed for later info()/read() calls. Write options are read during the write operation; the encoder does not retain the options object or its metadata after the call.

Compatibility with procedural GD APIs

The object-oriented codec APIs are additive. Existing functions such as imagecreatefrompng() and imagepng() remain available, as do the corresponding public libgd procedural C functions. The normalized API routes new codec work through options-aware native entry points where available, but legacy wrappers remain supported and preserve their established defaults and return behavior.

Capability matrix

The matrix describes the public capability, not whether every capability is available in every build. A bundled note means the API depends on the bundled libgd implementation; external system GD builds may expose only the legacy or older extended API advertised by that installation.

Codec One-shot read/write Reader + Info Animation (read/write) Multi-page Read options Still write options Metadata container/API Corpus-tested
JPEG Yes / Yes Yes N/A N/A Yes Yes Yes Yes
PNG Yes / Yes Yes N/A N/A No Yes Yes Yes
GIF Yes / Yes Yes (bundled) Yes / Yes N/A No Yes Limited / no normalized rich metadata Yes
WebP Yes / Yes Yes (bundled) Yes / Yes N/A Yes Yes Yes Yes
BMP Yes / Yes Yes (bundled) N/A N/A Empty no-op ReadOptions Yes, including bit depth, compression, quantization, V4, RGB555 No; shared metadata option ignored Yes
TIFF Yes / Yes Yes N/A Yes / Yes No Yes Yes Yes
JXL Yes / Yes Yes Yes / Yes N/A No Yes Yes Yes
QOI Yes / Yes Yes N/A N/A Empty no-op ReadOptions Colorspace tag Kept during UHDR ops Yes
UHDR* Yes / Yes* No general Reader No No Yes Limited Yes Yes
HEIF Yes / Yes Yes No No Yes Yes Yes / implementation-dependent Yes
AVIF Yes / Yes Yes No No Yes Yes Yes Yes

* UHDR can read the standard image from a UHDR file but cannot create a new UHDR gain-map image from an ordinary GdImage. Its write support is limited to the operations explicitly supported by libuhdr. All metadata are not available like other codecs as many of them are directly impacting UHDR quality or results. GD does not support full UHDR yet, so only pure libultrauhdr are supported yet.

The matrix is a summary. The per-codec stubs are the authoritative PHP-facing interface, and build feature guards determine which optional classes are declared at runtime.

A short internal model

The PHP classes are bindings over a small native vocabulary:

  • gd<Codec>Info contains scalar file/container facts and does not own

metadata unless the codec explicitly defines that ownership;

  • gd<Codec>GetInfo* probes file, gdIOCtx, or memory inputs;
  • gd<Codec>WriteOptions contains all still-image encoder settings;
  • gdImage<Codec>*WithOptions performs the actual write for FILE, context,

or pointer destinations;

  • legacy gdImage<Codec>* and *Ex functions remain convenience

wrappers where the format already had them.

The PHP layer may buffer a stream or string so a Reader can inspect the input and decode it later. This is an implementation detail that gives PHP the deferred Info/read() behavior; applications only depend on the Reader methods and the documented Info fields.

For a full description of the native codec implementation boundaries, see https://github.com/libgd/libgd/blob/master/docs/codecs.md.

Metadata

Exif example

An example how to get an image metadata, process it and use it to save to a new file. Here using FFI and libexif but any user-land implementation would work too.

<?php
<?php
 
declare(strict_types=1);
 
/**
 * PHP FFI port of the libgd C metadata example: read a JPEG's EXIF profile
 * via Gd\Metadata, update Exif.Image.Artist through libexif, and write the
 * modified profile back via Gd\Metadata::with(), optionally to both JPEG
 * and PNG.
 *
 * Usage: php exif_read_write.php input.jpg output.jpg [output.png]
 *
 * IMPORTANT: the ExifData/ExifContent/ExifEntry struct layouts below are
 * simplified to the fields this example touches. FFI requires the layout
 * to match the ABI of the linked libexif exactly (field order, sizes, and
 * padding), so before relying on this for real tests, verify the struct
 * layout against the actual libexif.h / libexif-internal headers for the
 * installed version (e.g. via `pahole` or the system exif-data.h), and
 * adjust the .so name/soversion for the target platform.
 */
 
$ffi = FFI::cdef(<<<'CDEF'
    typedef struct _ExifData ExifData;
    typedef struct _ExifContent ExifContent;
    typedef struct _ExifEntry ExifEntry;
    typedef unsigned short ExifTag;
 
    typedef enum {
        EXIF_FORMAT_BYTE      = 1,
        EXIF_FORMAT_ASCII     = 2,
        EXIF_FORMAT_SHORT     = 3,
        EXIF_FORMAT_LONG      = 4,
        EXIF_FORMAT_RATIONAL  = 5,
        EXIF_FORMAT_SBYTE     = 6,
        EXIF_FORMAT_UNDEFINED = 7,
        EXIF_FORMAT_SSHORT    = 8,
        EXIF_FORMAT_SLONG     = 9,
        EXIF_FORMAT_SRATIONAL = 10,
        EXIF_FORMAT_FLOAT     = 11,
        EXIF_FORMAT_DOUBLE    = 12
    } ExifFormat;
 
    typedef enum {
        EXIF_IFD_0 = 0,
        EXIF_IFD_1,
        EXIF_IFD_EXIF,
        EXIF_IFD_GPS,
        EXIF_IFD_INTEROPERABILITY,
        EXIF_IFD_COUNT
    } ExifIfd;
 
    struct _ExifEntry {
        ExifTag tag;
        ExifFormat format;
        unsigned long components;
        unsigned char *data;
        unsigned int size;
        ExifContent *parent;
        void *priv;
    };
 
    struct _ExifContent {
        ExifEntry **entries;
        unsigned int count;
        ExifData *parent;
        void *priv;
    };
 
    struct _ExifData {
        ExifContent *ifd[EXIF_IFD_COUNT];
        void *data;
        unsigned int size;
        void *priv;
    };
 
    ExifData *exif_data_new_from_data(const unsigned char *data, unsigned int size);
    void      exif_data_unref(ExifData *data);
    void      exif_data_save_data(ExifData *data, unsigned char **data_out, unsigned int *size_out);
 
    ExifEntry  *exif_content_get_entry(ExifContent *content, ExifTag tag);
    const char *exif_entry_get_value(ExifEntry *entry, char *val, unsigned int maxlen);
    const char *exif_tag_get_name(ExifTag tag);
 
    void free(void *ptr);
CDEF, 'libexif.so.12');
 
// Only the one tag this example needs; libexif's real ExifTag enum has
// several hundred values, not worth declaring in full for this port.
const EXIF_TAG_ARTIST = 0x013B;
 
function isNullPointer(mixed $pointer): bool
{
	// PHP FFI may expose a NULL C pointer as PHP null rather than as a
	// nullable FFI\CData value. FFI::isNull() only accepts the latter.
	return $pointer === null || FFI::isNull($pointer);
}
 
function printEntry(FFI $ffi, FFI\CData $exifData, int $ifd, int $tag): void
{
	$name = $ffi->exif_tag_get_name($tag);
	$name = is_string($name) ? $name : FFI::string($name);
	$entry = $ffi->exif_content_get_entry($exifData->ifd[$ifd], $tag);
	if (isNullPointer($entry)) {
		printf("%s: <not present>\n", $name);
		return;
	}
	$value = $ffi->new('char[256]');
	$ffi->exif_entry_get_value($entry, $value, 256);
	printf("%s: %s\n", $name, FFI::string($value));
}
 
function setArtist(FFI $ffi, FFI\CData $exifData, int $ifd, int $tag, string $artist): bool
{
    $entry = $ffi->exif_content_get_entry($exifData->ifd[$ifd], $tag);
    $size = strlen($artist) + 1;
 
    // Mirrors the C example: only updates in place if the existing ASCII
    // entry has room, keeping the example focused on metadata handling
    // rather than libexif entry (re)allocation.
	if (isNullPointer($entry) || $entry->format !== $ffi->EXIF_FORMAT_ASCII || $entry->size < $size) {
        return false;
    }
 
    FFI::memset($entry->data, 0, $entry->size);
    FFI::memcpy($entry->data, $artist, $size - 1);
    $entry->components = $size;
    $entry->size = $size;
 
    return true;
}
 
if ($argc !== 3 && $argc !== 4) {
    fwrite(STDERR, "usage: php {$argv[0]} input.jpg output.jpg [output.png]\n");
    exit(1);
}
 
[, $input, $output] = $argv;
$pngOutput = $argv[3] ?? null;
 
$reader = Gd\Jpeg\Reader::fromFile($input);
$image = $reader->read();
$metadata = $reader->info()->metadata;
 
if (!$metadata->has('exif')) {
    fwrite(STDERR, "input JPEG has no usable EXIF profile\n");
    exit(1);
}
 
$exifBytes = $metadata->get('exif');
// libexif expects the Exif APP1 identifier, while gd's normalized metadata
// API deliberately exposes the TIFF payload without that container framing.
$libexifBytes = "Exif\0\0" . $exifBytes;
$exifBuffer = $ffi->new("unsigned char[" . strlen($libexifBytes) . "]");
FFI::memcpy($exifBuffer, $libexifBytes, strlen($libexifBytes));
$exifData = $ffi->exif_data_new_from_data($ffi->cast('unsigned char *', $exifBuffer), strlen($libexifBytes));
 
if (isNullPointer($exifData)) {
    fwrite(STDERR, "libexif could not parse the EXIF profile\n");
    exit(1);
}
 
try {
    printEntry($ffi, $exifData, 0 /* EXIF_IFD_0 */, EXIF_TAG_ARTIST);
 
    if (!setArtist($ffi, $exifData, 0, EXIF_TAG_ARTIST, 'libgd')) {
        fwrite(STDERR, "could not update Exif.Image.Artist\n");
        exit(1);
    }
 
    $dataOut = $ffi->new('unsigned char*');
    $sizeOut = $ffi->new('unsigned int');
    $ffi->exif_data_save_data($exifData, FFI::addr($dataOut), FFI::addr($sizeOut));
 
	if (isNullPointer($dataOut) || $sizeOut->cdata === 0) {
        fwrite(STDERR, "could not serialize updated EXIF\n");
        exit(1);
    }
 
    $serializedExif = FFI::string($dataOut, $sizeOut->cdata);
    $ffi->free($dataOut);
} finally {
    $ffi->exif_data_unref($exifData);
}
 
// Convert libexif's APP1-formatted result back to gd's canonical TIFF-only
// metadata representation before handing it to the JPEG and PNG codecs.
if (strncmp($serializedExif, "Exif\0\0", 6) === 0) {
    $serializedExif = substr($serializedExif, 6);
}
 
$metadata = $metadata->with('exif', $serializedExif);
 
Gd\Jpeg\Codec::toFile($image, $output, new Gd\Jpeg\WriteOptions(metadata: $metadata));
 
if ($pngOutput !== null) {
    Gd\Png\Codec::toFile($image, $pngOutput, new Gd\Png\WriteOptions(metadata: $metadata));
}

Tiff Tags example

Tiff Tags using FFI for parsing/alter them

<?php
 
declare(strict_types=1);
 
/**
 * Read a TIFF and its metadata through GD, write a new TIFF with that
 * metadata, then inspect the new file with libtiff FFI.
 *
 * Usage:
 *   php tiff_tag_ffi.php input.tif output.tif [tag] [replacement]
 *
 * The example intentionally uses GeoTIFF's variable-length ASCII tag 34737
 * by default for the optional replacement. Other TIFF tags require a
 * different TIFFGetField argument layout, so the FFI verification below uses
 * the count/data call only for variable-length tags whose layout is known
 * from Gd\Metadata's envelope.
 */
 
$ffi = FFI::cdef(<<<'CDEF'
    typedef struct tiff TIFF;
    typedef unsigned int uint32_t;
 
    TIFF *TIFFOpen(const char *name, const char *mode);
    void TIFFClose(TIFF *tif);
    int TIFFGetField(TIFF *tif, uint32_t tag, void *count, void *data);
    int TIFFSetField(TIFF *tif, uint32_t tag, ...);
    int TIFFWriteDirectory(TIFF *tif);
CDEF, getenv('LIBTIFF_SO') ?: 'libtiff.so.6');
 
const TIFF_ASCII = 2;
 
function readVariableTag(FFI $ffi, string $path, int $tag, int $elementSize): ?array
{
    $tif = $ffi->TIFFOpen($path, 'r');
    if (FFI::isNull($tif)) {
        throw new RuntimeException("libtiff could not open {$path}");
    }
 
    try {
        $count = $ffi->new('uint32_t');
        $value = $ffi->new('void *');
        if (!$ffi->TIFFGetField($tif, $tag, FFI::addr($count), FFI::addr($value)) || FFI::isNull($value)) {
            return null;
        }
 
        return [
            'count' => (int) $count->cdata,
            // Copy the value before TIFFClose(); libtiff owns this memory.
            'bytes' => FFI::string($ffi->cast('char *', $value), $count->cdata * $elementSize),
        ];
    } finally {
        $ffi->TIFFClose($tif);
    }
}
 
function parseTiffEnvelope(string $profile): array
{
    if (strlen($profile) < 16 || substr($profile, 0, 4) !== 'GDTF' || ord($profile[4]) !== 1) {
        throw new UnexpectedValueException('profile does not contain a supported TIFF envelope');
    }
 
    $header = unpack('vtype/Vcount/Vsize', substr($profile, 6, 10));
    if ($header === false || $header['size'] !== strlen($profile) - 16) {
        throw new UnexpectedValueException('invalid TIFF envelope payload size');
    }
 
    return [
        'type' => (int) $header['type'],
        'count' => (int) $header['count'],
        'payload' => substr($profile, 16),
    ];
}
 
function makeTiffEnvelope(int $type, int $count, string $payload): string
{
    return pack('a4CCvVV', 'GDTF', 1, 1, $type, $count, strlen($payload)) . $payload;
}
 
function setAsciiTag(FFI $ffi, string $path, int $tag, string $value): void
{
    $tif = $ffi->TIFFOpen($path, 'r+');
    if (FFI::isNull($tif)) {
        throw new RuntimeException("libtiff could not reopen {$path} for update");
    }
 
    try {
        $buffer = $ffi->new('char[' . (strlen($value) + 1) . ']');
        FFI::memcpy($buffer, $value, strlen($value));
        $buffer[strlen($value)] = 0;
        if (!$ffi->TIFFSetField($tif, $tag, strlen($value) + 1, $ffi->cast('char *', $buffer)) || !$ffi->TIFFWriteDirectory($tif)) {
            throw new RuntimeException("libtiff could not update TIFF tag {$tag}");
        }
    } finally {
        $ffi->TIFFClose($tif);
    }
}
 
if ($argc < 3 || $argc > 5) {
    fwrite(STDERR, "usage: php {$argv[0]} input.tif output.tif [tag] [replacement]\n");
    exit(1);
}
 
[, $input, $output] = $argv;
$tag = isset($argv[3]) ? (int) $argv[3] : 34737;
$replacement = $argv[4] ?? null;
 
$reader = Gd\Tiff\Reader::fromFile($input);
$page = $reader->next();
if ($page === null) {
    throw new RuntimeException('input TIFF has no readable first page');
}
$image = $page->image;
$metadata = $reader->info()->metadata;
$profileKey = "tiff:tag:{$tag}";
 
echo "Metadata read through GD:\n";
foreach ($metadata->keys() as $key) {
    $envelope = parseTiffEnvelope($metadata->get($key));
    printf("  %s: TIFF type %d, %d element(s), %d payload byte(s)\n", $key, $envelope['type'], $envelope['count'], strlen($envelope['payload']));
}
 
if ($replacement !== null) {
    if (!$metadata->has($profileKey)) {
        throw new RuntimeException("input TIFF has no {$profileKey} profile");
    }
    $envelope = parseTiffEnvelope($metadata->get($profileKey));
    if ($envelope['type'] !== TIFF_ASCII) {
        throw new InvalidArgumentException('replacement is supported only for ASCII TIFF tags');
    }
}
 
Gd\Tiff\Codec::toFile($image, $output, new Gd\Tiff\WriteOptions(metadata: $metadata));
printf("wrote %s\n", $output);
 
if ($replacement !== null) {
    setAsciiTag($ffi, $output, $tag, $replacement);
    printf("libtiff updated and stored %s\n", $profileKey);
}
 
// Read the newly written file through GD again to prove the metadata round
// trip before asking libtiff to inspect the resulting directory.
$outputMetadata = Gd\Tiff\Reader::fromFile($output)->info()->metadata;
echo "Metadata read back through GD:\n";
foreach ($outputMetadata->keys() as $key) {
    printf("  %s: %s\n", $key, $metadata->get($key) === $outputMetadata->get($key) ? 'preserved' : 'changed');
}
 
// Finally, ask libtiff to parse selected variable-length tags from the new
// output. This validates the GD writer's encoding, rather than duplicating
// GD's input path.
foreach ($outputMetadata->keys() as $key) {
    $outputEnvelope = parseTiffEnvelope($outputMetadata->get($key));
    $outputTag = (int) substr($key, 9);
    if (!in_array($outputTag, [34735, 34736, 34737], true)) {
        continue;
    }
    $elementSize = $outputEnvelope['type'] === 3 ? 2 : ($outputEnvelope['type'] === 12 ? 8 : 1);
    $raw = readVariableTag($ffi, $output, $outputTag, $elementSize);
    if ($raw === null) {
        printf("%s: libtiff did not expose the written tag\n", $key);
        continue;
    }
    printf("%s: libtiff returned %d element(s) from output\n", $key, $raw['count']);
    if ($outputEnvelope['type'] === TIFF_ASCII) {
        printf("  Value: %s\n", str_replace("\0", '\\0', $raw['bytes']));
    } else {
        printf("  Value bytes: %s\n", bin2hex($raw['bytes']));
    }
}
./sapi/cli/php ../tiff_tag_ffi.php ../geo_tiff_sample.tif out.tif 34737 "new value by php"
# ...
 
tiffinfo out.tif
TIFFReadDirectory: Warning, Unknown field with tag 33550 (0x830e) encountered.
TIFFReadDirectory: Warning, Unknown field with tag 33922 (0x8482) encountered.
TIFFReadDirectory: Warning, Unknown field with tag 34735 (0x87af) encountered.
TIFFReadDirectory: Warning, Unknown field with tag 34737 (0x87b1) encountered.
TIFFFetchNormalTag: Warning, ASCII value for tag "Tag 34737" does not end in null byte. Forcing it to be null.
=== TIFF directory 0 ===
TIFF Directory at offset 0x31c7a (203898)
  Image Width: 1001 Image Length: 1001
  Resolution: 96, 96 pixels/inch
  Bits/Sample: 8
  Compression Scheme: Deflate
  Photometric Interpretation: RGB color
  Extra Samples: 1<unassoc-alpha>
  Samples/Pixel: 4
  Rows/Strip: 2
  Planar Configuration: single image plane
  Tag 33550: 10.000000,10.000000,0.000000
  Tag 33922: 0.000000,0.000000,0.000000,590520.000000,5790630.000000,0.000000
  Tag 34735: 1,1,0,7,1024,0,1,1,1025,0,1,1,1026,34737,22,0,2049,34737,7,22,2054,0,1,9102,3072,0,1,32631,3076,0,1,9001
  Tag 34737: new value by php
  Predictor: horizontal differencing 2 (0x2)

Exceptions

namespace Gd {
    /** @strict-properties */
    class GdException extends \Exception
    {
    }
}
 
namespace Gd\Codec {
    /** @strict-properties */
    class CodecException extends \Gd\GdException
    {
    }
}
 
namespace Gd\Text {
    /** @strict-properties */
    class TextException extends \Gd\GdException
    {
    }
 
    /** @strict-properties */
    class InvalidTextException extends TextException
    {
    }
 
    /** @strict-properties */
    class UnavailableException extends TextException
    {
    }
 
    /** @strict-properties */
    class FontException extends TextException
    {
    }
 
    /** @strict-properties */
    class LayoutException extends TextException
    {
    }
 
    /** @strict-properties */
    class MemoryException extends TextException
    {
    }
}

Proposed PHP Version(s)

PHP 8.6.0

RFC Impact

To the Ecosystem

  • New APIs exposed, no new language syntax
  • Exact pixels compatibility for destructive operations (ops other than simple flips) is not kept
  • Also introduce the previous point as policy, algorithms keep improving, new ones may come. Pixels exact BC cannot be kept but correctness
  • Opens the door for php's user-land meta parser
  • Freetype non axis aligned rendering and bounding box being fixed, usage will need to adapt. There is no BC layer or ways to “keep the bug”, and will not have

To Existing Extensions

ext/gd

Open Questions

I am leading towards disabling the new APIs or options for libgd < 2.4. Some could be kept but the results of some, like interpolations, will reproduce the issues present in previous versions. I am open to revisit this and see what would be the ideal strategy here, avoiding having the same, some existing, bug reports but through a different flow. Doing 2.4+ only could also be an incentive to upgrade earlier.

One addition could help is some static method to check if a given feature or codec is supported. I am not sure about the interface, but something along

<?php 
namespace Gd {
    final readonly class Support {
        public function has($someEnum) {
        }
    }
}

Enums as it avoids typos or similar errors. That could definitively helps. I had some requests as well to be able to access the versions of each underlying libraries, build time and runtime. Both being needed as ABI could be compatible but not the actual features available, or new bugs introduced etc.

I was not sure whether to create a base exception for all new GD exception. If one does not care about specific ones or as a convenient catch. Now they extend RuntimeException.

Not directly related to this RFC additions to PHP but for the Windows Build. Pointed out by Shivam (PHPF) while we were adding or updating the new dependencies, x265 plugin for HEIF codec is GPL v2. We do have other similar indirect gpl usage, but I can't remember one where we do provide the builds ourselves. I asked for a clarification or exception (open question), follow-up here.

Open Issues

GdImage vs Gd\Image

When resources moved to class/object, gd's image ended as GdImage. While I would rather follow the php's standard namespace somehow and have Gd\Image. It can't be changed at this point as it is key to keep any GdImage usages (procedural APIs) BC. I am not sure if aliases can work or other extends GdImage yet. It is not a priority but would be consistent and nice to have.

External dependencies minimal versions

Some dependencies have been kept to oldest version available in currently active LTS for common distributions. By the time php 8.6 is released, many of them will have reached end of support, or will only have a short life span left. I tend to think to bump it to latest LTS. f.e. libwebp, from >=1.3.0 to >=1.4.0, all latest LTS provide 1.4.x and some 1.5.x.

External LibGD 2.3.x as requirements for now. May increase to 2.4.x in php 8.7+, that can be done in a separate RFC.

Future Scope

latest libgd prepares the road to libgd 2.4+ and 3.0 which adds more internal buffer formats and ability to handle HDR natively. Fromats like actual 32bits ARGB, floating points buffers, etc. These formats will allow significantly higher quality for all operations. GD 2.4 does some operations using them, converting back to 31bits (per rows/sections). This step already improve overall quality for the 2D APIs and some transformations.

The new APIs allow smooth additions of new options, formats while keeping BC. Planned new internal formats like true ARGB 32bits pre multiplied/srgb or FP are in the work. These additional formats allow then to nicely export using the existing codecs with more accurately or targeting specific output. FP format also brings HDR to the game.

Color Profile supports likely comes with GD 2.5. Metadata is still open, like exif/iptc/etc. While some implementations work it is a tricky area, where to start/stop for the infamous amount of various standards.

A pipeline based processing, using the same fluent interface. The pipeline then optimizes the operations to minimize memory usage and maximize performance while keeping quality.

More control over Codec output format, including quantization, compression mode, or format specific options, using the writer options APIs.

Eventually, php 9 and gd 3, replace legacy drawing anti-aliased functions implementation with the 2D APIs. That will make them work out of the box as they should. It is not possible yet to do it, many projects rely on them now, with workaround.

Voting Choices

Pick a title that reflects the concrete choice people will vote on.

Please consult the php/policies repository for the current voting guidelines.


Primary Vote requiring a 2/3 majority to accept the RFC:

Implement $feature as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

Implementation

There are two separate PRs:

  1. The libgd only sync merged but needs updates as well. Part of the PRs.
    1. The winbuilds already include the new dependencyes
  2. The additions and changes to ext/gd available to date in this https://github.com/pierrejoye/php-src-syncgd

References

Rejected Features

Keep this updated with features that were discussed on the mail lists.

Changelog

  • 2026-07-19 15:00 Add Gd\GdException base exception. And all added exceptions extends Gd\GdException. thanks Tim to point out this Coding Standard break.
rfc/ext-gd-2.4.txt · Last modified: by pajoye