Skip to content

Commit

Permalink
:octocat: +QRMatrix::setLogoSpace(), QRMatrix::M_FINDER_DOT (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
codemasher committed Nov 18, 2020
1 parent 8838eec commit 9941a0a
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 7 deletions.
97 changes: 90 additions & 7 deletions src/Data/QRMatrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class QRMatrix{
public const M_FORMAT = 0x0e;
public const M_VERSION = 0x10;
public const M_QUIETZONE = 0x12;
# public const M_LOGO = 0x14; // @todo
public const M_LOGO = 0x14;
public const M_FINDER_DOT = 0x16;

public const M_TEST = 0xff;

Expand Down Expand Up @@ -352,12 +353,18 @@ public function setFinderPattern():QRMatrix{
foreach($pos as $c){
for($y = 0; $y < 7; $y++){
for($x = 0; $x < 7; $x++){
$this->set(
$c[0] + $y,
$c[1] + $x,
!(($x > 0 && $x < 6 && ($y === 1 || $y === 5)) || ($y > 0 && $y < 6 && ($x === 1 || $x === 5))),
$this::M_FINDER
);
// outer (dark) 7*7 square
if($x === 0 || $x === 6 || $y === 0 || $y === 6){
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER);
}
// inner (light) 5*5 square
elseif($x === 1 || $x === 5 || $y === 1 || $y === 5){
$this->set($c[0] + $y, $c[1] + $x, false, $this::M_FINDER);
}
// 3*3 dot
else{
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER_DOT);
}
}
}
}
Expand Down Expand Up @@ -551,6 +558,82 @@ public function setQuietZone(int $size = null):QRMatrix{
return $this;
}

/**
* Clears a space of $width * $height in order to add a logo or text.
*
* Additionally, the logo space can be positioned within the QR Code - respecting the main functional patterns -
* using $startX and $startY. If either of these are null, the logo space will be centered in that direction.
* ECC level "H" (30%) is required.
*
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
* created images may become unreadable, especially when printed with a chance to receive damage.
* Please test thoroughly before using this feature in production.
*
* This method should be called from within an output module (after the matrix has been filled with data).
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
*
* @link https://github.com/chillerlan/php-qrcode/issues/52
*
* @param int $width
* @param int $height
* @param int|null $startX
* @param int|null $startY
*
* @return \chillerlan\QRCode\Data\QRMatrix
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{

// for logos we operate in ECC H (30%) only
if($this->eclevel !== 0b10){
throw new QRCodeDataException('ECC level "H" required to add logo space');
}

// we need uneven sizes, adjust if needed
if(($width % 2) === 0){
$width++;
}

if(($height % 2) === 0){
$height++;
}

// $this->moduleCount includes the quiet zone (if created), we need the QR size here
$length = $this->version * 4 + 17;

// throw if the logo space exceeds the maximum error correction capacity
if($width * $height > floor($length * $length * 0.2)){
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
}

// quiet zone size
$qz = ($this->moduleCount - $length) / 2;
// skip quiet zone and the first 9 rows/columns (finder-, mode-, version- and timing patterns)
$start = $qz + 9;
// skip quiet zone
$end = $this->moduleCount - $qz;

// determine start coordinates
$startX = ($startX !== null ? $startX : ($length - $width) / 2) + $qz;
$startY = ($startY !== null ? $startY : ($length - $height) / 2) + $qz;

// clear the space
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
// out of bounds, skip
if($x < $start || $y < $start ||$x >= $end || $y >= $end){
continue;
}
// a match
if($x >= $startX && $x < ($startX + $width) && $y >= $startY && $y < ($startY + $height)){
$this->set($x, $y, false, $this::M_LOGO);
}
}
}

return $this;
}

/**
* Maps the binary $data array from QRDataInterface::maskECC() on the matrix, using $maskPattern
*
Expand Down
2 changes: 2 additions & 0 deletions src/Output/QROutputInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface QROutputInterface{
QRMatrix::M_FORMAT => false, // 14
QRMatrix::M_VERSION => false, // 16
QRMatrix::M_QUIETZONE => false, // 18
QRMatrix::M_LOGO => false, // 20
QRMatrix::M_TEST => false, // 255
// dark
QRMatrix::M_DARKMODULE << 8 => true, // 512
Expand All @@ -38,6 +39,7 @@ interface QROutputInterface{
QRMatrix::M_TIMING << 8 => true, // 3072
QRMatrix::M_FORMAT << 8 => true, // 3584
QRMatrix::M_VERSION << 8 => true, // 4096
QRMatrix::M_FINDER_DOT << 8 => true, // 5632
QRMatrix::M_TEST << 8 => true, // 65280
];

Expand Down
66 changes: 66 additions & 0 deletions tests/Data/QRMatrixTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace chillerlan\QRCodeTest\Data;

use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
use chillerlan\QRCodeTest\QRTestAbstract;
use ReflectionClass;
Expand Down Expand Up @@ -191,4 +192,69 @@ public function testSetQuietZoneException(){
$this->matrix->setQuietZone();
}

public function testSetLogoSpaceOrientation():void{
$o = new QROptions;
$o->version = 10;
$o->eccLevel = QRCode::ECC_H;
$o->addQuietzone = false;

$matrix = (new QRCode($o))->getMatrix('testdata');
// also testing size adjustment to uneven numbers
$matrix->setLogoSpace(20, 14);

// NW corner
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(17, 20));
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(18, 21));

// SE corner
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(38, 35));
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(39, 36));
}

public function testSetLogoSpacePosition():void{
$o = new QROptions;
$o->version = 10;
$o->eccLevel = QRCode::ECC_H;
$o->addQuietzone = true;
$o->quietzoneSize = 10;

$m = (new QRCode($o))->getMatrix('testdata');

// logo space should not overwrite quiet zone & function patterns
$m->setLogoSpace(21, 21, -10, -10);
$this::assertSame(QRMatrix::M_QUIETZONE, $m->get(9, 9));
$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(10, 10));
$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(16, 16));
$this::assertSame(QRMatrix::M_SEPARATOR, $m->get(17, 17));
$this::assertSame(QRMatrix::M_FORMAT << 8, $m->get(18, 18));
$this::assertSame(QRMatrix::M_LOGO, $m->get(19, 19));
$this::assertSame(QRMatrix::M_LOGO, $m->get(20, 20));
$this::assertNotSame(QRMatrix::M_LOGO, $m->get(21, 21));

// i just realized that setLogoSpace() could be called multiple times
// on the same instance and i'm not going to do anything about it :P
$m->setLogoSpace(21, 21, 45, 45);
$this::assertNotSame(QRMatrix::M_LOGO, $m->get(54, 54));
$this::assertSame(QRMatrix::M_LOGO, $m->get(55, 55));
$this::assertSame(QRMatrix::M_QUIETZONE, $m->get(67, 67));
}

public function testSetLogoSpaceInvalidEccException():void{
$this->expectException(QRCodeDataException::class);
$this->expectExceptionMessage('ECC level "H" required to add logo space');

(new QRCode)->getMatrix('testdata')->setLogoSpace(50, 50);
}

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 = QRCode::ECC_H;

(new QRCode($o))->getMatrix('testdata')->setLogoSpace(50, 50);
}

}

0 comments on commit 9941a0a

Please sign in to comment.