diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 94a3fb374d..336b15f7e5 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -8,6 +8,8 @@ use Appwrite\SDK\MethodType; use Appwrite\SDK\Response as SDKResponse; use Appwrite\URL\URL as URLParse; use Appwrite\Utopia\Response; +use chillerlan\QRCode\Common\EccLevel; +use chillerlan\QRCode\Output\QRImagick; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use Utopia\App; @@ -485,17 +487,28 @@ App::get('/v1/avatars/qr') contentType: ContentType::IMAGE_PNG )) ->param('text', '', new Text(512), 'Plain text to be converted to QR code image.') - ->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true) - ->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) + ->param('size', 512, new Range(32, 1024), 'QR code size. Pass an integer between 32 to 1024. Defaults to 512.', true) + ->param('margin', 1, new Range(0, 128), 'Margin from edge. Pass an integer between 0 to 128. Defaults to 1.', true) ->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) ->inject('response') - ->action(function (string $text, int $size, int $margin, bool $download, Response $response) { - + ->action(function (string $text, int $size, int $margin, mixed $download, Response $response) { $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); + $options = new QROptions([ - 'addQuietzone' => true, 'quietzoneSize' => $margin, - 'outputType' => QRCode::OUTPUT_IMAGICK, + 'addQuietzone' => $margin > 0, + 'readerUseImagickIfAvailable' => true, + 'outputInterface' => QRGdRounded::class, + 'returnResource' => true, + 'outputBase64' => false, + 'scale' => 64, + 'quality' => 75, + 'imagickFormat' => 'png', + 'drawCircularModules' => true, + 'addLogoSpace' => true, + 'eccLevel' => EccLevel::H, // Needed for addLogoSpace + 'logoSpaceWidth' => 10, + 'logoSpaceHeight' => 10, ]); $qrcode = new QRCode($options); @@ -507,10 +520,33 @@ App::get('/v1/avatars/qr') $image = new Image($qrcode->render($text)); $image->crop((int) $size, (int) $size); + $logo = \file_get_contents('https://appwrite.io/assets/logomark/logo.png'); + $logoImg = new Image($logo); + $logoImg->crop(128 + 16, 128); + + $image->image->setImageColorspace($logoImg->image->getImageColorspace()); + + $image->image->compositeImage($logoImg->image, Imagick::COMPOSITE_OVER, 192 - 8, 192); + + $markerUrl = 'https://cloud.appwrite.io/v1/storage/buckets/67c4487d0010bd67a4f4/files/marker3/view?project=67c2cb23003706429442'; + $marker = \file_get_contents($markerUrl); + + $markerImg = new Image($marker); + $markerImg->crop(120, 120); + $image->image->compositeImage($markerImg->image, Imagick::COMPOSITE_OVER, 16, 16); + + $markerImg->image->rotateImage('transparent', 90); + $image->image->compositeImage($markerImg->image, Imagick::COMPOSITE_OVER, 376, 16); + + $markerImg->image->rotateImage('transparent', 180); + $image->image->compositeImage($markerImg->image, Imagick::COMPOSITE_OVER, 16, 376); + + // TODO: Overlay last small marker too + $response ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days ->setContentType('image/png') - ->send($image->output('png', 9)); + ->send($image->output('png', 75)); }); App::get('/v1/avatars/initials') @@ -1277,3 +1313,81 @@ App::get('/v1/cards/cloud-og') ->setContentType('image/png') ->file($baseImage->getImageBlob()); }); + +class QRGdRounded extends QRImagick +{ + protected function module(int $x, int $y, int $M_TYPE): void + { + if (!$this->matrix->isDark($M_TYPE)) { + return; + } + + $this->imagickDraw->setFillColor($this->getModuleValue($M_TYPE)); + + + /* + $this->imagickDraw->circle( + (($x + 0.5) * $this->scale), + (($y + 0.5) * $this->scale), + ((($x + 0.5 + $this->circleRadius) * $this->scale) - 8), + ((($y + 0.5) * $this->scale) - 8) + ); + + return; + */ + + $neighbours = $this->matrix->checkNeighbours($x, $y); + $x1 = ($x * $this->scale); + $y1 = ($y * $this->scale); + $x2 = (($x + 1) * $this->scale); + $y2 = (($y + 1) * $this->scale); + $rectsize = (int)($this->scale / 2); + + // ------------------ + // Outer rounding + // ------------------ + + if (($neighbours & (1 << 7))) { // neighbour left + // top left + $this->imagickDraw->rectangle($x1, $y1, ($x1 + $rectsize), ($y1 + $rectsize)); + // bottom left + $this->imagickDraw->rectangle($x1, ($y2 - $rectsize), ($x1 + $rectsize), $y2); + } + + if (($neighbours & (1 << 3))) { // neighbour right + // top right + $this->imagickDraw->rectangle(($x2 - $rectsize), $y1, $x2, ($y1 + $rectsize)); + // bottom right + $this->imagickDraw->rectangle(($x2 - $rectsize), ($y2 - $rectsize), $x2, $y2); + } + + if (($neighbours & (1 << 1))) { // neighbour top + // top left + $this->imagickDraw->rectangle($x1, $y1, ($x1 + $rectsize), ($y1 + $rectsize)); + // top right + $this->imagickDraw->rectangle(($x2 - $rectsize), $y1, $x2, ($y1 + $rectsize)); + } + + if (($neighbours & (1 << 5))) { // neighbour bottom + // bottom left + $this->imagickDraw->rectangle($x1, ($y2 - $rectsize), ($x1 + $rectsize), $y2); + // bottom right + $this->imagickDraw->rectangle(($x2 - $rectsize), ($y2 - $rectsize), $x2, $y2); + } + + $this->imagickDraw->circle( + (($x + 0.5) * $this->scale), + (($y + 0.5) * $this->scale), + ((($x + 0.5 + $this->circleRadius) * $this->scale) + 2), + ((($y + 0.5) * $this->scale) + 2) + ); + + /* + $this->imagickDraw->rectangle( + $x1, $y1, $x2, $y2 + ); + */ + + return; + } +} diff --git a/composer.json b/composer.json index 5f40f4d593..10a3e5c450 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,7 @@ "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", "phpmailer/phpmailer": "6.9.1", - "chillerlan/php-qrcode": "4.3.4", + "chillerlan/php-qrcode": "dev-main", "adhocore/jwt": "1.1.2", "spomky-labs/otphp": "^10.0", "webonyx/graphql-php": "14.11.*", diff --git a/composer.lock b/composer.lock index 3690e25ed0..8b52ad61c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6883b3e81cfb0c5355997def668d5df2", + "content-hash": "4e5e640d4909991707c193d0e348d677", "packages": [ { "name": "adhocore/jwt", @@ -279,16 +279,16 @@ }, { "name": "brick/math", - "version": "0.12.2", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40", - "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -327,7 +327,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.2" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -335,50 +335,70 @@ "type": "github" } ], - "time": "2025-02-26T10:21:45+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "chillerlan/php-qrcode", - "version": "4.3.4", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d" + "reference": "b5ff1258a9a8bf6eff15d8c12365400ba60d652c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", - "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/b5ff1258a9a8bf6eff15d8c12365400ba60d652c", + "reference": "b5ff1258a9a8bf6eff15d8c12365400ba60d652c", "shasum": "" }, "require": { - "chillerlan/php-settings-container": "^2.1.4", + "chillerlan/php-settings-container": "^3.2.1", "ext-mbstring": "*", - "php": "^7.4 || ^8.0" + "php": "^8.2" }, "require-dev": { - "phan/phan": "^5.3", - "phpunit/phpunit": "^9.5", - "setasign/fpdf": "^1.8.2" + "chillerlan/php-authenticator": "^5.2.1", + "ext-fileinfo": "*", + "intervention/image": "^3.8", + "phpbench/phpbench": "^1.3", + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpunit/phpunit": "^11.3", + "setasign/fpdf": "^1.8.2", + "slevomat/coding-standard": "^8.15", + "squizlabs/php_codesniffer": "^3.10" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", - "setasign/fpdf": "Required to use the QR FPDF output." + "intervention/image": "More advanced GD and ImageMagick output.", + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { - "chillerlan\\QRCode\\": "src/" + "chillerlan\\QRCode\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "MIT", + "Apache-2.0" ], "authors": [ { "name": "Kazuhiko Arase", - "homepage": "https://github.com/kazuhikoarase" + "homepage": "https://github.com/kazuhikoarase/qrcode-generator" + }, + { + "name": "ZXing Authors", + "homepage": "https://github.com/zxing/zxing" + }, + { + "name": "Ashot Khanamiryan", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" }, { "name": "Smiley", @@ -390,54 +410,53 @@ "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" } ], - "description": "A QR code generator. PHP 7.4+", + "description": "A QR Code generator and reader with a user-friendly API. PHP 8.2+", "homepage": "https://github.com/chillerlan/php-qrcode", "keywords": [ "phpqrcode", "qr", "qr code", + "qr-reader", "qrcode", - "qrcode-generator" + "qrcode-generator", + "qrcode-reader" ], "support": { + "docs": "https://php-qrcode.readthedocs.io", "issues": "https://github.com/chillerlan/php-qrcode/issues", - "source": "https://github.com/chillerlan/php-qrcode/tree/4.3.4" + "source": "https://github.com/chillerlan/php-qrcode" }, "funding": [ - { - "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", - "type": "custom" - }, { "url": "https://ko-fi.com/codemasher", - "type": "ko_fi" + "type": "Ko-Fi" } ], - "time": "2022-07-25T09:12:45+00:00" + "time": "2024-11-27T19:19:56+00:00" }, { "name": "chillerlan/php-settings-container", - "version": "2.1.6", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb" + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/5553558bd381fce5108c6d0343c12e488cfec6bb", - "reference": "5553558bd381fce5108c6d0343c12e488cfec6bb", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", "shasum": "" }, "require": { "ext-json": "*", - "php": "^7.4 || ^8.0" + "php": "^8.1" }, "require-dev": { "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1.11", "phpstan/phpstan-deprecation-rules": "^1.2", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.10" }, "type": "library", @@ -457,10 +476,9 @@ "homepage": "https://github.com/codemasher" } ], - "description": "A container class for immutable settings objects. Not a DI container. PHP 7.4+", + "description": "A container class for immutable settings objects. Not a DI container.", "homepage": "https://github.com/chillerlan/php-settings-container", "keywords": [ - "PHP7", "Settings", "configuration", "container", @@ -480,7 +498,7 @@ "type": "ko_fi" } ], - "time": "2024-07-17T01:04:28+00:00" + "time": "2024-07-16T11:13:48+00:00" }, { "name": "composer/semver", @@ -2371,16 +2389,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", "shasum": "" }, "require": { @@ -2388,25 +2406,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -2444,19 +2459,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-02T04:48:29+00:00" }, { "name": "ramsey/uuid", @@ -8806,7 +8811,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "chillerlan/php-qrcode": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 394505aeb4..83aee811b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -85,6 +85,7 @@ services: - ./public:/usr/src/code/public - ./src:/usr/src/code/src - ./dev:/usr/src/code/dev + - ./vendor/utopia-php/image:/usr/src/code/vendor/utopia-php/image # Exposed private image to do Imagick operations depends_on: - mariadb - redis