ggoitea / laravel-argidreader
Laravel package to read Argentine DNI cards from front/back images or PDFs using PDF417/QR barcodes, TD1 MRZ OCR and address extraction.
Requires
- php: ^8.3
- ext-fileinfo: *
- ext-mbstring: *
- illuminate/support: ^13.0
- nesbot/carbon: ^3.8
- psr/log: ^1.0 || ^2.0 || ^3.0
- symfony/process: ^7.4 || ^8.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel package to read Argentine DNI cards from front and back images or PDFs. It extracts the data available on the document by combining PDF417/QR barcode reading with TD1 MRZ OCR on the back side, and also extracts the address printed on the card.
The package returns a structured, immutable result and never resolves front/back conflicts silently.
Features
- Reads JPG, JPEG, PNG, WebP and PDF files.
- Supports a single 2-page PDF for both sides, or two separate files.
- First tries the PDF417/QR barcodes (front and back).
- Falls back to MRZ TD1 OCR + address extraction when required fields are missing.
- Validates MRZ check digits, DNI, trámite, sex and CUIL.
- Merges barcode and MRZ data and reports conflicts.
- Does not log personal data.
- Cleans up temporary files internally.
- Ships with Laravel auto-discovery.
How it works
DniReadPipelineruns the barcode stage first.- If
dni,tramiteandsexowere obtained, the result is returned. - Otherwise, the MRZ stage reads the back side with Tesseract, using several rotations/enhancements, and extracts the address.
DniDataMergercombines both sources. Differences indni,tramiteorsexoproduce aconflictoresult instead of picking a value silently.
Requirements
PHP
- PHP 8.3 or newer.
- Laravel 13.x.
- PHP extensions:
fileinfo,mbstring.
External binaries
This package wraps external command-line tools. They are not bundled with the package and must be available on PATH:
| Tool | Binary | Used for |
|---|---|---|
| ZXing-C++ | ZXingReader |
PDF417 and QR barcode reading |
| Tesseract OCR | tesseract |
MRZ and address OCR |
| ImageMagick | magick, identify |
Image probing and preprocessing |
| Poppler | pdfinfo, pdftoppm |
PDF page count and rasterization |
Tesseract needs the Spanish (spa) and English (eng) language data. Spanish is used for MRZ/address, English is used as an additional MRZ attempt.
Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y \
zxing-cpp-tools \
tesseract-ocr tesseract-ocr-spa tesseract-ocr-eng \
imagemagick \
poppler-utils
macOS (Homebrew)
brew install zxing-cpp tesseract tesseract-lang imagemagick poppler
Verify the installation
ZXingReader --version tesseract --version tesseract --list-langs magick -version pdfinfo -v pdftoppm -v
tesseract --list-langs must include both spa and eng.
Installation
composer require ggoitea/laravel-argidreader
Laravel auto-discovery
Laravel discovers Ggoitea\LaravelArgIdReader\ArgIdReaderProvider automatically through the extra.laravel.providers entry in composer.json.
If your application has package discovery disabled, register the provider manually in bootstrap/providers.php:
return [ // ... Ggoitea\LaravelArgIdReader\ArgIdReaderProvider::class, ];
Usage
Basic example
use Ggoitea\LaravelArgIdReader\Contracts\ReadPipeline; use Ggoitea\LaravelArgIdReader\Domain\DniImages; use Ggoitea\LaravelArgIdReader\Domain\ReadStatus; $reader = app(ReadPipeline::class); $images = new DniImages( frente: storage_path('app/dni/frente.jpg'), dorso: storage_path('app/dni/dorso.jpg'), ); $report = $reader->read($images); if ($report->status === ReadStatus::Completo && $report->data !== null) { $data = $report->data->toArray(); }
You can also inject the concrete implementation:
use Ggoitea\LaravelArgIdReader\Pipeline\DniReadPipeline; $reader = app(DniReadPipeline::class);
Controller example with uploaded files
Uploaded temporary files usually have no extension. Store them first so the resolver can detect the format, and delete them after reading:
namespace App\Http\Controllers; use Ggoitea\LaravelArgIdReader\Contracts\ReadPipeline; use Ggoitea\LaravelArgIdReader\Domain\DniImages; use Ggoitea\LaravelArgIdReader\Domain\ReadStatus; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; final class DniController extends Controller { public function __invoke(Request $request, ReadPipeline $reader): JsonResponse { $validated = $request->validate([ 'frente' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,pdf', 'max:25600'], 'dorso' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,pdf', 'max:25600'], ]); $frente = $validated['frente']->store('dni-tmp', 'local'); $dorso = $validated['dorso']->store('dni-tmp', 'local'); try { $report = $reader->read(new DniImages( frente: Storage::disk('local')->path($frente), dorso: Storage::disk('local')->path($dorso), )); return match ($report->status) { ReadStatus::Completo => response()->json([ 'status' => 'ok', 'data' => $report->data?->toArray(), ]), ReadStatus::Parcial => response()->json([ 'status' => 'partial', 'data' => $report->data?->toArray(), 'missing_fields' => $report->missingFields, ], 422), ReadStatus::Conflicto => response()->json([ 'status' => 'conflict', 'conflicts' => array_keys($report->conflicts), ], 409), default => response()->json([ 'status' => $report->status->value, ], 422), }; } finally { Storage::disk('local')->delete([$frente, $dorso]); } } }
One PDF with both sides
If the front and back are two pages of the same PDF, pass the same path twice. The pipeline uses page 1 for the front and page 2 for the back automatically:
$pdf = storage_path('app/dni/dni.pdf'); $report = $reader->read(new DniImages( frente: $pdf, dorso: $pdf, ));
Result
$data = $report->data?->toArray();
Example shape (synthetic data):
{
"dni": "12345678",
"tramite": "000000000",
"apellido": "APELLIDO",
"nombre": "NOMBRE",
"sexo": "M",
"fecha_nacimiento": "1990-01-01",
"emision": "2024-01-01",
"expiracion": null,
"ejemplar": "A",
"nacionalidad": "ARG",
"cuil": null,
"tipo_tarjeta": "2024",
"domicilio": "CALLE FALSA 123, CIUDAD, PROVINCIA"
}
Dates are formatted as Y-m-d. Fields that could not be read are null.
Statuses
| Status | Meaning |
|---|---|
completo |
dni, tramite and sexo were obtained. May come from barcodes only or from barcode + MRZ. |
parcial |
Some data was read, but at least one required field is missing. |
conflicto |
Front and back sources disagree on dni, tramite or sexo. Check $report->conflicts. |
sin_codigo |
Neither a usable barcode nor MRZ was found. |
sin_datos |
The stages ran but no parseable data was extracted. |
no_soportado |
A payload/format was recognized but is not supported. |
error |
Document, image or external process error. |
Debugging stages
Every pipeline stage is reported with a status, duration and non-sensitive metadata:
foreach ($report->stages as $stage) { echo $stage->stage.': '.$stage->status->value.' ('.round($stage->durationSeconds * 1000).'ms)'.PHP_EOL; print_r($stage->meta); }
The final report also exposes:
$report->missingFields; // list<string> $report->conflicts; // array<string, string> $report->durationSeconds;
Supported formats
Barcode payloads
| Version | @ parts |
Structure | Notes |
|---|---|---|---|
| DNI 2012 | 9 | tramite@apellido@nombre@sexo@dni@ejemplar@dd/mm/YYYY@dd/mm/YYYY@sufijo |
Provides sex and derives CUIL. tipo_tarjeta = 2012. |
| DNI 2024 | 8 | tramite@apellido@nombre@dni@ejemplar@dd/mm/yy@dd/mm/yy@jwt |
Does not provide sex/CUIL. tipo_tarjeta = 2024. |
| Other/legacy | other | — | Not supported by the current parsers; returns no_soportado. |
MRZ TD1
The back side is expected to contain a 3×30 TD1 MRZ:
IDARG12345678<1<<<<<<<<<<<<<<<
9001015M3001011ARG<<<<<<<<<<<8
APELLIDO<<NOMBRE<FEDERICO<<<<<
The parser repairs common OCR confusions (O/0, I/1, S/5, etc.), validates check digits and resolves ambiguous centuries.
Address
The package looks for a block starting at DOMICILIO and ending at LUGAR DE NACIMIENTO on the back side. It retries with different preprocessing to improve the scan.
Custom implementations
Every external capability is behind a contract. You can override any binding after the package provider is registered:
use Ggoitea\LaravelArgIdReader\Contracts\BarcodeReader; $this->app->bind(BarcodeReader::class, MyBarcodeReader::class);
Available contracts:
BarcodeReaderOcrReaderImagePreprocessorImageProbeProcessRunnerReadPipeline
This is useful if you want to use a different barcode CLI, a different OCR engine, a different image library, or a process runner with your own timeout/logging policy.
Privacy and data handling
ReadReport::toLogContext()excludes personal data and file paths.- Pipelines do not log OCR text or raw payloads.
- Temporary images created by the package are cleaned up in
finallyblocks. - The package does not persist the result or the uploaded files.
- Delete the original images/PDFs after processing unless your application is legally allowed to keep them.
- Do not commit real DNI images or real personal data to this repository.
- In Argentina, DNI data is personal data protected by Ley 25.326. Apply the retention and security rules that correspond to your use case.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
status = error, probe.* = error, code imagen_chica |
Image is too small | Use an image with at least 300 px on the shorter side. |
status = error, code imagen_demasiado_grande |
More than 40 million pixels | Reduce the image resolution before processing. |
barcode.* = sin_codigo and no MRZ data |
Blurry, dark or glared photo | Improve lighting, avoid flash reflections, keep the card straight. |
parse.* = no_soportado |
Unknown payload or new schema | Inspect the raw payload in a controlled environment and check the @ count. |
status = conflicto |
Front and back contain different values | Verify that both images belong to the same card. |
parse.mrz = sin_datos |
MRZ is unreadable or check digits fail | Retake the back side, improve contrast on the lower third. |
domicilio is null |
OCR did not find the address block | Make sure the back side shows DOMICILIO and LUGAR DE NACIMIENTO. |
| External process errors | Missing binaries | Run the verification commands above and check PATH. |
Testing
composer install
composer test
The runtime supports PHP 8.3+, but PHPUnit 13 requires PHP 8.4.1+ to run the development test suite.
License
MIT. See LICENSE.