Skip to content

Commit 9941a0a

Browse files
committed
:octocat: +QRMatrix::setLogoSpace(), QRMatrix::M_FINDER_DOT (#52)
1 parent 8838eec commit 9941a0a

File tree

3 files changed

+158
-7
lines changed

3 files changed

+158
-7
lines changed

src/Data/QRMatrix.php

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class QRMatrix{
3232
public const M_FORMAT = 0x0e;
3333
public const M_VERSION = 0x10;
3434
public const M_QUIETZONE = 0x12;
35-
# public const M_LOGO = 0x14; // @todo
35+
public const M_LOGO = 0x14;
36+
public const M_FINDER_DOT = 0x16;
3637

3738
public const M_TEST = 0xff;
3839

@@ -352,12 +353,18 @@ public function setFinderPattern():QRMatrix{
352353
foreach($pos as $c){
353354
for($y = 0; $y < 7; $y++){
354355
for($x = 0; $x < 7; $x++){
355-
$this->set(
356-
$c[0] + $y,
357-
$c[1] + $x,
358-
!(($x > 0 && $x < 6 && ($y === 1 || $y === 5)) || ($y > 0 && $y < 6 && ($x === 1 || $x === 5))),
359-
$this::M_FINDER
360-
);
356+
// outer (dark) 7*7 square
357+
if($x === 0 || $x === 6 || $y === 0 || $y === 6){
358+
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER);
359+
}
360+
// inner (light) 5*5 square
361+
elseif($x === 1 || $x === 5 || $y === 1 || $y === 5){
362+
$this->set($c[0] + $y, $c[1] + $x, false, $this::M_FINDER);
363+
}
364+
// 3*3 dot
365+
else{
366+
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER_DOT);
367+
}
361368
}
362369
}
363370
}
@@ -551,6 +558,82 @@ public function setQuietZone(int $size = null):QRMatrix{
551558
return $this;
552559
}
553560

561+
/**
562+
* Clears a space of $width * $height in order to add a logo or text.
563+
*
564+
* Additionally, the logo space can be positioned within the QR Code - respecting the main functional patterns -
565+
* using $startX and $startY. If either of these are null, the logo space will be centered in that direction.
566+
* ECC level "H" (30%) is required.
567+
*
568+
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
569+
* created images may become unreadable, especially when printed with a chance to receive damage.
570+
* Please test thoroughly before using this feature in production.
571+
*
572+
* This method should be called from within an output module (after the matrix has been filled with data).
573+
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
574+
*
575+
* @link https://github.com/chillerlan/php-qrcode/issues/52
576+
*
577+
* @param int $width
578+
* @param int $height
579+
* @param int|null $startX
580+
* @param int|null $startY
581+
*
582+
* @return \chillerlan\QRCode\Data\QRMatrix
583+
* @throws \chillerlan\QRCode\Data\QRCodeDataException
584+
*/
585+
public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{
586+
587+
// for logos we operate in ECC H (30%) only
588+
if($this->eclevel !== 0b10){
589+
throw new QRCodeDataException('ECC level "H" required to add logo space');
590+
}
591+
592+
// we need uneven sizes, adjust if needed
593+
if(($width % 2) === 0){
594+
$width++;
595+
}
596+
597+
if(($height % 2) === 0){
598+
$height++;
599+
}
600+
601+
// $this->moduleCount includes the quiet zone (if created), we need the QR size here
602+
$length = $this->version * 4 + 17;
603+
604+
// throw if the logo space exceeds the maximum error correction capacity
605+
if($width * $height > floor($length * $length * 0.2)){
606+
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
607+
}
608+
609+
// quiet zone size
610+
$qz = ($this->moduleCount - $length) / 2;
611+
// skip quiet zone and the first 9 rows/columns (finder-, mode-, version- and timing patterns)
612+
$start = $qz + 9;
613+
// skip quiet zone
614+
$end = $this->moduleCount - $qz;
615+
616+
// determine start coordinates
617+
$startX = ($startX !== null ? $startX : ($length - $width) / 2) + $qz;
618+
$startY = ($startY !== null ? $startY : ($length - $height) / 2) + $qz;
619+
620+
// clear the space
621+
foreach($this->matrix as $y => $row){
622+
foreach($row as $x => $val){
623+
// out of bounds, skip
624+
if($x < $start || $y < $start ||$x >= $end || $y >= $end){
625+
continue;
626+
}
627+
// a match
628+
if($x >= $startX && $x < ($startX + $width) && $y >= $startY && $y < ($startY + $height)){
629+
$this->set($x, $y, false, $this::M_LOGO);
630+
}
631+
}
632+
}
633+
634+
return $this;
635+
}
636+
554637
/**
555638
* Maps the binary $data array from QRDataInterface::maskECC() on the matrix, using $maskPattern
556639
*

src/Output/QROutputInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface QROutputInterface{
2929
QRMatrix::M_FORMAT => false, // 14
3030
QRMatrix::M_VERSION => false, // 16
3131
QRMatrix::M_QUIETZONE => false, // 18
32+
QRMatrix::M_LOGO => false, // 20
3233
QRMatrix::M_TEST => false, // 255
3334
// dark
3435
QRMatrix::M_DARKMODULE << 8 => true, // 512
@@ -38,6 +39,7 @@ interface QROutputInterface{
3839
QRMatrix::M_TIMING << 8 => true, // 3072
3940
QRMatrix::M_FORMAT << 8 => true, // 3584
4041
QRMatrix::M_VERSION << 8 => true, // 4096
42+
QRMatrix::M_FINDER_DOT << 8 => true, // 5632
4143
QRMatrix::M_TEST << 8 => true, // 65280
4244
];
4345

tests/Data/QRMatrixTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
namespace chillerlan\QRCodeTest\Data;
1414

1515
use chillerlan\QRCode\QRCode;
16+
use chillerlan\QRCode\QROptions;
1617
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
1718
use chillerlan\QRCodeTest\QRTestAbstract;
1819
use ReflectionClass;
@@ -191,4 +192,69 @@ public function testSetQuietZoneException(){
191192
$this->matrix->setQuietZone();
192193
}
193194

195+
public function testSetLogoSpaceOrientation():void{
196+
$o = new QROptions;
197+
$o->version = 10;
198+
$o->eccLevel = QRCode::ECC_H;
199+
$o->addQuietzone = false;
200+
201+
$matrix = (new QRCode($o))->getMatrix('testdata');
202+
// also testing size adjustment to uneven numbers
203+
$matrix->setLogoSpace(20, 14);
204+
205+
// NW corner
206+
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(17, 20));
207+
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(18, 21));
208+
209+
// SE corner
210+
$this::assertSame(QRMatrix::M_LOGO, $matrix->get(38, 35));
211+
$this::assertNotSame(QRMatrix::M_LOGO, $matrix->get(39, 36));
212+
}
213+
214+
public function testSetLogoSpacePosition():void{
215+
$o = new QROptions;
216+
$o->version = 10;
217+
$o->eccLevel = QRCode::ECC_H;
218+
$o->addQuietzone = true;
219+
$o->quietzoneSize = 10;
220+
221+
$m = (new QRCode($o))->getMatrix('testdata');
222+
223+
// logo space should not overwrite quiet zone & function patterns
224+
$m->setLogoSpace(21, 21, -10, -10);
225+
$this::assertSame(QRMatrix::M_QUIETZONE, $m->get(9, 9));
226+
$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(10, 10));
227+
$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(16, 16));
228+
$this::assertSame(QRMatrix::M_SEPARATOR, $m->get(17, 17));
229+
$this::assertSame(QRMatrix::M_FORMAT << 8, $m->get(18, 18));
230+
$this::assertSame(QRMatrix::M_LOGO, $m->get(19, 19));
231+
$this::assertSame(QRMatrix::M_LOGO, $m->get(20, 20));
232+
$this::assertNotSame(QRMatrix::M_LOGO, $m->get(21, 21));
233+
234+
// i just realized that setLogoSpace() could be called multiple times
235+
// on the same instance and i'm not going to do anything about it :P
236+
$m->setLogoSpace(21, 21, 45, 45);
237+
$this::assertNotSame(QRMatrix::M_LOGO, $m->get(54, 54));
238+
$this::assertSame(QRMatrix::M_LOGO, $m->get(55, 55));
239+
$this::assertSame(QRMatrix::M_QUIETZONE, $m->get(67, 67));
240+
}
241+
242+
public function testSetLogoSpaceInvalidEccException():void{
243+
$this->expectException(QRCodeDataException::class);
244+
$this->expectExceptionMessage('ECC level "H" required to add logo space');
245+
246+
(new QRCode)->getMatrix('testdata')->setLogoSpace(50, 50);
247+
}
248+
249+
public function testSetLogoSpaceMaxSizeException():void{
250+
$this->expectException(QRCodeDataException::class);
251+
$this->expectExceptionMessage('logo space exceeds the maximum error correction capacity');
252+
253+
$o = new QROptions;
254+
$o->version = 5;
255+
$o->eccLevel = QRCode::ECC_H;
256+
257+
(new QRCode($o))->getMatrix('testdata')->setLogoSpace(50, 50);
258+
}
259+
194260
}

0 commit comments

Comments
 (0)