Drag & drop your image here, or

CSS filters are very useful in web development, but they can’t be applied to background images: recent builds of Safari have backdrop-filter, but that still leaves other browsers out in the cold.

While blur effects aren’t usually associated with , the vector format does retain one significant advantage in this area: it allows the creation of abstract backgrounds from any bitmap image, as shown in the demo at the top of this article.

The Joys of the Very Very Small

Figure 1. An 8 × 4 pixel image (shown magnified approximately 6000%)

One of the nicest hidden features of Webkit-based browsers is the visual effect they apply to very small images made extremely large. Given the 8 × 4 pixel image pixi.png shown in figure 1, combined with this CSS:

body {
    background: url(pixi.png) no-repeat;
    background-size: cover;
}

The result is not the pixelated mess that you might expect; instead, the browser blurs the few pixels it has into a rather lovely result that covers the entire background:

Figure 2: Webkit rendering a very small image at fullscreen

In theory, this effect should be replicated in other browsers. Unfortunately, most don’t yet implement the image-rendering CSS property on backgrounds in the same way, meaning that the result is pixelated:

Figure 3. The same background-image & CSS rendered in Firefox

Even when successful, building the effect requires work in a bitmap editor to produce the original tiny “seed” image. I wanted to make the process as simple as possible, while building in cross-browser compatibility.

A Tiny WebApp

To make the process easy, I created a very small and simple web application. To process the image, I want to provide two ways to upload it: a drag and drop target and a file upload element. The canvas element, at a default size of 10 by 8 pixels, will be used to reduce the image:

<canvas id="canvas" width="10" height="8"></canvas>

<div id="droptarget">
    <span>Drag & drop your image here, or
        <input type="file" id="fileupload">
    </span>
</div>

The main work is done by a script added to the bottom of the page. First, we identify the elements we’re going to use:

var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
target = document.getElementById("droptarget");

By default the uploaded image will be smoothed and antialiased. We don’t want that, we we’ll turn off this behaviour: note that it is vendor prefixed, in much the same way some experimental CSS properties and values are:

ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

To enable the functionality of the file upload UI:

fileupload.addEventListener("change", function(){
    loadImage(this.files[0]);
});

target.addEventListener("dragover", function(e){
    e.preventDefault();
}, true);

target.addEventListener("drop", function(e){
    e.preventDefault(); 
    loadImage(e.dataTransfer.files[0]);
}, true);

The drag and drop events need to be prevented from executing their default behaviour (dragging and dropping an image into a browser window will usually replace the window content with the image).

Both the drop and fileupload call the loadImage function, passing their content.

function loadImage(src){
    if(!src.type.match(/image.*/)){
        console.log("Not an image: ", src.type);
        return;
    }
    var reader = new FileReader();
    reader.onload = function(e){
        pixelate(e.target.result);
    };
    reader.readAsDataURL(src);
}

The function checks that the upload is an image, converts it into a DataURI, and passes the result to the pixelate function:

function pixelate(src) {
  img = new Image();
  img.src = src;
  var aspectRatio = img.width / img.height,
  canvasHeight = Math.floor(10 / aspectRatio);
  canvas.setAttribute("height", canvasHeight);
  var w = canvas.width,
  h = canvas.height;
  target.style.backgroundImage = "url("+img.src+")";
  ctx.drawImage(img, 0, 0, w, h);
  ctx.drawImage(canvas, 0, 0, w, h, 0, 0, w, h);
  document.body.style.backgroundImage = "url(data:image/svg+xml;base64," +  
  window.btoa("<svg xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink= "http://www.w3.org/1999/xlink" viewBox="1 1 8 " + (canvasHeight - 2) + "">
  <image filter="url(#blur)" width="10" height=""+ canvasHeight + "" 
  xlink:href=""+canvas.toDataURL()+""/>
  <filter id="blur">
  <feGaussianBlur stdDeviation=".5" />
  </filter></svg>")+")";
}

The new image’s aspect ratio is determined by dividing the picture’s width by its height; the canvas element is just 10 pixels wide, but needs to be set to the appropriate scaled height to contain the shrunken version of the image.

The full-size image is written as the background of the drag-and-drop target to remind the user just what they are looking at, and the tiny version is drawn to the canvas.

Making a Blurry SVG

It’s easiest to understand the final result from the inside out. The reduced canvas image is the key: in the pixelate function, it’s converted into a base64 encoding with canvas.toDataURL(). This data is then embedded inside the SVG. Shown in its expanded, hard-coded form (and assuming that the image is reduced to 10 × 8 pixels, and referenced as pixelData), the SVG will be:

<svg xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink= "http://www.w3.org/1999/xlink" viewBox="1 1 8 6">
    <image filter="url(#blur)" width="10" height="8" xlink:href="pixelData">
    <filter id="blur">
        <feGaussianBlur stdDeviation=".5" />
    </filter>
</svg>

Note that the viewBox for the SVG is reduced, clipping its edges; otherwise, the SVG blur would include the assumed white background of the image.

Since most browsers won’t take a raw SVG text string like this as a background, the result is turned into a base64 DateURI itself. Since the reduced bitmap it incorporates is only a few pixels wide, the result is very small indeed, and entirely inline:

body {
    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9z…1ciI+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjUiIC8+PC9maWx0ZXI+PC9zdmc+");
} 

This technique is derived from an article by Oliver Rivo, which was further derived from code by Tibor Szász.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/MyQqxw