
BlurHash: Algorithm to generate compact representation of an image placeholder - pcr910303
https://blurha.sh/
======
Scaevolus
For comparison, Instagram sends ~200 byte base64 thumbnails in its API
responses. It's a low-res JPEG with a fixed header to save a bit more space.
This is also used to blur sensitive images. You can get even better results
with webp.

It requires minimal javascript support-- just enough to build an img tag with
the appropriate data url, and renders just as fast as any other tiny image.
Upscaling blurs away the imperfections.

[https://stackoverflow.com/q/49625771](https://stackoverflow.com/q/49625771)

~~~
alephnan
Can you clarify the significance of the fixed header?

~~~
Scaevolus
JPEGs were never meant to be <1KB, so the header can be rather large-- looking
at one example, the truncated JPEG data Instagram sends is 248B, and the
additional fixed header information is 607B, resulting in an 848B thumbnail
(the truncated data includes dimensions).

That 607B of fixed header includes some compression settings that don't need
to change between images:

\- 132B of quantization tables, saying how much to divide every coefficient
by-- for subtle details the difference between "128" and "129" doesn't matter,
and you can just send "2 * 64".

\- a 418B Huffman table, which stores the frequencies of different coefficient
values after quantization-- small values are much more common than large ones,
given the divisors. You can have smaller image data with properly tuned
tables, but that only matters for large images. For 250B of image data, it's a
waste.

\- Miscellaneous other details that are fixed, like the file type magic
headers, the color space information, and start/end markers.

You can dig in more by putting a thumbnail like
[https://i.imgur.com/M0EsJH1.jpg](https://i.imgur.com/M0EsJH1.jpg) into this
site: [https://cyber.meme.tips/jpdump/](https://cyber.meme.tips/jpdump/)

------
DagAgren
Hello, I am the main author of this. Also make sure to check out the README on
Github for more technical details!
[https://github.com/woltapp/blurhash](https://github.com/woltapp/blurhash)

~~~
animalnewbie
Very nice work. Appreciate you open sourcing this

------
css
Judging by the example string on the website,
`LEHV6nWB2yk8pyoJadR*.7kCMdnj`[0], you cannot use these hash strings as
filenames because they can have disallowed characters.

You could save a step by just sending which image to download and use the
filename as the hash you use to render the result, but this algorithm requires
you store the relationship between the hash and the image somewhere (unless I
am missing something obvious).

[0] source code: [https://github.com/woltapp/blurhash-
python/blob/7469c813ea64...](https://github.com/woltapp/blurhash-
python/blob/7469c813ea646ac022ca2879de76cc6e453237a1/src/encode.c#L125)

~~~
lvh
1\. Using the hashes as principal indices seems unsafe in general (which
includes image upload) anyway: it looks pretty simple to generate collisions.
The suggested use case (save the hash in the database next to the original
image or a reference to it) sounds fine.

2\. Which disallowed characters? The dictionary appears to be
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".
No slashes or null bytes, which are the common disallowed characters on server
platforms. It does have characters your shell cares about, but that's only a
problem if you're not quoting.

~~~
css
`:` is disallowed on MacOS and several of those characters are disallowed on
Windows. Generally I view images on a frontend, not on my servers.

~~~
lvh
Fair! I would not expect the image be saved to a Windows path when displaying
a blurred image in the example use cases (mobile apps and web pages). You're
thinking Electron, perhaps?

Point of order though: I don't think that's right for MacOS, though you're
certainly right for Windows. HFS+ lets you use any Unicode symbol including
NUL (because the filenames are encoded Pascal-style). I don't know the details
how how APFS does it, but it also appears to support any Unicode code point,
though it additionally mandates UTF-8. [0]

[0]
[https://developer.apple.com/library/archive/documentation/Fi...](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html)
grep "filenames"

~~~
css
Interesting, I was not aware about MacOS. If I run `touch :.:` it will run
fine and create the file, but Finder disallows this for whatever reason:

    
    
        The name “:.:” can’t be used.
        Try using a name with fewer characters, or with no punctuation marks.

~~~
DagAgren
Finder exchanges : and / in filenames, for compatibility with classic MacOS,
which used : as a directory separator and allowed / in filenames. You can
create a file named / in Finder, and it will show up as : on the file system.

------
asadkn
This looks great. For browsers, would have been even better if the result was
an SVG or pure CSS gradient. Canvas can still have performance hit on mobiles.

The JS implementation of Gradify [1] did something similar but by using CSS
linear-gradient which is even better in terms of performance. I wonder if the
same could be implemented here.

[1] [https://github.com/fraser-hemp/gradify](https://github.com/fraser-
hemp/gradify)

~~~
WorldMaker
You don't have to keep it displayed on canvas given it isn't "animated". You
can use temporary canvases for the initial render then convert it to a Blob
(and from a Blob to an Object URL). Most browsers at that point use the GPU's
transforms to quickly convert the image data to a basic compressed JPG or PNG
and then the usual browser performance mechanics of an image apply (and as a
URL you can use it in regular IMG tags or CSS things like background-image).
Presuming you remember to revoke the Object URLs when you are done with the
hashes to avoid Blob leaks, performance is improved over the "simpler" canvas
techniques that just display the rendered canvases.

If code helps, here's how it looks as a React Hook in Typescript:

[https://gist.github.com/WorldMaker/a3cbe0059acd827edee568198...](https://gist.github.com/WorldMaker/a3cbe0059acd827edee568198376b95a)

(I offered the code to the react-blurhash repository in its Issues.)

~~~
londons_explore
The cost is in the initial render to a canvas. Once on a canvas, there is no
advantage to converting it to a blob for display in an IMG tag as far as
performance goes.

~~~
WorldMaker
That's not what my testing has shown in practice. Memory use drops
dramatically in the conversion from canvas to blob when the browser compresses
the render buffer to JPG/PNG under the hood. GPU usage seems to drop in the
browsers I had under performance tools, but it subtle enough it could be just
margin of error (as memory pressure was my bigger issue, that wasn't something
I was keeping that good of an eye on).

But yes the biggest performance gains aren't in pure, static IMG tag usage
scenarios, the biggest performance gains I saw were in combo with CSS
animations, and that was something important to what I was studying. As
Blurhases are useful loading states this seems a common use case to me of
having a Blurhash shown/involved in things like navigation transitions, and it
seems pretty clear browsers have a lot more tricks for optimizing static
images involved in CSS Animations than they do for canvas surfaces.

I was looking for a way to better amortize or skip the initial render as well.
It should be possible to take the TypedArray `decode` buffer and directly
construct a Blob from it, but I couldn't find a MIME Type that matched the
Bitmap format Blurhash produces (and Canvas setImageData reads) in the time
I've had to poke at the project so far. As I mentioned, memory pressure was a
performance concern in my testing, so I'd also be curious about the
performance trade-off of paying for an initial canvas render and getting what
seems to be a "nearly free" compression to JPG or PNG from the GPU in
converting that to blob, versus using a larger bitmap blob but no canvas
render step.

------
smlacy
Forgot to mention one important detail: How big is the JavaScript source to
the BlurHash decoding algorithm?

I'm guessing that it's approximately the size of a single image, possibly
quite a bit more.

~~~
hombre_fatal
22kb

Edit: Nevermind, derp,
[https://blurha.sh/blurhash.01ae00eea611ada5e9c7.js](https://blurha.sh/blurhash.01ae00eea611ada5e9c7.js)
contains all of the JS of the entire website.

~~~
DagAgren
It is a lot smaller than that, that is probably the size of all JS code on the
web page?

The algorithm itself fits into a few k.

~~~
hombre_fatal
Yeah, you're right. I found the first JS file in my network tab and judged by
its blurhash.js filename that it was the algorithm:
[https://blurha.sh/blurhash.01ae00eea611ada5e9c7.js](https://blurha.sh/blurhash.01ae00eea611ada5e9c7.js)

Of course, my first clue should have been that it was also the _only_
Javascript file.

------
mneubrand
I implemented something similar when I was playing around on a personal page.
It's built by Jekyll and inlines a base64 version of the jpeg with a CSS blur
filter, then lazyloads the real image and transitions it in via opacity 0->1\.
You can see it in action here: [https://www.road-beers.com/](https://www.road-
beers.com/)

~~~
zerd
I noticed that it only inlines the base64 for the first pictures/"above the
fold", and then loads the rest async. Clever.

------
BeeOnRope
The examples discussed seem to assume a dynamic site where you load the images
(and the blurha placeholder) from a database, but how easy would this be to
use for a static site with large images, perhaps embedding the blurha directly
in the HTML?

~~~
sp332
You could put the hash in a custom HTML attribute, and have the JS load it
from there.

<canvas width="100px" height="100px" imgsrc="hirez.jpg" blurhash="abcd..." />

------
marcusjt
I'm surprised this isn't more web focused, i.e. a NodeJS library which encodes
an image as a BASE64 data URL which any browser can render immediately while
the real image lazy loads

~~~
DagAgren
It's a lot more efficient to do it once, and store it.

Also, it's really simple! Go ahead and create a library for it! The algorithm
is tiny, and easily ported, so it's quite fun.

------
dang
[https://wolt.com/blog/hq/2019/07/01/how-we-came-to-
create-a-...](https://wolt.com/blog/hq/2019/07/01/how-we-came-to-create-a-new-
image-placeholder-algorithm-blurhash/) is related.

Via
[https://news.ycombinator.com/item?id=20339442](https://news.ycombinator.com/item?id=20339442)
(1 comment).

------
busymom0
Funny coincidence that I am working on an iOS app and was looking for
something similar just couple days ago. I ended up building something but your
swift solution seems much better than what I have so I will switch to yours.
Thank you!

~~~
DagAgren
Nice!

------
asah
How did this compare with progressive JPEG ?

~~~
lvh
I think the biggest difference is in visual effect. A progressive JPEG looks
pretty terrible at first, whereas a blurred image may look preferable even
though it technically has less information.

~~~
ronyfadel
Agreed, +1 for browser makers to make progressive JPEG loading not look like a
dog just barfed the original image

~~~
DagAgren
Do modern browsers even support progressive display of JPEGs? I have not seen
it happen in years and years.

~~~
londons_explore
In the blink rendering engine, partial rendering of a progressive jpeg is
pretty much the lowest priority task for CPU time. That means unless you have
a very slow network and very fast CPU, you will never see half rendered
progressive jpegs.

That's the right design decision, because rendering a progressive jpeg
repeatedly is rather expensive - as well as redecoding the whole jpeg, you
also need to re-render any text or effects overlaying the jpeg, and
recomposite the frame. And then you're gonna have to do all that work again
when a few more bytes of progressive jpeg arrive from the network...

------
speedgoose
It's cool, I have no use for it but it's cool.

------
rgovostes
The accompanying blog post is unfortunately unreadable with an ad blocker on
that blocks third-party trackers from Google Analytics, Facebook, and New
Relic. It hijacks scrolling and continually scrolls back to the top. (Mac
Safari + 1Blocker + PiHole.)

------
krossitalk
Interesting bug on the demo page:

\- Upload any image

\- Now upload a transparent png

\- It does not clear the previous image, layering them together

\- It hashes the combination picture. Super neat!

------
867-5309
any ideas how this would impact on SEO/Pagespeed? if you are doubling the
amount of pictures being loaded, albeit a tiny placeholder for a thumbnail -
not from a size perspective, but from a network perspective surely this would
double the amount of image fetches

~~~
DagAgren
The whole idea here is that you do not need to fetch an extra image. The image
is generated in code on the client side from the short string. So there is no
network connection needed, and the placeholder is there instantly.

~~~
867-5309
then there must be some computational cost for the client

~~~
DagAgren
Of course. The cost is fairly small, though, since you only need to generate
very tiny thumbnails, and can then just scale them up.

------
ampdepolymerase
What's the advantage of this over using a Gaussian?

~~~
DagAgren
The amount of data to send is miniscule.

------
tantalor
> Your client then takes the string, and decodes it into an image that it
> shows while the real image is loading over the network.

That's not a "hash", that's an encoding. Hashes are one-way.

~~~
lvh
This /is/ one-way by virtue of throwing information away: you can't rebuild
the original image. If you're thinking of preimage resistance (it must be hard
to find an input that hashes to a given output) that's a property of
cryptographic hashes, but not hashes in general (canonical example FNV).

