Generate QR Codes With PHP and Google Charts API
posted by Stephan Brumme
QR Codes
Almost 20 years ago the Japanese company Denso Wave (owned by Toyota) invented QR codes. These 2D images can encode pretty much any Unicode string and are best known for encoding URLs. Apps for all kind of mobile phones are available, often for free.Please keep in mind: it's a patented technology but standardized as ISO 18004. Denso Wave owns all rights (incl. a registered trademark on "QR code") but they claim on their website to not exercise their rights.
If you don't care about the technical stuff and want to go straight to my final PHP code, then click here.
Google Charts API
Google Charts is a free online service that can generate a huge variety of charts. Think of it as Excel without the spreadsheets ;-)With Google Charts it's extremely simple to create a QR code. As the bare minimum, you need to provide only two pieces of information: the URL and the image size.
hide
Generate QR Code
And the following image will be displayed:
$width = $height = 100;
$url = urlencode("http://create.stephan-brumme.com");
echo "<img src=\"http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url\" />";
Error Correction
QR codes include an error correction scheme which comes with a certain overhead. Four levels of error correction are available - the more error-resistant the code, the more rows/columns of pixels are needed:L
copes with up to 7% data lossM
copes with up to 15% data lossQ
copes with up to 25% data lossH
copes with up to 30% data loss
chld
.
hide
The images get slightly bigger (from left to right:
$width = $height = 100;
$url = urlencode("http://create.stephan-brumme.com");
$error = "H"; // handle up to 30% data loss, or "L" (7%), "M" (15%), "Q" (25%)
echo "<img src=\"http://chart.googleapis.com/chart?".
"chs={$width}x{$height}&cht=qr&chld=$error&chl=$url\" />";
L
, M
, Q
, H
),
but more important the number of pixels per side grows:Reducing Image Size
There is a huge amount of unused white space in the QR code. The optional parametermargin
is 4
by default.
Below I added red lines to indicate the overhead:The huge default border is intended to keep surrounding text/images away from the QR code and help QR code readers to actually capture only the QR code and not some irrelevant noise. In my experience, readers have gotten a lot better and you can safely reduce the border to
1
:The border is part of the optional
chld
parameter:
hide
If you really want to save more screen space, you have to shorten the URL.
For example, my main homepage http://www.stephan-brumme.com
is much longer than http://brum.me which points to the same location:
$width = $height = 100;
$url = urlencode("http://create.stephan-brumme.com");
$error = "H"; // handle up to 30% data loss, or "L" (7%), "M" (15%), "Q" (25%)
$border = 1;
echo "<img src=\"http://chart.googleapis.com/chart?".
"chs={$width}x{$height}&cht=qr&chld=$error|$border&chl=$url\" />";
Popular URL shortening services are bit.ly, tinyurl.com and many, many more.
Caching
At the moment there is no official limit on the number of images created per day. Google states "we reserve the right to block any use that we regard as abusive" which sounds fair enough.However, most QR codes don't change over time and therefore I improved my PHP script to include a simple caching mechanism.
hide
This image is fetched only once from Google and then stored locally.
All subsequent visits load the QR code straight from your web server, not from Google anymore:
$filename = "./qr-create-stephan-brumme-com.png";
$width = $height = 100;
if (!file_exists($filename))
{
$url = urlencode("http://create.stephan-brumme.com");
$qr = file_get_contents("http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url");
file_put_contents($filename, $qr);
}
echo "<img src=\"$filename\" width=\"$width\" height=\"$height\" alt=\"Scan my QR !\" />";
Note: some hosters disable the HTTP wrapper of
file_get_contents
.
In this case the technique shown above doesn't work.
Faster QR Codes Without Local Caching
Many browsers limit the number of parallel downloads from the same server (often 8 concurrent accesses per server). If you happen to have tons of QR codes on your website, then you can circumvent this problem by modifying the Google Charts URL:Instead of
http://chart.googleapis.com/chart?your_usual_parameters
you link to one of these 10 servers:
http://0.chart.apis.google.com/chart?your_usual_parameters
,http://1.chart.apis.google.com/chart?your_usual_parameters
,http://2.chart.apis.google.com/chart?your_usual_parameters
,...
http://9.chart.apis.google.com/chart?your_usual_parameters
,You can choose the numbers 0 to 9 randomly, i.e.
rand(0,9);
, but there is a more
browser-cache friendly way: compute a hash based on the image parameters and compute modulo 10.
This way the same image will always be loaded from the same Google server but there is a good chance
that all your different QR code requests will be distributed evenly over these 10 servers.I like the CRC32 hash algorithm because it's fast and simple:
hide
Surprise, surprise, it's the same image ...
$width = $height = 100;
$url = urlencode("http://create.stephan-brumme.com");
$parameters = "chs={$width}x{$height}&cht=qr&chl=$url";
$host = abs(crc32($parameters) % 10);
echo "<img src=\"http://$host.chart.apis.google.com/chart?$parameters\" alt=\"\" />";
CRC32('chs=100x100&cht=qr&chl=http%3A%2F%2Fcreate.stephan-brumme.com') = 7b07a370
abs(7b07a370 modulo 10) = 0
therefore delivered by server 0.
However, the caching mechanism shown above is far superior if you work with static QR codes.
More Secure QR Codes
If your website is accessed via a secure connection (HTTPS) then it might be a good idea to get the QR codes in a secure way as well: changehttp
to
https
in the Google URLs and you're done.
No Byte Shall Be Wasted
During my tests, Google always returned the QR codes as PNG files. PNG is one of best mainstream lossless file formats with regards to compression ratio (same algorithm as Zip). I tried to recompress the original PNG files with PHP's GD library and best settings (level 9, all PNG filters) but ended up with the same file size as the original.However, for very small images GIF can be a better alternative due to its lower file overhead. These two images are pixel-wise 100% identical but the left image is 593 bytes (GIF) while the right one is 734 bytes (PNG):
Here is the caching code including on-the-fly GIF conversion:
hide
Interlacing (I refer to
$filename = "./qr-create-stephan-brumme-com.gif";
$width = $height = 100;
if (!file_exists($filename))
{
$url = urlencode("http://create.stephan-brumme.com");
$qr = file_get_contents("http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url");
$img = imagecreatefromstring($qr);
imagegif($img, $filename);
imagedestroy($img);
}
echo "<img src=\"$filename\" width=\"$width\" height=\"$height\" alt=\"Scan my QR !\" />";
imageinterlace($img, 1);
) should be avoided:
it increases the filesize in exchange for a better file preview during loading.
But QR codes are often less than 1k so there is no use of having a file preview.I strongly recommend NOT to convert to JPEG because it's a lossy file format, that means the QR codes may become blurry, and the resulting images are typically larger than PNG or GIF. Here is a JPEG with default quality 75, filesize 3304 bytes (more than 10x larger than best PNG):
The quality loss is seen best when zooming in:
With PHP 4.3 or newer you can save some memory by getting rid of the variable
$qrRaw
:
hide
The main drawback is the assumption that Google will always generate their QR codes as PNG files.
It's true at the moment but may change in the future.
// old code (any input file format):
$qr = file_get_contents("http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url");
$img = imagecreatefromstring($qr);
// replace by (requires PNG input):
$img = imagecreatefrompng("http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url");
Absolutely No Byte Shall Be Wasted !
As I told you before, the QR code went from 734 to 593 bytes after PNG-to-GIF conversion. When I played around PNGOUT (freeware, great tool !), the original PNG file dropped from 734 to only 291 bytes. What happened ?Irfanview revealed that Google is returning a true-color PNG file. Mmmm ... but we need only 2 of the 16.7 million colors available. And of course, PNGOUT's result is a two-color PNG file (with cleverly optimized Huffman tables). If we can reduce the color depth of Google's QR code, we should come closer to PNGOUT's result, too.
Therefore I create a new PNG image
$imgOut
from scratch which is palette-based
(at most 256 colors) and NOT truecolor.
In the next step, all pixels are copied from the original truecolor PNG image $imgIn
to $imgOut
.
Then, $imgOut
is written to disk as before.
PHP's GD image library indeed outputs a two-color PNG. Sweet !
hide
Again, all pixels are identical. Left PNG image: 302 bytes, right PNG image (original): 734 bytes.
$filename = "./qrsmall-create-stephan-brumme-com.png";
$width = $height = 100;
if (!file_exists($filename))
{
$url = urlencode("http://create.stephan-brumme.com");
$qr = file_get_contents("http://chart.googleapis.com/chart?chs={$width}x{$height}&cht=qr&chl=$url");
$imgIn = imagecreatefromstring($qr);
$imgOut = imagecreate($width, $height);
imagecopy($imgOut, $imgIn, 0,0, 0,0, $width,$height);
imagepng($imgOut, $filename, 9);
imagedestroy($imgIn);
imagedestroy($imgOut);
}
echo "<img src=\"$filename\" width=\"$width\" height=\"$height\" alt=\"Scan my QR !\" />";
It's a habit of mine to always use the parameter
PNG_ALL_FILTERS
whenever
I create a PNG image to push the compression ratio to its limit.
For reasons unknown it doesn't work very well with black'n'white images:
the QR code actually grows from 302 bytes to 383 bytes. Maybe one day I figure out why.
Come Together
Here's my final code - everything wrapped into a nice functionqr
and with
automatically generated filename for local caching:
hide
Now it comes down to:
function qr($url, $width = 100, $height = 100, $border = 1,
$error = "L", $https = false, $loadBalance = false)
{
// create valid filename
$filename = str_replace(array("http://", "https://"), "", $url);
$filename = str_replace("%", "_", urlencode($filename));
$filename = "./qr-$error$border-$filename.png";
if (!file_exists($filename))
{
// build Google Charts URL:
// secure connection ?
$protocol = $https ? "https" : "http";
// load balancing
$host = "chart.googleapis.com";
if ($loadBalance)
$host = abs(crc32($parameters) % 10).".chart.apis.google.com";
// safe URL
$url = urlencode($url);
// put everything together
$qrUrl = "$protocol://$host/chart?chs={$width}x{$height}&cht=qr&chld=$error|$border&chl=$url";
// get QR code from Google's servers
$qr = file_get_contents($qrUrl);
// optimize PNG and save locally
$imgIn = imagecreatefromstring($qr);
$imgOut = imagecreate($width, $height);
imagecopy($imgOut, $imgIn, 0,0, 0,0, $width,$height);
imagepng($imgOut, $filename, 9, PNG_ALL_FILTERS);
imagedestroy($imgIn);
imagedestroy($imgOut);
}
// serve image from local server
echo "<img src=\"$filename\" width=\"$width\" height=\"$height\" alt=\"Scan my QR !\" />";
}
qr("http://create.stephan-brumme.com");
I set the default border to
1
instead of 4
,
that's why the image's filesize is 408 bytes.For print-outs I recommend to switch to a better error correction scheme, e.g.
Q
.
// same as before, but resistant to 25% data loss
qr("http://create.stephan-brumme.com", 100, 100, 4, "Q");
It's interesting to see that the border magically grows in order to ensure the increased reliability because anything below
4
is ignored by Google for "Q"
.
That's all folks !