Reading QR codes with Laravel and increasing the QR reading rate with image processing

Oğuzhan KARACABAY
7 min readNov 29, 2022

--

Hello, in this article, I will talk about how we can cut a QR code from a document, how we can read the QR code in this cut section and how we can increase the reading rate with Laravel.

Creating Laravel project

If you have composer installed on your computer, you can create a Laravel project as follows. If Composer is not installed, you need to install it.

composer create-project laravel/laravel qr-project

We can install the packages we need to use with Composer as follows:

composer require simplesoftwareio/simple-qrcode "~4"
composer require tarfin-labs/zbar-php
composer require laraveldaily/laravel-invoices:^3.0

Don’t forget to install the laravel-invoices package:

php artisan invoices:install
php artisan invoices:update

You must also install the zbar and imagemagick tools in order to run some of the packages. Thanks to the zbar tool, we will be able to read QR code, and thanks to the imagemagick we will be able to perform image processing. You can access the installation pages from the links below:

macOS and Homebrew users can easily install both Tools using the following bash commands:

brew install pkg-config imagemagick
brew install zbar

After installing Imagemagick, it should not be forgotten to install the relevant PHP Extension, we also install the extension using PECL:

/opt/homebrew/opt/php@8.1/bin/pecl install imagick

Since I’m using Homebrew, my php/bin location is as above, yours may be different, so you need to change the location yourself.

After the installation process is finished, let’s open our project with the help of any IDE. Let’s create a folder called Services under the app directory in order to be able to create and view invoices, and inside this folder, let’s create two more folders called InvoiceService and Facades. After the process is finished, our directory structure should be as follows:

Next, let’s create an empty class named InvoiceService under our InvoiceService folder, then create another class called InvoiceServiceProvider and change its content as follows:

<?php

namespace App\Services\InvoiceService;

use Illuminate\Support\ServiceProvider;

class InvoiceServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(InvoiceService::class, function ($app) {
return new InvoiceService();
});
}
}

Thus, we have created a singleton for our InvoiceService class, thanks to this singleton pattern, we will be able to access and use all functions in our class as if they are static functions. All that remains is to create a Facade, let’s create a PHP class named InvoiceService in our Services/Facades folder:

Now we can introduce our ServiceProvider to Laravel and access our InvoiceService class as a singleton from anywhere in the project.

To introduce the ServiceProvider to Laravel, let’s open config/app.php and add it as follows:

Finally, we introduce our Facade to the project and conclude:

Creating InvoiceService

Now that we can start writing our InvoiceService class, let’s first write an ordinary invoice document creation function:

<?php

namespace App\Services\InvoiceService;

use Illuminate\Http\Response;
use LaravelDaily\Invoices\Classes\Buyer;
use LaravelDaily\Invoices\Classes\InvoiceItem;
use LaravelDaily\Invoices\Invoice;

class InvoiceService
{
/**
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws \Exception
*/
public function createInvoiceAsPdf(): string
{
$customer = new Buyer([
'name' => 'John Doe',
'custom_fields' => [
'email' => 'test@example.com',
],
]);

$item = (new InvoiceItem())->title('Service 1')->pricePerUnit(2);

$invoice = Invoice::make()
->buyer($customer)
->discountByPercent(10)
->taxRate(15)
->shipping(2)
->addItem($item);

return $invoice->render()->output;
}
}

After this process, we can access our function as InvoiceService::createInvoiceAsPdf()

Creating InvoiceController

Let’s run the following artisan command in terminal:

php artisan make:controller InvoiceController --invokable

Then, let’s code the InvoiceController class as follows:

<?php

namespace App\Http\Controllers;

use App\Services\Facades\InvoiceService;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;

class InvoiceController extends Controller
{
public function __invoke()
{
return response()
->streamDownload(
callback: function (): void {
echo InvoiceService::createInvoiceAsPdf();
},
name: 'invoice.pdf'
);
}
}

Creating Route

Let’s open the routes/web.php file and change it as follows:

<?php

use App\Http\Controllers\InvoiceController;
use Illuminate\Support\Facades\Route;

Route::get('/get-invoice', InvoiceController::class);

Now, when we enter the get-invoice URL in our project, we expect the file to be downloaded as follows:

Adding QR code to invoice

We have successfully created our invoice, next to add a QR code to our invoice, first we open our InvoiceService class and add a custom order_id and qr_data variable to our invoice template:

<?php

namespace App\Services\InvoiceService;

use Illuminate\Http\Response;
use LaravelDaily\Invoices\Classes\Buyer;
use LaravelDaily\Invoices\Classes\InvoiceItem;
use LaravelDaily\Invoices\Invoice;
use SimpleSoftwareIO\QrCode\Facades\QrCode;

class InvoiceService
{
/**
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws \Exception
*/
public function createInvoiceAsPdf(): string
{
$customer = new Buyer([
'name' => 'John Doe',
'custom_fields' => [
'email' => 'test@example.com',
],
]);

$item = (new InvoiceItem())->title('Service 1')->pricePerUnit(2);

$invoice = Invoice::make()
->buyer($customer)
->discountByPercent(10)
->taxRate(15)
->shipping(2)
->addItem($item);

$invoice->setCustomData([
'order_id' => $orderId = random_int(100000,999999),
'qr_data' => base64_encode(QrCode::format('png')->size(100)->generate($orderId)),
]);

return $invoice->render()->output;
}
}

Then we open our blade file located in resources/views/vendor/invoices/templates/default.blade.php and add something as follows:

        {{-- Header --}}
@if($invoice->logo)
<img src="{{ $invoice->getLogo() }}" alt="logo" height="100">
@endif

<p class="text-right m-5">
<img
src="data:image/png;base64,
{!! $invoice->getCustomData()['qr_data'] !!} "
>
</p>

And we add this class to the Style part:

       .m-5 {
margin-right: 3rem !important;
margin-top: 3rem !important;
}

When we save the changes in our blade file and create the invoice again, we should have seen an image like this:

Reading QR code from invoice

First of all, let’s copy the invoice we created with the file path as storage/app/public/invoice.pdf

Now, we will cut this QR code from the invoice file we have created and scan it. If we try to read the QR code without cutting it, the probability of capturing the QR code in the whole file decreases considerably, so we have to perform the cutting process. Let’s create a structure like this:

Let’s not forget to add our ServiceProvider and Facade alias in config/app.php

Let’s open our ImageProcessingService class and edit it as follows:

<?php

namespace App\Services\ImageProcessingService;

use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Imagick;
use TarfinLabs\ZbarPhp\Exceptions\ZbarError;
use TarfinLabs\ZbarPhp\Zbar;

class ImageProcessingService
{
public int $edgeLength = 240;

/**
* @throws \ImagickException
* @throws \TarfinLabs\ZbarPhp\Exceptions\UnableToOpen
* @throws \TarfinLabs\ZbarPhp\Exceptions\InvalidFormat
* @throws \Exception
*/
public function readQrCode(string $filePath): string|null
{
$cropedQRPath = 'public/'.Str::random(40).'.png';
$imagickInstance = $this->cropQrCodeFromPDF($filePath);

Storage::put($cropedQRPath, $imagickInstance->getImageBlob());

try {
$readedCode = (new Zbar(Storage::path($cropedQRPath)))->scan();
} catch (ZbarError) {
$readedCode = null;
}

Storage::delete($cropedQRPath);

return $readedCode;
}

/**
* @throws \Exception
*/
public function cropQrCodeFromPDF(string $filePath): Imagick
{
$imagick = new Imagick();
$imagick->setResolution(128, 128);
$imagick->readImageBlob(Storage::get($filePath));

$imageWidth = $imagick->getImageWidth() - $this->edgeLength;
$imagick->cropImage($this->edgeLength + 80, $this->edgeLength + 80, $imageWidth - 80 , 0);
$imagick->resizeImage($this->edgeLength + 80, $this->edgeLength + 80, 1, 0);

$imagick->setImageFormat('png');

return $imagick;
}


}

After editing our class, let’s create a Controller named ImageProcessingController :

php artisan make:controller ImageProcessingController --invokable

And let’s edit its content as follows:

<?php

namespace App\Http\Controllers;

use App\Services\Facades\ImageProcessingService;
use Illuminate\Http\JsonResponse;

class ImageProcessingController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(): JsonResponse
{
return \response()->json([
'code' => ImageProcessingService::readQrCode('public/invoice.pdf')
]);
}
}

Then let’s open routes/web.php and edit it as follows:

<?php

use App\Http\Controllers\ImageProcessingController;
use App\Http\Controllers\InvoiceController;
use Illuminate\Support\Facades\Route;

Route::get('/get-invoice', InvoiceController::class);
Route::get('/process-qr-code', ImageProcessingController::class);

Let’s open our browser and enter the URL /process-qr-code and see the result:

As you can see, the QR code we produced was successfully read and the result was sent to us in Response.

Increasing QR reading rate with image processing

QR codes may not be very readable in documents scanned in daily life, so there are a few image processing techniques that can be done to increase the reading rate, i won’t go into too much detail about the operations done because if we include the image processing part in the article, it’s sure this will be a very long article :) but if i need to explain in a small way, the operations performed help us to obtain a more distinctive QR code by whitening the pixels that do not belong to the QR code in the background and making the QR code part black.

Let’s open our ImageProcessingService class and edit the readerQrCode function as follows:

/**
* @throws \ImagickException
* @throws \TarfinLabs\ZbarPhp\Exceptions\UnableToOpen
* @throws \TarfinLabs\ZbarPhp\Exceptions\InvalidFormat
* @throws \Exception
*/
public function readQrCode(string $filePath): string|null
{
$cropedQRPath = 'public/'.Str::random(40).'.png';
$imagickInstance = $this->cropQrCodeFromPDF($filePath);

Storage::put($cropedQRPath, $imagickInstance->getImageBlob());

try {
$readedCode = (new Zbar(Storage::path($cropedQRPath)))->scan();
} catch (ZbarError) {
$imagickInstance->setImageBackgroundColor('#FFFFFF');
$imagickInstance->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
$imagickInstance->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
$imagickInstance->autoLevelImage();

$imagickInstance->autoThresholdImage(1); // 1 means we choose OTSU threshold method

$kernel = ImagickKernel::fromBuiltIn(Imagick::KERNEL_SQUARE, '1');
$imagickInstance->morphology(\Imagick::MORPHOLOGY_OPEN, 1, $kernel);

Storage::disk('local')->put($cropedQRPath, $imagickInstance->getImageBlob());

try {
$readedCode = (new Zbar(Storage::path($cropedQRPath)))->scan();
} catch (ZbarError) {
$readedCode = null;
}
}

Storage::delete($cropedQRPath);

return $readedCode;
}

The reason why we do these operations after the catch (ZbarError) part is that if the QR code reading is successful, we do not need to take any extra action, and if we failed to read the QR code, the image processing algorithms will save us time.

You can read this article in both Turkish and English languages.

--

--