Checking for transparency in PNG files with PHP

Recently, I was testing PHP code that checked if a PNG image used transparency. The code was straightforward and made sense with my mental model of raster image data.

That mental model suggests to a brute-force algorithm that says loop over each pixel of the image and check if the color at that pixel uses the alpha channel for transparency. If any pixel does, then we can say that the image does use transparency.

Here is an approximation of the code, variations of which float around Stack Overflow:

function usesTransparency(string $file): bool {
$image = imagecreatefrompng($file);
$xs = imagesx($image);
$ys = imagesy($image);

for ($x = 0; $x < $xs; $x++) {
for ($y = 0; $y < $ys; $y++) {
$color = imagecolorat($image, $x, $y);
$transparency = ($color >> 24) & 0x7F;

if ($transparency !== 0) {
return true;
}
}
}

return false;
}

Incorrect

Unfortunately, I found PNG images that did use transparency but returned false from the function. So what gives?

Types of PNGs

As I discovered, the PNG specification defines more than one way to represent color. The specification lists five different color types. Each type specifies what each pixel in the file will represent.

To determine which color type a file uses, we can read a byte from the file at a defined offset.

In PHP, that looks like:

$colorTypeByte = file_get_contents($file, false, null, 25, 1);

Handling transparent colors

PNG documentation says that more than one color may be use transparency in palette-based color types.

The PHP source code notes that it looks for the first fully transparent entry to use as the transparent color.

Knowing all the above, we can write a more correct version of the original code.

class PNGType
{
const GRAYSCALE = 0;
const RGB = 2;
const PALETTE = 3;
const GRAYSCALE_ALPHA = 4;
const RGBA = 6;

// Bit offsets
const ColorTypeOffset = 25;
}

function usesTransparency(string $file): bool {
if ($colorTypeByte = file_get_contents($file, false, null, PNGType::ColorTypeOffset, 1)) {
$type = ord($colorTypeByte);
$image = imagecreatefrompng($file);

// Palette-based PNGs may have one or more values that correspond to the color to use as transparent
// PHP returns the first fully transparent color for palette-based images
$transparentColor = imagecolortransparent($image);

// Grayscale, RGB, and Palette-based images must define a color that will be used for transparency
// if none is set, we can bail early because we know it is a fully opaque image
if ($transparentColor === -1 && in_array($type, [PNGType::GRAYSCALE, PNGType::RGB, PNGType::PALETTE])) {
return false;
}

$xs = imagesx($image);
$ys = imagesy($image);

for ($x = 0; $x < $xs; $x++) {
for ($y = 0; $y < $ys; $y++) {
$color = imagecolorat($image, $x, $y);

if ($transparentColor === -1) {
$shift = $type === PNGType::RGBA ? 3 : 1;
$transparency = ($color >> ($shift * 8)) & 0x7F;

if (
($type === PNGType::RGBA && $transparency !== 0) ||
($type === PNGType::GRAYSCALE_ALPHA && $transparency === 0)
) {
return true;
}
} else if ($color === $transparentColor) {
return true;
}
}
}
}

return false;
}

Things I still don't fully understand

If a files defines multiple fully transparent colors in a palette, but only one is used -- would this code catch the used color? I'm not sure and have not had a chance to test that case yet.

Conclusion

This is a more correct version of the original code. It will catch more PNG files that use some type of transparency. There may be a few bugs, but it serves as a starting point for a more robust solution.