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 $width = $height = 100; $url = urlencode(""); echo "<img src=\"{$width}x{$height}&cht=qr&chl=$url\" />";
And the following image will be displayed:

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:
  1. L copes with up to 7% data loss
  2. M copes with up to 15% data loss
  3. Q copes with up to 25% data loss
  4. H copes with up to 30% data loss
You set the scheme by adding the optional parameter chld.
hide $width = $height = 100; $url = urlencode(""); $error = "H"; // handle up to 30% data loss, or "L" (7%), "M" (15%), "Q" (25%) echo "<img src=\"". "chs={$width}x{$height}&cht=qr&chld=$error&chl=$url\" />";
The images get slightly bigger (from left to right: 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 parameter margin 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 $width = $height = 100; $url = urlencode(""); $error = "H"; // handle up to 30% data loss, or "L" (7%), "M" (15%), "Q" (25%) $border = 1; echo "<img src=\"". "chs={$width}x{$height}&cht=qr&chld=$error|$border&chl=$url\" />";
If you really want to save more screen space, you have to shorten the URL. For example, my main homepage is much longer than which points to the same location:

Popular URL shortening services are, and many, many more.


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 $filename = "./qr-create-stephan-brumme-com.png"; $width = $height = 100; if (!file_exists($filename)) { $url = urlencode(""); $qr = file_get_contents("{$width}x{$height}&cht=qr&chl=$url"); file_put_contents($filename, $qr); } echo "<img src=\"$filename\" width=\"$width\" height=\"$height\" alt=\"Scan my QR !\" />";
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:
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
you link to one of these 10 servers:,,,

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 $width = $height = 100; $url = urlencode(""); $parameters = "chs={$width}x{$height}&cht=qr&chl=$url"; $host = abs(crc32($parameters) % 10); echo "<img src=\"http://$$parameters\" alt=\"\" />";
Surprise, surprise, it's the same image ...

CRC32('chs=100x100&cht=qr&') = 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: change http 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):
Scan my QR ! Scan my QR !
Here is the caching code including on-the-fly GIF conversion:
hide $filename = "./qr-create-stephan-brumme-com.gif"; $width = $height = 100; if (!file_exists($filename)) { $url = urlencode(""); $qr = file_get_contents("{$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 !\" />";
Interlacing (I refer to 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):
Scan my QR !
The quality loss is seen best when zooming in:
Scan my QR !Scan my QR !
With PHP 4.3 or newer you can save some memory by getting rid of the variable $qrRaw:
hide // old code (any input file format): $qr = file_get_contents("{$width}x{$height}&cht=qr&chl=$url"); $img = imagecreatefromstring($qr); // replace by (requires PNG input): $img = imagecreatefrompng("{$width}x{$height}&cht=qr&chl=$url");
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.

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 $filename = "./qrsmall-create-stephan-brumme-com.png"; $width = $height = 100; if (!file_exists($filename)) { $url = urlencode(""); $qr = file_get_contents("{$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 !\" />";
Again, all pixels are identical. Left PNG image: 302 bytes, right PNG image (original): 734 bytes.
Scan my QR ! 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 function qr and with automatically generated filename for local caching:
hide 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 = ""; if ($loadBalance) $host = abs(crc32($parameters) % 10).""; // 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 !\" />"; }
Now it comes down to:
And that's what we get:
Scan my QR !
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("", 100, 100, 4, "Q");
About 10% more bytes (457 vs. 408):
Scan my QR !
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 !