init commit

This commit is contained in:
2026-03-17 09:56:00 +08:00
commit e2c8ae752d
6827 changed files with 1211784 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\InvalidLogoException;
use Endroid\QrCode\Exception\MissingExtensionException;
use Endroid\QrCode\QrCodeInterface;
abstract class AbstractWriter implements WriterInterface
{
protected function getMimeType(string $path): string
{
if (false !== filter_var($path, FILTER_VALIDATE_URL)) {
return $this->getMimeTypeFromUrl($path);
}
return $this->getMimeTypeFromPath($path);
}
private function getMimeTypeFromUrl(string $url): string
{
/** @var mixed $format */
$format = PHP_VERSION > 80000 ? true : 1;
$headers = get_headers($url, $format);
if (!is_array($headers) || !isset($headers['Content-Type'])) {
throw new InvalidLogoException(sprintf('Content type could not be determined for logo URL "%s"', $url));
}
return $headers['Content-Type'];
}
private function getMimeTypeFromPath(string $path): string
{
if (!function_exists('mime_content_type')) {
throw new MissingExtensionException('You need the ext-fileinfo extension to determine logo mime type');
}
$mimeType = mime_content_type($path);
if (!is_string($mimeType)) {
throw new InvalidLogoException('Could not determine mime type');
}
if (!preg_match('#^image/#', $mimeType)) {
throw new GenerateImageException('Logo path is not an image');
}
// Passing mime type image/svg results in invisible images
if ('image/svg' === $mimeType) {
return 'image/svg+xml';
}
return $mimeType;
}
public function writeDataUri(QrCodeInterface $qrCode): string
{
$dataUri = 'data:'.$this->getContentType().';base64,'.base64_encode($this->writeString($qrCode));
return $dataUri;
}
public function writeFile(QrCodeInterface $qrCode, string $path): void
{
$string = $this->writeString($qrCode);
file_put_contents($path, $string);
}
public static function supportsExtension(string $extension): bool
{
return in_array($extension, static::getSupportedExtensions());
}
public static function getSupportedExtensions(): array
{
return [];
}
abstract public function getName(): string;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
class BinaryWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$rows = [];
$data = $qrCode->getData();
foreach ($data['matrix'] as $row) {
$values = '';
foreach ($row as $value) {
$values .= $value;
}
$rows[] = $values;
}
return implode("\n", $rows);
}
public static function getContentType(): string
{
return 'text/plain';
}
public static function getSupportedExtensions(): array
{
return ['bin', 'txt'];
}
public function getName(): string
{
return 'binary';
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
use Exception;
use ReflectionClass;
class DebugWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$data = [];
$skip = ['getData'];
$reflectionClass = new ReflectionClass($qrCode);
foreach ($reflectionClass->getMethods() as $method) {
$methodName = $method->getShortName();
if (0 === strpos($methodName, 'get') && 0 == $method->getNumberOfParameters() && !in_array($methodName, $skip)) {
$value = $qrCode->{$methodName}();
if (is_array($value) && !is_object(current($value))) {
$value = '['.implode(', ', $value).']';
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
} elseif (is_string($value)) {
$value = '"'.$value.'"';
} elseif (is_null($value)) {
$value = 'null';
}
try {
$data[] = $methodName.': '.$value;
} catch (Exception $exception) {
}
}
}
$string = implode(" \n", $data);
return $string;
}
public static function getContentType(): string
{
return 'text/plain';
}
public function getName(): string
{
return 'debug';
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
class EpsWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$data = $qrCode->getData();
$epsData = [];
$epsData[] = '%!PS-Adobe-3.0 EPSF-3.0';
$epsData[] = '%%BoundingBox: 0 0 '.$data['outer_width'].' '.$data['outer_height'];
$epsData[] = '/F { rectfill } def';
$epsData[] = number_format($qrCode->getBackgroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor';
$epsData[] = '0 0 '.$data['outer_width'].' '.$data['outer_height'].' F';
$epsData[] = number_format($qrCode->getForegroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor';
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
$x = $data['margin_left'] + $data['block_size'] * $column;
$y = $data['margin_left'] + $data['block_size'] * $row;
$epsData[] = $x.' '.$y.' '.$data['block_size'].' '.$data['block_size'].' F';
}
}
}
return implode("\n", $epsData);
}
public static function getContentType(): string
{
return 'image/eps';
}
public static function getSupportedExtensions(): array
{
return ['eps'];
}
public function getName(): string
{
return 'eps';
}
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\MissingFunctionException;
use Endroid\QrCode\Exception\MissingLogoHeightException;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\LabelAlignment;
use Endroid\QrCode\QrCodeInterface;
use Zxing\QrReader;
class PngWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
if (!extension_loaded('gd')) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$image = $this->createImage($qrCode->getData(), $qrCode);
$logoPath = $qrCode->getLogoPath();
if (null !== $logoPath) {
$image = $this->addLogo($image, $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight());
}
$label = $qrCode->getLabel();
if (null !== $label) {
$image = $this->addLabel($image, $label, $qrCode->getLabelFontPath(), $qrCode->getLabelFontSize(), $qrCode->getLabelAlignment(), $qrCode->getLabelMargin(), $qrCode->getForegroundColor(), $qrCode->getBackgroundColor());
}
$string = $this->imageToString($image);
if (PHP_VERSION_ID < 80000) {
imagedestroy($image);
}
if ($qrCode->getValidateResult()) {
$reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB);
if ($reader->text() !== $qrCode->getText()) {
throw new ValidationException('Built-in validation reader read "'.$reader->text().'" instead of "'.$qrCode->getText().'".
Adjust your parameters to increase readability or disable built-in validation.');
}
}
return $string;
}
/**
* @param array<mixed> $data
*
* @return mixed
*/
private function createImage(array $data, QrCodeInterface $qrCode)
{
$baseSize = $qrCode->getRoundBlockSize() ? $data['block_size'] : 25;
$baseImage = $this->createBaseImage($baseSize, $data, $qrCode);
$interpolatedImage = $this->createInterpolatedImage($baseImage, $data, $qrCode);
if (PHP_VERSION_ID < 80000) {
imagedestroy($baseImage);
}
return $interpolatedImage;
}
/**
* @param array<mixed> $data
*
* @return mixed
*/
private function createBaseImage(int $baseSize, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['block_count'] * $baseSize, $data['block_count'] * $baseSize);
if (!$image) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocatealpha($image, $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b'], $qrCode->getForegroundColor()['a']);
if (!is_int($foregroundColor)) {
throw new GenerateImageException('Foreground color could not be allocated');
}
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($image, 0, 0, $backgroundColor);
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
imagefilledrectangle($image, $column * $baseSize, $row * $baseSize, intval(($column + 1) * $baseSize), intval(($row + 1) * $baseSize), $foregroundColor);
}
}
}
return $image;
}
/**
* @param mixed $baseImage
* @param array<mixed> $data
*
* @return mixed
*/
private function createInterpolatedImage($baseImage, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['outer_width'], $data['outer_height']);
if (!$image) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($image, 0, 0, $backgroundColor);
imagecopyresampled($image, $baseImage, (int) $data['margin_left'], (int) $data['margin_left'], 0, 0, (int) $data['inner_width'], (int) $data['inner_height'], imagesx($baseImage), imagesy($baseImage));
if ($qrCode->getBackgroundColor()['a'] > 0) {
imagesavealpha($image, true);
}
return $image;
}
/**
* @param mixed $sourceImage
*
* @return mixed
*/
private function addLogo($sourceImage, string $logoPath, int $logoWidth = null, int $logoHeight = null)
{
$mimeType = $this->getMimeType($logoPath);
$logoImage = imagecreatefromstring(strval(file_get_contents($logoPath)));
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (!$logoImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
}
$logoSourceWidth = imagesx($logoImage);
$logoSourceHeight = imagesy($logoImage);
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
$logoX = imagesx($sourceImage) / 2 - $logoWidth / 2;
$logoY = imagesy($sourceImage) / 2 - $logoHeight / 2;
imagecopyresampled($sourceImage, $logoImage, intval($logoX), intval($logoY), 0, 0, $logoWidth, $logoHeight, $logoSourceWidth, $logoSourceHeight);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImage);
}
return $sourceImage;
}
/**
* @param mixed $sourceImage
* @param array<int> $labelMargin
* @param array<int> $foregroundColor
* @param array<int> $backgroundColor
*
* @return mixed
*/
private function addLabel($sourceImage, string $label, string $labelFontPath, int $labelFontSize, string $labelAlignment, array $labelMargin, array $foregroundColor, array $backgroundColor)
{
if (!function_exists('imagettfbbox')) {
throw new MissingFunctionException('Missing function "imagettfbbox", please make sure you installed the FreeType library');
}
$labelBox = imagettfbbox($labelFontSize, 0, $labelFontPath, $label);
if (!$labelBox) {
throw new GenerateImageException('Unable to add label: check your GD installation');
}
$labelBoxWidth = intval($labelBox[2] - $labelBox[0]);
$labelBoxHeight = intval($labelBox[0] - $labelBox[7]);
$sourceWidth = imagesx($sourceImage);
$sourceHeight = imagesy($sourceImage);
$targetWidth = $sourceWidth;
$targetHeight = $sourceHeight + $labelBoxHeight + $labelMargin['t'] + $labelMargin['b'];
// Create empty target image
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
if (!$targetImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocate($targetImage, $foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']);
if (!is_int($foregroundColor)) {
throw new GenerateImageException('Foreground color could not be allocated');
}
$backgroundColor = imagecolorallocate($targetImage, $backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($targetImage, 0, 0, $backgroundColor);
// Copy source image to target image
imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight);
if (PHP_VERSION_ID < 80000) {
imagedestroy($sourceImage);
}
switch ($labelAlignment) {
case LabelAlignment::LEFT:
$labelX = $labelMargin['l'];
break;
case LabelAlignment::RIGHT:
$labelX = $targetWidth - $labelBoxWidth - $labelMargin['r'];
break;
default:
$labelX = intval($targetWidth / 2 - $labelBoxWidth / 2);
break;
}
$labelY = $targetHeight - $labelMargin['b'];
imagettftext($targetImage, $labelFontSize, 0, $labelX, $labelY, $foregroundColor, $labelFontPath, $label);
return $targetImage;
}
/**
* @param mixed $image
*/
private function imageToString($image): string
{
ob_start();
imagepng($image);
return (string) ob_get_clean();
}
public static function getContentType(): string
{
return 'image/png';
}
public static function getSupportedExtensions(): array
{
return ['png'];
}
public function getName(): string
{
return 'png';
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\MissingLogoHeightException;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\QrCodeInterface;
use SimpleXMLElement;
class SvgWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$options = $qrCode->getWriterOptions();
if ($qrCode->getValidateResult()) {
throw new ValidationException('Built-in validation reader can not check SVG images: please disable via setValidateResult(false)');
}
$data = $qrCode->getData();
$svg = new SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>');
$svg->addAttribute('version', '1.1');
$svg->addAttribute('width', $data['outer_width'].'px');
$svg->addAttribute('height', $data['outer_height'].'px');
$svg->addAttribute('viewBox', '0 0 '.$data['outer_width'].' '.$data['outer_height']);
$svg->addChild('defs');
// Block definition
$block_id = isset($options['rect_id']) && $options['rect_id'] ? $options['rect_id'] : 'block';
$blockDefinition = $svg->defs->addChild('rect');
$blockDefinition->addAttribute('id', $block_id);
$blockDefinition->addAttribute('width', strval($data['block_size']));
$blockDefinition->addAttribute('height', strval($data['block_size']));
$blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b']));
$blockDefinition->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getForegroundColor()['a'])));
// Background
$background = $svg->addChild('rect');
$background->addAttribute('x', '0');
$background->addAttribute('y', '0');
$background->addAttribute('width', strval($data['outer_width']));
$background->addAttribute('height', strval($data['outer_height']));
$background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b']));
$background->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getBackgroundColor()['a'])));
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
$block = $svg->addChild('use');
$block->addAttribute('x', strval($data['margin_left'] + $data['block_size'] * $column));
$block->addAttribute('y', strval($data['margin_left'] + $data['block_size'] * $row));
$block->addAttribute('xlink:href', '#'.$block_id, 'http://www.w3.org/1999/xlink');
}
}
}
$logoPath = $qrCode->getLogoPath();
if (is_string($logoPath)) {
$forceXlinkHref = false;
if (isset($options['force_xlink_href']) && $options['force_xlink_href']) {
$forceXlinkHref = true;
}
$this->addLogo($svg, $data['outer_width'], $data['outer_height'], $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight(), $forceXlinkHref);
}
$xml = $svg->asXML();
if (!is_string($xml)) {
throw new GenerateImageException('Unable to save SVG XML');
}
if (isset($options['exclude_xml_declaration']) && $options['exclude_xml_declaration']) {
$xml = str_replace("<?xml version=\"1.0\"?>\n", '', $xml);
}
return $xml;
}
private function addLogo(SimpleXMLElement $svg, int $imageWidth, int $imageHeight, string $logoPath, int $logoWidth = null, int $logoHeight = null, bool $forceXlinkHref = false): void
{
$mimeType = $this->getMimeType($logoPath);
$imageData = file_get_contents($logoPath);
if (!is_string($imageData)) {
throw new GenerateImageException('Unable to read image data: check your logo path');
}
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (null === $logoHeight || null === $logoWidth) {
$logoImage = imagecreatefromstring(strval($imageData));
if (!$logoImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
}
/** @var mixed $logoImage */
$logoSourceWidth = imagesx($logoImage);
$logoSourceHeight = imagesy($logoImage);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImage);
}
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
}
$logoX = $imageWidth / 2 - $logoWidth / 2;
$logoY = $imageHeight / 2 - $logoHeight / 2;
$imageDefinition = $svg->addChild('image');
$imageDefinition->addAttribute('x', strval($logoX));
$imageDefinition->addAttribute('y', strval($logoY));
$imageDefinition->addAttribute('width', strval($logoWidth));
$imageDefinition->addAttribute('height', strval($logoHeight));
$imageDefinition->addAttribute('preserveAspectRatio', 'none');
// xlink:href is actually deprecated, but still required when placing the qr code in a pdf.
// SimpleXML strips out the xlink part by using addAttribute(), so it must be set directly.
if ($forceXlinkHref) {
$imageDefinition['xlink:href'] = 'data:'.$mimeType.';base64,'.base64_encode($imageData);
} else {
$imageDefinition->addAttribute('href', 'data:'.$mimeType.';base64,'.base64_encode($imageData));
}
}
private function getOpacity(int $alpha): float
{
$opacity = 1 - $alpha / 127;
return $opacity;
}
public static function getContentType(): string
{
return 'image/svg+xml';
}
public static function getSupportedExtensions(): array
{
return ['svg'];
}
public function getName(): string
{
return 'svg';
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
interface WriterInterface
{
public function writeString(QrCodeInterface $qrCode): string;
public function writeDataUri(QrCodeInterface $qrCode): string;
public function writeFile(QrCodeInterface $qrCode, string $path): void;
public static function getContentType(): string;
public static function supportsExtension(string $extension): bool;
public static function getSupportedExtensions(): array;
public function getName(): string;
}