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",