Skip to main content
Builder’s Journal

The PHP GD Traps That Silently Break Auto-Generated Images

· · 5 min read

If PHP’s GD extension is producing broken auto-generated images, three traps cause most of it: passing float colour components to imagecolorallocate() throws a TypeError in PHP 8; GD’s alpha channel runs 0–127, not 0–255, so an out-of-range value makes the shape silently fail to draw; and curly “smart” punctuation renders as empty tofu boxes unless you normalise it to ASCII first. Cast, clamp, and normalise.

The symptom

I auto-generate a 1200×630 featured/OG image on every publish. When it went wrong it went wrong in three different ways at once. The gradient background threw a fatal error and killed the whole render. Once I got past that, some translucent shapes simply weren’t there, as if the drawing calls had been skipped. And the text that did draw was wrong: an eyebrow label reading “Builder’s Journal” came out as “Builder□s Journal”, with a hollow box where the apostrophe should be.

None of these threw an obvious, self-explaining error. Two of them didn’t throw anything at all. That is what makes GD frustrating to debug: it fails quietly and leaves you staring at a plausible-looking but broken PNG.

The environment

This runs on my WordPress site using PHP’s GD extension with FreeType for text. The composition is drawn from scratch on publish: a vertical gradient background, some translucent circles, a scrim overlay to darken the lower third, an eyebrow label, the wrapped post title, and a small tech logo composited top-centre. Text is drawn with imagettftext() using a DejaVuSans-Bold font build.

What I first suspected

My first instinct on the fatal error was that something was wrong with the image resource itself, or that GD wasn’t installed with the features I needed. For the missing shapes I assumed a coordinate or maths bug placing them off-canvas. And for the boxes in the text, my first guess was a broken or missing font file. All three guesses were wrong. Each problem had a specific, unglamorous cause.

The verified root causes

Trap 1 — PHP 8 rejects float colour components

To build the vertical gradient I interpolate each row’s colour between two endpoints. The interpolation is a floating-point expression:

$r = $c1[0] + ($c2[0]-$c1[0])*$t;

That produces a float. Passing it straight into imagecolorallocate($im, $r, $g, $b) throws a TypeError under PHP 8, because the function expects integer arguments and PHP 8 enforces that strictly. Under PHP 7 the same code ran fine: it silently coerced the float to an int. So this is the classic upgrade trap, code that “worked” for years because the old runtime papered over it.

Trap 2 — the alpha channel is 0–127, not 0–255

GD’s alpha channel runs from 0 to 127, where 0 is fully opaque and 127 is fully transparent. This is not the 0–255 range you know from CSS rgba() or most other image APIs. I initially passed 128, reasoning about it like an 8-bit alpha value. GD returns false from imagecolorallocatealpha() for the out-of-range value, the colour never allocates, and the shape it was meant to fill just doesn’t draw. No warning, no error, no shape.

Trap 3 — curly punctuation becomes tofu

With this DejaVuSans-Bold build, “smart” curly punctuation has no glyph and renders as an empty box (tofu). The offenders are the curly apostrophe U+2019, the curly quotes U+201C and U+201D, the en and em dashes U+2013 and U+2014, and the middot U+00B7.

The reason these bytes reach my drawing code at all is WordPress. Its wptexturize filter rewrites straight quotes and hyphens into their typographic curly equivalents before I ever get the title. So a perfectly innocent apostrophe I typed becomes U+2019 in the string I hand to imagettftext(), and the font has nothing to draw for it.

The fix

Each trap has a small, targeted fix.

Cast the colour components to int before allocating:

$r = (int)( $c1[0] + ($c2[0]-$c1[0])*$t );
$g = (int)( $c1[1] + ($c2[1]-$c1[1])*$t );
$b = (int)( $c1[2] + ($c2[2]-$c1[2])*$t );
imageline( $im, 0, $y, $W, $y, imagecolorallocate( $im, $r, $g, $b ) );

Keep every alpha value at or below 127. The working values in my composition are:

  • imagecolorallocatealpha( $im, ..., 58 ) for the scrim overlay
  • ..., 22 for a badge
  • 90 + $k*7 for the stacked translucent circles

Normalise curly punctuation to ASCII before drawing, applied to both the title and the eyebrow. I match on the UTF-8 byte sequences of each curly character:

$text = strtr( $text, array(
    "\xe2\x80\x99" => "'",   // curly apostrophe U+2019
    "\xe2\x80\x98" => "'",   // left single quote  U+2018
    "\xe2\x80\x9c" => '"',   // left double quote  U+201C
    "\xe2\x80\x9d" => '"',   // right double quote U+201D
    "\xe2\x80\x94" => '-',   // em dash            U+2014
    "\xe2\x80\x93" => '-',   // en dash            U+2013
    "\xc2\xb7"     => '-',   // middot             U+00B7
) );

How I verified it

After all three fixes, a fresh publish produced a correct image: the gradient renders with the right colours instead of throwing, every translucent shape is present, and the title and eyebrow show real apostrophes instead of boxes. “Builder’s Journal” reads as “Builder’s Journal”.

When it may not apply

  • The 0–127 alpha range is specific to imagecolorallocatealpha() and the related alpha/blending functions such as imagesavealpha(). It is not a universal rule for every colour call.
  • Trap 3 is font-dependent. A font that actually contains the curly glyphs will render them fine and won’t tofu, so the normalisation is only needed for builds like this DejaVuSans-Bold one that lack them.
  • The PHP 8 integer strictness applies to GD colour functions generally, not just the gradient. Anywhere you compute a colour component with maths, cast it.

Reusable checklist

  1. Any colour component computed with arithmetic? Cast it to (int) before passing to a GD colour function.
  2. Using alpha? Clamp to 0–127, not 0–255. Remember 0 is opaque, 127 is transparent.
  3. A shape silently not drawing? Check whether its imagecolorallocatealpha() returned false from an out-of-range alpha.
  4. Drawing text that came through WordPress? Assume wptexturize has curled the quotes, and normalise to ASCII before imagettftext().
  5. Seeing tofu boxes? It’s the font missing the glyph, not a broken font file. Normalise the text or switch to a font that has the glyphs.

Takeaway

GD rarely tells you what’s wrong, so treat float colours, the 0–127 alpha range, and curly punctuation as the three things to rule out first.

AI assisted with drafting this article; the code and environment are from my real project.

Share this article