crazy-goat / scanmephp
Pure PHP QR code generator with zero dependencies
Requires
- php: ^8.2
- composer-plugin-api: ^2.0
Requires (Dev)
- ext-gd: *
- brianium/paratest: ^7.6
- composer/composer: ^2
- friendsofphp/php-cs-fixer: ^3
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11.5
- rector/rector: ^2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.x-dev
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.11
- v0.4.10
- v0.4.9
- v0.4.8
- v0.4.7
- v0.4.6
- v0.4.5
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- 0.2.0
- v0.1.0
- dev-fix/issue-75-bug-empty-data-rejects-the-valid-payload
- dev-chore/pr-240-windows-ci-probe
- dev-fix/issue-71-bug-source-build-fallback-copies-the-lib
- dev-fix/issue-193-bug-binarydownloader-calls-curl-close-a
- dev-feature/examples-gallery-committed
- dev-perf/2026-08-speed-pass
- dev-docs/issue-185-security-existing-binaries-on-disk-are
- dev-pin-c-extension-name
- dev-feature/svg-renderer-modulesize
This package is auto-updated.
Last update: 2026-10-04 17:53:58 UTC
README
The fastest pure PHP QR code generator with optional native C++ acceleration.
Generate QR codes in PHP without dependencies β then go 100Γ faster with a single C++ library. Zero bloat, maximum speed, production-ready.
QR encoding algorithms are based on Nayuki's QR Code generator.
Why ScanMePHP?
π Blazing Fast β 3-Tier Performance
- Fast pure PHP: bitwise mask selection and packed ReedβSolomon β a v10 code in ~60 Β΅s with the JIT, no extensions needed
- Native C++ via FFI / extension: another 6β8Γ (a v10 code in ~8β9 Β΅s); SIMD mask selection with runtime AVX2/AVX-512 dispatch on x86-64, NEON on arm64
- 64-bit Optimized: 2Γ faster with int-pair bit packing (no extensions needed)
- Portable Fallback: Works on any PHP 8.2+, 32-bit or 64-bit
Auto-selects the fastest encoder available β no configuration needed.
π¦ Zero Dependencies
- No Composer packages to install
- No GD, Imagick, or extensions required
- Single
composer require, instant QR codes
π¨ 8 Output Formats SVG, PNG (pure PHP, 1-bit), HTML (div/table), ASCII (3 styles). Works in terminals, browsers, emails, and print.
π§ Full QR Spec Support
- All versions v1βv40 (17 to 2953 bytes)
- All error correction levels (L/M/Q/H)
- Custom styling, colors, labels, dark mode
Features
- Zero dependencies β no external packages, no PHP extensions required
- 8 built-in renderers β SVG, PNG, HTML (div/table), ASCII (full/half/simple blocks)
- All QR versions β v1βv40, all error correction levels (L/M/Q/H)
- High performance β pure PHP encodes v1 in ~15 Β΅s / v10 in ~60 Β΅s (JIT); native C++ extension/FFI adds another 6β8Γ
- Customizable β module styles, colors, labels, dark mode, margins
- Type-safe β strict types, enums, readonly properties, PHP 8.2+ idioms
Installation
composer require crazy-goat/scanmephp
Binary Auto-Download
When you install or update the package via Composer, the library will automatically:
- Detect your platform (Linux glibc/musl, macOS Intel/ARM)
- Try to download and install the PHP extension (
scanmeqr) β fastest option (190β360Γ faster) - Fall back to FFI library if extension is not available β 90β130Γ faster
- Use pure PHP encoder as final fallback β works everywhere
If no binary matches your platform β arm64 Linux, an unusual PHP build β the extension can be
compiled on the spot with PIE: pie install crazy-goat/qrcode-ext.
Checksum Verification
A downloaded binary is hashed with SHA-256 right after the transfer, compared with the checksum the
installer expects, and deleted again when it does not match. A binary that is already in
vendor/crazy-goat/scanmephp/{ext,ffi}-binaries/ is checked the same way whenever the plugin runs,
that is when ScanMePHP is installed or updated β not on a plain composer install that leaves the
installed version alone. Verification is fail-closed: without a checksum for the requested version
and binary, nothing is kept and the plugin prints which binary it wanted.
Checksums are read from extra.scanmephp.checksums in your composer.json β the installer
never trusts a digest it fetched next to the binary it is checking. Every release publishes the
digests of all its binaries in checksums.txt, so pinning the one for your platform is a
copy-paste. That file exists from the first release that ships this feature; earlier releases have
none, so there is nothing to copy from them and the pure PHP encoder is used until the release you
install has one.
gh release download vX.Y.Z -p checksums.txt
grep libscanme_qr-linux-glibc checksums.txt # "<digest> <binary name>"
{
"extra": {
"scanmephp": {
"checksums": {
"X.Y.Z": {
"libscanme_qr-linux-glibc-x86_64.so": "<the digest, first column only>"
}
}
}
}
}
Copy the 64-character digest only. A whole sha256sum line pasted as the value is a pin that never
matches, so every install deletes the binary and then refuses to download it again.
The same file verifies a manual download; check just what you have with
sha256sum -c --ignore-missing checksums.txt (Linux) or shasum -a 256 -c checksums.txt (macOS).
Until you pin a digest, composer install installs no native code and the pure PHP encoder is used.
PHP Extension Installation (Recommended)
The PHP extension provides the best performance. The Composer plugin will attempt to download it automatically.
Auto-Download
During composer install or composer update, the plugin will:
- Check if the
scanmeqrextension is already loaded - Download the appropriate prebuilt binary for your platform
- Provide instructions to enable it in
php.ini
Manual Installation
- Download the appropriate binary from GitHub Releases:
| Platform | PHP 8.2 | PHP 8.3 | PHP 8.4 |
|---|---|---|---|
| Linux (glibc) | php-ext-linux-glibc-x86_64-php82.so |
php-ext-linux-glibc-x86_64-php83.so |
php-ext-linux-glibc-x86_64-php84.so |
| Linux (musl/Alpine) | php-ext-linux-musl-x86_64-php82.so |
php-ext-linux-musl-x86_64-php83.so |
php-ext-linux-musl-x86_64-php84.so |
| macOS Intel | php-ext-macos-x86_64-php82.so |
php-ext-macos-x86_64-php83.so |
php-ext-macos-x86_64-php84.so |
| macOS Apple Silicon | php-ext-macos-arm64-php82.so |
php-ext-macos-arm64-php83.so |
php-ext-macos-arm64-php84.so |
Note: Binaries are built for specific PHP versions due to ABI compatibility. Make sure to download the binary matching your PHP version (check with
php -v).
-
Copy to your PHP extensions directory:
cp php-ext-linux-glibc-x86_64.so $(php-config --extension-dir)/ -
Add to your
php.ini:extension=scanmeqr.so -
Restart your web server or PHP-FPM:
sudo systemctl restart php-fpm # or sudo systemctl restart apache2 -
Verify installation:
php -m | grep scanmeqr
Installing with PIE
The extension is published as a PIE package, which builds it from source for whatever PHP you are running β including platforms no prebuilt binary covers, such as arm64 Linux:
composer require crazy-goat/scanmephp pie install crazy-goat/qrcode-ext
Both halves are needed: the extension builds a CrazyGoat\ScanMePHP\Matrix and can only throw
without the library loaded. Building needs a C++20 compiler and takes a few seconds; there is
nothing else to install, since the C++ core is compiled into the extension rather than linked
against libscanme_qr.
crazy-goat/qrcode-ext is generated from php-ext/
and clib/ by bin/build-ext-mirror.sh β issues and pull requests belong in this repository.
Building from Source
Requirements:
- PHP 8.2+ with
php-dev/phpize - C++20 compiler (GCC 10+ or Clang 12+)
- Make
cd php-ext phpize ./configure # finds ../clib on its own make -j$(nproc) make install cd ..
Then add extension=scanmeqr.so to your php.ini.
CMake is only needed for the FFI library and the C++ test suite; the extension does not use it.
FFI Library Installation
If the PHP extension is not available, the plugin will download the FFI library instead.
Requirements for Auto-Download
- FFI extension (
extension=ffiin php.ini) - cURL extension for downloading
- Write permissions to
ffi-binaries/directory in your project
Manual Binary Installation
If auto-download doesn't work, you can manually download binaries from the GitHub releases page and place them in your project directory.
Prebuilt FFI library binaries are available for:
| Platform | Binary |
|---|---|
| Linux (glibc) | libscanme_qr-linux-glibc-x86_64.so |
| Linux (musl/Alpine) | libscanme_qr-linux-musl-x86_64.so |
| macOS Intel | libscanme_qr-macos-x86_64.dylib |
| macOS Apple Silicon | libscanme_qr-macos-arm64.dylib |
Windows: no prebuilt binaries are published. ScanMePHP still works β it falls back to the pure-PHP encoder, which needs no extension and no FFI. For native speed on Windows, build
clib/from source with MSVC and pointFfiEncoderat the resultingscanme_qr.dll.
Quick Start
use CrazyGoat\ScanMePHP\QRCode; $qr = new QRCode('https://example.com'); echo $qr->render();
Renderers
ScanMePHP ships with 8 renderers. Each implements RendererInterface and can be passed as the engine parameter.
| Renderer | Output | Constructor Options |
|---|---|---|
FullBlocksRenderer |
ASCII β blocks |
sideMargin (int, default: 0) |
HalfBlocksRenderer |
ASCII βββ compact |
sideMargin (int, default: 0) |
SimpleRenderer |
ASCII β dots |
sideMargin (int, default: 0) |
SvgRenderer |
SVG XML | moduleSize (int, default: 10) |
PngRenderer |
PNG image (1-bit) | moduleSize (int, default: 10), compressionLevel (int 0β9, default: 1) |
HtmlDivRenderer |
HTML <div> grid |
moduleSize (int, default: 10), fullHtml (bool, default: false) |
HtmlTableRenderer |
HTML <table> |
moduleSize (int, default: 10), fullHtml (bool, default: false) |
ASCII β FullBlocksRenderer (default)
Example: qrcode_fullblocks.txt
use CrazyGoat\ScanMePHP\QRCode; use CrazyGoat\ScanMePHP\QRCodeConfig; use CrazyGoat\ScanMePHP\Renderer\FullBlocksRenderer; $config = new QRCodeConfig( engine: new FullBlocksRenderer(sideMargin: 4), label: 'ScanMePHP', ); $qr = new QRCode('https://example.com', $config); echo $qr->render();
ASCII β HalfBlocksRenderer
Example: qrcode_halfblocks.txt
Compact output β two rows per character using βββ half-block characters.
use CrazyGoat\ScanMePHP\Renderer\HalfBlocksRenderer; $config = new QRCodeConfig( engine: new HalfBlocksRenderer(sideMargin: 4), );
ASCII β SimpleRenderer
Example: qrcode_simple.txt
Uses β dots. Works in terminals without full Unicode block support.
use CrazyGoat\ScanMePHP\Renderer\SimpleRenderer; $config = new QRCodeConfig( engine: new SimpleRenderer(sideMargin: 4), );
SVG β SvgRenderer
Examples: qrcode.svg | qrcode_rounded.svg | qrcode_dark.svg | qrcode_with_label.svg
use CrazyGoat\ScanMePHP\Renderer\SvgRenderer; use CrazyGoat\ScanMePHP\ModuleStyle; $config = new QRCodeConfig( engine: new SvgRenderer(moduleSize: 12), moduleStyle: ModuleStyle::Rounded, // Square, Rounded, or Dot label: 'Scan Me!', ); $qr = new QRCode('https://example.com', $config); $qr->saveToFile('qrcode.svg');
PNG β PngRenderer
Examples: qrcode.png | qrcode_small.png | qrcode_large.png | qrcode_high_ecc.png
Generates valid PNG files in pure PHP β no GD, no Imagick, no external libraries. Black and white only, 1-bit monochrome. Ideal for email attachments, API responses, and print. Repeated pixel rows are stored with the PNG Up filter, so the default zlib level 1 already gives ~2 KB files in ~0.1 ms; pass compressionLevel: 6 for the smallest output.
Note: Labels are not supported in PNG output (no font engine). Passing a
labelwill throw aRenderException.
use CrazyGoat\ScanMePHP\Renderer\PngRenderer; $config = new QRCodeConfig( engine: new PngRenderer(moduleSize: 10), ); $qr = new QRCode('https://example.com', $config); $qr->saveToFile('qrcode.png'); // Or use as data URI (e.g. in <img> tags) $dataUri = $qr->getDataUri(); // data:image/png;base64,...
HTML β HtmlDivRenderer
Examples: qrcode_div.html | qrcode_div_full.html | qrcode_div_inverted.html | qrcode_div_label.html
Renders QR as a <div> flexbox grid with inline styles. No external CSS needed.
use CrazyGoat\ScanMePHP\Renderer\HtmlDivRenderer; $config = new QRCodeConfig( engine: new HtmlDivRenderer(moduleSize: 10, fullHtml: false), label: 'ScanMePHP', ); $qr = new QRCode('https://example.com', $config); // Fragment only (for embedding) $html = $qr->render(); // Full HTML page $config = new QRCodeConfig( engine: new HtmlDivRenderer(fullHtml: true), );
HTML β HtmlTableRenderer
Examples: qrcode_table.html | qrcode_table_full.html | qrcode_table_inverted.html | qrcode_table_label.html
Same as above but uses <table> with <td> elements.
use CrazyGoat\ScanMePHP\Renderer\HtmlTableRenderer; $config = new QRCodeConfig( engine: new HtmlTableRenderer(moduleSize: 8, fullHtml: true), );
Configuration
All options are set via QRCodeConfig:
use CrazyGoat\ScanMePHP\QRCodeConfig; use CrazyGoat\ScanMePHP\ErrorCorrectionLevel; use CrazyGoat\ScanMePHP\ModuleStyle; use CrazyGoat\ScanMePHP\Renderer\SvgRenderer; $config = new QRCodeConfig( engine: new SvgRenderer(), // renderer instance errorCorrectionLevel: ErrorCorrectionLevel::Medium, // Low, Medium, Quartile, High label: 'My QR Code', // optional label below QR size: 0, // QR version 1-40, 0 = auto margin: 4, // quiet zone in modules foregroundColor: '#000000', backgroundColor: '#FFFFFF', moduleStyle: ModuleStyle::Square, // Square, Rounded, Dot (SVG only) invert: false, // swap foreground/background );
Dark Mode (Inverted)
$config = new QRCodeConfig( engine: new FullBlocksRenderer(sideMargin: 4), invert: true, label: 'Dark Mode', );
For SVG/HTML renderers, set explicit colors:
$config = new QRCodeConfig( engine: new SvgRenderer(), invert: true, foregroundColor: '#FFFFFF', backgroundColor: '#000000', );
Output Methods
$qr = new QRCode('https://example.com', $config); $qr->render(); // returns string $qr->saveToFile('qr.svg'); // writes to file $qr->getDataUri(); // data:image/svg+xml;base64,... $qr->toBase64(); // raw base64 $qr->toHttpResponse(); // sends Content-Type header, outputs, exits $qr->getMatrix(); // raw Matrix object $qr->validate(); // true if data fits in QR version echo $qr; // __toString() calls render()
Custom Renderer
Implement RendererInterface:
use CrazyGoat\ScanMePHP\RendererInterface; use CrazyGoat\ScanMePHP\Matrix; use CrazyGoat\ScanMePHP\RenderOptions; class MyCustomRenderer implements RendererInterface { public function render(Matrix $matrix, RenderOptions $options): string { $size = $matrix->getSize(); for ($y = 0; $y < $size; $y++) { for ($x = 0; $x < $size; $x++) { $isDark = $matrix->get($x, $y); // ... your rendering logic } } return $output; } public function getContentType(): string { return 'text/plain'; } }
Performance
ScanMePHP includes four encoder implementations. QRCode auto-selects the fastest available:
| Encoder | Versions | Requirements | Relative Speed |
|---|---|---|---|
NativeEncoderExt |
v1βv40 | 64-bit PHP + scanmeqr extension |
7β9Γ faster (a v10 code in ~7 Β΅s) |
FfiEncoder |
v1βv40 | 64-bit PHP + FFI + libscanme_qr.so |
6β8Γ faster (a v10 code in ~8 Β΅s) |
FastEncoder |
v1βv27 | 64-bit PHP | baseline (bitset fast path) |
Encoder |
v1βv40 | 64-bit PHP 8.2+ | baseline β same fast path for v1βv27, scalar pipeline for v28βv40 |
Capacity (Byte Mode)
Maximum data length for URL/text encoding (Byte mode) at different QR versions:
| Version | Size | L (Low) | M (Medium) | Q (Quartile) | H (High) |
|---|---|---|---|---|---|
| v1 | 21Γ21 | 17 | 14 | 11 | 7 |
| v10 | 57Γ57 | 271 | 213 | 151 | 119 |
| v27 | 125Γ125 | 1465 | 1125 | 805 | 625 |
| v40 | 177Γ177 | 2953 | 2331 | 1663 | 1273 |
Note: FastEncoder supports up to v27 (1465 bytes max). For larger data, the portable Encoder's v28βv40 pipeline is automatically used.
Benchmark Results
Measured on PHP 8.5 (opcache.jit=tracing) / Apple M-series, 500 iterations per case, median latency:
| Test case | Encoder | FastEncoder | FfiEncoder | NativeEncoderExt | Speedup (Encoder/Ext) |
|---|---|---|---|---|---|
| v1 (21Γ21) L | 0.016 ms | 0.015 ms | 0.003 ms | 0.002 ms | 7Γ |
| v5 (37Γ37) L | 0.031 ms | 0.031 ms | 0.004 ms | 0.004 ms | 8Γ |
| v10 (57Γ57) L | 0.061 ms | 0.060 ms | 0.008 ms | 0.009 ms | 7Γ |
| v20 (97Γ97) L | 0.195 ms | 0.200 ms | 0.025 ms | 0.030 ms | 6.5Γ |
Before the 2026-08 optimisation pass pure PHP took 0.425 ms (v1) and 3.2 ms (v10); the pure-PHP encoders are now 20β50Γ faster, so the native tiers matter mostly for high-volume generation. Without the JIT pure PHP is ~4Γ slower.
The C++ library alone encodes v1 in ~1.5 Β΅s, v10 in ~6 Β΅s and v40 in ~80 Β΅s
(clib/bench/scanme_bench); the rest is the PHP boundary.
All four encoders produce identical, spec-compliant QR codes verified against nayuki's reference implementation.
Run the benchmark yourself:
php bench/benchmark_encoder.php # 200 iterations php bench/benchmark_encoder.php 500 # 500 iterations php -d extension=php-ext/modules/scanmeqr.so bench/benchmark_all.php 500 # incl. the extension
See BENCHMARK.md for full results, the C++-only benchmark and a description of the SIMD mask-selection kernel.
Building the C++ Library (optional)
The native C++ encoder is optional β ScanMePHP works without it. To enable FfiEncoder:
cmake -B clib/build -S clib -DCMAKE_BUILD_TYPE=Release cmake --build clib/build -j$(nproc) cp clib/build/libscanme_qr.so .
Then pass the library path when creating the encoder:
use CrazyGoat\ScanMePHP\FfiEncoder; $encoder = new FfiEncoder(__DIR__ . '/libscanme_qr.so'); $qr = new QRCode('https://example.com', encoder: $encoder);
Or let QRCode auto-detect it (looks for clib/build/libscanme_qr.so in the project root).
Prebuilt Binaries
Prebuilt binaries are available from GitHub Releases. Download the appropriate binary for your platform:
PHP Extension Binaries (Recommended)
| Platform | Binary | Download |
|---|---|---|
| Linux (glibc) | php-ext-linux-glibc-x86_64.so |
Latest Release |
| Linux (musl/Alpine) | php-ext-linux-musl-x86_64.so |
Latest Release |
| macOS Intel | php-ext-macos-x86_64.so |
Latest Release |
| macOS Apple Silicon | php-ext-macos-arm64.so |
Latest Release |
FFI Library Binaries
| Platform | Binary | Download |
|---|---|---|
| Linux (glibc) | libscanme_qr-linux-glibc-x86_64.so |
Latest Release |
| Linux (musl/Alpine) | libscanme_qr-linux-musl-x86_64.so |
Latest Release |
| macOS Intel | libscanme_qr-macos-x86_64.dylib |
Latest Release |
| macOS Apple Silicon | libscanme_qr-macos-arm64.dylib |
Latest Release |
Windows: no prebuilt binaries are published. ScanMePHP still works β it falls back to the pure-PHP encoder, which needs no extension and no FFI. For native speed on Windows, build
clib/from source with MSVC and pointFfiEncoderat the resultingscanme_qr.dll.
Place the downloaded binary in your project directory. The FfiEncoder will automatically detect and load it.
Requirements
- PHP >= 8.2
- No extensions required
- No external dependencies
- Optional: C++20 compiler + CMake for native FFI encoder
Testing
composer test
Examples
See the examples/ directory. Run any example:
php examples/ascii_fullblocks.php php examples/svg_example.php php examples/png_example.php php examples/html_div.php php examples/html_table.php
Generated output files are saved to examples/generated-assets/.
License
MIT β see LICENSE.
Contributing
See CONTRIBUTING.md.