Skip to content

Commit

Permalink
using blurhash for entry thumbnail blurred background (#333)
Browse files Browse the repository at this point in the history
quick and dirty thumbnail filler background image using blurhash

- can be generated on twig side using `blurhash_image` component,
  feeding blurhash as input props
- the image is embedded in `<img>` tag using `data:image/png;base64`,
  with default resolution of 20x20 and stretched with css for display,
  replacing .image-filler css background-image + blur filter
  • Loading branch information
asdfzdfj authored Dec 7, 2023
1 parent 97a189d commit afdf728
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 14 deletions.
2 changes: 1 addition & 1 deletion assets/styles/components/_comment.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ $comment-margin-sm: .3rem;
z-index: 4;

& > a.active,
& > li button.active, {
& > li button.active {
text-decoration: underline;
}

Expand Down
17 changes: 10 additions & 7 deletions assets/styles/components/_entry.scss
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,19 @@
}

.image-filler {
background: var(--kbin-vote-bg);
position: absolute;
background-position: center;
filter: blur(8px);
width: 110%;
height: 110%;
top: -5%;
left: -5%;
width: 100%;
height: 100%;

img {
object-fit: cover;
filter: brightness(85%);
}
}

.rounded-edges & {
.rounded-edges &,
.rounded-edges & .image-filler {
border-radius: var(--kbin-rounded-edges-radius);
}

Expand Down
49 changes: 49 additions & 0 deletions src/Twig/Components/BlurhashImageComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace App\Twig\Components;

use kornrunner\Blurhash\Blurhash;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent('blurhash_image')]
final class BlurhashImageComponent
{
public string $blurhash;
public int $width = 20;
public int $height = 20;

public function __construct(private CacheInterface $cache)
{
}

public function createImage(string $blurhash, int $width = 20, int $height = 20): string
{
return $this->cache->get(
'bh_'.hash('sha256', $blurhash),
function (ItemInterface $item) use ($blurhash, $width, $height) {
$item->expiresAfter(3600);

$pixels = Blurhash::decode($blurhash, $width, $height);
$image = imagecreatetruecolor($width, $height);
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
[$r, $g, $b] = $pixels[$y][$x];
imagesetpixel($image, $x, $y, imagecolorallocate($image, $r, $g, $b));
}
}

// I do not like this
ob_start();
imagepng($image);
$out = ob_get_contents();
ob_end_clean();

return 'data:image/png;base64,'.base64_encode($out);
}
);
}
}
1 change: 1 addition & 0 deletions templates/components/blurhash_image.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img src="{{ this.createImage(blurhash, width, height) }}" />
16 changes: 10 additions & 6 deletions templates/components/entry.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@
{% if entry.image %}
{% if entry.type is same as 'link' or entry.type is same as 'video' %}
<figure>
<div class="image-filler"
style="background-image: url({{ asset(entry.image.filePath) |imagine_filter('entry_thumb') }})"
aria-hidden="true"></div>
<div class="image-filler" aria-hidden="true">
{% if entry.image.blurhash %}
{{ component('blurhash_image', {blurhash: entry.image.blurhash}) }}
{% endif %}
</div>
<a href="{{ is_route_name('entry_single') ? entry.url : entry_url(entry) }}"
rel="{{ get_rel(is_route_name('entry_single') ? entry.url : entry_url(entry)) }}">
<img class="thumb-subject {{ entry.isAdult ? 'image-adult' : '' }}"
Expand All @@ -99,9 +101,11 @@
</figure>
{% elseif entry.type is same as 'image' or entry.type is same as 'article' %}
<figure>
<div class="image-filler"
style="background-image: url({{ asset(entry.image.filePath) |imagine_filter('entry_thumb') }})"
aria-hidden="true"></div>
<div class="image-filler" aria-hidden="true">
{% if entry.image.blurhash %}
{{ component('blurhash_image', {blurhash: entry.image.blurhash}) }}
{% endif %}
</div>
<a href="{{ is_route_name('entry_single') ? uploaded_asset(entry.image.filePath) : entry_url(entry) }}"
class="{{ html_classes({'thumb': is_route_name('entry_single')}) }}">
<img class="thumb-subject {{ entry.isAdult ? 'image-adult' : '' }}"
Expand Down

0 comments on commit afdf728

Please sign in to comment.