├── assets └── open_sans.ttf ├── src ├── Encoding │ ├── EncodingInterface.php │ └── Encoding.php ├── Label │ ├── Font │ │ ├── FontInterface.php │ │ ├── OpenSans.php │ │ └── Font.php │ ├── LabelAlignment.php │ ├── Margin │ │ ├── MarginInterface.php │ │ └── Margin.php │ ├── LabelInterface.php │ └── Label.php ├── ErrorCorrectionLevel.php ├── RoundBlockSizeMode.php ├── Matrix │ ├── MatrixFactoryInterface.php │ ├── MatrixInterface.php │ └── Matrix.php ├── Builder │ ├── BuilderRegistryInterface.php │ ├── BuilderRegistry.php │ ├── BuilderInterface.php │ └── Builder.php ├── Writer │ ├── ValidatingWriterInterface.php │ ├── Result │ │ ├── GifResult.php │ │ ├── ResultInterface.php │ │ ├── EpsResult.php │ │ ├── PdfResult.php │ │ ├── PngResult.php │ │ ├── AbstractResult.php │ │ ├── GdResult.php │ │ ├── WebPResult.php │ │ ├── BinaryResult.php │ │ ├── SvgResult.php │ │ ├── ConsoleResult.php │ │ └── DebugResult.php │ ├── WriterInterface.php │ ├── BinaryWriter.php │ ├── GifWriter.php │ ├── ConsoleWriter.php │ ├── WebPWriter.php │ ├── PngWriter.php │ ├── DebugWriter.php │ ├── EpsWriter.php │ ├── PdfWriter.php │ ├── AbstractGdWriter.php │ └── SvgWriter.php ├── Logo │ ├── LogoInterface.php │ └── Logo.php ├── Color │ ├── ColorInterface.php │ └── Color.php ├── QrCodeInterface.php ├── Bacon │ ├── ErrorCorrectionLevelConverter.php │ └── MatrixFactory.php ├── Exception │ └── ValidationException.php ├── ImageData │ ├── LabelImageData.php │ └── LogoImageData.php └── QrCode.php ├── LICENSE ├── composer.json └── README.md /assets/open_sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasschnelli/qr-code/main/assets/open_sans.ttf -------------------------------------------------------------------------------- /src/Encoding/EncodingInterface.php: -------------------------------------------------------------------------------- 1 | */ 18 | public function toArray(): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/Writer/Result/GifResult.php: -------------------------------------------------------------------------------- 1 | image); 13 | 14 | return strval(ob_get_clean()); 15 | } 16 | 17 | public function getMimeType(): string 18 | { 19 | return 'image/gif'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Writer/Result/ResultInterface.php: -------------------------------------------------------------------------------- 1 | size; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Color/ColorInterface.php: -------------------------------------------------------------------------------- 1 | */ 22 | public function toArray(): array; 23 | } 24 | -------------------------------------------------------------------------------- /src/Matrix/MatrixInterface.php: -------------------------------------------------------------------------------- 1 | $options */ 15 | public function write(QrCodeInterface $qrCode, ?LogoInterface $logo = null, ?LabelInterface $label = null, array $options = []): ResultInterface; 16 | } 17 | -------------------------------------------------------------------------------- /src/Label/LabelInterface.php: -------------------------------------------------------------------------------- 1 | $lines */ 14 | private readonly array $lines, 15 | ) { 16 | parent::__construct($matrix); 17 | } 18 | 19 | public function getString(): string 20 | { 21 | return implode("\n", $this->lines); 22 | } 23 | 24 | public function getMimeType(): string 25 | { 26 | return 'image/eps'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builder/BuilderRegistry.php: -------------------------------------------------------------------------------- 1 | */ 10 | private array $builders = []; 11 | 12 | public function set(string $name, BuilderInterface $builder): void 13 | { 14 | $this->builders[$name] = $builder; 15 | } 16 | 17 | public function get(string $name): BuilderInterface 18 | { 19 | if (!isset($this->builders[$name])) { 20 | throw new \Exception(sprintf('Builder with name "%s" not available from registry', $name)); 21 | } 22 | 23 | return $this->builders[$name]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QrCodeInterface.php: -------------------------------------------------------------------------------- 1 | fpdf; 21 | } 22 | 23 | public function getString(): string 24 | { 25 | return $this->fpdf->Output('S'); 26 | } 27 | 28 | public function getMimeType(): string 29 | { 30 | return 'application/pdf'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Writer/Result/PngResult.php: -------------------------------------------------------------------------------- 1 | image, quality: $this->quality); 23 | 24 | return strval(ob_get_clean()); 25 | } 26 | 27 | public function getMimeType(): string 28 | { 29 | return 'image/png'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Label/Font/Font.php: -------------------------------------------------------------------------------- 1 | assertValidPath($path); 14 | } 15 | 16 | private function assertValidPath(string $path): void 17 | { 18 | if (!file_exists($path)) { 19 | throw new \Exception(sprintf('Invalid font path "%s"', $path)); 20 | } 21 | } 22 | 23 | public function getPath(): string 24 | { 25 | return $this->path; 26 | } 27 | 28 | public function getSize(): int 29 | { 30 | return $this->size; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Writer/BinaryWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 20 | 21 | return new BinaryResult($matrix); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Writer/Result/AbstractResult.php: -------------------------------------------------------------------------------- 1 | matrix; 19 | } 20 | 21 | public function getDataUri(): string 22 | { 23 | return 'data:'.$this->getMimeType().';base64,'.base64_encode($this->getString()); 24 | } 25 | 26 | public function saveToFile(string $path): void 27 | { 28 | $string = $this->getString(); 29 | file_put_contents($path, $string); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Writer/GifWriter.php: -------------------------------------------------------------------------------- 1 | getMatrix(), $gdResult->getImage()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Writer/Result/GdResult.php: -------------------------------------------------------------------------------- 1 | image; 21 | } 22 | 23 | public function getString(): string 24 | { 25 | throw new \Exception('You can only use this method in a concrete implementation'); 26 | } 27 | 28 | public function getMimeType(): string 29 | { 30 | throw new \Exception('You can only use this method in a concrete implementation'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Writer/ConsoleWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 20 | 21 | return new ConsoleResult($matrix, $qrCode->getForegroundColor(), $qrCode->getBackgroundColor()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Encoding/Encoding.php: -------------------------------------------------------------------------------- 1 | value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Logo/Logo.php: -------------------------------------------------------------------------------- 1 | path; 20 | } 21 | 22 | public function getResizeToWidth(): ?int 23 | { 24 | return $this->resizeToWidth; 25 | } 26 | 27 | public function getResizeToHeight(): ?int 28 | { 29 | return $this->resizeToHeight; 30 | } 31 | 32 | public function getPunchoutBackground(): bool 33 | { 34 | return $this->punchoutBackground; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Bacon/ErrorCorrectionLevelConverter.php: -------------------------------------------------------------------------------- 1 | BaconErrorCorrectionLevel::valueOf('L'), 16 | ErrorCorrectionLevel::Medium => BaconErrorCorrectionLevel::valueOf('M'), 17 | ErrorCorrectionLevel::Quartile => BaconErrorCorrectionLevel::valueOf('Q'), 18 | ErrorCorrectionLevel::High => BaconErrorCorrectionLevel::valueOf('H'), 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ValidationException.php: -------------------------------------------------------------------------------- 1 | image, quality: $this->quality); 27 | 28 | return strval(ob_get_clean()); 29 | } 30 | 31 | public function getMimeType(): string 32 | { 33 | return 'image/webp'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Writer/Result/BinaryResult.php: -------------------------------------------------------------------------------- 1 | getMatrix(); 19 | 20 | $binaryString = ''; 21 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 22 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 23 | $binaryString .= $matrix->getBlockValue($rowIndex, $columnIndex); 24 | } 25 | $binaryString .= "\n"; 26 | } 27 | 28 | return $binaryString; 29 | } 30 | 31 | public function getMimeType(): string 32 | { 33 | return 'text/plain'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Label/Margin/Margin.php: -------------------------------------------------------------------------------- 1 | top; 20 | } 21 | 22 | public function getRight(): int 23 | { 24 | return $this->right; 25 | } 26 | 27 | public function getBottom(): int 28 | { 29 | return $this->bottom; 30 | } 31 | 32 | public function getLeft(): int 33 | { 34 | return $this->left; 35 | } 36 | 37 | /** @return array */ 38 | public function toArray(): array 39 | { 40 | return [ 41 | 'top' => $this->top, 42 | 'right' => $this->right, 43 | 'bottom' => $this->bottom, 44 | 'left' => $this->left, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Writer/WebPWriter.php: -------------------------------------------------------------------------------- 1 | getMatrix(), $gdResult->getImage(), $options[self::WRITER_OPTION_QUALITY]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 (c) Jeroen van den Enden 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Writer/PngWriter.php: -------------------------------------------------------------------------------- 1 | getMatrix(), $gdResult->getImage(), $options[self::WRITER_OPTION_COMPRESSION_LEVEL]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Writer/Result/SvgResult.php: -------------------------------------------------------------------------------- 1 | xml; 22 | } 23 | 24 | public function getString(): string 25 | { 26 | $string = $this->xml->asXML(); 27 | 28 | if (!is_string($string)) { 29 | throw new \Exception('Could not save SVG XML to string'); 30 | } 31 | 32 | if ($this->excludeXmlDeclaration) { 33 | $string = str_replace("\n", '', $string); 34 | } 35 | 36 | return $string; 37 | } 38 | 39 | public function getMimeType(): string 40 | { 41 | return 'image/svg+xml'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Writer/DebugWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 20 | 21 | return new DebugResult($matrix, $qrCode, $logo, $label, $options); 22 | } 23 | 24 | public function validateResult(ResultInterface $result, string $expectedData): void 25 | { 26 | if (!$result instanceof DebugResult) { 27 | throw new \Exception('Unable to write logo: instance of DebugResult expected'); 28 | } 29 | 30 | $result->setValidateResult(true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Color/Color.php: -------------------------------------------------------------------------------- 1 | red; 20 | } 21 | 22 | public function getGreen(): int 23 | { 24 | return $this->green; 25 | } 26 | 27 | public function getBlue(): int 28 | { 29 | return $this->blue; 30 | } 31 | 32 | public function getAlpha(): int 33 | { 34 | return $this->alpha; 35 | } 36 | 37 | public function getOpacity(): float 38 | { 39 | return 1 - $this->alpha / 127; 40 | } 41 | 42 | public function getHex(): string 43 | { 44 | return sprintf('#%02x%02x%02x', $this->red, $this->green, $this->blue); 45 | } 46 | 47 | public function toArray(): array 48 | { 49 | return [ 50 | 'red' => $this->red, 51 | 'green' => $this->green, 52 | 'blue' => $this->blue, 53 | 'alpha' => $this->alpha, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Bacon/MatrixFactory.php: -------------------------------------------------------------------------------- 1 | getErrorCorrectionLevel()); 18 | $baconMatrix = Encoder::encode($qrCode->getData(), $baconErrorCorrectionLevel, strval($qrCode->getEncoding()))->getMatrix(); 19 | 20 | $blockValues = []; 21 | $columnCount = $baconMatrix->getWidth(); 22 | $rowCount = $baconMatrix->getHeight(); 23 | for ($rowIndex = 0; $rowIndex < $rowCount; ++$rowIndex) { 24 | $blockValues[$rowIndex] = []; 25 | for ($columnIndex = 0; $columnIndex < $columnCount; ++$columnIndex) { 26 | $blockValues[$rowIndex][$columnIndex] = $baconMatrix->get($columnIndex, $rowIndex); 27 | } 28 | } 29 | 30 | return new Matrix($blockValues, $qrCode->getSize(), $qrCode->getMargin(), $qrCode->getRoundBlockSizeMode()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Label/Label.php: -------------------------------------------------------------------------------- 1 | text; 28 | } 29 | 30 | public function getFont(): FontInterface 31 | { 32 | return $this->font; 33 | } 34 | 35 | public function getAlignment(): LabelAlignment 36 | { 37 | return $this->alignment; 38 | } 39 | 40 | public function getMargin(): MarginInterface 41 | { 42 | return $this->margin; 43 | } 44 | 45 | public function getTextColor(): ColorInterface 46 | { 47 | return $this->textColor; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ImageData/LabelImageData.php: -------------------------------------------------------------------------------- 1 | getText(), "\n")) { 20 | throw new \Exception('Label does not support line breaks'); 21 | } 22 | 23 | if (!function_exists('imagettfbbox')) { 24 | throw new \Exception('Function "imagettfbbox" does not exist: check your FreeType installation'); 25 | } 26 | 27 | $labelBox = imagettfbbox($label->getFont()->getSize(), 0, $label->getFont()->getPath(), $label->getText()); 28 | 29 | if (!is_array($labelBox)) { 30 | throw new \Exception('Unable to generate label image box: check your FreeType installation'); 31 | } 32 | 33 | return new self( 34 | intval($labelBox[2] - $labelBox[0]), 35 | intval($labelBox[0] - $labelBox[7]) 36 | ); 37 | } 38 | 39 | public function getWidth(): int 40 | { 41 | return $this->width; 42 | } 43 | 44 | public function getHeight(): int 45 | { 46 | return $this->height; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "endroid/qr-code", 3 | "description": "Endroid QR Code", 4 | "keywords": ["endroid", "qrcode", "qr", "code", "php"], 5 | "homepage": "https://github.com/endroid/qr-code", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jeroen van den Enden", 11 | "email": "info@endroid.nl" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.2", 16 | "bacon/bacon-qr-code": "^3.0" 17 | }, 18 | "require-dev": { 19 | "ext-gd": "*", 20 | "endroid/quality": "dev-main", 21 | "khanamiryan/qrcode-detector-decoder": "^2.0.2", 22 | "setasign/fpdf": "^1.8.2" 23 | }, 24 | "suggest": { 25 | "ext-gd": "Enables you to write PNG images", 26 | "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator", 27 | "roave/security-advisories": "Makes sure package versions with known security issues are not installed", 28 | "setasign/fpdf": "Enables you to use the PDF writer" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Endroid\\QrCode\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Endroid\\QrCode\\Tests\\": "tests/" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true, 42 | "preferred-install": { 43 | "endroid/*": "source" 44 | }, 45 | "allow-plugins": { 46 | "endroid/installer": true 47 | } 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-main": "6.x-dev" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Builder/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | |null $writerOptions */ 20 | public function build( 21 | ?WriterInterface $writer = null, 22 | ?array $writerOptions = null, 23 | ?bool $validateResult = null, 24 | // QrCode options 25 | ?string $data = null, 26 | ?EncodingInterface $encoding = null, 27 | ?ErrorCorrectionLevel $errorCorrectionLevel = null, 28 | ?int $size = null, 29 | ?int $margin = null, 30 | ?RoundBlockSizeMode $roundBlockSizeMode = null, 31 | ?ColorInterface $foregroundColor = null, 32 | ?ColorInterface $backgroundColor = null, 33 | // Label options 34 | ?string $labelText = null, 35 | ?FontInterface $labelFont = null, 36 | ?LabelAlignment $labelAlignment = null, 37 | ?MarginInterface $labelMargin = null, 38 | ?ColorInterface $labelTextColor = null, 39 | // Logo options 40 | ?string $logoPath = null, 41 | ?int $logoResizeToWidth = null, 42 | ?int $logoResizeToHeight = null, 43 | ?bool $logoPunchoutBackground = null, 44 | ): ResultInterface; 45 | } 46 | -------------------------------------------------------------------------------- /src/QrCode.php: -------------------------------------------------------------------------------- 1 | data; 29 | } 30 | 31 | public function getEncoding(): EncodingInterface 32 | { 33 | return $this->encoding; 34 | } 35 | 36 | public function getErrorCorrectionLevel(): ErrorCorrectionLevel 37 | { 38 | return $this->errorCorrectionLevel; 39 | } 40 | 41 | public function getSize(): int 42 | { 43 | return $this->size; 44 | } 45 | 46 | public function getMargin(): int 47 | { 48 | return $this->margin; 49 | } 50 | 51 | public function getRoundBlockSizeMode(): RoundBlockSizeMode 52 | { 53 | return $this->roundBlockSizeMode; 54 | } 55 | 56 | public function getForegroundColor(): ColorInterface 57 | { 58 | return $this->foregroundColor; 59 | } 60 | 61 | public function getBackgroundColor(): ColorInterface 62 | { 63 | return $this->backgroundColor; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Writer/Result/ConsoleResult.php: -------------------------------------------------------------------------------- 1 | ' ', 14 | 1 => "\xe2\x96\x80", 15 | 2 => "\xe2\x96\x84", 16 | 3 => "\xe2\x96\x88", 17 | ]; 18 | 19 | private string $colorEscapeCode; 20 | 21 | public function __construct( 22 | MatrixInterface $matrix, 23 | ColorInterface $foreground, 24 | ColorInterface $background, 25 | ) { 26 | parent::__construct($matrix); 27 | 28 | $this->colorEscapeCode = sprintf( 29 | "\e[38;2;%d;%d;%dm\e[48;2;%d;%d;%dm", 30 | $foreground->getRed(), 31 | $foreground->getGreen(), 32 | $foreground->getBlue(), 33 | $background->getRed(), 34 | $background->getGreen(), 35 | $background->getBlue() 36 | ); 37 | } 38 | 39 | public function getMimeType(): string 40 | { 41 | return 'text/plain'; 42 | } 43 | 44 | public function getString(): string 45 | { 46 | $matrix = $this->getMatrix(); 47 | 48 | $side = $matrix->getBlockCount(); 49 | $marginLeft = $this->colorEscapeCode.self::TWO_BLOCKS[0].self::TWO_BLOCKS[0]; 50 | $marginRight = self::TWO_BLOCKS[0].self::TWO_BLOCKS[0]."\e[0m".PHP_EOL; 51 | $marginVertical = $marginLeft.str_repeat(self::TWO_BLOCKS[0], $side).$marginRight; 52 | 53 | $qrCodeString = $marginVertical; 54 | for ($rowIndex = 0; $rowIndex < $side; $rowIndex += 2) { 55 | $qrCodeString .= $marginLeft; 56 | for ($columnIndex = 0; $columnIndex < $side; ++$columnIndex) { 57 | $combined = $matrix->getBlockValue($rowIndex, $columnIndex); 58 | if ($rowIndex + 1 < $side) { 59 | $combined |= $matrix->getBlockValue($rowIndex + 1, $columnIndex) << 1; 60 | } 61 | $qrCodeString .= self::TWO_BLOCKS[$combined]; 62 | } 63 | $qrCodeString .= $marginRight; 64 | } 65 | $qrCodeString .= $marginVertical; 66 | 67 | return $qrCodeString; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Writer/EpsWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 22 | 23 | $lines = [ 24 | '%!PS-Adobe-3.0 EPSF-3.0', 25 | '%%BoundingBox: 0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize(), 26 | '/F { rectfill } def', 27 | number_format($qrCode->getBackgroundColor()->getRed() / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()->getGreen() / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()->getBlue() / 100, 2, '.', ',').' setrgbcolor', 28 | '0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize().' F', 29 | number_format($qrCode->getForegroundColor()->getRed() / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()->getGreen() / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()->getBlue() / 100, 2, '.', ',').' setrgbcolor', 30 | ]; 31 | 32 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 33 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 34 | if (1 === $matrix->getBlockValue($matrix->getBlockCount() - 1 - $rowIndex, $columnIndex)) { 35 | $x = $matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex; 36 | $y = $matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex; 37 | $lines[] = number_format($x, self::DECIMAL_PRECISION, '.', '').' '.number_format($y, self::DECIMAL_PRECISION, '.', '').' '.number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', '').' '.number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', '').' F'; 38 | } 39 | } 40 | } 41 | 42 | return new EpsResult($matrix, $lines); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Matrix/Matrix.php: -------------------------------------------------------------------------------- 1 | > $blockValues */ 18 | public function __construct( 19 | private array $blockValues, 20 | int $size, 21 | int $margin, 22 | RoundBlockSizeMode $roundBlockSizeMode, 23 | ) { 24 | $blockSize = $size / $this->getBlockCount(); 25 | $innerSize = $size; 26 | $outerSize = $size + 2 * $margin; 27 | 28 | switch ($roundBlockSizeMode) { 29 | case RoundBlockSizeMode::Enlarge: 30 | $blockSize = intval(ceil($blockSize)); 31 | $innerSize = intval($blockSize * $this->getBlockCount()); 32 | $outerSize = $innerSize + 2 * $margin; 33 | break; 34 | case RoundBlockSizeMode::Shrink: 35 | $blockSize = intval(floor($blockSize)); 36 | $innerSize = intval($blockSize * $this->getBlockCount()); 37 | $outerSize = $innerSize + 2 * $margin; 38 | break; 39 | case RoundBlockSizeMode::Margin: 40 | $blockSize = intval(floor($blockSize)); 41 | $innerSize = intval($blockSize * $this->getBlockCount()); 42 | break; 43 | } 44 | 45 | if ($blockSize < 1) { 46 | throw new \Exception('Too much data: increase image dimensions or lower error correction level'); 47 | } 48 | 49 | $this->blockSize = $blockSize; 50 | $this->innerSize = $innerSize; 51 | $this->outerSize = $outerSize; 52 | $this->marginLeft = intval(($this->outerSize - $this->innerSize) / 2); 53 | $this->marginRight = $this->outerSize - $this->innerSize - $this->marginLeft; 54 | } 55 | 56 | public function getBlockValue(int $rowIndex, int $columnIndex): int 57 | { 58 | return $this->blockValues[$rowIndex][$columnIndex]; 59 | } 60 | 61 | public function getBlockCount(): int 62 | { 63 | return count($this->blockValues[0]); 64 | } 65 | 66 | public function getBlockSize(): float 67 | { 68 | return $this->blockSize; 69 | } 70 | 71 | public function getInnerSize(): int 72 | { 73 | return $this->innerSize; 74 | } 75 | 76 | public function getOuterSize(): int 77 | { 78 | return $this->outerSize; 79 | } 80 | 81 | public function getMarginLeft(): int 82 | { 83 | return $this->marginLeft; 84 | } 85 | 86 | public function getMarginRight(): int 87 | { 88 | return $this->marginRight; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Writer/Result/DebugResult.php: -------------------------------------------------------------------------------- 1 | $options */ 22 | private readonly array $options = [], 23 | ) { 24 | parent::__construct($matrix); 25 | } 26 | 27 | public function setValidateResult(bool $validateResult): void 28 | { 29 | $this->validateResult = $validateResult; 30 | } 31 | 32 | public function getString(): string 33 | { 34 | $debugLines = []; 35 | 36 | $debugLines[] = 'Data: '.$this->qrCode->getData(); 37 | $debugLines[] = 'Encoding: '.$this->qrCode->getEncoding(); 38 | $debugLines[] = 'Error Correction Level: '.get_class($this->qrCode->getErrorCorrectionLevel()); 39 | $debugLines[] = 'Size: '.$this->qrCode->getSize(); 40 | $debugLines[] = 'Margin: '.$this->qrCode->getMargin(); 41 | $debugLines[] = 'Round block size mode: '.get_class($this->qrCode->getRoundBlockSizeMode()); 42 | $debugLines[] = 'Foreground color: ['.implode(', ', $this->qrCode->getForegroundColor()->toArray()).']'; 43 | $debugLines[] = 'Background color: ['.implode(', ', $this->qrCode->getBackgroundColor()->toArray()).']'; 44 | 45 | foreach ($this->options as $key => $value) { 46 | $debugLines[] = 'Writer option: '.$key.': '.$value; 47 | } 48 | 49 | if (isset($this->logo)) { 50 | $debugLines[] = 'Logo path: '.$this->logo->getPath(); 51 | $debugLines[] = 'Logo resize to width: '.$this->logo->getResizeToWidth(); 52 | $debugLines[] = 'Logo resize to height: '.$this->logo->getResizeToHeight(); 53 | $debugLines[] = 'Logo punchout background: '.($this->logo->getPunchoutBackground() ? 'true' : 'false'); 54 | } 55 | 56 | if (isset($this->label)) { 57 | $debugLines[] = 'Label text: '.$this->label->getText(); 58 | $debugLines[] = 'Label font path: '.$this->label->getFont()->getPath(); 59 | $debugLines[] = 'Label font size: '.$this->label->getFont()->getSize(); 60 | $debugLines[] = 'Label alignment: '.get_class($this->label->getAlignment()); 61 | $debugLines[] = 'Label margin: ['.implode(', ', $this->label->getMargin()->toArray()).']'; 62 | $debugLines[] = 'Label text color: ['.implode(', ', $this->label->getTextColor()->toArray()).']'; 63 | } 64 | 65 | $debugLines[] = 'Validate result: '.($this->validateResult ? 'true' : 'false'); 66 | 67 | return implode("\n", $debugLines); 68 | } 69 | 70 | public function getMimeType(): string 71 | { 72 | return 'text/plain'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ImageData/LogoImageData.php: -------------------------------------------------------------------------------- 1 | getPath()); 25 | 26 | if (!is_string($data)) { 27 | $errorDetails = error_get_last()['message'] ?? 'invalid data'; 28 | throw new \Exception(sprintf('Could not read logo image data from path "%s": %s', $logo->getPath(), $errorDetails)); 29 | } 30 | 31 | if (false !== filter_var($logo->getPath(), FILTER_VALIDATE_URL)) { 32 | $mimeType = self::detectMimeTypeFromUrl($logo->getPath()); 33 | } else { 34 | $mimeType = self::detectMimeTypeFromPath($logo->getPath()); 35 | } 36 | 37 | $width = $logo->getResizeToWidth(); 38 | $height = $logo->getResizeToHeight(); 39 | 40 | if ('image/svg+xml' === $mimeType) { 41 | if (null === $width || null === $height) { 42 | throw new \Exception('SVG Logos require an explicitly set resize width and height'); 43 | } 44 | 45 | return new self($data, null, $mimeType, $width, $height, $logo->getPunchoutBackground()); 46 | } 47 | 48 | error_clear_last(); 49 | $image = @imagecreatefromstring($data); 50 | 51 | if (!$image) { 52 | $errorDetails = error_get_last()['message'] ?? 'invalid data'; 53 | throw new \Exception(sprintf('Unable to parse image data at path "%s": %s', $logo->getPath(), $errorDetails)); 54 | } 55 | 56 | // No target width and height specified: use from original image 57 | if (null !== $width && null !== $height) { 58 | return new self($data, $image, $mimeType, $width, $height, $logo->getPunchoutBackground()); 59 | } 60 | 61 | // Only target width specified: calculate height 62 | if (null !== $width && null === $height) { 63 | return new self($data, $image, $mimeType, $width, intval(imagesy($image) * $width / imagesx($image)), $logo->getPunchoutBackground()); 64 | } 65 | 66 | // Only target height specified: calculate width 67 | if (null === $width && null !== $height) { 68 | return new self($data, $image, $mimeType, intval(imagesx($image) * $height / imagesy($image)), $height, $logo->getPunchoutBackground()); 69 | } 70 | 71 | return new self($data, $image, $mimeType, imagesx($image), imagesy($image), $logo->getPunchoutBackground()); 72 | } 73 | 74 | public function getData(): string 75 | { 76 | return $this->data; 77 | } 78 | 79 | public function getImage(): \GdImage 80 | { 81 | if (!$this->image instanceof \GdImage) { 82 | throw new \Exception('SVG Images have no image resource'); 83 | } 84 | 85 | return $this->image; 86 | } 87 | 88 | public function getMimeType(): string 89 | { 90 | return $this->mimeType; 91 | } 92 | 93 | public function getWidth(): int 94 | { 95 | return $this->width; 96 | } 97 | 98 | public function getHeight(): int 99 | { 100 | return $this->height; 101 | } 102 | 103 | public function getPunchoutBackground(): bool 104 | { 105 | return $this->punchoutBackground; 106 | } 107 | 108 | public function createDataUri(): string 109 | { 110 | return 'data:'.$this->mimeType.';base64,'.base64_encode($this->data); 111 | } 112 | 113 | private static function detectMimeTypeFromUrl(string $url): string 114 | { 115 | $headers = get_headers($url, true); 116 | 117 | if (!is_array($headers)) { 118 | throw new \Exception(sprintf('Could not retrieve headers to determine content type for logo URL "%s"', $url)); 119 | } 120 | 121 | $headers = array_combine(array_map('strtolower', array_keys($headers)), $headers); 122 | 123 | if (!isset($headers['content-type'])) { 124 | throw new \Exception(sprintf('Content type could not be determined for logo URL "%s"', $url)); 125 | } 126 | 127 | return is_array($headers['content-type']) ? $headers['content-type'][1] : $headers['content-type']; 128 | } 129 | 130 | private static function detectMimeTypeFromPath(string $path): string 131 | { 132 | if (!function_exists('mime_content_type')) { 133 | throw new \Exception('You need the ext-fileinfo extension to determine logo mime type'); 134 | } 135 | 136 | error_clear_last(); 137 | $mimeType = @mime_content_type($path); 138 | 139 | if (!is_string($mimeType)) { 140 | $errorDetails = error_get_last()['message'] ?? 'invalid data'; 141 | throw new \Exception(sprintf('Could not determine mime type: %s', $errorDetails)); 142 | } 143 | 144 | if (!preg_match('#^image/#', $mimeType)) { 145 | throw new \Exception('Logo path is not an image'); 146 | } 147 | 148 | // Passing mime type image/svg results in invisible images 149 | if ('image/svg' === $mimeType) { 150 | return 'image/svg+xml'; 151 | } 152 | 153 | return $mimeType; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Builder/Builder.php: -------------------------------------------------------------------------------- 1 | */ 32 | private array $writerOptions = [], 33 | private bool $validateResult = false, 34 | // QrCode options 35 | private string $data = '', 36 | private EncodingInterface $encoding = new Encoding('UTF-8'), 37 | private ErrorCorrectionLevel $errorCorrectionLevel = ErrorCorrectionLevel::Low, 38 | private int $size = 300, 39 | private int $margin = 10, 40 | private RoundBlockSizeMode $roundBlockSizeMode = RoundBlockSizeMode::Margin, 41 | private ColorInterface $foregroundColor = new Color(0, 0, 0), 42 | private ColorInterface $backgroundColor = new Color(255, 255, 255), 43 | // Label options 44 | private string $labelText = '', 45 | private FontInterface $labelFont = new Font(__DIR__.'/../../assets/open_sans.ttf', 16), 46 | private LabelAlignment $labelAlignment = LabelAlignment::Center, 47 | private MarginInterface $labelMargin = new Margin(0, 10, 10, 10), 48 | private ColorInterface $labelTextColor = new Color(0, 0, 0), 49 | // Logo options 50 | private string $logoPath = '', 51 | private ?int $logoResizeToWidth = null, 52 | private ?int $logoResizeToHeight = null, 53 | private bool $logoPunchoutBackground = false, 54 | ) { 55 | } 56 | 57 | /** @param array|null $writerOptions */ 58 | public function build( 59 | ?WriterInterface $writer = null, 60 | ?array $writerOptions = null, 61 | ?bool $validateResult = null, 62 | // QrCode options 63 | ?string $data = null, 64 | ?EncodingInterface $encoding = null, 65 | ?ErrorCorrectionLevel $errorCorrectionLevel = null, 66 | ?int $size = null, 67 | ?int $margin = null, 68 | ?RoundBlockSizeMode $roundBlockSizeMode = null, 69 | ?ColorInterface $foregroundColor = null, 70 | ?ColorInterface $backgroundColor = null, 71 | // Label options 72 | ?string $labelText = null, 73 | ?FontInterface $labelFont = null, 74 | ?LabelAlignment $labelAlignment = null, 75 | ?MarginInterface $labelMargin = null, 76 | ?ColorInterface $labelTextColor = null, 77 | // Logo options 78 | ?string $logoPath = null, 79 | ?int $logoResizeToWidth = null, 80 | ?int $logoResizeToHeight = null, 81 | ?bool $logoPunchoutBackground = null, 82 | ): ResultInterface { 83 | if ($this->validateResult && !$this->writer instanceof ValidatingWriterInterface) { 84 | throw ValidationException::createForUnsupportedWriter(get_class($this->writer)); 85 | } 86 | 87 | $writer = $writer ?? $this->writer; 88 | $writerOptions = $writerOptions ?? $this->writerOptions; 89 | $validateResult = $validateResult ?? $this->validateResult; 90 | 91 | $createLabel = $this->labelText || $labelText; 92 | $createLogo = $this->logoPath || $logoPath; 93 | 94 | $qrCode = new QrCode( 95 | data: $data ?? $this->data, 96 | encoding: $encoding ?? $this->encoding, 97 | errorCorrectionLevel: $errorCorrectionLevel ?? $this->errorCorrectionLevel, 98 | size: $size ?? $this->size, 99 | margin: $margin ?? $this->margin, 100 | roundBlockSizeMode: $roundBlockSizeMode ?? $this->roundBlockSizeMode, 101 | foregroundColor: $foregroundColor ?? $this->foregroundColor, 102 | backgroundColor: $backgroundColor ?? $this->backgroundColor 103 | ); 104 | 105 | $logo = $createLogo ? new Logo( 106 | path: $logoPath ?? $this->logoPath, 107 | resizeToWidth: $logoResizeToWidth ?? $this->logoResizeToWidth, 108 | resizeToHeight: $logoResizeToHeight ?? $this->logoResizeToHeight, 109 | punchoutBackground: $logoPunchoutBackground ?? $this->logoPunchoutBackground 110 | ) : null; 111 | 112 | $label = $createLabel ? new Label( 113 | text: $labelText ?? $this->labelText, 114 | font: $labelFont ?? $this->labelFont, 115 | alignment: $labelAlignment ?? $this->labelAlignment, 116 | margin: $labelMargin ?? $this->labelMargin, 117 | textColor: $labelTextColor ?? $this->labelTextColor 118 | ) : null; 119 | 120 | $result = $writer->write($qrCode, $logo, $label, $writerOptions); 121 | 122 | if ($validateResult && $writer instanceof ValidatingWriterInterface) { 123 | $writer->validateResult($result, $qrCode->getData()); 124 | } 125 | 126 | return $result; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Writer/PdfWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 26 | 27 | $unit = 'mm'; 28 | if (isset($options[self::WRITER_OPTION_UNIT])) { 29 | $unit = $options[self::WRITER_OPTION_UNIT]; 30 | } 31 | 32 | $allowedUnits = ['mm', 'pt', 'cm', 'in']; 33 | if (!in_array($unit, $allowedUnits)) { 34 | throw new \Exception(sprintf('PDF Measure unit should be one of [%s]', implode(', ', $allowedUnits))); 35 | } 36 | 37 | $labelSpace = 0; 38 | if ($label instanceof LabelInterface) { 39 | $labelSpace = 30; 40 | } 41 | 42 | if (!class_exists(\FPDF::class)) { 43 | throw new \Exception('Unable to find FPDF: check your installation'); 44 | } 45 | 46 | $foregroundColor = $qrCode->getForegroundColor(); 47 | if ($foregroundColor->getAlpha() > 0) { 48 | throw new \Exception('PDF Writer does not support alpha channels'); 49 | } 50 | $backgroundColor = $qrCode->getBackgroundColor(); 51 | if ($backgroundColor->getAlpha() > 0) { 52 | throw new \Exception('PDF Writer does not support alpha channels'); 53 | } 54 | 55 | if (isset($options[self::WRITER_OPTION_PDF])) { 56 | $fpdf = $options[self::WRITER_OPTION_PDF]; 57 | if (!$fpdf instanceof \FPDF) { 58 | throw new \Exception('pdf option must be an instance of FPDF'); 59 | } 60 | } else { 61 | // @todo Check how to add label height later 62 | $fpdf = new \FPDF('P', $unit, [$matrix->getOuterSize(), $matrix->getOuterSize() + $labelSpace]); 63 | $fpdf->AddPage(); 64 | } 65 | 66 | $x = 0; 67 | if (isset($options[self::WRITER_OPTION_X])) { 68 | $x = $options[self::WRITER_OPTION_X]; 69 | } 70 | $y = 0; 71 | if (isset($options[self::WRITER_OPTION_Y])) { 72 | $y = $options[self::WRITER_OPTION_Y]; 73 | } 74 | 75 | $fpdf->SetFillColor($backgroundColor->getRed(), $backgroundColor->getGreen(), $backgroundColor->getBlue()); 76 | $fpdf->Rect($x, $y, $matrix->getOuterSize(), $matrix->getOuterSize(), 'F'); 77 | $fpdf->SetFillColor($foregroundColor->getRed(), $foregroundColor->getGreen(), $foregroundColor->getBlue()); 78 | 79 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 80 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 81 | if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { 82 | $fpdf->Rect( 83 | $x + $matrix->getMarginLeft() + ($columnIndex * $matrix->getBlockSize()), 84 | $y + $matrix->getMarginLeft() + ($rowIndex * $matrix->getBlockSize()), 85 | $matrix->getBlockSize(), 86 | $matrix->getBlockSize(), 87 | 'F' 88 | ); 89 | } 90 | } 91 | } 92 | 93 | if ($logo instanceof LogoInterface) { 94 | $this->addLogo($logo, $fpdf, $x, $y, $matrix->getOuterSize()); 95 | } 96 | 97 | if ($label instanceof LabelInterface) { 98 | $fpdf->SetXY($x, $y + $matrix->getOuterSize() + $labelSpace - 25); 99 | $fpdf->SetFont('Helvetica', '', $label->getFont()->getSize()); 100 | $fpdf->Cell($matrix->getOuterSize(), 0, $label->getText(), 0, 0, 'C'); 101 | } 102 | 103 | if (isset($options[self::WRITER_OPTION_LINK])) { 104 | $link = $options[self::WRITER_OPTION_LINK]; 105 | $fpdf->Link($x, $y, $x + $matrix->getOuterSize(), $y + $matrix->getOuterSize(), $link); 106 | } 107 | 108 | return new PdfResult($matrix, $fpdf); 109 | } 110 | 111 | private function addLogo(LogoInterface $logo, \FPDF $fpdf, float $x, float $y, float $size): void 112 | { 113 | $logoPath = $logo->getPath(); 114 | $logoHeight = $logo->getResizeToHeight(); 115 | $logoWidth = $logo->getResizeToWidth(); 116 | 117 | if (null === $logoHeight || null === $logoWidth) { 118 | $imageSize = \getimagesize($logoPath); 119 | if (!$imageSize) { 120 | throw new \Exception(sprintf('Unable to read image size for logo "%s"', $logoPath)); 121 | } 122 | [$logoSourceWidth, $logoSourceHeight] = $imageSize; 123 | 124 | if (null === $logoWidth) { 125 | $logoWidth = (int) $logoSourceWidth; 126 | } 127 | 128 | if (null === $logoHeight) { 129 | $aspectRatio = $logoWidth / $logoSourceWidth; 130 | $logoHeight = (int) ($logoSourceHeight * $aspectRatio); 131 | } 132 | } 133 | 134 | $logoX = $x + $size / 2 - $logoWidth / 2; 135 | $logoY = $y + $size / 2 - $logoHeight / 2; 136 | 137 | $fpdf->Image($logoPath, $logoX, $logoY, $logoWidth, $logoHeight); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Writer/AbstractGdWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 28 | } 29 | 30 | public function write(QrCodeInterface $qrCode, ?LogoInterface $logo = null, ?LabelInterface $label = null, array $options = []): ResultInterface 31 | { 32 | if (!extension_loaded('gd')) { 33 | throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly'); 34 | } 35 | 36 | $matrix = $this->getMatrix($qrCode); 37 | 38 | $baseBlockSize = RoundBlockSizeMode::None === $qrCode->getRoundBlockSizeMode() ? 10 : intval($matrix->getBlockSize()); 39 | $baseImage = imagecreatetruecolor($matrix->getBlockCount() * $baseBlockSize, $matrix->getBlockCount() * $baseBlockSize); 40 | 41 | if (!$baseImage) { 42 | throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly'); 43 | } 44 | 45 | /** @var int $foregroundColor */ 46 | $foregroundColor = imagecolorallocatealpha( 47 | $baseImage, 48 | $qrCode->getForegroundColor()->getRed(), 49 | $qrCode->getForegroundColor()->getGreen(), 50 | $qrCode->getForegroundColor()->getBlue(), 51 | $qrCode->getForegroundColor()->getAlpha() 52 | ); 53 | 54 | /** @var int $transparentColor */ 55 | $transparentColor = imagecolorallocatealpha($baseImage, 255, 255, 255, 127); 56 | 57 | imagefill($baseImage, 0, 0, $transparentColor); 58 | 59 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 60 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 61 | if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { 62 | imagefilledrectangle( 63 | $baseImage, 64 | $columnIndex * $baseBlockSize, 65 | $rowIndex * $baseBlockSize, 66 | ($columnIndex + 1) * $baseBlockSize - 1, 67 | ($rowIndex + 1) * $baseBlockSize - 1, 68 | $foregroundColor 69 | ); 70 | } 71 | } 72 | } 73 | 74 | $targetWidth = $matrix->getOuterSize(); 75 | $targetHeight = $matrix->getOuterSize(); 76 | 77 | if ($label instanceof LabelInterface) { 78 | $labelImageData = LabelImageData::createForLabel($label); 79 | $targetHeight += $labelImageData->getHeight() + $label->getMargin()->getTop() + $label->getMargin()->getBottom(); 80 | } 81 | 82 | $targetImage = imagecreatetruecolor($targetWidth, $targetHeight); 83 | 84 | if (!$targetImage) { 85 | throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly'); 86 | } 87 | 88 | /** @var int $backgroundColor */ 89 | $backgroundColor = imagecolorallocatealpha( 90 | $targetImage, 91 | $qrCode->getBackgroundColor()->getRed(), 92 | $qrCode->getBackgroundColor()->getGreen(), 93 | $qrCode->getBackgroundColor()->getBlue(), 94 | $qrCode->getBackgroundColor()->getAlpha() 95 | ); 96 | 97 | imagefill($targetImage, 0, 0, $backgroundColor); 98 | 99 | imagecopyresampled( 100 | $targetImage, 101 | $baseImage, 102 | $matrix->getMarginLeft(), 103 | $matrix->getMarginLeft(), 104 | 0, 105 | 0, 106 | $matrix->getInnerSize(), 107 | $matrix->getInnerSize(), 108 | imagesx($baseImage), 109 | imagesy($baseImage) 110 | ); 111 | 112 | if ($qrCode->getBackgroundColor()->getAlpha() > 0) { 113 | imagesavealpha($targetImage, true); 114 | } 115 | 116 | $result = new GdResult($matrix, $targetImage); 117 | 118 | if ($logo instanceof LogoInterface) { 119 | $result = $this->addLogo($logo, $result); 120 | } 121 | 122 | if ($label instanceof LabelInterface) { 123 | $result = $this->addLabel($label, $result); 124 | } 125 | 126 | return $result; 127 | } 128 | 129 | private function addLogo(LogoInterface $logo, GdResult $result): GdResult 130 | { 131 | $logoImageData = LogoImageData::createForLogo($logo); 132 | 133 | if ('image/svg+xml' === $logoImageData->getMimeType()) { 134 | throw new \Exception('PNG Writer does not support SVG logo'); 135 | } 136 | 137 | $targetImage = $result->getImage(); 138 | $matrix = $result->getMatrix(); 139 | 140 | if ($logoImageData->getPunchoutBackground()) { 141 | /** @var int $transparent */ 142 | $transparent = imagecolorallocatealpha($targetImage, 255, 255, 255, 127); 143 | imagealphablending($targetImage, false); 144 | $xOffsetStart = intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2); 145 | $yOffsetStart = intval($matrix->getOuterSize() / 2 - $logoImageData->getHeight() / 2); 146 | for ($xOffset = $xOffsetStart; $xOffset < $xOffsetStart + $logoImageData->getWidth(); ++$xOffset) { 147 | for ($yOffset = $yOffsetStart; $yOffset < $yOffsetStart + $logoImageData->getHeight(); ++$yOffset) { 148 | imagesetpixel($targetImage, $xOffset, $yOffset, $transparent); 149 | } 150 | } 151 | } 152 | 153 | imagecopyresampled( 154 | $targetImage, 155 | $logoImageData->getImage(), 156 | intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2), 157 | intval($matrix->getOuterSize() / 2 - $logoImageData->getHeight() / 2), 158 | 0, 159 | 0, 160 | $logoImageData->getWidth(), 161 | $logoImageData->getHeight(), 162 | imagesx($logoImageData->getImage()), 163 | imagesy($logoImageData->getImage()) 164 | ); 165 | 166 | return new GdResult($matrix, $targetImage); 167 | } 168 | 169 | private function addLabel(LabelInterface $label, GdResult $result): GdResult 170 | { 171 | $targetImage = $result->getImage(); 172 | 173 | $labelImageData = LabelImageData::createForLabel($label); 174 | 175 | /** @var int $textColor */ 176 | $textColor = imagecolorallocatealpha( 177 | $targetImage, 178 | $label->getTextColor()->getRed(), 179 | $label->getTextColor()->getGreen(), 180 | $label->getTextColor()->getBlue(), 181 | $label->getTextColor()->getAlpha() 182 | ); 183 | 184 | $x = intval(imagesx($targetImage) / 2 - $labelImageData->getWidth() / 2); 185 | $y = imagesy($targetImage) - $label->getMargin()->getBottom(); 186 | 187 | if (LabelAlignment::Left === $label->getAlignment()) { 188 | $x = $label->getMargin()->getLeft(); 189 | } elseif (LabelAlignment::Right === $label->getAlignment()) { 190 | $x = imagesx($targetImage) - $labelImageData->getWidth() - $label->getMargin()->getRight(); 191 | } 192 | 193 | imagettftext($targetImage, $label->getFont()->getSize(), 0, $x, $y, $textColor, $label->getFont()->getPath(), $label->getText()); 194 | 195 | return new GdResult($result->getMatrix(), $targetImage); 196 | } 197 | 198 | public function validateResult(ResultInterface $result, string $expectedData): void 199 | { 200 | $string = $result->getString(); 201 | 202 | if (!class_exists(QrReader::class)) { 203 | throw ValidationException::createForMissingPackage('khanamiryan/qrcode-detector-decoder'); 204 | } 205 | 206 | $reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB); 207 | if ($reader->text() !== $expectedData) { 208 | throw ValidationException::createForInvalidData($expectedData, strval($reader->text())); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Writer/SvgWriter.php: -------------------------------------------------------------------------------- 1 | create($qrCode); 45 | 46 | $xml = new \SimpleXMLElement(''); 47 | $xml->addAttribute('version', '1.1'); 48 | if (!$options[self::WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT]) { 49 | $xml->addAttribute('width', $matrix->getOuterSize().'px'); 50 | $xml->addAttribute('height', $matrix->getOuterSize().'px'); 51 | } 52 | $xml->addAttribute('viewBox', '0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize()); 53 | 54 | $background = $xml->addChild('rect'); 55 | $background->addAttribute('x', '0'); 56 | $background->addAttribute('y', '0'); 57 | $background->addAttribute('width', strval($matrix->getOuterSize())); 58 | $background->addAttribute('height', strval($matrix->getOuterSize())); 59 | $background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()->getRed(), $qrCode->getBackgroundColor()->getGreen(), $qrCode->getBackgroundColor()->getBlue())); 60 | $background->addAttribute('fill-opacity', strval($qrCode->getBackgroundColor()->getOpacity())); 61 | 62 | if ($options[self::WRITER_OPTION_COMPACT]) { 63 | $this->writePath($xml, $qrCode, $matrix); 64 | } else { 65 | $this->writeBlockDefinitions($xml, $qrCode, $matrix, $options); 66 | } 67 | 68 | $result = new SvgResult($matrix, $xml, boolval($options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION])); 69 | 70 | if ($logo instanceof LogoInterface) { 71 | $this->addLogo($logo, $result, $options); 72 | } 73 | 74 | return $result; 75 | } 76 | 77 | private function writePath(\SimpleXMLElement $xml, QrCodeInterface $qrCode, MatrixInterface $matrix): void 78 | { 79 | $path = ''; 80 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 81 | $left = $matrix->getMarginLeft(); 82 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 83 | if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { 84 | // When we are at the first column or when the previous column was 0 set new left 85 | if (0 === $columnIndex || 0 === $matrix->getBlockValue($rowIndex, $columnIndex - 1)) { 86 | $left = $matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex; 87 | } 88 | // When we are at the 89 | if ($columnIndex === $matrix->getBlockCount() - 1 || 0 === $matrix->getBlockValue($rowIndex, $columnIndex + 1)) { 90 | $top = $matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex; 91 | $bottom = $matrix->getMarginLeft() + $matrix->getBlockSize() * ($rowIndex + 1); 92 | $right = $matrix->getMarginLeft() + $matrix->getBlockSize() * ($columnIndex + 1); 93 | $path .= 'M'.$this->formatNumber($left).','.$this->formatNumber($top); 94 | $path .= 'L'.$this->formatNumber($right).','.$this->formatNumber($top); 95 | $path .= 'L'.$this->formatNumber($right).','.$this->formatNumber($bottom); 96 | $path .= 'L'.$this->formatNumber($left).','.$this->formatNumber($bottom).'Z'; 97 | } 98 | } 99 | } 100 | } 101 | 102 | $pathDefinition = $xml->addChild('path'); 103 | $pathDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()->getRed(), $qrCode->getForegroundColor()->getGreen(), $qrCode->getForegroundColor()->getBlue())); 104 | $pathDefinition->addAttribute('fill-opacity', strval($qrCode->getForegroundColor()->getOpacity())); 105 | $pathDefinition->addAttribute('d', $path); 106 | } 107 | 108 | /** @param array $options */ 109 | private function writeBlockDefinitions(\SimpleXMLElement $xml, QrCodeInterface $qrCode, MatrixInterface $matrix, array $options): void 110 | { 111 | $xml->addChild('defs'); 112 | 113 | $blockDefinition = $xml->defs->addChild('rect'); 114 | $blockDefinition->addAttribute('id', strval($options[self::WRITER_OPTION_BLOCK_ID])); 115 | $blockDefinition->addAttribute('width', $this->formatNumber($matrix->getBlockSize())); 116 | $blockDefinition->addAttribute('height', $this->formatNumber($matrix->getBlockSize())); 117 | $blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()->getRed(), $qrCode->getForegroundColor()->getGreen(), $qrCode->getForegroundColor()->getBlue())); 118 | $blockDefinition->addAttribute('fill-opacity', strval($qrCode->getForegroundColor()->getOpacity())); 119 | 120 | for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) { 121 | for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) { 122 | if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) { 123 | $block = $xml->addChild('use'); 124 | $block->addAttribute('x', $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex)); 125 | $block->addAttribute('y', $this->formatNumber($matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex)); 126 | $block->addAttribute('xlink:href', '#'.$options[self::WRITER_OPTION_BLOCK_ID], 'http://www.w3.org/1999/xlink'); 127 | } 128 | } 129 | } 130 | } 131 | 132 | /** @param array $options */ 133 | private function addLogo(LogoInterface $logo, SvgResult $result, array $options): void 134 | { 135 | if ($logo->getPunchoutBackground()) { 136 | throw new \Exception('The SVG writer does not support logo punchout background'); 137 | } 138 | 139 | $logoImageData = LogoImageData::createForLogo($logo); 140 | 141 | if (!isset($options[self::WRITER_OPTION_FORCE_XLINK_HREF])) { 142 | $options[self::WRITER_OPTION_FORCE_XLINK_HREF] = false; 143 | } 144 | 145 | $xml = $result->getXml(); 146 | 147 | /** @var \SimpleXMLElement $xmlAttributes */ 148 | $xmlAttributes = $xml->attributes(); 149 | 150 | $x = intval($xmlAttributes->width) / 2 - $logoImageData->getWidth() / 2; 151 | $y = intval($xmlAttributes->height) / 2 - $logoImageData->getHeight() / 2; 152 | 153 | $imageDefinition = $xml->addChild('image'); 154 | $imageDefinition->addAttribute('x', strval($x)); 155 | $imageDefinition->addAttribute('y', strval($y)); 156 | $imageDefinition->addAttribute('width', strval($logoImageData->getWidth())); 157 | $imageDefinition->addAttribute('height', strval($logoImageData->getHeight())); 158 | $imageDefinition->addAttribute('preserveAspectRatio', 'none'); 159 | 160 | if ($options[self::WRITER_OPTION_FORCE_XLINK_HREF]) { 161 | $imageDefinition->addAttribute('xlink:href', $logoImageData->createDataUri(), 'http://www.w3.org/1999/xlink'); 162 | } else { 163 | $imageDefinition->addAttribute('href', $logoImageData->createDataUri()); 164 | } 165 | } 166 | 167 | private function formatNumber(float $number): string 168 | { 169 | $string = number_format($number, self::DECIMAL_PRECISION, '.', ''); 170 | $string = rtrim($string, '0'); 171 | 172 | return rtrim($string, '.'); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QR Code 2 | 3 | *By [endroid](https://endroid.nl/)* 4 | 5 | If you like my work please support me by visiting the [sponsor page](https://github.com/sponsors/endroid) or [buying me a coffee](https://www.buymeacoffee.com/endroid) :coffee: 6 | 7 | [![Latest Stable Version](http://img.shields.io/packagist/v/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code) 8 | [![Build Status](https://github.com/endroid/qr-code/workflows/CI/badge.svg)](https://github.com/endroid/qr-code/actions) 9 | [![Total Downloads](http://img.shields.io/packagist/dt/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code) 10 | [![Monthly Downloads](http://img.shields.io/packagist/dm/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code) 11 | [![License](http://img.shields.io/packagist/l/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code) 12 | 13 | This library helps you generate QR codes in a jiffy. Makes use of [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode) 14 | to generate the matrix and [khanamiryan/qrcode-detector-decoder](https://github.com/khanamiryan/php-qrcode-detector-decoder) 15 | for validating generated QR codes. Further extended with Twig extensions, generation routes, a factory and a 16 | Symfony bundle for easy installation and configuration. Different writers are provided to generate the QR code 17 | as PNG, WebP, SVG, EPS or in binary format. 18 | 19 | ## Sponsored by 20 | 21 | [![Blackfire.io](.github/blackfire.png)](https://www.blackfire.io) 22 | 23 | ## Installation 24 | 25 | Use [Composer](https://getcomposer.org/) to install the library. Also make sure you have enabled and configured the 26 | [GD extension](https://www.php.net/manual/en/book.image.php) if you want to generate images. 27 | 28 | ``` bash 29 | composer require endroid/qr-code 30 | ``` 31 | 32 | ## Usage: using the builder 33 | 34 | ```php 35 | use Endroid\QrCode\Builder\Builder; 36 | use Endroid\QrCode\Encoding\Encoding; 37 | use Endroid\QrCode\ErrorCorrectionLevel; 38 | use Endroid\QrCode\Label\LabelAlignment; 39 | use Endroid\QrCode\Label\Font\OpenSans; 40 | use Endroid\QrCode\RoundBlockSizeMode; 41 | use Endroid\QrCode\Writer\PngWriter; 42 | 43 | $builder = new Builder( 44 | writer: new PngWriter(), 45 | writerOptions: [], 46 | validateResult: false, 47 | data: 'Custom QR code contents', 48 | encoding: new Encoding('UTF-8'), 49 | errorCorrectionLevel: ErrorCorrectionLevel::High, 50 | size: 300, 51 | margin: 10, 52 | roundBlockSizeMode: RoundBlockSizeMode::Margin, 53 | logoPath: __DIR__.'/assets/symfony.png', 54 | logoResizeToWidth: 50, 55 | logoPunchoutBackground: true, 56 | labelText: 'This is the label', 57 | labelFont: new OpenSans(20), 58 | labelAlignment: LabelAlignment::Center 59 | ); 60 | 61 | $result = $builder->build(); 62 | ``` 63 | 64 | ## Usage: without using the builder 65 | 66 | ```php 67 | use Endroid\QrCode\Color\Color; 68 | use Endroid\QrCode\Encoding\Encoding; 69 | use Endroid\QrCode\ErrorCorrectionLevel; 70 | use Endroid\QrCode\QrCode; 71 | use Endroid\QrCode\Label\Label; 72 | use Endroid\QrCode\Logo\Logo; 73 | use Endroid\QrCode\RoundBlockSizeMode; 74 | use Endroid\QrCode\Writer\PngWriter; 75 | use Endroid\QrCode\Writer\ValidationException; 76 | 77 | $writer = new PngWriter(); 78 | 79 | // Create QR code 80 | $qrCode = new QrCode( 81 | data: 'Life is too short to be generating QR codes', 82 | encoding: new Encoding('UTF-8'), 83 | errorCorrectionLevel: ErrorCorrectionLevel::Low, 84 | size: 300, 85 | margin: 10, 86 | roundBlockSizeMode: RoundBlockSizeMode::Margin, 87 | foregroundColor: new Color(0, 0, 0), 88 | backgroundColor: new Color(255, 255, 255) 89 | ); 90 | 91 | // Create generic logo 92 | $logo = new Logo( 93 | path: __DIR__.'/assets/symfony.png', 94 | resizeToWidth: 50, 95 | punchoutBackground: true 96 | ); 97 | 98 | // Create generic label 99 | $label = new Label( 100 | text: 'Label', 101 | textColor: new Color(255, 0, 0) 102 | ); 103 | 104 | $result = $writer->write($qrCode, $logo, $label); 105 | 106 | // Validate the result 107 | $writer->validateResult($result, 'Life is too short to be generating QR codes'); 108 | ``` 109 | 110 | ## Usage: working with results 111 | 112 | ```php 113 | 114 | // Directly output the QR code 115 | header('Content-Type: '.$result->getMimeType()); 116 | echo $result->getString(); 117 | 118 | // Save it to a file 119 | $result->saveToFile(__DIR__.'/qrcode.png'); 120 | 121 | // Generate a data URI to include image data inline (i.e. inside an tag) 122 | $dataUri = $result->getDataUri(); 123 | ``` 124 | 125 | ![QR Code](.github/example.png) 126 | 127 | ### Writer options 128 | 129 | Some writers provide writer options. Each available writer option is can be 130 | found as a constant prefixed with WRITER_OPTION_ in the writer class. 131 | 132 | * `PdfWriter` 133 | * `unit`: unit of measurement (default: mm) 134 | * `fpdf`: PDF to place the image in (default: new PDF) 135 | * `x`: image offset (default: 0) 136 | * `y`: image offset (default: 0) 137 | * `link`: a URL or an identifier returned by `AddLink()`. 138 | * `PngWriter` 139 | * `compression_level`: compression level (0-9, default: -1 = zlib default) 140 | * `SvgWriter` 141 | * `block_id`: id of the block element for external reference (default: block) 142 | * `exclude_xml_declaration`: exclude XML declaration (default: false) 143 | * `exclude_svg_width_and_height`: exclude width and height (default: false) 144 | * `force_xlink_href`: forces xlink namespace in case of compatibility issues (default: false) 145 | * `compact`: create using `path` element, otherwise use `defs` and `use` (default: true) 146 | * `WebPWriter` 147 | * `quality`: image quality (0-100, default: 80) 148 | 149 | You can provide any writer options like this. 150 | 151 | ```php 152 | use Endroid\QrCode\Writer\SvgWriter; 153 | 154 | $builder = new Builder( 155 | writer: new SvgWriter(), 156 | writerOptions: [ 157 | SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true 158 | ] 159 | ); 160 | ``` 161 | 162 | ### Encoding 163 | 164 | If you use a barcode scanner you can have some troubles while reading the 165 | generated QR codes. Depending on the encoding you chose you will have an extra 166 | amount of data corresponding to the ECI block. Some barcode scanner are not 167 | programmed to interpret this block of information. To ensure a maximum 168 | compatibility you can use the `ISO-8859-1` encoding that is the default 169 | encoding used by barcode scanners (if your character set supports it, 170 | i.e. no Chinese characters are present). 171 | 172 | ### Round block size mode 173 | 174 | By default block sizes are rounded to guarantee sharp images and improve 175 | readability. However some other rounding variants are available. 176 | 177 | * `margin (default)`: the size of the QR code is shrunk if necessary but the size 178 | of the final image remains unchanged due to additional margin being added. 179 | * `enlarge`: the size of the QR code and the final image are enlarged when 180 | rounding differences occur. 181 | * `shrink`: the size of the QR code and the final image are 182 | shrunk when rounding differences occur. 183 | * `none`: No rounding. This mode can be used when blocks don't need to be rounded 184 | to pixels (for instance SVG). 185 | 186 | ## Readability 187 | 188 | The readability of a QR code is primarily determined by the size, the input 189 | length, the error correction level and any possible logo over the image so you 190 | can tweak these parameters if you are looking for optimal results. You can also 191 | check $qrCode->getRoundBlockSize() value to see if block dimensions are rounded 192 | so that the image is more sharp and readable. Please note that rounding block 193 | size can result in additional padding to compensate for the rounding difference. 194 | And finally the encoding (default UTF-8 to support large character sets) can be 195 | set to `ISO-8859-1` if possible to improve readability. 196 | 197 | ## Validating the generated QR code 198 | 199 | If you need to be extra sure the QR code you generated is readable and contains 200 | the exact data you requested you can enable the validation reader, which is 201 | disabled by default. You can do this either via the builder or directly on any 202 | writer that supports validation. See the examples above. 203 | 204 | Please note that validation affects performance so only use it in case of problems. 205 | 206 | ## Symfony integration 207 | 208 | The [endroid/qr-code-bundle](https://github.com/endroid/qr-code-bundle) 209 | integrates the QR code library in Symfony for an even better experience. 210 | 211 | * Configure your defaults (like image size, default writer etc.) 212 | * Support for multiple configurations and injection via aliases 213 | * Generate QR codes for defined configurations via URL like /qr-code//Hello 214 | * Generate QR codes or URLs directly from Twig using dedicated functions 215 | 216 | Read the [bundle documentation](https://github.com/endroid/qr-code-bundle) 217 | for more information. 218 | 219 | ## Versioning 220 | 221 | Version numbers follow the MAJOR.MINOR.PATCH scheme. Backwards compatibility 222 | breaking changes will be kept to a minimum but be aware that these can occur. 223 | Lock your dependencies for production and test your code when upgrading. 224 | 225 | ## License 226 | 227 | This bundle is under the MIT license. For the full copyright and license 228 | information please view the LICENSE file that was distributed with this source code. 229 | --------------------------------------------------------------------------------