Created
September 10, 2023 15:36
-
-
Save wildstray/bd4e51ebf1da82e831bdf96495d49a61 to your computer and use it in GitHub Desktop.
Owncloud twofactor_totp 0.8.0 patch for using php-gd instead of php-imagick (BaconQrCode PR #114)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -urN twofactor_totp.orig/lib/Service/OtpGen.php twofactor_totp/lib/Service/OtpGen.php | |
--- twofactor_totp.orig/lib/Service/OtpGen.php 2023-07-29 15:23:22.000000000 +0200 | |
+++ twofactor_totp/lib/Service/OtpGen.php 2023-09-10 12:15:29.411654688 +0200 | |
@@ -17,9 +17,7 @@ | |
namespace OCA\TwoFactor_Totp\Service; | |
-use BaconQrCode\Renderer\ImageRenderer; | |
-use BaconQrCode\Renderer\Image\ImagickImageBackEnd; | |
-use BaconQrCode\Renderer\RendererStyle\RendererStyle; | |
+use BaconQrCode\Renderer\GDLibRenderer; | |
use BaconQrCode\Writer; | |
use OCP\Defaults; | |
use OCP\IUser; | |
@@ -62,10 +60,7 @@ | |
*/ | |
public function generateOtpQR(IUser $user, string $secret) { | |
$data = $this->generateOtpUrl($user, $secret); | |
- $renderer = new ImageRenderer( | |
- new RendererStyle(170), | |
- new ImagickImageBackEnd() | |
- ); | |
+ $renderer = new GDLibRenderer(280); | |
$writer = new Writer($renderer); | |
return 'data:image/png;base64,' . \base64_encode($writer->writeString($data)); | |
} | |
diff -urN twofactor_totp.orig/lib/Service/OtpGen.php.bak twofactor_totp/lib/Service/OtpGen.php.bak | |
--- twofactor_totp.orig/lib/Service/OtpGen.php.bak 1970-01-01 01:00:00.000000000 +0100 | |
+++ twofactor_totp/lib/Service/OtpGen.php.bak 2023-08-22 20:56:32.112281311 +0200 | |
@@ -0,0 +1,72 @@ | |
+<?php | |
+ | |
+/** | |
+ * This code is free software: you can redistribute it and/or modify | |
+ * it under the terms of the GNU Affero General Public License, version 3, | |
+ * as published by the Free Software Foundation. | |
+ * | |
+ * This program is distributed in the hope that it will be useful, | |
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
+ * GNU Affero General Public License for more details. | |
+ * | |
+ * You should have received a copy of the GNU Affero General Public License, version 3, | |
+ * along with this program. If not, see <http://www.gnu.org/licenses/> | |
+ * | |
+ */ | |
+ | |
+namespace OCA\TwoFactor_Totp\Service; | |
+ | |
+use BaconQrCode\Renderer\ImageRenderer; | |
+use BaconQrCode\Renderer\Image\ImagickImageBackEnd; | |
+use BaconQrCode\Renderer\RendererStyle\RendererStyle; | |
+use BaconQrCode\Writer; | |
+use OCP\Defaults; | |
+use OCP\IUser; | |
+ | |
+class OtpGen { | |
+ /** @var Defaults */ | |
+ private $defaults; | |
+ | |
+ public function __construct(Defaults $defaults) { | |
+ $this->defaults = $defaults; | |
+ } | |
+ | |
+ /** | |
+ * Return an otpauth URI, for example "otpauth://totp/mySecretName?secret=ABCDEF&issuer=myself" | |
+ * @param IUser $user | |
+ * @param string $secret | |
+ * @return string the otpauth URI | |
+ */ | |
+ public function generateOtpUrl(IUser $user, string $secret) { | |
+ $productName = $this->defaults->getName(); | |
+ $userName = $user->getCloudId(); | |
+ | |
+ $secretName = \rawurlencode("$productName:$userName"); | |
+ $issuer = \rawurlencode($productName); | |
+ | |
+ return "otpauth://totp/$secretName?secret=$secret&issuer=$issuer"; | |
+ } | |
+ | |
+ /** | |
+ * Return a base64-encoded PNG image of the QR code containing the generated otpauth URI. | |
+ * The returned string will always contain the "data:image/png;base64," prefix, which is | |
+ * suitable to use in an "img" HTML tag | |
+ * ``` | |
+ * $qr = $otpGen->genOtpQR($user, $secret); | |
+ * $img = "<img src=\"$qr\" />" | |
+ * ``` | |
+ * @param IUser $user | |
+ * @param string $secret | |
+ * @return string a base64-encoded PNG image of the QR, with the "data:image/png;base64," prefix | |
+ */ | |
+ public function generateOtpQR(IUser $user, string $secret) { | |
+ $data = $this->generateOtpUrl($user, $secret); | |
+ $renderer = new ImageRenderer( | |
+ new RendererStyle(170), | |
+ new ImagickImageBackEnd() | |
+ ); | |
+ $writer = new Writer($renderer); | |
+ return 'data:image/png;base64,' . \base64_encode($writer->writeString($data)); | |
+ } | |
+} | |
diff -urN twofactor_totp.orig/vendor/bacon/bacon-qr-code/README.md twofactor_totp/vendor/bacon/bacon-qr-code/README.md | |
--- twofactor_totp.orig/vendor/bacon/bacon-qr-code/README.md 2023-07-29 15:23:22.000000000 +0200 | |
+++ twofactor_totp/vendor/bacon/bacon-qr-code/README.md 2023-09-10 12:28:50.029268968 +0200 | |
@@ -37,3 +37,10 @@ | |
- `ImagickImageBackEnd`: renders raster images using the Imagick library | |
- `SvgImageBackEnd`: renders SVG files using XMLWriter | |
- `EpsImageBackEnd`: renders EPS files | |
+ | |
+### GDLib Renderer | |
+GD library has so many limitations, that GD support is not added as backend, but as separated rendered. | |
+Use `GDLibRendered` instead of `ImageRenderer`. Here are limitations: | |
+ | |
+ - Does not support gradient. | |
+ - Does not support any curves, so you QR code is always squared. | |
diff -urN twofactor_totp.orig/vendor/bacon/bacon-qr-code/src/Renderer/GDLibRenderer.php twofactor_totp/vendor/bacon/bacon-qr-code/src/Renderer/GDLibRenderer.php | |
--- twofactor_totp.orig/vendor/bacon/bacon-qr-code/src/Renderer/GDLibRenderer.php 1970-01-01 01:00:00.000000000 +0100 | |
+++ twofactor_totp/vendor/bacon/bacon-qr-code/src/Renderer/GDLibRenderer.php 2023-09-10 11:58:22.959542393 +0200 | |
@@ -0,0 +1,281 @@ | |
+<?php | |
+ | |
+declare(strict_types=1); | |
+ | |
+namespace BaconQrCode\Renderer; | |
+ | |
+use BaconQrCode\Encoder\ByteMatrix; | |
+use BaconQrCode\Encoder\MatrixUtil; | |
+use BaconQrCode\Encoder\QrCode; | |
+use BaconQrCode\Exception\InvalidArgumentException; | |
+use BaconQrCode\Exception\RuntimeException; | |
+use BaconQrCode\Renderer\Color\Alpha; | |
+use BaconQrCode\Renderer\Color\ColorInterface; | |
+use BaconQrCode\Renderer\RendererStyle\EyeFill; | |
+use BaconQrCode\Renderer\RendererStyle\Fill; | |
+use GdImage; | |
+ | |
+final class GDLibRenderer implements RendererInterface | |
+{ | |
+ /** | |
+ * @var int | |
+ */ | |
+ private $size; | |
+ | |
+ /** | |
+ * @var int | |
+ */ | |
+ private $margin; | |
+ | |
+ /** | |
+ * @var string | |
+ */ | |
+ private $imageFormat; | |
+ | |
+ /** | |
+ * @var int | |
+ */ | |
+ private $compressionQuality; | |
+ | |
+ /** | |
+ * @var Fill | |
+ */ | |
+ private $fill; | |
+ | |
+ /** | |
+ * @var GdImage | |
+ */ | |
+ private $image; | |
+ | |
+ /** | |
+ * @var array<string, int> | |
+ */ | |
+ private $colors; | |
+ | |
+ public function __construct( | |
+ int $size, | |
+ int $margin = 4, | |
+ string $imageFormat = 'png', | |
+ int $compressionQuality = 9, | |
+ ?Fill $fill = null | |
+ ) { | |
+ if (! extension_loaded('gd') || ! function_exists('gd_info')) { | |
+ throw new RuntimeException('You need to install the GD extension to use this back end'); | |
+ } | |
+ | |
+ $this->size = $size; | |
+ $this->margin = $margin; | |
+ $this->imageFormat = $imageFormat; | |
+ $this->compressionQuality = $compressionQuality; | |
+ $this->fill = $fill; | |
+ if ($this->fill === null) { | |
+ $this->fill = Fill::default(); | |
+ } | |
+ if ($this->fill->hasGradientFill()) { | |
+ throw new InvalidArgumentException('GDLibRenderer does not support gradients'); | |
+ } | |
+ } | |
+ | |
+ /** | |
+ * @throws InvalidArgumentException if matrix width doesn't match height | |
+ */ | |
+ public function render(QrCode $qrCode): string | |
+ { | |
+ $matrix = $qrCode->getMatrix(); | |
+ $matrixSize = $matrix->getWidth(); | |
+ | |
+ if ($matrixSize !== $matrix->getHeight()) { | |
+ throw new InvalidArgumentException('Matrix must have the same width and height'); | |
+ } | |
+ | |
+ MatrixUtil::removePositionDetectionPatterns($matrix); | |
+ $this->newImage(); | |
+ $this->draw($matrix); | |
+ | |
+ return $this->renderImage(); | |
+ } | |
+ | |
+ private function newImage(): void | |
+ { | |
+ $img = imagecreatetruecolor($this->size, $this->size); | |
+ if ($img === false) { | |
+ throw new RuntimeException('Failed to create image of that size'); | |
+ } | |
+ | |
+ $this->image = $img; | |
+ imagealphablending($this->image, false); | |
+ imagesavealpha($this->image, true); | |
+ | |
+ | |
+ $bg = $this->getColor($this->fill->getBackgroundColor()); | |
+ imagefilledrectangle($this->image, 0, 0, $this->size, $this->size, $bg); | |
+ imagealphablending($this->image, true); | |
+ } | |
+ | |
+ private function draw(ByteMatrix $matrix): void | |
+ { | |
+ $matrixSize = $matrix->getWidth(); | |
+ | |
+ $pointsOnSide = $matrix->getWidth() + $this->margin * 2; | |
+ $pointInPx = $this->size / $pointsOnSide; | |
+ | |
+ $this->drawEye(0, 0, $pointInPx, $this->fill->getTopLeftEyeFill()); | |
+ $this->drawEye($matrixSize - 7, 0, $pointInPx, $this->fill->getTopRightEyeFill()); | |
+ $this->drawEye(0, $matrixSize - 7, $pointInPx, $this->fill->getBottomLeftEyeFill()); | |
+ | |
+ $rows = $matrix->getArray()->toArray(); | |
+ $color = $this->getColor($this->fill->getForegroundColor()); | |
+ for ($y = 0; $y < $matrixSize; $y += 1) { | |
+ for ($x = 0; $x < $matrixSize; $x += 1) { | |
+ if (! $rows[$y][$x]) { | |
+ continue; | |
+ } | |
+ | |
+ $points = $this->normalizePoints([ | |
+ ($this->margin + $x) * $pointInPx, ($this->margin + $y) * $pointInPx, | |
+ ($this->margin + $x + 1) * $pointInPx, ($this->margin + $y) * $pointInPx, | |
+ ($this->margin + $x + 1) * $pointInPx, ($this->margin + $y + 1) * $pointInPx, | |
+ ($this->margin + $x) * $pointInPx, ($this->margin + $y + 1) * $pointInPx, | |
+ ]); | |
+ $this->imageFilledPolygon($points, $color); | |
+ } | |
+ } | |
+ } | |
+ | |
+ private function drawEye(int $xOffset, int $yOffset, float $pointInPx, EyeFill $eyeFill): void | |
+ { | |
+ $internalColor = $this->getColor($eyeFill->inheritsInternalColor() | |
+ ? $this->fill->getForegroundColor() | |
+ : $eyeFill->getInternalColor()); | |
+ | |
+ $externalColor = $this->getColor($eyeFill->inheritsExternalColor() | |
+ ? $this->fill->getForegroundColor() | |
+ : $eyeFill->getExternalColor()); | |
+ | |
+ for ($y = 0; $y < 7; $y += 1) { | |
+ for ($x = 0; $x < 7; $x += 1) { | |
+ if ((($y === 1 || $y === 5) && $x > 0 && $x < 6) || (($x === 1 || $x === 5) && $y > 0 && $y < 6)) { | |
+ continue; | |
+ } | |
+ | |
+ $points = $this->normalizePoints([ | |
+ ($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx, | |
+ ($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx, | |
+ ($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx, | |
+ ($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx, | |
+ ]); | |
+ | |
+ if ($y > 1 && $y < 5 && $x > 1 && $x < 5) { | |
+ $this->imageFilledPolygon($points, $internalColor); | |
+ } else { | |
+ $this->imageFilledPolygon($points, $externalColor); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ /** | |
+ * Normalize points will trim right and bottom line by 1 pixel. | |
+ * Otherwise pixels of neighbors are overlapping which leads to issue with transparency and small QR codes. | |
+ */ | |
+ private function normalizePoints(array $points): array | |
+ { | |
+ $maxX = $maxY = 0; | |
+ for ($i = 0; $i < count($points); $i += 2) { | |
+ // Do manual round as GD just removes decimal part | |
+ $points[$i] = $newX = round($points[$i]); | |
+ $points[$i + 1] = $newY = round($points[$i + 1]); | |
+ | |
+ $maxX = max($maxX, $newX); | |
+ $maxY = max($maxY, $newY); | |
+ } | |
+ | |
+ // Do trimming only if there are 4 points (8 coordinates), assumes this is square. | |
+ | |
+ for ($i = 0; $i < count($points); $i += 2) { | |
+ $points[$i] = min($points[$i], $maxX - 1); | |
+ $points[$i + 1] = min($points[$i + 1], $maxY - 1); | |
+ } | |
+ | |
+ return $points; | |
+ } | |
+ | |
+ private function renderImage(): string | |
+ { | |
+ ob_start(); | |
+ $quality = $this->compressionQuality; | |
+ switch ($this->imageFormat) { | |
+ case 'png': | |
+ if ($quality > 9 || $quality < 0) { | |
+ $quality = 9; | |
+ } | |
+ imagepng($this->image, null, $quality); | |
+ break; | |
+ | |
+ case 'gif': | |
+ imagegif($this->image, null); | |
+ break; | |
+ | |
+ case 'jpeg': | |
+ case 'jpg': | |
+ if ($quality > 100 || $quality < 0) { | |
+ $quality = 85; | |
+ } | |
+ imagejpeg($this->image, null, $quality); | |
+ break; | |
+ default: | |
+ ob_end_clean(); | |
+ throw new InvalidArgumentException( | |
+ 'Supported image formats are jpeg, png and gif, got: ' . $this->imageFormat | |
+ ); | |
+ } | |
+ | |
+ imagedestroy($this->image); | |
+ $this->colors = []; | |
+ $this->image = null; | |
+ | |
+ return ob_get_clean(); | |
+ } | |
+ | |
+ private function getColor(ColorInterface $color): int | |
+ { | |
+ $alpha = 100; | |
+ | |
+ if ($color instanceof Alpha) { | |
+ $alpha = $color->getAlpha(); | |
+ $color = $color->getBaseColor(); | |
+ } | |
+ | |
+ $rgb = $color->toRgb(); | |
+ | |
+ $colorKey = sprintf('%02X%02X%02X%02X', $rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha); | |
+ | |
+ if (! isset($this->colors[$colorKey])) { | |
+ $colorId = imagecolorallocatealpha( | |
+ $this->image, | |
+ $rgb->getRed(), | |
+ $rgb->getGreen(), | |
+ $rgb->getBlue(), | |
+ (int)((100 - $alpha) / 100 * 127) // Alpha for GD is in range 0 (opaque) - 127 (transparent) | |
+ ); | |
+ | |
+ if ($colorId === false) { | |
+ throw new RuntimeException('Failed to create color: #' . $colorKey); | |
+ } | |
+ | |
+ $this->colors[$colorKey] = $colorId; | |
+ } | |
+ | |
+ return $this->colors[$colorKey]; | |
+ } | |
+ | |
+ private function imageFilledPolygon(array $points, int $color): bool | |
+ { | |
+ if (\PHP_VERSION_ID >= 80000) { | |
+ // PHP 8.0 supports this method without $num_points. | |
+ // And PHP 8.1 marks the other one as deprecated. | |
+ return imagefilledpolygon($this->image, $points, $color); | |
+ } | |
+ return imagefilledpolygon($this->image, $points, count($points) / 2, $color); | |
+ } | |
+} | |
diff -urN twofactor_totp.orig/vendor/bacon/bacon-qr-code/test/Integration/GDLibRenderingTest.php twofactor_totp/vendor/bacon/bacon-qr-code/test/Integration/GDLibRenderingTest.php | |
--- twofactor_totp.orig/vendor/bacon/bacon-qr-code/test/Integration/GDLibRenderingTest.php 1970-01-01 01:00:00.000000000 +0100 | |
+++ twofactor_totp/vendor/bacon/bacon-qr-code/test/Integration/GDLibRenderingTest.php 2023-09-10 12:26:04.367279612 +0200 | |
@@ -0,0 +1,113 @@ | |
+<?php | |
+ | |
+declare(strict_types=1); | |
+ | |
+namespace BaconQrCodeTest\Integration; | |
+ | |
+use BaconQrCode\Exception\InvalidArgumentException; | |
+use BaconQrCode\Exception\RuntimeException; | |
+use BaconQrCode\Renderer\Color\Alpha; | |
+use BaconQrCode\Renderer\Color\Rgb; | |
+use BaconQrCode\Renderer\Eye\EyeInterface; | |
+use BaconQrCode\Renderer\Eye\SimpleCircleEye; | |
+use BaconQrCode\Renderer\Eye\SquareEye; | |
+use BaconQrCode\Renderer\GDLibRenderer; | |
+use BaconQrCode\Renderer\ImageRenderer; | |
+use BaconQrCode\Renderer\Image\GDImageBackEnd; | |
+use BaconQrCode\Renderer\Module\DotsModule; | |
+use BaconQrCode\Renderer\Module\RoundnessModule; | |
+use BaconQrCode\Renderer\RendererStyle\EyeFill; | |
+use BaconQrCode\Renderer\RendererStyle\Fill; | |
+use BaconQrCode\Renderer\RendererStyle\Gradient; | |
+use BaconQrCode\Renderer\RendererStyle\GradientType; | |
+use BaconQrCode\Renderer\RendererStyle\RendererStyle; | |
+use BaconQrCode\Writer; | |
+use PHPUnit\Framework\TestCase; | |
+use Spatie\Snapshots\MatchesSnapshots; | |
+ | |
+/** | |
+ * @group integration | |
+ */ | |
+final class GDLibRenderingTest extends TestCase | |
+{ | |
+ use MatchesSnapshots; | |
+ | |
+ /** | |
+ * @requires extension gd | |
+ */ | |
+ public function testGenericQrCode(): void | |
+ { | |
+ $renderer = new GDLibRenderer(400); | |
+ $writer = new Writer($renderer); | |
+ $tempName = tempnam(sys_get_temp_dir(), 'test') . '.png'; | |
+ $writer->writeFile('Hello World!', $tempName); | |
+ | |
+ $this->assertMatchesFileSnapshot($tempName); | |
+ unlink($tempName); | |
+ } | |
+ | |
+ /** | |
+ * @requires extension gd | |
+ */ | |
+ public function testDifferentColorsQrCode(): void | |
+ { | |
+ $renderer = new GDLibRenderer( | |
+ 400, | |
+ 10, | |
+ 'png', | |
+ 9, | |
+ Fill::withForegroundColor( | |
+ new Alpha(25, new Rgb(0, 0, 0)), | |
+ new Rgb(0, 0, 0), | |
+ new EyeFill(new Rgb(220, 50, 50), new Alpha(50, new Rgb(220, 50, 50))), | |
+ new EyeFill(new Rgb(50, 220, 50), new Alpha(50, new Rgb(50, 220, 50))), | |
+ new EyeFill(new Rgb(50, 50, 220), new Alpha(50, new Rgb(50, 50, 220))), | |
+ ) | |
+ ); | |
+ $writer = new Writer($renderer); | |
+ $tempName = tempnam(sys_get_temp_dir(), 'test') . '.png'; | |
+ $writer->writeFile('Hello World!', $tempName); | |
+ | |
+ $this->assertMatchesFileSnapshot($tempName); | |
+ unlink($tempName); | |
+ } | |
+ | |
+ | |
+ /** | |
+ * @requires extension gd | |
+ */ | |
+ public function testFailsOnGradient(): void | |
+ { | |
+ $this->expectException(InvalidArgumentException::class); | |
+ $this->expectExceptionMessage('GDLibRenderer does not support gradients'); | |
+ | |
+ new GDLibRenderer( | |
+ 400, | |
+ 10, | |
+ 'png', | |
+ 9, | |
+ Fill::withForegroundGradient( | |
+ new Alpha(25, new Rgb(0, 0, 0)), | |
+ new Gradient(new Rgb(255, 255, 0), new Rgb(255, 0, 255), GradientType::DIAGONAL()), | |
+ new EyeFill(new Rgb(220, 50, 50), new Alpha(50, new Rgb(220, 50, 50))), | |
+ new EyeFill(new Rgb(50, 220, 50), new Alpha(50, new Rgb(50, 220, 50))), | |
+ new EyeFill(new Rgb(50, 50, 220), new Alpha(50, new Rgb(50, 50, 220))), | |
+ ) | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * @requires extension gd | |
+ */ | |
+ public function testFailsOnInvalidFormat(): void | |
+ { | |
+ $renderer = new GDLibRenderer(400, 4, 'tiff'); | |
+ | |
+ $this->expectException(InvalidArgumentException::class); | |
+ $this->expectExceptionMessage('Supported image formats are jpeg, png and gif, got: tiff'); | |
+ | |
+ $writer = new Writer($renderer); | |
+ $tempName = tempnam(sys_get_temp_dir(), 'test') . '.png'; | |
+ $writer->writeFile('Hello World!', $tempName); | |
+ } | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment