diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php index db4cc1344..ee959c03a 100644 --- a/src/Decoder/GDLuminanceSource.php +++ b/src/Decoder/GDLuminanceSource.php @@ -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); } } diff --git a/src/Decoder/IMagickLuminanceSource.php b/src/Decoder/IMagickLuminanceSource.php index c0356ed47..edf8a3975 100644 --- a/src/Decoder/IMagickLuminanceSource.php +++ b/src/Decoder/IMagickLuminanceSource.php @@ -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); } } diff --git a/src/Decoder/LuminanceSourceAbstract.php b/src/Decoder/LuminanceSourceAbstract.php index 1fa3dabda..8b6221bea 100644 --- a/src/Decoder/LuminanceSourceAbstract.php +++ b/src/Decoder/LuminanceSourceAbstract.php @@ -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 = []; } diff --git a/src/Decoder/LuminanceSourceInterface.php b/src/Decoder/LuminanceSourceInterface.php index 871e381c4..56ecbb86b 100644 --- a/src/Decoder/LuminanceSourceInterface.php +++ b/src/Decoder/LuminanceSourceInterface.php @@ -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; } diff --git a/src/QRCode.php b/src/QRCode.php index da5c67ae6..b6d23d661 100755 --- a/src/QRCode.php +++ b/src/QRCode.php @@ -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)); } /** diff --git a/src/QROptions.php b/src/QROptions.php index b10105d88..6f5c10ea5 100644 --- a/src/QROptions.php +++ b/src/QROptions.php @@ -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; diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index 475cde55d..342959e95 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -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; } diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php index 3d98ef1ce..5d396b743 100644 --- a/tests/QRCodeReaderTest.php +++ b/tests/QRCodeReaderTest.php @@ -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); } diff --git a/tests/qrcodes/example_svg_dots.png b/tests/qrcodes/example_svg_dots.png new file mode 100644 index 000000000..c51cbd4d9 Binary files /dev/null and b/tests/qrcodes/example_svg_dots.png differ