Skip to content

Commit

Permalink
feat(SLB-458): branded preview qr (#1565)
Browse files Browse the repository at this point in the history
  • Loading branch information
colorfield authored Aug 14, 2024
1 parent 9dc44d0 commit 2330e1f
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ function silverback_preview_link_theme(array $existing, string $type, string $th
'variables' => [
'title' => NULL,
'preview_url' => NULL,
'preview_qr_code' => NULL,
'preview_qr_code_alt' => NULL,
'link_description' => NULL,
'actions_description' => NULL,
'remaining_lifetime' => NULL,
'preview_qr_code_url' => NULL,
'expiry_description' => NULL,
'actions_description' => NULL
],
],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ silverback_preview_link.preview.access:
_auth: ['oauth2']
no_cache: TRUE

silverback_preview_link.qr_code:
path: '/preview/qr-code/{base64_url}'
defaults:
_controller: '\Drupal\silverback_preview_link\Controller\PreviewController::getQRCode'
requirements:
# Keep it very generic, it's just to prevent anonymous access / bots.
_permission: 'access administration pages'

silverback_preview_link.preview_link.access:
path: '/preview/link-access'
defaults:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Drupal\silverback_preview_link\Controller;

use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\silverback_preview_link\QRCodeWithLogo;
use Drupal\user\Entity\User;
use Symfony\Component\HttpFoundation\JsonResponse;

Expand Down Expand Up @@ -61,4 +63,14 @@ public function hasLinkAccess() {
], 403);
}

/**
* Returns the QR SVG file.
*/
public function getQRCode(string $base64_url): CacheableResponse {
$decodedUrl = base64_decode($base64_url);
$qrCode = new QRCodeWithLogo();
$result = $qrCode->getQRCode($decodedUrl);
return new CacheableResponse($result, 200, ['Content-Type' => 'image/svg+xml']);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Drupal\silverback_preview_link\Form;

use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
Expand All @@ -19,12 +20,15 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\silverback_preview_link\Entity\SilverbackPreviewLink;
use Drupal\silverback_preview_link\PreviewLinkExpiry;
use Drupal\silverback_preview_link\PreviewLinkHostInterface;
use Drupal\silverback_preview_link\PreviewLinkStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\silverback_preview_link\QRCodeLogo;
use Drupal\silverback_preview_link\QRCodeWithLogo;

/**
* Preview link form.
Expand Down Expand Up @@ -145,7 +149,8 @@ public function buildForm(array $form, FormStateInterface $form_state, RouteMatc
$remainingSeconds = max(0, ($this->entity->getExpiry()?->getTimestamp() ?? 0) - $this->time->getRequestTime());
$remainingAgeFormatted = $this->dateFormatter->formatInterval($remainingSeconds);
$isNewToken = $this->linkExpiry->getLifetime() === $remainingSeconds;
$qrCode = NULL;
$displayQRCode = TRUE;
$qrCodeUrlString = NULL;
$actionsDescription = NULL;

if ($isNewToken) {
Expand All @@ -160,28 +165,31 @@ public function buildForm(array $form, FormStateInterface $form_state, RouteMatc
':url' => $externalPreviewUrlString,
'@entity_label' => $host->label(),
]);
$displayQRCode = FALSE;
}
else {
$expiryDescription = $this->t('Live preview link for <em>@entity_label</em> expires in @lifetime.</p>', [
':url' => $externalPreviewUrlString,
'@entity_label' => $host->label(),
'@lifetime' => $remainingAgeFormatted,
]);
$qrCode = (new QRCode)->render($externalPreviewUrlString);
}
$actionsDescription = $this->t('If a new link is generated, active preview link will get invalidated.');
$actionsDescription = $this->t('If a new link is generated, the active link becomes invalid.');
}

if ($displayQRCode) {
$qrCodeEncodedUrl = base64_encode($externalPreviewUrlString);
$qrCodeUrlString = Url::fromRoute('silverback_preview_link.qr_code', ['base64_url' => $qrCodeEncodedUrl])->toString();
}

$form['preview_link'] = [
'#theme' => 'preview_link',
'#title' => $this->t('Preview link'),
'#weight' => -9999,
'#preview_qr_code' => $qrCode,
'#preview_qr_alt' => $externalPreviewUrlString,
'#preview_url' => $externalPreviewUrlString,
'#preview_qr_code_url' => $qrCodeUrlString,
'#expiry_description' => $expiryDescription,
'#actions_description' => $actionsDescription,
'#remaining_lifetime' => $remainingAgeFormatted,
'#preview_url' => $externalPreviewUrlString,
];

if (!$isNewToken) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Drupal\silverback_preview_link;

use Drupal\Tests\Component\Annotation\Doctrine\Fixtures\Annotation\Version;
use chillerlan\QRCode\{QRCode, QRCodeException, QROptions};
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Common\EccLevel;
use Symfony\Component\HttpFoundation\Response;
use function file_exists, gzencode, header, is_readable, max, min;

/**
* Creates and renders a QR Code with embedded SVG logo.
*/
class QRCodeWithLogo {

private $config = [
'svgLogo' => __DIR__ . '/images/amazee-labs_logo-square-green.svg',
'svgLogoScale' => 1,
'svgLogoCssClass' => 'dark',
'version' => QRCode::VERSION_AUTO,
'outputType' => QRCode::OUTPUT_CUSTOM,
'outputInterface' => QRMarkupSVGWithLogo::class,
'imageBase64' => FALSE,
// ECC level H is necessary when using logos.
'eccLevel' => EccLevel::H,
'addQuietzone' => TRUE,
// If set to TRUE, the light modules won't be rendered.
'imageTransparent' => FALSE,
// Empty the default value to remove the fill* attributes from the <path> elements
'markupDark' => '',
'markupLight' => '',
'drawCircularModules' => TRUE,
'circleRadius' => 0.45,
'svgConnectPaths' => TRUE,
'keepAsSquare' => [
QRMatrix::M_FINDER | QRMatrix::IS_DARK,
QRMatrix::M_FINDER_DOT,
QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK,
],
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
'svgDefs' => '
<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#951b81" offset="0"/>
<stop stop-color="#00a29a" offset="0.8"/>
</linearGradient>
<style><![CDATA[
.dark{fill: url(#gradient);}
.light{fill: #fff;}
]]></style>',
];

private function getOptions(): QROptions {
// Augment the QROptions class.
return new class ($this->config) extends QROptions {

protected string $svgLogo;

// Logo scale in % of QR Code size, clamped to 10%-30%.
protected float $svgLogoScale = 0.20;

// CSS class for the logo (defined in $svgDefs).
protected string $svgLogoCssClass = '';

protected function set_svgLogo(string $svgLogo): void {
if (!file_exists($svgLogo) || !is_readable($svgLogo)) {
throw new QRCodeException('invalid svg logo');
}
$this->svgLogo = $svgLogo;
}

// Clamp logo scale.
protected function set_svgLogoScale(float $svgLogoScale): void {
$this->svgLogoScale = max(0.05, min(0.3, $svgLogoScale));
}

};
}

public function getQRCode(string $data) {
return (new QRCode($this->getOptions()))->render($data);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Drupal\silverback_preview_link;

use chillerlan\QRCode\Output\QRMarkupSVG;

/**
* Output interface for QRCode::OUTPUT_CUSTOM.
*/
class QRMarkupSVGWithLogo extends QRMarkupSVG {

/**
* {@inheritdoc}
*/
protected function paths(): string {
$size = (int) ceil($this->moduleCount * $this->options->svgLogoScale);
// Calling QRMatrix::setLogoSpace() manually,
// so QROptions::$addLogoSpace has no effect.
$this->matrix->setLogoSpace($size, $size);
$svg = parent::paths();
$svg .= $this->getLogo();
return $svg;
}

/**
* {@inheritdoc}
*/
protected function path(string $path, int $M_TYPE): string {
// Omit the "fill" and "opacity" attributes on the path element.
return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
}

/**
* Returns a <g> element that contains the SVG logo and positions
* it properly within the QR Code.
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
*/
protected function getLogo(): string {
return sprintf(
'%5$s<g transform="translate(%1$s %1$s) scale(%2$s)" class="%3$s">%5$s%4$s%5$s</g>',
(($this->moduleCount - ($this->moduleCount * $this->options->svgLogoScale)) / 2),
$this->options->svgLogoScale,
$this->options->svgLogoCssClass,
file_get_contents($this->options->svgLogo),
$this->options->eol
);
}

}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@
<h2 class="preview-link__title hidden">{{ title }}</h2>
{% if preview_url is not empty %}
<div class="preview-link__copy js-form-item form-item js-form-type-textfield form-type--textfield">
<input id="preview-link__copy-text"
<input id="preview-link__copy--text"
disabled
name="preview-link__copy-text"
name="preview-link__copy--text"
type="text"
value="{{ preview_url }}"
size="32"
class="form-text required form-element form-element--type-text form-element--api-textfield"
>
<button class="button">{{ 'Copy' }}</button>
<div id="preview-link__copy-result" class="form-item__description">
<div id="preview-link__copy--result" class="form-item__description">
&nbsp;
</div>
</div>
{% endif %}
{% if preview_qr_code is not empty %}
{% if preview_qr_code_url is not empty %}
<div class="preview-link__qr">
<p>{{ 'Scan the QR Code to open the preview on another device.' }}</p>
<img src="{{ preview_qr_code }}" alt="{{ preview_qr_alt }}" width="200" height="200" />
<div class="preview-link__qr--wrapper" style="display: flex; align-items: center; justify-content: center;">
<img src="{{ preview_qr_code_url }}" alt="{{ preview_url }}" width="300" height="300" />
</div>
</div>
{% endif %}
{% if expiry_description is not empty %}
Expand Down

0 comments on commit 2330e1f

Please sign in to comment.