Pixelart and the image-rendering Paradox
Tags: css, webdev, art
Published: 2021-08-07 21:00:45

<abbr title="Cascading Style Sheets">CSS</abbr> offers a nifty property called `image-rendering` which lets you influence how images scale. Normally, when you take a small image and make it bigger, the image becomes blurry. That's kinda ok for photos, but for pixelart, the effect is... rather devastating.

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0xjoto6lsltth0y5pswj.png" alt="A pixelart blue-haired character is upscaled into a fuzzy mess." />
<figcaption><strong>Credit:</strong> Lea, a character from the game <a href="http://cross-code.com">Cross Code</a></figcaption>
</figure>

{% details <small>What is **pixelart**?</small> %} <small>Pixelart is a way of creating pictures. Rather than using brush strokes, each individual pixel is carefully colored. It's like if you were given just a few hundred square tiles and asked to make a mosaic out of them.</small> {% enddetails %}

However, thanks to `image-rendering`, it's possible to upscale images in a way that highlights their pixelated nature! Just a couple lines of code later and...

```css
.pixelart {
    /* This order matters! */
    image-rendering: crisp-edges;
    image-rendering: pixelated;
}
```

{% codepen https://codepen.io/auroratide/pen/RwVywNK %}

...hooray! It looks fantastic!

_Except, wait just a second._ Why on earth does the above example CSS specify `image-rendering` twice, and with two different values? Something's fishy!

## The Paradox

It turns out there's a bit of a **paradox** in the browser support for `image-rendering`. Waddle on over to [Can I Use](https://caniuse.com/css-crisp-edges) and we see the following weirdness.

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqh5bo9rfeup40ud4hv2.png" alt="Chrome supports the pixelated value, but not crisp-edges." width="380" height="260" />
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cikpwtv4inj3pt9s9n9g.png" alt="Firefox supports the crisp-edges value, but not pixelated." width="380" height="260" />
<figcaption><p>Chrome and Firefox notate that they support opposite properties</p></figcaption>
</figure>

That's right! Chrome supports `pixelated` but not `crisp-edges`, and Firefox supports `crisp-edges` but not `pixelated`. In order to support both browsers, both values had to be specified in the example above, utilizing the fact of CSS that if one value is invalid then the other will be used.

What I learned, though, is that resolving this paradox is **not** as simple as "just specify both properties", because the properties have different _semantics_. That is, even though `crisp-edges` and `pixelated` accomplish the same _result_, they _mean_ different things.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbr8agy0ynrsu5qlexcl.png)

To resolve this, we'll need to embark on a mystical journey through different versions of the specification, discussions on implementations, and—

Well actually, I already did all that! Instead, I'll walk through what I discovered by answering three key questions:

* Why is there a paradox?
* What is the difference between `pixelated` and `crisp-edges`?
* What CSS should I use for pixelated images? What about for images with crisp edges?

## What's with the paradox?

Without being involved in the dialogue directly, it's hard to pinpoint the precise reasons why `image-rendering` has the support it has. After delving through documentation, discussions, and definitions <small>(oh my!)</small>, there was one truth underpinning it all.

**The rules for how `image-rendering` should work are not set in stone.**

In fact, despite this property having first appeared in 2012, just a few months ago in <time datetime="2021-02-25">February 2021</time> the **[images specification](https://drafts.csswg.org/css-images-3/#the-image-rendering)** changed! And furthermore, it had seen periodic change over its nine-year history. In other words, `image-rendering` is undergoing active discussion, at least some of which has come from feedback from browsers implementing the feature for testing.

{% details <small>What is the **CSS Specification**?</small> %} <small>The specification is a set of documents detailing what the features of CSS are, how they should be used, and how they should be implemented by browsers. It's basically the source of truth for what CSS is and will be, and has been in constant development since its inception decades ago. Feel free to read more about the [CSS standardization process](https://www.w3.org/Style/2011/CSS-process).</small> {% enddetails %}

As a result, the property has only ever been implemented to different degrees, with Firefox and Chrome having taken different routes. And since the spec is still under discussion, no one has a complete implementation.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jktu93f4rcw6j3f9iyqf.png)

In the end, Firefox developed `crisp-edges` because it already supported the non-standard property `-moz-crisp-edges`, which was meant to be the same thing. Chrome had implemented `pixelated` because, at the time in 2014, the spec for `pixelated` was more straightforward.

## Pixelated or Crisp Edges?

To grasp the `pixelated` and `crisp-edges` values, it's important to understand the purpose of the `image-rendering` property.

### The Semantics of image-rendering

I'm gonna unoriginally paste a direct quote from the CSS spec, emphasis added:

> The image-rendering property provides a hint to the user-agent about **what aspects of an image are most important to preserve** when the image is scaled...

When an image is scaled, the computer either has to fill in missing details when scaled up or choose what to collapse when scaled down. That can be tricky, kinda like doubling a cooking recipe but realizing you don't have enough ingredients. And so, there's no single correct strategy for scaling images, leading to a diversity of **scaling algorithms** meant to do the job.

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chvd3vza423gy6odgumq.png" alt="Lea is doubled in size, but the gaps in the large image are not filled in." />
<figcaption><p>Scaling algorithms fill in the gaps.</p></figcaption>
</figure>

That said, notice that the spec does _not_ say that the purpose of `image-rendering` is to choose a scaling algorithm. Rather, the goal is to specify <q cite="https://www.w3.org/TR/2020/CRD-css-images-3-20201217/">what aspects of an image are most important to preserve</q>. For example, when we scale an image, do we care more about the way colors blend, or about keeping the edges sharp? Depending on the answer, one algorithm may be better than another.

Though a scaling algorithm will be ultimately chosen, the point of `image-rendering` is really to provide the browser additional information so it knows better how to treat the image!

### The Semantics of pixelated and crisp-edges

Knowing that `image-rendering` is all about identifying what aspects of the image are important to preserve, we can see how `pixelated` and `crisp-edges` are defined.

<dl>
<dt><strong>pixelated</strong></dt>
<dd><p>The image is scaled in a way that <strong>preserves the pixelated nature</strong> of the original as much as possible.</p></dd>
<dt><strong>crisp-edges</strong></dt>
<dd><p>The image is scaled in a way that <strong>preserves contrast and edges</strong>, and which does not smooth colors or introduce blur to the image in the process.</p></dd>
</dl>

For pixelated images, the emphasis is on the pixels, but for crispy images, the emphasis is on the edges. The key point here is that pixels are _not_ the same as edges!

**The `pixelated` and `crisp-edges` values are not semantically interchangeable.**

We can illustrate the difference by scaling up our pixelart Lea image by a non-integral factor, say 2.5 times the original size, using algorithms the spec currently mandates.

{% details <small>What algorithms does the spec mandate?</small> %} <small>For `crisp-edges`, the [nearest neighbor](https://tech-algorithm.com/articles/nearest-neighbor-image-scaling/) algorithm is used. For `pixelated`, nearest neighbor is used to take the image to the nearest integer scale. Afterward, a smooth-scaling algorithm takes the image the rest of the way.</small> {% enddetails %}

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sv02t1bll40cw2vy7elv.png" alt="A close-up of Lea's pixelart." />
<figcaption><p>The original for reference</p></figcaption>
</figure>

<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ywbyj4z4otw3zqi7751x.png" alt="The version upscaled with pixelated has minor blurring between pixel boundaries. The crisp-edges version has no blurring, but pixels are different sizes." />
<figcaption><p>Pixelated introduces blurring, but crisp-edges distorts the pixelation.</p></figcaption>
</figure>

* For `pixelated`, pixels must be square, and the only way to preserve that property is to allow the enlarged pixels to overlap. The blurring on cell boundaries represent places where pixels are overlapping.
* For `crisp-edges`, blurring is not allowed since the contrast between colors is most important. Resizing a pixelart image, therefore, results in cells that are not square, which distorts the pixelation aesthetic.

## Resolving the Paradox

Equipped with the history and semantics of `image-rendering`, we can resolve the paradox!

For pixelart, it is clear the `pixelated` value should be used; that's what most closely matches the semantics of the art. However, since Firefox does not yet support `pixelated`, we can fall back onto its currently provided solution, `crisp-edges`, which will resolve to the nearest neighbor algorithm.

```css
.pixelart {
    image-rendering: crisp-edges;
    image-rendering: pixelated;
}
```

The fact that `pixelated` is last is _very important_! If we imagine a future where Firefox has implemented `pixelated`, then we want that value to be applied instead of `crisp-edges`. Letting the most semantically appropriate value be last future-proofs the solution.

<small>For maximum compatibility, the undead Internet Explorer browser can be supported with <code>-ms-interpolation-mode: nearest-neighbor</code>.</small>

----------

And what about images which should have high contrast?

Chrome and Safari do not support `crisp-edges`, but instead support a webkit property called `-webkit-optimize-contrast` which bears similar semantics. Therefore, rather than use `pixelated`, it is better to use something that more closely resembles what `crisp-edges` means:

```css
.crispy-art {
    image-rendering: -webkit-optimize-contrast;
    image-rendering: crisp-edges;
}
```

--------------

And with that, we can celebrate with the final demo!

{% codepen https://codepen.io/auroratide/pen/wvdYgwv %}

## Resources

* Pixelart is of Lea from the game [Cross Code](http://cross-code.com) by [Radical Fish Games](http://www.radicalfishgames.com/)
* [Can I Use](https://caniuse.com/css-crisp-edges) - current browser support for `image-rendering`
* <cite><a href="https://drafts.csswg.org/css-images-3/#the-image-rendering">CSS Images Module Level 3</a></cite> - the current Editor's Draft of the specification for CSS images
* [Chromium Issue](https://bugs.chromium.org/p/chromium/issues/detail?id=317991) - Tracking Chrome's supprt
* [Mozilla Issue](https://bugzilla.mozilla.org/show_bug.cgi?id=856337) - Tracking Firefox's support
* [Pixelated Github Issue](https://github.com/w3c/csswg-drafts/issues/5837) - Tracking recent updates to the spec

And some extra stuff on my website, cut from this article for brevity:

* [Timeline of image-rendering](https://auroratide.com/posts/pixelart-and-the-image-rendering-paradox#timeline-of-image-rendering)
* [What changed in February 2021?](https://auroratide.com/posts/pixelart-and-the-image-rendering-paradox#what-changed-in-february-2021)