Under the Hood #1:
Is a PNG 32-Bit? in One Line
To begin with, I wanted to showcase something very simple, that one could spend many lines of code
on. For the art and photo sections of the site, I have an upload form
that attaches an image as an RSS enclosure to the post, and resizes the image for a preview on the home page.
I’m touchy when it comes to image quality and use PNG wherever possible. I compress my PNGs with PNGCrush
(Specifically an excellent Dashboard widget called PNGPong)
But I also love transparency in PNGs. It’s incredibly flexible, and I’d rather (for maintenance reasons) be able
to upload 32-bit images when possible. Thankfully PHP can resize 32-bit PNGs without destroying the alpha channel,
via the
imagealphablending
/
imagesavealpha
commands.
Choosing the Right Preview
The programming issue is of being able to choose the right file type for the preview image shown on the site.
- JPG upload → JPG preview
-
Obviously
- PNG (without transparency) upload → JPG preview
-
Why would I want to save the preview image as a JPG if I’m fussy about quality?
Simply because this site is also supposed to be fast to load, and a PNG image that is resized has many more
colours due to the anti-aliasing when resizing, causing the file size to bloat massively.
- PNG (with transparency) upload → PNG preview
-
A resized transparent PNG, without keeping the transparency would just have a horrible black background. We
can’t be having that
The Code
All credit for this however goes to Wesley Gunn who posted a
solution on the PHP website,
and that I improved upon to condense it into one line. Here’s the original:
$readPng = fopen ($argSourceImagePath, "rb");
$readAlp = fread ($readPng, 52);
fclose ($readPng);
if(substr(bin2hex($readAlp),50,2) == "04" || substr(bin2hex($readAlp),50,2) == "06")
echo("Png has alpha");
Here’s a break down of what’s happening here:
//open the image file, "Read Binary"
$readPng = fopen ($argSourceImagePath, "rb");
//read 52 Bytes. this is the PNG header up to the byte(s) that specify transparency
$readAlp = fread ($readPng, 52);
//close the file
fclose ($readPng);
//`bin2hex` converts the PNG header to hexadecimal codes, and `substr` strips out the two bytes that
//specify the PNG transparency - "04" is for a greyscale PNG with transparency, and "06" for a 32-bit PNG
if(substr(bin2hex($readAlp),50,2) == "04" || substr(bin2hex($readAlp),50,2) == "06")
echo("Png has alpha");
Whilst this is a very good way to achieve the task, I hate spawning temporary variables and would like to do this
same thing inline so that it could be combined with any if
statement. The first thing to do is to read
the file without having to use a file handle to open and close the file.
Here is my redesign of this code:
$is_alpha = ord (file_get_contents ($file_path, false, null, 25, 1)) & 4;
file_get_contents
returns the contents of a
file without having to open and close handles, but has the added ability to return a chosen number of bytes,
starting at a set offset. Perfect! This isn’t binary however, and thus each pair of bytes is treated as a single
‘letter’, meaning that the transparency flag is the 25th along using file_get_contents
instead of 50. We return just one ‘letter’, and
ord
returns the ASCII
numerical representation of that ‘letter’, either 4 or 6.
This is also a super fast solution, as file_get_contents
is only reading two bytes and that’s it,
this operation is also cached by PHP.
Finally the “& 4;
” bitwise ANDs the result ignoring all other values that do not contain
transparency. An example below demonstrates the bitwise operation filtering out both values 4 and 6, and ignoring
other values
00000110 = Binary 6 - a transparent PNG
00000100 = “& 4” mask, only a 1 on the top row and 1 on the bottom row pass through, giving:
--------
00000100 = 4
00000100 = Binary 4 - a greyscale transparent PNG
00000100 = “& 4”
--------
00000100 = 4
An example of an unwanted value
00000011 = Binary 3 - not a transparent PNG
00000100 = “& 4”
--------
00000000 = 0
Therefore my code will return 4 if the PNG is transparent, and 0 if not. In PHP, zero is considered false, and any
number higher than zero is considered true. Thus you can easy place this code in an if
without having
to specifically check for 4
, e.g.
if (ord (file_get_contents ($file_path, false, null, 25, 1)) & 4) {
//PNG is transparent...
} else {
//PNG is not transparent...
}
Limitations
This code will not detect a 256 colour (8-bit) PNG, with transparency (like a GIF). The normal
transparency flag is not set in this instance; instead this could be detected by the presence of both “PLTE”
(‘Palette’ - 256 colours) and “tRNS” in the file. Although the precise location of these is not fixed
because the size of the palette can vary.
In order to work around this, I’d just re-save any 8-bit PNG with transparency as a 32-bit one - it’d increase
file size, but not by a painful amount.