Files
php-qrcode/tests/Data/QRMatrixTest.php
T
2023-06-15 17:15:33 +02:00

617 lines
17 KiB
PHP
Executable File

<?php
/**
* Class QRMatrixTest
*
* @created 17.11.2017
* @author Smiley <smiley@chillerlan.net>
* @copyright 2017 Smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Data;
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
use chillerlan\QRCode\Output\{QROutputInterface, QRString};
use PHPUnit\Framework\TestCase;
use Generator;
use function defined;
/**
* Tests the QRMatrix class
*/
final class QRMatrixTest extends TestCase{
private const version = 40;
private QRMatrix $matrix;
/**
* invokes a QRMatrix object
*/
protected function setUp():void{
$this->matrix = new QRMatrix(
new Version($this::version),
new EccLevel(EccLevel::L)
);
}
/**
* Matrix debugging console output
*/
public static function debugMatrix(QRMatrix $matrix):void{
/** @noinspection PhpUndefinedConstantInspection - see phpunit.xml.dist */
if(defined('TEST_IS_CI') && TEST_IS_CI === true){
return;
}
$opt = new QROptions;
$opt->outputType = QROutputInterface::STRING_TEXT;
$opt->eol = "\n";
$opt->moduleValues = [
// this is not ideal but it seems it's not possible anymore to colorize emoji via ansi codes
// 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤
// 🟥 🟧 🟨 🟩 🟦 🟪 ⬛ ⬜ 🟫
// finder
QRMatrix::M_FINDER_DARK => '🟥', // dark (true)
QRMatrix::M_FINDER => '🔴', // light (false)
QRMatrix::M_FINDER_DOT => '🟥', // finder dot, dark (true)
// alignment
QRMatrix::M_ALIGNMENT_DARK => '🟧',
QRMatrix::M_ALIGNMENT => '🟠',
// timing
QRMatrix::M_TIMING_DARK => '🟨',
QRMatrix::M_TIMING => '🟡',
// format
QRMatrix::M_FORMAT_DARK => '🟪',
QRMatrix::M_FORMAT => '🟣',
// version
QRMatrix::M_VERSION_DARK => '🟩',
QRMatrix::M_VERSION => '🟢',
// data
QRMatrix::M_DATA_DARK => '🟦',
QRMatrix::M_DATA => '🔵',
// dark module
QRMatrix::M_DARKMODULE => '🟫',
// separator
QRMatrix::M_SEPARATOR => '⚪️',
// quiet zone
QRMatrix::M_QUIETZONE => '⬜',
// logo space
QRMatrix::M_LOGO => '⬜',
// empty
QRMatrix::M_NULL => '🟤',
// data
QRMatrix::M_TEST_DARK => '⬛',
QRMatrix::M_TEST => '⚫️',
];
$out = (new QRString($opt, $matrix))->dump();
echo "\n\n".$out."\n\n";
}
/**
* debugging shortcut - limit to a single version when using with matrixProvider
*
* @see QRMatrixTest::matrixProvider()
*/
protected function dm(QRMatrix $matrix):void{
// limit
if($matrix->getVersion()->getVersionNumber() !== 7){
return;
}
self::debugMatrix($matrix);
}
/**
* Validates the QRMatrix instance
*/
public function testInstance():void{
$this::assertInstanceOf(QRMatrix::class, $this->matrix);
}
/**
* Tests if size() returns the actual matrix size/count
*/
public function testSize():void{
$this::assertCount($this->matrix->getSize(), $this->matrix->getMatrix(true));
}
/**
* Tests if version() returns the current (given) version
*/
public function testVersion():void{
$this::assertSame($this::version, $this->matrix->getVersion()->getVersionNumber());
}
/**
* Tests if eccLevel() returns the current (given) ECC level
*/
public function testECC():void{
$this::assertSame(EccLevel::L, $this->matrix->getEccLevel()->getLevel());
}
/**
* Tests if maskPattern() returns the current (or default) mask pattern
*/
public function testMaskPattern():void{
// set via matrix evaluation
$matrix = (new QRCode)->addByteSegment('testdata')->getQRMatrix();
$this::assertInstanceOf(MaskPattern::class, $matrix->getMaskPattern());
$this::assertSame(MaskPattern::PATTERN_100, $matrix->getMaskPattern()->getPattern());
}
/**
* Tests the set(), get() and check() methods
*/
public function testGetSetCheck():void{
$this->matrix->set(10, 10, true, QRMatrix::M_TEST);
$this::assertSame(QRMatrix::M_TEST_DARK, $this->matrix->get(10, 10));
$this::assertTrue($this->matrix->check(10, 10));
$this->matrix->set(20, 20, false, QRMatrix::M_TEST);
$this::assertSame(QRMatrix::M_TEST, $this->matrix->get(20, 20));
$this::assertFalse($this->matrix->check(20, 20));
// get proper results when using a *_DARK constant
$this->matrix->set(30, 30, true, QRMatrix::M_TEST_DARK);
$this::assertSame(QRMatrix::M_TEST_DARK, $this->matrix->get(30, 30));
$this->matrix->set(40, 40, false, QRMatrix::M_TEST_DARK);
$this::assertSame(QRMatrix::M_TEST, $this->matrix->get(40, 40));
// out of range
$this::assertFalse($this->matrix->check(-1, -1));
$this::assertSame(-1, $this->matrix->get(-1, -1));
}
/**
* Version data provider for several pattern tests
*/
public static function matrixProvider():Generator{
$ecc = new EccLevel(EccLevel::L);
for($i = 1; $i <= 40; $i++){
yield 'version: '.$i => [new QRMatrix(new Version($i), $ecc)];
}
}
/**
* Tests setting the dark module and verifies its position
*
* @dataProvider matrixProvider
*/
public function testSetDarkModule(QRMatrix $matrix):void{
$matrix->setDarkModule();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get(8, ($matrix->getSize() - 8)));
}
/**
* Tests setting the finder patterns and verifies their positions
*
* @dataProvider matrixProvider
*/
public function testSetFinderPattern(QRMatrix $matrix):void{
$matrix->setFinderPattern();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_FINDER_DARK, $matrix->get(0, 0));
$this::assertSame(QRMatrix::M_FINDER_DARK, $matrix->get(0, ($matrix->getSize() - 1)));
$this::assertSame(QRMatrix::M_FINDER_DARK, $matrix->get(($matrix->getSize() - 1), 0));
}
/**
* Tests the separator patterns and verifies their positions
*
* @dataProvider matrixProvider
*/
public function testSetSeparators(QRMatrix $matrix):void{
$matrix->setSeparators();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_SEPARATOR, $matrix->get(7, 0));
$this::assertSame(QRMatrix::M_SEPARATOR, $matrix->get(0, 7));
$this::assertSame(QRMatrix::M_SEPARATOR, $matrix->get(0, ($matrix->getSize() - 8)));
$this::assertSame(QRMatrix::M_SEPARATOR, $matrix->get(($matrix->getSize() - 8), 0));
}
/**
* Tests the alignment patterns and verifies their positions - version 1 (no pattern) skipped
*
* @dataProvider matrixProvider
*/
public function testSetAlignmentPattern(QRMatrix $matrix):void{
$version = $matrix->getVersion();
if($version->getVersionNumber() === 1){
/** @noinspection PhpUnitTestFailedLineInspection */
$this::markTestSkipped('N/A (Version 1 has no alignment pattern)');
}
$matrix
->setFinderPattern()
->setAlignmentPattern()
;
$this->dm($matrix);
$alignmentPattern = $version->getAlignmentPattern();
foreach($alignmentPattern as $py){
foreach($alignmentPattern as $px){
// skip finder pattern
if(!$matrix->checkTypeIn($px, $py, [QRMatrix::M_FINDER, QRMatrix::M_FINDER_DOT])){
$this::assertSame(QRMatrix::M_ALIGNMENT_DARK, $matrix->get($px, $py));
}
}
}
}
/**
* Tests the timing patterns and verifies their positions
*
* @dataProvider matrixProvider
*/
public function testSetTimingPattern(QRMatrix $matrix):void{
$matrix
->setFinderPattern()
->setAlignmentPattern()
->setTimingPattern()
;
$this->dm($matrix);
$size = $matrix->getSize();
for($i = 7; $i < ($size - 7); $i++){
if(($i % 2) === 0){
// skip alignment pattern
if(!$matrix->checkTypeIn(6, $i, [QRMatrix::M_ALIGNMENT])){
$this::assertSame(QRMatrix::M_TIMING_DARK, $matrix->get(6, $i));
$this::assertSame(QRMatrix::M_TIMING_DARK, $matrix->get($i, 6));
}
}
}
}
/**
* Tests the version patterns and verifies their positions - version < 7 skipped
*
* @dataProvider matrixProvider
*/
public function testSetVersionNumber(QRMatrix $matrix):void{
if($matrix->getVersion()->getVersionNumber() < 7){
/** @noinspection PhpUnitTestFailedLineInspection */
$this::markTestSkipped('N/A (Version < 7)');
}
$matrix->setVersionNumber();
$this->dm($matrix);
$this::assertTrue($matrix->checkType(($matrix->getSize() - 9), 0, QRMatrix::M_VERSION));
$this::assertTrue($matrix->checkType(($matrix->getSize() - 11), 5, QRMatrix::M_VERSION));
$this::assertTrue($matrix->checkType(0, ($matrix->getSize() - 9), QRMatrix::M_VERSION));
$this::assertTrue($matrix->checkType(5, ($matrix->getSize() - 11), QRMatrix::M_VERSION));
}
/**
* Tests the format patterns and verifies their positions
*
* @dataProvider matrixProvider
*/
public function testSetFormatInfo(QRMatrix $matrix):void{
$matrix->setFormatInfo(new MaskPattern(MaskPattern::PATTERN_000));
$this->dm($matrix);
$this::assertTrue($matrix->checkType(8, 0, QRMatrix::M_FORMAT));
$this::assertTrue($matrix->checkType(0, 8, QRMatrix::M_FORMAT));
$this::assertTrue($matrix->checkType(($matrix->getSize() - 1), 8, QRMatrix::M_FORMAT));
$this::assertTrue($matrix->checkType(($matrix->getSize() - 8), 8, QRMatrix::M_FORMAT));
}
/**
* Tests the quiet zone pattern and verifies its position
*
* @dataProvider matrixProvider
*/
public function testSetQuietZone(QRMatrix $matrix):void{
$size = $matrix->getSize();
$quietZoneSize = 5;
$matrix->set(0, 0, true, QRMatrix::M_TEST);
$matrix->set(($size - 1), ($size - 1), true, QRMatrix::M_TEST);
$matrix->setQuietZone($quietZoneSize);
$s = ($size + 2 * $quietZoneSize);
$this::assertCount($s, $matrix->getMatrix(true));
$this::assertCount($s, $matrix->getMatrix(true)[($size - 1)]);
$size = $matrix->getSize();
$this->dm($matrix);
$this::assertTrue($matrix->checkType(0, 0, QRMatrix::M_QUIETZONE));
$this::assertTrue($matrix->checkType(($size - 1), ($size - 1), QRMatrix::M_QUIETZONE));
$s = ($size - 1 - $quietZoneSize);
$this::assertSame(QRMatrix::M_TEST_DARK, $matrix->get($quietZoneSize, $quietZoneSize));
$this::assertSame(QRMatrix::M_TEST_DARK, $matrix->get($s, $s));
}
/**
* Tests if an exception is thrown in an attempt to create the quiet zone before data was written
*/
public function testSetQuietZoneException():void{
$this->expectException(QRCodeDataException::class);
$this->expectExceptionMessage('use only after writing data');
$this->matrix->setQuietZone(42);
}
/**
* Tests rotating the matrix by 90 degrees CW
*
* @dataProvider matrixProvider
*/
public function testRotate90(QRMatrix $matrix):void{
$matrix->initFunctionalPatterns();
// matrix size
$size = $matrix->getSize();
// quiet zone size
$qz = (($size - $matrix->getVersion()->getDimension()) / 2);
// initial dark module position
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get((8 + $qz), ($size - 8 - $qz)));
// first rotation
$matrix->rotate90();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get((7 + $qz), (8 + $qz)));
// second rotation
$matrix->rotate90();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get(($size - 9 - $qz), (7 + $qz)));
// third rotation
$matrix->rotate90();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get(($size - 8 - $qz), ($size - 9 - $qz)));
// fourth rotation
$matrix->rotate90();
$this->dm($matrix);
$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get((8 + $qz), ($size - 8 - $qz)));
}
/**
* Tests if the logo space is drawn square if one of the dimensions is omitted
*/
public function testSetLogoSpaceOmitHeight():void{
$o = new QROptions;
$o->version = 2;
$o->eccLevel = EccLevel::H;
$o->addQuietzone = false;
$o->addLogoSpace = true;
$o->logoSpaceHeight = 5;
$matrix = (new QRCode($o))->addByteSegment('testdata')->getQRMatrix();
self::debugMatrix($matrix);
$this::assertFalse($matrix->checkType(9, 9, QRMatrix::M_LOGO));
$this::assertTrue($matrix->checkType(10, 10, QRMatrix::M_LOGO));
$this::assertTrue($matrix->checkType(14, 14, QRMatrix::M_LOGO));
$this::assertFalse($matrix->checkType(15, 15, QRMatrix::M_LOGO));
}
/**
* Tests the auto orientation of the logo space
*/
public function testSetLogoSpaceOrientation():void{
$o = new QROptions;
$o->version = 10;
$o->eccLevel = EccLevel::H;
$o->addQuietzone = false;
$matrix = (new QRCode($o))->addByteSegment('testdata')->getQRMatrix();
// also testing size adjustment to uneven numbers
$matrix->setLogoSpace(20, 14);
self::debugMatrix($matrix);
// NW corner
$this::assertFalse($matrix->checkType(17, 20, QRMatrix::M_LOGO));
$this::assertTrue($matrix->checkType(18, 21, QRMatrix::M_LOGO));
// SE corner
$this::assertTrue($matrix->checkType(38, 35, QRMatrix::M_LOGO));
$this::assertFalse($matrix->checkType(39, 36, QRMatrix::M_LOGO));
}
/**
* Tests the manual positioning of the logo space
*/
public function testSetLogoSpacePosition():void{
$o = new QROptions;
$o->version = 10;
$o->eccLevel = EccLevel::H;
$o->addQuietzone = true;
$o->quietzoneSize = 10;
$matrix = (new QRCode($o))->addByteSegment('testdata')->getQRMatrix();
self::debugMatrix($matrix);
// logo space should not overwrite quiet zone & function patterns
$matrix->setLogoSpace(21, 21, -10, -10);
$this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get(9, 9));
$this::assertSame(QRMatrix::M_FINDER_DARK, $matrix->get(10, 10));
$this::assertSame(QRMatrix::M_FINDER_DARK, $matrix->get(16, 16));
$this::assertSame(QRMatrix::M_SEPARATOR, $matrix->get(17, 17));
$this::assertSame(QRMatrix::M_FORMAT_DARK, $matrix->get(18, 18));
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(19, 19));
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(20, 20));
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(21, 21));
// Ii just realized that setLogoSpace() could be called multiple times
// on the same instance, and I'm not going to do anything about it :P
$matrix->setLogoSpace(21, 21, 45, 45);
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(54, 54));
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(55, 55));
$this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get(67, 67));
}
/**
* Tests whether an exception is thrown when an ECC level other than "H" is set when attempting to add logo space
*/
public function testSetLogoSpaceInvalidEccException():void{
$this->expectException(QRCodeDataException::class);
$this->expectExceptionMessage('ECC level "H" required to add logo space');
(new QRCode)->addByteSegment('testdata')->getQRMatrix()->setLogoSpace(50, 50);
}
/**
* Tests whether an exception is thrown when width or height exceed the matrix size
*/
public function testSetLogoSpaceExceedsException():void{
$this->expectException(QRCodeDataException::class);
$this->expectExceptionMessage('logo dimensions exceed matrix size');
$o = new QROptions;
$o->version = 5;
$o->eccLevel = EccLevel::H;
(new QRCode($o))->addByteSegment('testdata')->getQRMatrix()->setLogoSpace(69, 1);
}
/**
* Tests whether an exception is thrown when the logo space size exceeds the maximum ECC capacity
*/
public function testSetLogoSpaceMaxSizeException():void{
$this->expectException(QRCodeDataException::class);
$this->expectExceptionMessage('logo space exceeds the maximum error correction capacity');
$o = new QROptions;
$o->version = 5;
$o->eccLevel = EccLevel::H;
(new QRCode($o))->addByteSegment('testdata')->getQRMatrix()->setLogoSpace(37, 37);
}
/**
* Tests checking whether the M_TYPE of a module is not one of an array of M_TYPES
*/
public function testCheckTypeIn():void{
$this->matrix->set(10, 10, true, QRMatrix::M_QUIETZONE);
$this::assertFalse($this->matrix->checkTypeIn(10, 10, [QRMatrix::M_DATA, QRMatrix::M_FINDER]));
$this::assertTrue($this->matrix->checkTypeIn(10, 10, [QRMatrix::M_QUIETZONE, QRMatrix::M_FINDER]));
}
/**
* Tests checking the adjacent modules
*/
public function testCheckNeighbours():void{
$this->matrix
->setFinderPattern()
->setAlignmentPattern()
;
/*
* center of finder pattern (surrounded by all dark)
*
* # # # # # # #
* # #
* # # # # #
* # # 0 # #
* # # # # #
* # #
* # # # # # # #
*/
$this::assertSame(0b11111111, $this->matrix->checkNeighbours(3, 3));
/*
* center of alignment pattern (surrounded by all light)
*
* # # # # #
* # #
* # 0 #
* # #
* # # # # #
*/
$this::assertSame(0b00000000, $this->matrix->checkNeighbours(30, 30));
/*
* top left light block of finder pattern
*
* # # #
* # 0
* # #
*/
$this::assertSame(0b11010111, $this->matrix->checkNeighbours(1, 1));
/*
* bottom left light block of finder pattern
*
* # #
* # 0
* # # #
*/
$this::assertSame(0b11110101, $this->matrix->checkNeighbours(1, 5));
/*
* top right light block of finder pattern
*
* # # #
* 0 #
* # #
*/
$this::assertSame(0b01011111, $this->matrix->checkNeighbours(5, 1));
/*
* bottom right light block of finder pattern
*
* # #
* 0 #
* # # #
*/
$this::assertSame(0b01111101, $this->matrix->checkNeighbours(5, 5));
/*
* M_TYPE check
*
* # # #
* 0
* X X X
*/
$this::assertSame(0b00000111, $this->matrix->checkNeighbours(3, 1, QRMatrix::M_FINDER));
$this::assertSame(0b01110000, $this->matrix->checkNeighbours(3, 1, QRMatrix::M_FINDER_DOT));
}
}