diff --git a/components/ILIAS/Data/src/QR/ErrorCorrectionLevel.php b/components/ILIAS/Data/src/QR/ErrorCorrectionLevel.php
new file mode 100644
index 000000000000..f58284de49aa
--- /dev/null
+++ b/components/ILIAS/Data/src/QR/ErrorCorrectionLevel.php
@@ -0,0 +1,45 @@
+raw_svg_string;
+ }
+}
diff --git a/components/ILIAS/Refinery/src/URI/Group.php b/components/ILIAS/Refinery/src/URI/Group.php
index 8a90f1c43443..406703e01e14 100755
--- a/components/ILIAS/Refinery/src/URI/Group.php
+++ b/components/ILIAS/Refinery/src/URI/Group.php
@@ -21,11 +21,27 @@
namespace ILIAS\Refinery\URI;
use ILIAS\Refinery\Transformation;
+use ILIAS\Data\QR\ErrorCorrectionLevel;
+use ILIAS\Refinery\URI\Transformation\ToStringTransformation;
+use ILIAS\Refinery\URI\Transformation\ToSvgQrCodeTransformation;
+use ILIAS\Refinery\URI\Transformation\FromSvgTransformation;
class Group
{
public function toString(): Transformation
{
- return new StringTransformation();
+ return new ToStringTransformation();
+ }
+
+ public function toSvgQrCode(
+ ErrorCorrectionLevel $error_correction_level = ErrorCorrectionLevel::MEDIUM,
+ int $size_in_px = 400,
+ ): Transformation {
+ return new ToSvgQrCodeTransformation($error_correction_level, $size_in_px);
+ }
+
+ public function fromSvg(): Transformation
+ {
+ return new FromSvgTransformation();
}
}
diff --git a/components/ILIAS/Refinery/src/URI/Transformation/FromSvgTransformation.php b/components/ILIAS/Refinery/src/URI/Transformation/FromSvgTransformation.php
new file mode 100644
index 000000000000..41791095a417
--- /dev/null
+++ b/components/ILIAS/Refinery/src/URI/Transformation/FromSvgTransformation.php
@@ -0,0 +1,48 @@
+__toString());
+ }
+}
diff --git a/components/ILIAS/Refinery/src/URI/StringTransformation.php b/components/ILIAS/Refinery/src/URI/Transformation/ToStringTransformation.php
similarity index 92%
rename from components/ILIAS/Refinery/src/URI/StringTransformation.php
rename to components/ILIAS/Refinery/src/URI/Transformation/ToStringTransformation.php
index c9f26ec1b9b1..170cc5237327 100755
--- a/components/ILIAS/Refinery/src/URI/StringTransformation.php
+++ b/components/ILIAS/Refinery/src/URI/Transformation/ToStringTransformation.php
@@ -18,7 +18,7 @@
declare(strict_types=1);
-namespace ILIAS\Refinery\URI;
+namespace ILIAS\Refinery\URI\Transformation;
use ILIAS\Data\URI;
use ILIAS\Refinery\ConstraintViolationException;
@@ -26,7 +26,7 @@
use ILIAS\Refinery\Transformation;
use ILIAS\Refinery\DeriveInvokeFromTransform;
-class StringTransformation implements Transformation
+class ToStringTransformation implements Transformation
{
use DeriveApplyToFromTransform;
use DeriveInvokeFromTransform;
diff --git a/components/ILIAS/Refinery/src/URI/Transformation/ToSvgQrCodeTransformation.php b/components/ILIAS/Refinery/src/URI/Transformation/ToSvgQrCodeTransformation.php
new file mode 100644
index 000000000000..a3ba73c74215
--- /dev/null
+++ b/components/ILIAS/Refinery/src/URI/Transformation/ToSvgQrCodeTransformation.php
@@ -0,0 +1,82 @@
+assertIntGreaterThanZero($size_in_px);
+ }
+
+ public function transform(mixed $from): SVG
+ {
+ if (!$from instanceof URI) {
+ throw new \InvalidArgumentException("Argument must be of type " . URI::class);
+ }
+
+ $writer = new External\Writer(
+ new External\Renderer\ImageRenderer(
+ new External\Renderer\RendererStyle\RendererStyle($this->size_in_px),
+ new External\Renderer\Image\SvgImageBackEnd(),
+ ),
+ );
+
+ $raw_svg_string = $writer->writeString(
+ $from->__toString(),
+ self::ENCODING,
+ $this->mapErrorCorrectionLevel($this->error_correction_level),
+ );
+
+ return new SVG($raw_svg_string);
+ }
+
+ protected function mapErrorCorrectionLevel(ErrorCorrectionLevel $level): External\Common\ErrorCorrectionLevel
+ {
+ return match ($level) {
+ ErrorCorrectionLevel::LOW => External\Common\ErrorCorrectionLevel::L(),
+ ErrorCorrectionLevel::MEDIUM => External\Common\ErrorCorrectionLevel::M(),
+ ErrorCorrectionLevel::QUARTILE => External\Common\ErrorCorrectionLevel::Q(),
+ ErrorCorrectionLevel::HIGH => External\Common\ErrorCorrectionLevel::H(),
+ };
+ }
+
+ protected function assertIntGreaterThanZero(int $number): void
+ {
+ if (0 >= $number) {
+ throw new \InvalidArgumentException("Number must be greater than zero.");
+ }
+ }
+}
diff --git a/components/ILIAS/Refinery/tests/URI/GroupTest.php b/components/ILIAS/Refinery/tests/URI/GroupTest.php
index e207e201203c..113485f59fd9 100755
--- a/components/ILIAS/Refinery/tests/URI/GroupTest.php
+++ b/components/ILIAS/Refinery/tests/URI/GroupTest.php
@@ -21,15 +21,31 @@
namespace ILIAS\Tests\Refinery\URI;
use ILIAS\Refinery\URI\Group as URIGroup;
-use ILIAS\Refinery\URI\StringTransformation;
+use ILIAS\Refinery\URI\Transformation\ToStringTransformation;
+use ILIAS\Refinery\URI\Transformation\FromSvgTransformation;
+use ILIAS\Refinery\URI\Transformation\ToSvgQrCodeTransformation;
use PHPUnit\Framework\TestCase;
class GroupTest extends TestCase
{
- public function testStringTransformationInstance(): void
+ public function testToStringTransformationInstance(): void
{
$group = new URIGroup();
$transformation = $group->toString();
- $this->assertInstanceOf(StringTransformation::class, $transformation);
+ $this->assertInstanceOf(ToStringTransformation::class, $transformation);
+ }
+
+ public function testToSvgTransformationInstance(): void
+ {
+ $group = new URIGroup();
+ $transformation = $group->toSvgQrCode();
+ $this->assertInstanceOf(ToSvgQrCodeTransformation::class, $transformation);
+ }
+
+ public function testFromSvgTransformationInstance(): void
+ {
+ $group = new URIGroup();
+ $transformation = $group->fromSvg();
+ $this->assertInstanceOf(FromSvgTransformation::class, $transformation);
}
}
diff --git a/components/ILIAS/Refinery/tests/URI/Transformation/FromSvgTransformationTest.php b/components/ILIAS/Refinery/tests/URI/Transformation/FromSvgTransformationTest.php
new file mode 100644
index 000000000000..2bbaee913739
--- /dev/null
+++ b/components/ILIAS/Refinery/tests/URI/Transformation/FromSvgTransformationTest.php
@@ -0,0 +1,60 @@
+expectException(\InvalidArgumentException::class);
+ $transformation->transform('');
+ }
+
+ public function testTransformWithSvgInstance(): void
+ {
+ $transformation = new FromSvgTransformation();
+ $this->expectNotToPerformAssertions();
+ $transformation->transform($this->createSvgMock());
+ }
+
+ #[Depends('testTransformWithSvgInstance')]
+ public function testTransformResult(): void
+ {
+ $transformation = new FromSvgTransformation();
+ $svg_mock = $this->createSvgMock();
+ $result = $transformation->transform($svg_mock);
+ $this->assertIsString($result);
+ $this->assertStringStartsWith("data:image/svg+xml;base64,", $result); // ensure correct data uri format
+ $this->assertStringEndsWith(base64_encode($svg_mock->__toString()), $result); // ensure base64 encoded value
+ }
+
+ protected function createSvgMock(): \ILIAS\Data\SVG & MockObject
+ {
+ $svg_mock = $this->createMock(\ILIAS\Data\SVG::class);
+ $svg_mock->method('__toString')->willReturn('');
+ return $svg_mock;
+ }
+}
diff --git a/components/ILIAS/Refinery/tests/URI/StringTransformationTest.php b/components/ILIAS/Refinery/tests/URI/Transformation/StringTransformationTest.php
similarity index 91%
rename from components/ILIAS/Refinery/tests/URI/StringTransformationTest.php
rename to components/ILIAS/Refinery/tests/URI/Transformation/StringTransformationTest.php
index ba38ba081cf0..3f3c36c13ec3 100755
--- a/components/ILIAS/Refinery/tests/URI/StringTransformationTest.php
+++ b/components/ILIAS/Refinery/tests/URI/Transformation/StringTransformationTest.php
@@ -18,20 +18,20 @@
declare(strict_types=1);
-namespace ILIAS\Tests\Refinery\URI;
+namespace ILIAS\Tests\Refinery\URI\Transformation;
use ILIAS\Data\URI;
use ILIAS\Refinery\ConstraintViolationException;
-use ILIAS\Refinery\URI\StringTransformation;
+use ILIAS\Refinery\URI\Transformation\ToStringTransformation;
use PHPUnit\Framework\TestCase;
class StringTransformationTest extends TestCase
{
- private StringTransformation $transformation;
+ private ToStringTransformation $transformation;
protected function setUp(): void
{
- $this->transformation = new StringTransformation();
+ $this->transformation = new ToStringTransformation();
}
public function testSimpleUri(): void
diff --git a/components/ILIAS/Refinery/tests/URI/Transformation/ToSvgTransformationTest.php b/components/ILIAS/Refinery/tests/URI/Transformation/ToSvgTransformationTest.php
new file mode 100644
index 000000000000..856911d790ba
--- /dev/null
+++ b/components/ILIAS/Refinery/tests/URI/Transformation/ToSvgTransformationTest.php
@@ -0,0 +1,111 @@
+expectException(\InvalidArgumentException::class);
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, 0);
+ }
+
+ public function testConstructorWithNegativeNumber(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, -1);
+ }
+
+ public function testConstructorWithPositiveNumber(): void
+ {
+ $this->expectNotToPerformAssertions();
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, 1);
+ }
+
+ #[Depends('testConstructorWithPositiveNumber')]
+ public function testTransformWithoutUriInstance(): void
+ {
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, 1);
+ $this->expectException(\InvalidArgumentException::class);
+ $transformation->transform('https://ilias.ch');
+ }
+
+ #[Depends('testConstructorWithPositiveNumber')]
+ public function testTransformWithUriInstance(): void
+ {
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, 1);
+ $this->expectNotToPerformAssertions();
+ $transformation->transform($this->createUriMock());
+ }
+
+ /** @return ErrorCorrectionLevel */
+ public static function getErrorCorrectionLevels(): array
+ {
+ return [
+ [ErrorCorrectionLevel::LOW],
+ [ErrorCorrectionLevel::MEDIUM],
+ [ErrorCorrectionLevel::QUARTILE],
+ [ErrorCorrectionLevel::HIGH],
+ ];
+ }
+
+ #[Depends('testConstructorWithPositiveNumber')]
+ #[DataProvider('getErrorCorrectionLevels')]
+ public function testTransformWithErrorCorrectionLevels(ErrorCorrectionLevel $level): void
+ {
+ $transformation = new ToSvgQrCodeTransformation($level, 1);
+ $this->expectNotToPerformAssertions();
+ $code = $transformation->transform($this->createUriMock());
+ }
+
+ /** @return array */
+ public static function getSizesInPx(): array
+ {
+ return [
+ [10],
+ [100],
+ [400],
+ [1_000],
+ ];
+ }
+
+ #[Depends('testConstructorWithPositiveNumber')]
+ #[DataProvider('getSizesInPx')]
+ public function testTransformWithSizes(int $size_in_px): void
+ {
+ $transformation = new ToSvgQrCodeTransformation(ErrorCorrectionLevel::LOW, $size_in_px);
+ $this->expectNotToPerformAssertions();
+ $code = $transformation->transform($this->createUriMock());
+ }
+
+ protected function createUriMock(): \ILIAS\Data\URI & MockObject
+ {
+ $uri_mock = $this->createMock(\ILIAS\Data\URI::class);
+ $uri_mock->method('__toString')->willReturn('https://ilias.ch');
+ return $uri_mock;
+ }
+}
diff --git a/components/ILIAS/UI/tests/Component/Tree/Node/NodeTest.php b/components/ILIAS/UI/tests/Component/Tree/Node/NodeTest.php
index 103adb189c50..7f0b91d54a97 100755
--- a/components/ILIAS/UI/tests/Component/Tree/Node/NodeTest.php
+++ b/components/ILIAS/UI/tests/Component/Tree/Node/NodeTest.php
@@ -25,7 +25,7 @@
use ILIAS\UI\Implementation\Component\Tree\Node\Node;
use ILIAS\UI\Implementation\Component as I;
use ILIAS\UI\Component\Clickable;
-use ILIAS\Refinery\URI\StringTransformation;
+use ILIAS\Refinery\URI\Transformation\ToStringTransformation;
/**
* Dummy-implementation for testing
@@ -116,7 +116,7 @@ public function testWithURI(Clickable $node): void
$node = $node->withLink($uri);
- $stringTransformation = new StringTransformation();
+ $stringTransformation = new ToStringTransformation();
$this->assertEquals('http://google.de:8080', $stringTransformation->transform($node->getLink()));
}
diff --git a/composer.json b/composer.json
index 6f44f9386248..8c3c96cad934 100755
--- a/composer.json
+++ b/composer.json
@@ -73,7 +73,8 @@
"psr/http-message": "^2.0",
"jumbojett/openid-connect-php": "dev-master#bc719cc9930990a4a9916cc127b839b4ed1c89ad",
"phpunit/phpunit": "^11.5",
- "phiki/phiki": "^2.0"
+ "phiki/phiki": "^2.0",
+ "bacon/bacon-qr-code": "^3.0"
},
"require-dev": {
"captainhook/captainhook": "^5.24",
diff --git a/composer.lock b/composer.lock
index 741b5a2a1c68..61ce71fb490b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,63 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6416031c9db2af93c7803503138cbfb8",
+ "content-hash": "ffd4d8aedd4395fb5ced5457f966659e",
"packages": [
+ {
+ "name": "bacon/bacon-qr-code",
+ "version": "v3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Bacon/BaconQrCode.git",
+ "reference": "3feed0e212b8412cc5d2612706744789b0615824"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3feed0e212b8412cc5d2612706744789b0615824",
+ "reference": "3feed0e212b8412cc5d2612706744789b0615824",
+ "shasum": ""
+ },
+ "require": {
+ "dasprid/enum": "^1.0.3",
+ "ext-iconv": "*",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "phly/keep-a-changelog": "^2.12",
+ "phpunit/phpunit": "^10.5.11 || ^11.0.4",
+ "spatie/phpunit-snapshot-assertions": "^5.1.5",
+ "spatie/pixelmatch-php": "^1.2.0",
+ "squizlabs/php_codesniffer": "^3.9"
+ },
+ "suggest": {
+ "ext-imagick": "to generate QR code images"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BaconQrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "BaconQrCode is a QR code generator for PHP.",
+ "homepage": "https://github.com/Bacon/BaconQrCode",
+ "support": {
+ "issues": "https://github.com/Bacon/BaconQrCode/issues",
+ "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.4"
+ },
+ "time": "2026-03-16T01:01:30+00:00"
+ },
{
"name": "brick/math",
"version": "0.14.8",
@@ -190,6 +245,56 @@
],
"time": "2024-11-12T16:29:46+00:00"
},
+ {
+ "name": "dasprid/enum",
+ "version": "1.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DASPRiD/Enum.git",
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1 <9.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DASPRiD\\Enum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP 7.1 enum implementation",
+ "keywords": [
+ "enum",
+ "map"
+ ],
+ "support": {
+ "issues": "https://github.com/DASPRiD/Enum/issues",
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
+ },
+ "time": "2025-09-16T12:23:56+00:00"
+ },
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",