Skip to content

Commit

Permalink
using blurhash for obscuring sensitive image (#342)
Browse files Browse the repository at this point in the history
replaced blur filter usage over sensitive image to obscure the image
with blurhash image, using hidden checkbox + css to control visibility
and buttons to switch between show and hide sensitive image

for image attachment in post and comments, hide the sensitive image
by putting blurhash image over along with the show button

for entry thumbnail, it works a bit differently:
- the sensitive image is hidden by hiding the main image and show
  existing blurhash background image instead
- due to space constraints, the show/hide button is icons only,
  additionally when in mobile layout and compact view, the hide button
  will not be shown after the image is revealed
  • Loading branch information
asdfzdfj authored Dec 13, 2023
1 parent d03a898 commit e91971c
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 71 deletions.
1 change: 1 addition & 0 deletions assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
@import 'components/vote';
@import 'components/entry';
@import 'components/comment';
@import 'components/figure_image';
@import 'components/post';
@import 'components/subject';
@import 'components/login';
Expand Down
6 changes: 1 addition & 5 deletions assets/styles/components/_comment.scss
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,8 @@ $comment-margin-sm: .3rem;
}

figure {
margin: 0;
}

figure img {
display: block;
margin: .5rem 0;
max-width: 100%;
}
}

Expand Down
8 changes: 8 additions & 0 deletions assets/styles/components/_entry.scss
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@
border-bottom-left-radius: 0 !important;
}

.sensitive-button-label {
line-height: 1rem;
}

@include media-breakpoint-down(lg) {
width: 140px;
height: calc(140px / 1.5); // 3:2 ratio
Expand All @@ -185,6 +189,10 @@
margin: 0 10px 10px 10px;
height: calc(100% - 10px);
width: calc(100% - 10px);

.sensitive-button-hide {
display: none;
}
}

.rounded-edges & {
Expand Down
128 changes: 128 additions & 0 deletions assets/styles/components/_figure_image.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// main wrapper
.figure-container {
position: relative;
width: fit-content;
height: fit-content;
}

// main image thumbnail
.figure-thumb {
.thumb {
display: block;
}

img {
display: block;
width: 100%;
height: 100%;
}
}

// blurhash image obscuring main image
.figure-blur {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;

img {
width: 100%;
height: 100%;
overflow: hidden;
object-fit: cover;
}
}

// checkbox to store sensitive state
input.sensitive-state {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
opacity: 0;
}

// button to toggle sensitive
.sensitive-button {
position: absolute;

&-label {
background: var(--kbin-button-secondary-bg);
padding: .5rem;

font-weight: normal;
font-size: .8rem;
text-align: center;

opacity: .8;

i {
font-size: 1rem;
}

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

&:hover,
&:active {
opacity: 1;
}
}

&-show {
top: 0;
left: 0;
width: 100%;
height: 100%;

.sensitive-button-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}

&-hide {
top: .5rem;
right: .5rem;

.sensitive-button-label {
opacity: .5;
line-height: 1rem;

i {
font-size: .9rem;
}

&:hover,
&:active {
opacity: .7;
}
}
}
}

// the magic part: toggle visibility depending on sensitive state
.sensitive-state {
~ .sensitive-checked--hide {
display: initial;
}

~ .sensitive-checked--show {
display: none;
}
}

.sensitive-state:checked {
~ .sensitive-checked--hide {
display: none;
}

~ .sensitive-checked--show {
display: revert;
}
}
13 changes: 2 additions & 11 deletions assets/styles/components/_post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
}
}

figure {
> figure {
grid-area: avatar;
margin: 0;
display: none;
Expand Down Expand Up @@ -152,16 +152,7 @@

figure {
display: block;

img {
margin: .5rem 0;
max-width: 600px;
max-height: 400px;

@include media-breakpoint-down(sm) {
max-width: 100%;
}
}
margin: .5rem 0;
}

button {
Expand Down
4 changes: 3 additions & 1 deletion src/Twig/Components/BlurhashImageComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ public function __construct(private CacheInterface $cache)

public function createImage(string $blurhash, int $width = 20, int $height = 20): string
{
$context = [$blurhash, $width, $height];

return $this->cache->get(
'bh_'.hash('sha256', $blurhash),
'bh_'.hash('sha256', serialize($context)),
function (ItemInterface $item) use ($blurhash, $width, $height) {
$item->expiresAfter(3600);

Expand Down
46 changes: 46 additions & 0 deletions templates/components/_figure_entry.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{# this fragment is only meant to be used in entry component #}
{% with {
sensitiveId: 'sensitive-check-'~entry.image.id,
isSingle: is_route_name('entry_single'),
route: isSingle ? singleRoute : entry_url(entry)
} %}
<figure>
<div class="image-filler" aria-hidden="true">
{% if entry.image.blurhash %}
{{ component('blurhash_image', {blurhash: entry.image.blurhash}) }}
{% endif %}
</div>
{% if entry.isAdult %}
<input id="{{ sensitiveId }}"
type="checkbox"
class="sensitive-state"
aria-label="{{ 'sensitive_toggle'|trans }}">
{% endif %}
<a href="{{ route }}"
class="{{ html_classes('sensitive-checked--show', {'thumb': isSingle and lightbox|default(false)}) }}"
rel="{{ setRel ? get_rel(route) : '' }}"
>
<img class="thumb-subject"
src="{{ asset(entry.image.filePath)|imagine_filter('entry_thumb') }}"
alt="{{ entry.image.altText }}">
</a>
{% if entry.isAdult %}
<label for="{{ sensitiveId }}"
class="sensitive-button sensitive-button-show sensitive-checked--hide"
title="{{ 'sensitive_show'|trans }}"
aria-label="{{ 'sensitive_show'|trans }}">
<div class="sensitive-button-label">
<i class="fa-solid fa-eye"></i>
</div>
</label>
<label for="{{ sensitiveId }}"
class="sensitive-button sensitive-button-hide sensitive-checked--show"
title="{{ 'sensitive_hide'|trans }}"
aria-label="{{ 'sensitive_hide'|trans }}">
<div class="sensitive-button-label">
<i class="fa-solid fa-eye-slash"></i>
</div>
</label>
{% endif %}
</figure>
{% endwith %}
37 changes: 37 additions & 0 deletions templates/components/_figure_image.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<figure>
<div class="figure-container">
<div class="figure-thumb">
<a href="{{ uploaded_asset(image.filePath) }}" class="thumb">
<img src="{{ asset(image.filePath)|imagine_filter(thumbFilter) }}"
alt="{{ image.altText }}">
</a>
</div>
{% if isAdult %}
{% with {sensitiveId: 'sensitive-check-'~image.id} %}
<input id="{{ sensitiveId }}"
type="checkbox"
class="sensitive-state"
aria-label="{{ 'sensitive_toggle'|trans }}">
<label for="{{ sensitiveId }}"
class="sensitive-button sensitive-button-show sensitive-checked--hide"
title="{{ 'sensitive_show'|trans }}">
<div class="figure-blur" aria-hidden="true">
{{ component('blurhash_image', {blurhash: image.blurhash, width: 32, height: 32}) }}
</div>
<div class="sensitive-button-label">
{{ 'sensitive_warning'|trans }} <br>
{{ 'sensitive_show'|trans }}
</div>
</label>
<label for="{{ sensitiveId }}"
class="sensitive-button sensitive-button-hide sensitive-checked--show"
title="{{ 'sensitive_hide'|trans }}"
aria-label="{{ 'sensitive_hide'|trans }}">
<div class="sensitive-button-label" >
<i class="fa-solid fa-eye-slash"></i>
</div>
</label>
{% endwith %}
{% endif %}
</div>
</figure>
38 changes: 10 additions & 28 deletions templates/components/entry.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -88,35 +88,17 @@
{% if SHOW_THUMBNAILS is same as V_TRUE %}
{% if entry.image %}
{% if entry.type is same as 'link' or entry.type is same as 'video' %}
<figure>
<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' : '' }}"
src="{{ asset(entry.image.filePath) |imagine_filter('entry_thumb') }}"
{% if entry.isAdult %}data-controller="thumb" data-action="mouseover->thumb#adult_image_hover mouseout->thumb#adult_image_hover_out"{% endif %}
alt="{{ entry.image.altText }}">
</a>
</figure>
{{ include('components/_figure_entry.html.twig', {
entry: entry,
singleRoute: entry.url,
setRel: true
}) }}
{% elseif entry.type is same as 'image' or entry.type is same as 'article' %}
<figure>
<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' : '' }}"
src="{{ asset(entry.image.filePath) |imagine_filter('entry_thumb') }}"
{% if entry.isAdult %}data-controller="thumb" data-action="mouseover->thumb#adult_image_hover mouseout->thumb#adult_image_hover_out"{% endif %}
alt="{{ entry.image.altText }}">
</a>
</figure>
{{ include('components/_figure_entry.html.twig', {
entry: entry,
singleRoute: uploaded_asset(entry.image.filePath),
lightbox: true
}) }}
{% endif %}
{% else %}
<div class="no-image-placeholder">
Expand Down
14 changes: 5 additions & 9 deletions templates/components/entry_comment.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,11 @@
{% endif %}
<footer>
{% if (comment.visibility in ['visible', 'private'] or comment.visibility is same as 'trashed' and this.canSeeTrashed) and comment.image %}
<figure>
<a href="{{ uploaded_asset(comment.image.filePath) }}"
class="thumb">
<img class="thumb-subject {{ comment.isAdult ? 'image-adult' : '' }}"
src="{{ asset(comment.image.filePath) |imagine_filter('post_thumb') }}"
{% if comment.isAdult %}data-controller="thumb" data-action="mouseover->thumb#adult_image_hover mouseout->thumb#adult_image_hover_out"{% endif %}
alt="{{ comment.image.altText }}">
</a>
</figure>
{{ include('components/_figure_image.html.twig', {
image: comment.image,
isAdult: comment.isAdult,
thumbFilter: 'post_thumb'
}) }}
{% endif %}
{% if comment.visibility in ['visible', 'private'] %}
<menu>
Expand Down
13 changes: 5 additions & 8 deletions templates/components/post.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,11 @@
{% endif %}
<footer>
{% if post.image %}
<figure>
<a class="thumb" href="{{ uploaded_asset(post.image.filePath) }}">
<img class="thumb-subject {{ post.isAdult ? 'image-adult' : '' }}"
src="{{ asset(post.image.filePath) |imagine_filter('post_thumb') }}"
{% if post.isAdult %}data-controller="thumb" data-action="mouseover->thumb#adult_image_hover mouseout->thumb#adult_image_hover_out"{% endif %}
alt="{{ post.image.altText }}">
</a>
</figure>
{{ include('components/_figure_image.html.twig', {
image: post.image,
isAdult: post.isAdult,
thumbFilter: 'post_thumb'
}) }}
{% endif %}
{% if post.visibility in ['visible', 'private'] %}
<menu>
Expand Down
Loading

0 comments on commit e91971c

Please sign in to comment.