mirror of
https://github.com/chillerlan/php-qrcode.git
synced 2026-08-31 04:28:03 +00:00
:octocat: allow contrast adjustment and grayscaling in the reader
This commit is contained in:
@@ -13,9 +13,10 @@
|
||||
|
||||
namespace chillerlan\QRCode\Decoder;
|
||||
|
||||
use chillerlan\Settings\SettingsContainerInterface;
|
||||
use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex,
|
||||
imagecreatefromstring, imagesx, imagesy, is_resource;
|
||||
use const PHP_MAJOR_VERSION;
|
||||
imagecreatefromstring, imagefilter, imagesx, imagesy, is_resource;
|
||||
use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, PHP_MAJOR_VERSION;
|
||||
|
||||
/**
|
||||
* This class is used to help decode images from files which arrive as GD Resource
|
||||
@@ -35,7 +36,7 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
|
||||
*/
|
||||
public function __construct($gdImage){
|
||||
public function __construct($gdImage, SettingsContainerInterface $options = null){
|
||||
|
||||
/** @noinspection PhpFullyQualifiedNameUsageInspection */
|
||||
if(
|
||||
@@ -45,7 +46,7 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
|
||||
throw new QRCodeDecoderException('Invalid GD image source.');
|
||||
}
|
||||
|
||||
parent::__construct(imagesx($gdImage), imagesy($gdImage));
|
||||
parent::__construct(imagesx($gdImage), imagesy($gdImage), $options);
|
||||
|
||||
$this->gdImage = $gdImage;
|
||||
|
||||
@@ -56,6 +57,16 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
|
||||
*
|
||||
*/
|
||||
private function setLuminancePixels():void{
|
||||
|
||||
if($this->options->readerGrayscale){
|
||||
imagefilter($this->gdImage, IMG_FILTER_GRAYSCALE);
|
||||
}
|
||||
|
||||
if($this->options->readerIncreaseContrast){
|
||||
imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100);
|
||||
imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100);
|
||||
}
|
||||
|
||||
for($j = 0; $j < $this->height; $j++){
|
||||
for($i = 0; $i < $this->width; $i++){
|
||||
$argb = imagecolorat($this->gdImage, $i, $j);
|
||||
@@ -64,18 +75,17 @@ final class GDLuminanceSource extends LuminanceSourceAbstract{
|
||||
$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public static function fromFile(string $path):self{
|
||||
$path = self::checkFile($path);
|
||||
|
||||
return new self(imagecreatefromstring(file_get_contents($path)));
|
||||
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
|
||||
return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public static function fromBlob(string $blob):self{
|
||||
return new self(imagecreatefromstring($blob));
|
||||
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
|
||||
return new self(imagecreatefromstring($blob), $options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
namespace chillerlan\QRCode\Decoder;
|
||||
|
||||
use chillerlan\Settings\SettingsContainerInterface;
|
||||
use Imagick;
|
||||
use function count;
|
||||
|
||||
@@ -31,8 +32,8 @@ final class IMagickLuminanceSource extends LuminanceSourceAbstract{
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(Imagick $imagick){
|
||||
parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight());
|
||||
public function __construct(Imagick $imagick, SettingsContainerInterface $options = null){
|
||||
parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options);
|
||||
|
||||
$this->imagick = $imagick;
|
||||
|
||||
@@ -43,29 +44,36 @@ final class IMagickLuminanceSource extends LuminanceSourceAbstract{
|
||||
*
|
||||
*/
|
||||
private function setLuminancePixels():void{
|
||||
$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
|
||||
|
||||
if($this->options->readerGrayscale){
|
||||
$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
|
||||
}
|
||||
|
||||
if($this->options->readerIncreaseContrast){
|
||||
for($i = 0; $i < 10; $i++){
|
||||
$this->imagick->contrastImage(0);
|
||||
}
|
||||
}
|
||||
|
||||
$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
|
||||
$count = count($pixels);
|
||||
|
||||
$countPixels = count($pixels);
|
||||
|
||||
for($i = 0; $i < $countPixels; $i += 3){
|
||||
for($i = 0; $i < $count; $i += 3){
|
||||
$this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public static function fromFile(string $path):self{
|
||||
$path = self::checkFile($path);
|
||||
|
||||
return new self(new Imagick($path));
|
||||
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
|
||||
return new self(new Imagick(self::checkFile($path)), $options);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public static function fromBlob(string $blob):self{
|
||||
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
|
||||
$im = new Imagick;
|
||||
$im->readImageBlob($blob);
|
||||
|
||||
return new self($im);
|
||||
return new self($im, $options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace chillerlan\QRCode\Decoder;
|
||||
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use chillerlan\Settings\SettingsContainerInterface;
|
||||
use function array_slice, array_splice, file_exists, is_file, is_readable, realpath;
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,8 @@ use function array_slice, array_splice, file_exists, is_file, is_readable, realp
|
||||
*/
|
||||
abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
|
||||
|
||||
/** @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */
|
||||
protected SettingsContainerInterface $options;
|
||||
protected array $luminances;
|
||||
protected int $width;
|
||||
protected int $height;
|
||||
@@ -31,11 +35,11 @@ abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct(int $width, int $height){
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
// In order to measure pure decoding speed, we convert the entire image to a greyscale array
|
||||
// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.
|
||||
public function __construct(int $width, int $height, SettingsContainerInterface $options = null){
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->options = $options ?? new QROptions;
|
||||
|
||||
$this->luminances = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,14 @@ interface LuminanceSourceInterface{
|
||||
*/
|
||||
public function getRow(int $y):array;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a LuminanceSource instance from the given file
|
||||
*/
|
||||
public static function fromFile(string $path):self;
|
||||
|
||||
/**
|
||||
* Creates a LuminanceSource instance from the given data blob
|
||||
*/
|
||||
public static function fromBlob(string $blob):self;
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -340,14 +340,14 @@ class QRCode{
|
||||
* Reads a QR Code from a given file
|
||||
*/
|
||||
public function readFromFile(string $path):DecoderResult{
|
||||
return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromFile($path));
|
||||
return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromFile($path, $this->options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a QR Code from the given data blob
|
||||
*/
|
||||
public function readFromBlob(string $blob):DecoderResult{
|
||||
return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromBlob($blob));
|
||||
return $this->readFromSource($this->options->getLuminanceSourceFQCN()::fromBlob($blob, $this->options));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-1
@@ -53,7 +53,9 @@ use chillerlan\Settings\SettingsContainerAbstract;
|
||||
* @property string|null $imagickBG
|
||||
* @property string $fpdfMeasureUnit
|
||||
* @property array|null $moduleValues
|
||||
* @property bool $useImagickIfAvailable
|
||||
* @property bool $readerUseImagickIfAvailable
|
||||
* @property bool $readerGrayscale
|
||||
* @property bool $readerIncreaseContrast
|
||||
*/
|
||||
class QROptions extends SettingsContainerAbstract{
|
||||
use QROptionsTrait;
|
||||
|
||||
+15
-3
@@ -270,7 +270,19 @@ trait QROptionsTrait{
|
||||
/**
|
||||
* use Imaagick (if available) when reading QR Codes
|
||||
*/
|
||||
protected bool $useImagickIfAvailable = false;
|
||||
protected bool $readerUseImagickIfAvailable = false;
|
||||
|
||||
/**
|
||||
* grayscale the image before reading
|
||||
*/
|
||||
protected bool $readerGrayscale = false;
|
||||
|
||||
/**
|
||||
* increase the contrast before reading
|
||||
*
|
||||
* note that applying contrast works different in GD and Imagick, so mileage may vary
|
||||
*/
|
||||
protected bool $readerIncreaseContrast = false;
|
||||
|
||||
/**
|
||||
* clamp min/max version number
|
||||
@@ -385,7 +397,7 @@ trait QROptionsTrait{
|
||||
* enables Imagick for the QR Code reader if the extension is available
|
||||
*/
|
||||
protected function set_useImagickIfAvailable(bool $useImagickIfAvailable):void{
|
||||
$this->useImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
|
||||
$this->readerUseImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,7 +407,7 @@ trait QROptionsTrait{
|
||||
*/
|
||||
public function getLuminanceSourceFQCN():string{
|
||||
// i still hate this
|
||||
return $this->useImagickIfAvailable
|
||||
return $this->readerUseImagickIfAvailable
|
||||
? IMagickLuminanceSource::class
|
||||
: GDLuminanceSource::class;
|
||||
}
|
||||
|
||||
+32
-20
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace chillerlan\QRCodeTest;
|
||||
|
||||
use chillerlan\Settings\SettingsContainerInterface;
|
||||
use Exception, Generator;
|
||||
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
|
||||
use chillerlan\QRCode\{QRCode, QROptions};
|
||||
@@ -34,6 +35,12 @@ class QRCodeReaderTest extends TestCase{
|
||||
.'They say everything looks better with odd numbers of things. But sometimes I put even numbers—just '
|
||||
.'to upset the critics. We\'ll lay all these little funky little things in there. ';
|
||||
|
||||
protected SettingsContainerInterface $options;
|
||||
|
||||
protected function setUp():void{
|
||||
$this->options = new QROptions;
|
||||
}
|
||||
|
||||
public function qrCodeProvider():array{
|
||||
return [
|
||||
'helloworld' => ['hello_world.png', 'Hello world!'],
|
||||
@@ -51,6 +58,7 @@ class QRCodeReaderTest extends TestCase{
|
||||
'tilted' => ['tilted.png', 'Hello world!'], // tilted 22° CCW
|
||||
'rotated' => ['rotated.png', 'Hello world!'], // rotated 90° CW
|
||||
'gradient' => ['example_svg.png', 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s'], // color gradient (from svg example)
|
||||
'dots' => ['example_svg_dots.png', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'], // color gradient (from svg example)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -58,7 +66,11 @@ class QRCodeReaderTest extends TestCase{
|
||||
* @dataProvider qrCodeProvider
|
||||
*/
|
||||
public function testReaderGD(string $img, string $expected):void{
|
||||
$this::assertSame($expected, (string)(new QRCode)->readFromSource(GDLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img)));
|
||||
$this->options->readerGrayscale = true;
|
||||
$this->options->readerIncreaseContrast = true;
|
||||
|
||||
$this::assertSame($expected, (string)(new QRCode)
|
||||
->readFromSource(GDLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img, $this->options)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,26 +82,28 @@ class QRCodeReaderTest extends TestCase{
|
||||
$this::markTestSkipped('imagick not installed');
|
||||
}
|
||||
|
||||
// Y THO?? https://github.com/chillerlan/php-qrcode/runs/4270411373
|
||||
// "could not find enough finder patterns"
|
||||
if($img === 'example_svg.png' && PHP_OS_FAMILY === 'Windows' && PHP_VERSION_ID < 80100){
|
||||
$this::markTestSkipped('random gradient example issue??');
|
||||
$this->options->readerGrayscale = true;
|
||||
$this->options->readerIncreaseContrast = true;
|
||||
|
||||
if($img === 'damaged.png'){
|
||||
// for some reason that don't work for the damaged example, GD does a better job here
|
||||
$this->options->readerIncreaseContrast = false;
|
||||
}
|
||||
|
||||
$this::assertSame($expected, (string)(new QRCode)->readFromSource(IMagickLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img)));
|
||||
$this::assertSame($expected, (string)(new QRCode)
|
||||
->readFromSource(IMagickLuminanceSource::fromFile(__DIR__.'/qrcodes/'.$img, $this->options)));
|
||||
}
|
||||
|
||||
public function testReaderMultiSegment():void{
|
||||
$options = new QROptions;
|
||||
$options->outputType = QRCode::OUTPUT_IMAGE_PNG;
|
||||
$options->imageBase64 = false;
|
||||
$this->options->outputType = QRCode::OUTPUT_IMAGE_PNG;
|
||||
$this->options->imageBase64 = false;
|
||||
|
||||
$numeric = '123456789012345678901234567890';
|
||||
$alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:';
|
||||
$kanji = '茗荷茗荷茗荷茗荷';
|
||||
$byte = 'https://smiley.codes/qrcode/';
|
||||
|
||||
$qrcode = (new QRCode($options))
|
||||
$qrcode = (new QRCode($this->options))
|
||||
->addNumericSegment($numeric)
|
||||
->addAlphaNumSegment($alphanum)
|
||||
->addKanjiSegment($kanji)
|
||||
@@ -119,22 +133,20 @@ class QRCodeReaderTest extends TestCase{
|
||||
* @dataProvider dataTestProvider
|
||||
*/
|
||||
public function testReadData(Version $version, EccLevel $ecc, string $expected):void{
|
||||
$options = new QROptions;
|
||||
|
||||
$options->outputType = QRCode::OUTPUT_IMAGE_PNG;
|
||||
# $options->imageTransparent = false;
|
||||
$options->eccLevel = $ecc->getLevel();
|
||||
$options->version = $version->getVersionNumber();
|
||||
$options->imageBase64 = false;
|
||||
$options->useImagickIfAvailable = true;
|
||||
$this->options->outputType = QRCode::OUTPUT_IMAGE_PNG;
|
||||
# $this->options->imageTransparent = false;
|
||||
$this->options->eccLevel = $ecc->getLevel();
|
||||
$this->options->version = $version->getVersionNumber();
|
||||
$this->options->imageBase64 = false;
|
||||
$this->options->readerUseImagickIfAvailable = true;
|
||||
// what's interesting is that a smaller scale seems to produce fewer reader errors???
|
||||
// usually from version 20 up, independend of the luminance source
|
||||
// scale 1-2 produces none, scale 3: 1 error, scale 4: 6 errors, scale 5: 5 errors, scale 10: 10 errors
|
||||
// @see \chillerlan\QRCode\Detector\GridSampler::checkAndNudgePoints()
|
||||
$options->scale = 2;
|
||||
$this->options->scale = 2;
|
||||
|
||||
try{
|
||||
$qrcode = new QRCode($options);
|
||||
$qrcode = new QRCode($this->options);
|
||||
$imagedata = $qrcode->render($expected);
|
||||
$result = $qrcode->readFromBlob($imagedata);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Reference in New Issue
Block a user