Skip to content

Commit 0e88c35

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 0e88c35

6 files changed

Lines changed: 379 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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
protected const string SUPPORTED_ENCODING = 'UTF-8';
35+
36+
public function __construct(
37+
protected ErrorCorrectionLevel $error_correction_level,
38+
protected int $size_in_px,
39+
) {
40+
$this->assertIntGreaterThanZero($size_in_px);
41+
}
42+
43+
public function transform(mixed $from): SVGCode
44+
{
45+
$this->assertString($from);
46+
$this->assertStringNotEmpty($from);
47+
48+
$writer = new External\Writer(
49+
new External\Renderer\ImageRenderer(
50+
new External\Renderer\RendererStyle\RendererStyle($this->size_in_px),
51+
new External\Renderer\Image\SvgImageBackEnd(),
52+
),
53+
);
54+
55+
$raw_svg_string = $writer->writeString(
56+
$from,
57+
self::SUPPORTED_ENCODING,
58+
$this->mapErrorCorrectionLevel($this->error_correction_level),
59+
);
60+
61+
return new SVGCode($raw_svg_string);
62+
}
63+
64+
protected function mapErrorCorrectionLevel(ErrorCorrectionLevel $level): External\Common\ErrorCorrectionLevel
65+
{
66+
return match ($level) {
67+
ErrorCorrectionLevel::LOW => External\Common\ErrorCorrectionLevel::L(),
68+
ErrorCorrectionLevel::MEDIUM => External\Common\ErrorCorrectionLevel::M(),
69+
ErrorCorrectionLevel::QUARTILE => External\Common\ErrorCorrectionLevel::Q(),
70+
ErrorCorrectionLevel::HIGH => External\Common\ErrorCorrectionLevel::H(),
71+
};
72+
}
73+
74+
protected function assertString(mixed $value): void
75+
{
76+
if (!is_string($value)) {
77+
throw new \InvalidArgumentException("Argument must be of type string.");
78+
}
79+
}
80+
81+
protected function assertStringNotEmpty(string $string): void
82+
{
83+
if (0 >= mb_strlen($string)) {
84+
throw new \InvalidArgumentException("String must not be empty.");
85+
}
86+
}
87+
88+
protected function assertIntGreaterThanZero(int $number): void
89+
{
90+
if (0 >= $number) {
91+
throw new \InvalidArgumentException("Number must be greater than zero.");
92+
}
93+
}
94+
}
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)