Skip to content

Commit 613b2fe

Browse files
committed
[FEATURE] Data,Refinery: add QR code support.
* Add `ILIAS\Data\QR\SVGCode` wrapper for generated QR codes. * Add `ILIAS\Data\QR\ErrorCorrectionLevel` for standardised error correction levels. * Add `ILIAS\Refinery\String\QRCode` transformation to convert string into a QR code. * Add unit tests covering the functionality above.
1 parent e7e034a commit 613b2fe

6 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace ILIAS\Data\QR;
21+
22+
/**
23+
* Error correction levels as defined by ISO/IEC 18004.
24+
*
25+
* Each level specifies the percentage of codewords that can be
26+
* restored if the QR code is damaged or partially obscured.
27+
* Please note that increasing the error correction level will
28+
* decrease the data capacity of its payload.
29+
*
30+
* @see https://www.qrcode.com/en/about/error_correction.html
31+
*/
32+
enum ErrorCorrectionLevel: string
33+
{
34+
/** ~7% of codewords can be restored. */
35+
case LOW = 'L';
36+
37+
/** ~15% of codewords can be restored (most fequently used). */
38+
case MEDIUM = 'M';
39+
40+
/** ~25% of codewords can be restored. */
41+
case QUARTILE = 'Q';
42+
43+
/** ~30% of codewords can be restored. */
44+
case HIGH = 'H';
45+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace ILIAS\Data\QR;
21+
22+
/**
23+
* Data transfer object that carries the raw SVG data and provides
24+
* funcionality to embed it as data uri.
25+
*/
26+
readonly class SVGCode
27+
{
28+
public function __construct(
29+
protected string $raw_svg_string,
30+
) {
31+
$this->assertStringNotEmpty($this->raw_svg_string);
32+
}
33+
34+
public function toDataUri(): string
35+
{
36+
return "data:image/svg+xml;base64," . base64_encode($this->raw_svg_string);
37+
}
38+
39+
protected function assertStringNotEmpty(string $string): void
40+
{
41+
if (0 >= mb_strlen($string)) {
42+
throw new \InvalidArgumentException("SVG data must not be empty.");
43+
}
44+
}
45+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
use ILIAS\Data\QR\SVGCode;
21+
use PHPUnit\Framework\Attributes\Depends;
22+
use PHPUnit\Framework\TestCase;
23+
24+
class QRCodeTest extends TestCase
25+
{
26+
public function testConstructorWithEmptyString(): void
27+
{
28+
$this->expectException(\InvalidArgumentException::class);
29+
$code = new SVGCode('');
30+
}
31+
32+
public function testConstructorWithNonEmptyString(): void
33+
{
34+
$this->expectNotToPerformAssertions();
35+
$code = new SVGCode('some svg contents');
36+
}
37+
38+
public function testConstructorWithEmoji(): void
39+
{
40+
$this->expectNotToPerformAssertions();
41+
$code = new SVGCode("\xF0\x9F\x98\x82"); // some emoji
42+
}
43+
44+
#[Depends('testConstructorWithNonEmptyString')]
45+
public function testToDataUri(): void
46+
{
47+
$pseudo_svg_string = 'some svg contents';
48+
$code = new SVGCode($pseudo_svg_string);
49+
$data_uri = $code->toDataUri();
50+
51+
$this->assertStringStartsWith("data:image/svg+xml;base64,", $data_uri); // ensure correct uri format
52+
$this->assertStringEndsWith(base64_encode($pseudo_svg_string), $data_uri); // ensure base64 encoded value
53+
$this->assertSame($data_uri, $code->toDataUri()); // ensure identical output
54+
}
55+
56+
#[Depends('testToDataUri')]
57+
public function testInstancesProcudeSameResult(): void
58+
{
59+
$pseudo_svg_string = 'some svg contents';
60+
$code_one = new SVGCode($pseudo_svg_string);
61+
$code_two = new SVGCode($pseudo_svg_string);
62+
$this->assertSame($code_one->toDataUri(), $code_two->toDataUri()); // ensure identical output
63+
}
64+
}

components/ILIAS/Refinery/src/String/Group.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ILIAS\Refinery\Constraint;
2525
use ILIAS\Refinery\Transformation;
2626
use ILIAS\Refinery\String\Encoding\Group as EncodingGroup;
27+
use ILIAS\Data\QR\ErrorCorrectionLevel;
2728

2829
class Group
2930
{
@@ -152,4 +153,14 @@ public function encoding(): EncodingGroup
152153
{
153154
return new EncodingGroup();
154155
}
156+
157+
/**
158+
* Creates a transformation to generate an {@see \ILIAS\Data\QR\SVGCode} from string.
159+
*/
160+
public function qrCode(
161+
ErrorCorrectionLevel $error_correction_level = ErrorCorrectionLevel::MEDIUM,
162+
int $size_in_px = 400,
163+
): Transformation {
164+
return new QRCode($error_correction_level, $size_in_px);
165+
}
155166
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace ILIAS\Refinery\String;
21+
22+
use ILIAS\Refinery\DeriveApplyToFromTransform;
23+
use ILIAS\Refinery\DeriveInvokeFromTransform;
24+
use ILIAS\Refinery\Transformation;
25+
use ILIAS\Data\QR\ErrorCorrectionLevel;
26+
use ILIAS\Data\QR\SVGCode;
27+
use BaconQrCode as External;
28+
29+
class QRCode implements Transformation
30+
{
31+
use DeriveApplyToFromTransform;
32+
use DeriveInvokeFromTransform;
33+
34+
public function __construct(
35+
protected ErrorCorrectionLevel $error_correction_level,
36+
protected int $size_in_px,
37+
) {
38+
$this->assertIntGreaterThanZero($size_in_px);
39+
}
40+
41+
public function transform(mixed $from): SVGCode
42+
{
43+
$this->assertString($from);
44+
$this->assertStringNotEmpty($from);
45+
46+
$writer = new External\Writer(
47+
new External\Renderer\ImageRenderer(
48+
new External\Renderer\RendererStyle\RendererStyle($this->size_in_px),
49+
new External\Renderer\Image\SvgImageBackEnd(),
50+
),
51+
);
52+
53+
$raw_svg_string = $writer->writeString(
54+
$from,
55+
External\Encoder\Encoder::DEFAULT_BYTE_MODE_ENCODING,
56+
$this->mapErrorCorrectionLevel($this->error_correction_level),
57+
);
58+
59+
return new SVGCode($raw_svg_string);
60+
}
61+
62+
protected function mapErrorCorrectionLevel(ErrorCorrectionLevel $level): External\Common\ErrorCorrectionLevel
63+
{
64+
return match ($level) {
65+
ErrorCorrectionLevel::LOW => External\Common\ErrorCorrectionLevel::L(),
66+
ErrorCorrectionLevel::MEDIUM => External\Common\ErrorCorrectionLevel::M(),
67+
ErrorCorrectionLevel::QUARTILE => External\Common\ErrorCorrectionLevel::Q(),
68+
ErrorCorrectionLevel::HIGH => External\Common\ErrorCorrectionLevel::H(),
69+
};
70+
}
71+
72+
protected function assertString(mixed $value): void
73+
{
74+
if (!is_string($value)) {
75+
throw new \InvalidArgumentException("Argument must be of type string.");
76+
}
77+
}
78+
79+
protected function assertStringNotEmpty(string $string): void
80+
{
81+
if (0 >= mb_strlen($string)) {
82+
throw new \InvalidArgumentException("String must not be empty.");
83+
}
84+
}
85+
86+
protected function assertIntGreaterThanZero(int $number): void
87+
{
88+
if (0 >= $number) {
89+
throw new \InvalidArgumentException("Number must be greater than zero.");
90+
}
91+
}
92+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace String;
21+
22+
use ILIAS\Refinery\String\QRCode;
23+
use ILIAS\Data\QR\ErrorCorrectionLevel;
24+
use ILIAS\Data\QR\SVGCode;
25+
use PHPUnit\Framework\Attributes\DataProvider;
26+
use PHPUnit\Framework\Attributes\Depends;
27+
use PHPUnit\Framework\TestCase;
28+
29+
class QRCodeTest extends TestCase
30+
{
31+
public function testConstructorWithZero(): void
32+
{
33+
$this->expectException(\InvalidArgumentException::class);
34+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 0);
35+
}
36+
37+
public function testConstructorWithNegativeNumber(): void
38+
{
39+
$this->expectException(\InvalidArgumentException::class);
40+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, -1);
41+
}
42+
43+
public function testConstructorWithPositiveNumber(): void
44+
{
45+
$this->expectNotToPerformAssertions();
46+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 1);
47+
}
48+
49+
#[Depends('testConstructorWithPositiveNumber')]
50+
public function testTransformWithoutString(): void
51+
{
52+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 1);
53+
$this->expectException(\InvalidArgumentException::class);
54+
$transformation->transform(1);
55+
}
56+
57+
#[Depends('testConstructorWithPositiveNumber')]
58+
public function testTransformWithEmptyString(): void
59+
{
60+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 1);
61+
$this->expectException(\InvalidArgumentException::class);
62+
$transformation->transform('');
63+
}
64+
65+
#[Depends('testConstructorWithPositiveNumber')]
66+
public function testTransformWithNonEmptyString(): void
67+
{
68+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 1);
69+
$this->expectNotToPerformAssertions();
70+
$transformation->transform('some arbitrary data.');
71+
}
72+
73+
#[Depends('testConstructorWithPositiveNumber')]
74+
public function testTransformWithEmoji(): void
75+
{
76+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, 1);
77+
$this->expectNotToPerformAssertions();
78+
$transformation->transform("\xF0\x9F\x98\x82"); // some emoji
79+
}
80+
81+
/** @return array<ErrorCorrectionLevel[]> */
82+
public static function getErrorCorrectionLevels(): array
83+
{
84+
return [
85+
[ErrorCorrectionLevel::LOW],
86+
[ErrorCorrectionLevel::MEDIUM],
87+
[ErrorCorrectionLevel::QUARTILE],
88+
[ErrorCorrectionLevel::HIGH],
89+
];
90+
}
91+
92+
#[Depends('testConstructorWithPositiveNumber')]
93+
#[DataProvider('getErrorCorrectionLevels')]
94+
public function testTransformWithErrorCorrectionLevels(ErrorCorrectionLevel $level): void
95+
{
96+
$transformation = new QRCode($level, 1);
97+
$this->expectNotToPerformAssertions();
98+
$code = $transformation->transform("some arbitrary data.");
99+
}
100+
101+
/** @return array<int[]> */
102+
public static function getSizesInPx(): array
103+
{
104+
return [
105+
[10],
106+
[100],
107+
[400],
108+
[1_000],
109+
];
110+
}
111+
112+
#[Depends('testConstructorWithPositiveNumber')]
113+
#[DataProvider('getSizesInPx')]
114+
public function testTransformWithSizes(int $size_in_px): void
115+
{
116+
$transformation = new QRCode(ErrorCorrectionLevel::LOW, $size_in_px);
117+
$this->expectNotToPerformAssertions();
118+
$code = $transformation->transform("some arbitrary data.");
119+
}
120+
}

0 commit comments

Comments
 (0)