QR code with a centre logo
Render a high error-correction SVG QR code and overlay a rounded logo plate in the middle, with no image extension required.
LaravelUtilitiesqrsvglogobacon-qr-code
Code
<?php
namespace App\Qr;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use InvalidArgumentException;
final class QrWithLogo
{
/** @param string $logoDataUri e.g. data:image/png;base64,... (null for a plain code) */
public static function svg(string $content, ?string $logoDataUri = null, int $size = 512, float $logoRatio = 0.22): string
{
// Level H repairs ~30% damage, which is what makes a covered centre scannable.
$style = new RendererStyle(
$size, 2, null, null,
Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(17, 24, 39))
);
$writer = new Writer(new ImageRenderer($style, new SvgImageBackEnd()));
$svg = $writer->writeString($content, 'UTF-8', \BaconQrCode\Common\ErrorCorrectionLevel::H());
if ($logoDataUri === null) {
return $svg;
}
if (! preg_match('#^data:image/(png|jpeg|svg\+xml);base64,[A-Za-z0-9+/=]+$#', $logoDataUri)) {
throw new InvalidArgumentException('Logo must be a base64 data URI.');
}
// Keep the logo under ~5% of the code area (0.22 side ratio) so scanners still decode it.
$logo = $size * min($logoRatio, 0.25);
$plate = $logo * 1.25;
$x = ($size - $plate) / 2;
$y = ($size - $plate) / 2;
$overlay = sprintf(
'<rect x="%1$.1f" y="%1$.1f" width="%2$.1f" height="%2$.1f" rx="%3$.1f" fill="#fff"/>'
.'<image x="%4$.1f" y="%4$.1f" width="%5$.1f" height="%5$.1f" href="%6$s" preserveAspectRatio="xMidYMid meet"/>',
$x, $plate, $plate * 0.18, ($size - $logo) / 2, $logo, htmlspecialchars($logoDataUri, ENT_QUOTES)
);
return str_replace('</svg>', $overlay.'</svg>', $svg);
}
}
Needs: bacon/bacon-qr-code@^3.0
How it works
What it solves
Branded QR codes with a logo in the middle are everywhere, but most tutorials require the GD or Imagick extension and produce a fixed-size bitmap. This version stays in SVG, so it is crisp at any print size and works on hosts without image extensions.
How it works
The code is generated by BaconQrCode with the highest error-correction level, H, which can lose roughly 30% of the modules and still decode. That redundancy is what makes it safe to cover the middle.
After rendering, the method injects two elements just before the closing </svg> tag: a white rounded rectangle that acts as a quiet plate behind the logo, and an <image> tag that holds the logo as a data URI. The plate is 25% larger than the logo so the artwork does not touch dark modules. Sizes derive from the output size and a ratio, which is capped at 0.25 of the side so the covered area stays small enough for phone cameras.
The logo is only accepted as a base64 data URI of PNG, JPEG or SVG, and the value is escaped before being written into the markup, so the output cannot be used to inject attributes.
Adapting it
- Pass
$sizeper call to produce print and screen variants. - Change the dark colour in
Fill::uniformColorto match a brand, keeping strong contrast with the background. - Rasterise the SVG afterwards with your own tool if you need PNG.
Pitfalls
- Test every design with a real phone; low-contrast colours and large logos fail first.
- Longer content makes the code denser, so shorten URLs before encoding.
How to use it
- Run
composer require bacon/bacon-qr-code. - Put
QrWithLogo.phpinapp/Qr/. - Build the data URI once and store it:
'data:image/png;base64,'.base64_encode(file_get_contents($path)). - Render:
$svg = QrWithLogo::svg('https://example.com/menu', $logoUri); - Return it with
response($svg, 200, ['Content-Type' => 'image/svg+xml']).
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.