Low Quality Image Placeholder (LQIP) Pipes

Hugo pipes have treated me well. Where would low quality image placeholder (LQIP) libraries fit in?

I’ve been able in the past to use Hugo pipes to export a low resolution placeholder and inline it as base64, but there are some cool libraries surfacing that make a much cooler spectacle of it.

There is a really popular library potrace (141,385 weekly downloads) that generates svgs from images. The following examples are svgs drawn by hand, potrace’s svgs are even better, but nonetheless it demonstrates the resulting effect.

And sqip does cool LQIPs (2,600 GitHub stars).

Both libraries are available as npm packages, so creating a custom per-project implementation beside Hugo wouldn’t be terribly difficult. Though, if Hugo could provide direct support for these sorts of libraries that are gaining more and more attention, maybe through a pipe like with postcss, it would make developing great sites even easier.

I am not sure where to start with this sort of possible integration with Hugo, which is why I am posting about it for some guidance.

2 Likes

You will probably end up with a build script that is running Hugo and creates your images, and then running over those images to create the low resolution with one of these libraries.

If you expect an easy way to link these libraries into your pipe processing then, nope, not possible (yet).

What might be possible is to use a srcset setup that has a very small image size to begin with, then use those images as preview (background image, scaled up to 100% width and height) until the proper images is loaded (thus overlaying the background image). You won’t achieve any of these artistic approaches though. More like a very blurry image (what we see in the LQIP preview).

Yeah, I am building out a pre-start script that processes the placeholders and labels the files predictably to be base64 inlined by Hugo.

I’ve never written Go, so I create a PR for this, or suggest how difficult it would be, but I think that adding a LQIP potrace or sqip pipe with a dict for options looks very similar to how other pips seem to work.

I am currently using the Gaussian blur filter to create small preview images. While it might not come with the desired oomph factor of the npm packages above, I think it’s good enough:

{{ $resource := .Resources.GetMatch "images/project.jpg" }}
{{ $projectImg_lazy := $resource.Resize "40x Gaussian" }}
{{ $projectImg_lazy = $projectImg_lazy.Content | base64Encode }}
<img class="lazy" data-src="{{ $projectImg.RelPermalink }}" alt=""
  src='{{ print "data:image/jpeg;base64," $projectImg_lazy | safeURL }}' />

Now if only Hugo provided a way to control how much the image gets blurred…

I would say Hugo “provides” it. By using the underlying library.

Try adding a .5 right behind Gaussian? I wonder if it’s possible to add additional parameters like this - if not, it should maybe an advanced feature.

I am afraid it does not. I might be wrong of course.

I think this works easier (for your solution, not the OPs): resize the image to something like 50px on the longest side (or 50x50), then set it as background image via style attribute as CDATA. Then add a blur via CSS (https://css-tricks.com/almanac/properties/f/filter/). Then the original images via srcset and src attributes. I think this way is best in multiple ways. No JS, newest browsers do what we expect, older browser show something pixelated, but people using old browsers will be accustomed to that. It will also rank better in speed-tools and “seo” checkers.

That’s definitely a quick way of doing it.

I see the lazy class and data-src attribute you have, so I am guessing you are using JavaScript to later set the actual src. The problem I found with that approach is when I swapped the src attribute, there sometimes is a flash of background color as the src changes. If you have that problem, a remedy I am using now is…

Remedy

… wrapping the img in a div and giving the img an absolutely positioned sibling div the width and size of the parent with a z-index that places it above the actual img tag. Inline the placeholder div a background-image set to the base64 of your placeholder image. When the HD img is loaded (use the image’s onload attribute to know when), transition the placeholder’s opacity to 0. That will reveal the HD image beneath it smoothly instead of potentially flashing white (in my case).

That’s pretty much what I would do. Added javascript can be useful for selectively loading only the currently visible images using either the observer api or a bounding rect calculation.

A quick Google find this:

It doesn not have many stars, but maybe it’s great …

We could certainly add a $image.Trace method or something that would produce a SVG.

There is also a related issue on GitHub about a $image.Overlay (overlay images on top, probably also with some simple font/text support).

1 Like

I have not tested the above, but the code looks good, and I created this:

2 Likes

Thanks for looking into this @bep. :+1:

I will do this: https://github.com/gohugoio/hugo/issues/6234

Then it will maybe not be so daunting for others to do add image related stuff.

1 Like

2 posts were split to a new topic: Change internal shortcode to support experimental non-standard browser lazy loading

Promising tests with gotrace:

7 Likes

It does not work. Also if you try {{ $image := (.Blur 0.5) }} or {{ $image := (.Blur "0.5") }} hugo will give executing "main" at <.Blur>: can't evaluate field Blur in type resource.Resource.

Looking at image.go I do not see a processing method for Blur.

So @bep can you please add an imaging processing method like:

{{ $image := $resource.Blur 0.5 }} ?

I believe it would make a nice alternative for a lazy loading placeholder from an image with resize/resample > blur > base64encode.

use .Fit "36x24" with10% of the size. HTML img should include width and height values.

For the “SVG trace”, see https://github.com/gohugoio/hugo/issues/6253

1 Like

I’d like to share with you my two findings related to low quality image placeholders.

LQIP
Turns out you can use existing Hugo functions and some CSS to generate pretty solid looking LQIP-like placeholders:

Simple example:

{{ $image := .Page.Resources.GetMatch "example.jpg" }}
<img src="data:{{ $image.MediaType }};base64,{{ ($image.Resize "5x").Content | base64Encode }}" alt="" style="filter: blur(30px);">

More magic to remove white blurred border:

{{ $image := .Page.Resources.GetMatch "example.jpg" }}
<div style="position: relative; overflow: hidden; width: {{ $image.Width }}px;">
<img src="data:{{ $image.MediaType }};base64,{{ ($image.Resize "5x").Content | base64Encode }}" alt="" style="filter: blur(30px); transform: scale(1.2)">

Explanation: This technique resizes your source image to five pixels in width and encodes the result into page output. Later, it’s blurred client side with CSS.

It should act as a solid base for your lazy loading technique. Ideally, full size image is loaded, swapped with low-res placeholder and blur is transitioned with CSS from 30px to 0px.

You can experiment with low-res image size (but be careful with the encoded size, it’s going to be inlined after all).

SQIP

SQIP, mentioned by OP, is a technique based on LQIP, but enchanced by using SVG primitives (circles, polygons, etc) instead of pixels. Turns out, that the original implementation uses Primitive to generate polygon representation, wich is in turn a Go library.

There is even a Go implementation of sqip: GitHub - denisbrodbeck/sqip: SQIP is a tool for SVG-based LQIP image creation written in go

It uses a different image processing library than Hugo, but the whole source code should not be hard to adapt for Hugo.

It would be awesome to have a function like $image.Sqip or sth, that outputs SVG placeholder for a given resource :slight_smile: Just an idea :wink:

3 Likes

The fact that there are plenty of “here is how I did it” posts and even your description of how to do it in 2 or 3 lines is a sign that there is no need to put this into an internal functionality. There is a render hook for images. Put it there :slight_smile: