Images

Compressing an image without visible damage

You cannot pick a quality setting from a table — the number only means something inside one encoder. What you can do is learn what damage looks like and where to look for it.

Every guide to JPEG quality hands you a table: 80 for photographs, 90 for print, 60 for thumbnails. The numbers are not wrong, but they do not mean what the table implies. A quality setting is a position on one encoder's dial, and the same number in another encoder, on another picture, produces a different amount of damage. You cannot look it up. You have to look at the picture, at the size it will be shown.

The slider is a step size

A JPEG does not store pixels. The encoder cuts the picture into eight by eight blocks, converts each one into one luma channel and two chroma channels, and runs a discrete cosine transform over the block. The transform rewrites the 64 pixel values as 64 coefficients, each measuring how much of one wave is present. The first is the block's average brightness. The last is a two-pixel checkerboard, the finest detail a block that size can describe.

Up to there, nothing has been lost. The loss is one step: every coefficient is divided by a number from a table, rounded to a whole number, and written out. That division is the whole of JPEG compression, and the table is where the slider lives. The slider does not choose a table, it scales one. In libjpeg, the encoder behind most of the JPEGs you have ever made, the scale is 200 minus twice the quality at 50 and above:

scale = 200 - 2q
step = (base_table[i] * scale + 50) / 100

quality   scale    DC step    finest detail step
   95       10         2               10
   85       30         5               30
   75       50         8               50
   65       70        11               69
   50      100        16               99
   25      200        32              198

A step is the resolution of one frequency, so a coefficient stored in steps of 30 has been rounded to the nearest multiple of 30 and the decoder's version of that wave can be wrong by up to fifteen. That error is a wave at one frequency, put back where the original had none, which is why the damage has shapes you can name rather than a general fuzz. The DC column repays a second look: it is eight times the block's average brightness, so a step of 32 at quality 25 lets two neighbouring blocks sit four code values apart, and four levels between one flat patch and the next is banding you can see from across the room.

Five points on the slider are not worth the same everywhere. From 85 to 80 takes the finest step from 30 to 40, a third coarser; from 60 to 55 takes it from 79 to 89, about an eighth. The top of the range is where each move costs the most, which is why the procedure below starts high and walks down.

None of that transfers to WebP, where the quality number feeds a quantiser index and a rate-distortion search that decides for itself where to spend bits. Photoshop's 60 is not libjpeg's 60, and a browser re-encoding an upload uses whatever encoder it ships. Export the same picture at 75 from two different encoders and getting two different files is normal, not a bug in either of them.

What damage actually looks like

Nearly everything you will see is one of four things, and each comes from a different part of the table.

Artefact What it looks like Which setting causes it
Ringing Faint ripples fanning out from a hard edge: a roofline against sky, dark lettering on a sign. Clustered thickly it is called mosquito noise. Coarse quantisation of the high-frequency coefficients in the blocks straddling the edge. The finest coefficient is a two-pixel checkerboard, so its error is a two-pixel ripple.
Blocking A faint grid, easiest to see in smooth areas, where each block drifts to its own average brightness. The DC coefficient, which is eight times the block average. At quality 25 its step is 32.
Banding A sky breaks into flat plateaus with visible steps between them, like a poster. The DC and first AC coefficients carry the whole gradient; quantise them coarsely and a ramp becomes a staircase.
Chroma bleed A coloured halo around coloured edges; red text goes grey with an orange shadow beside it. Chroma subsampling, plus the chroma table, which is far coarser than the luma one.
Texture loss Hair, foliage and fabric turn into flat blobs while the edges of the frame still look sharp. The middle and high frequency coefficients, where fine texture lives. It goes before the edges do.
Generation mush Nothing looks broken, but the picture is soft and its edges have a doubled, ghosted look. Encoding a file that was already a JPEG.

The third column is the useful one: it says which lever to reach for. Ringing is a quality problem, bleed is a subsampling problem, and mush is not a setting at all.

Where to look is not the whole picture but the four hardest places in it. The highest-contrast edge in the frame. The smoothest gradient. A patch of fine texture: hair, grass, knitwear, leaves. And any saturated colour on a background of a different colour, especially a thin one.

The only size that matters

Zoom to 100 percent and almost any JPEG above quality 60 looks damaged. That is the wrong test. Every artefact has a size in pixels, and the question is whether that size is visible at the size the picture will be shown. Show a 1600-pixel image in an 800-pixel slot and the browser combines pairs of pixels. A ripple with a period of two pixels averages to a constant and disappears, and the finest coefficient in the transform is exactly that two-pixel checkerboard, so much of the ringing you can see at 100 percent never reaches a screen.

What survives a downscale is the low-frequency damage. Blocking, because an 8-pixel block is still a structure after the average. Banding, because a wide flat plateau averages to a wide flat plateau. And texture loss, because the texture was real detail and the average removes that too. The artefacts that shout at 100 percent are mostly the ones nobody will see, and the ones that will be seen are the ones a zoomed crop makes too small to notice.

It also settles the order of operations. If the display throws away the top of the frequency range anyway, encoding it is waste. Resize to the slot first, and count the slot in device pixels rather than CSS pixels, because a 400-pixel slot on a screen with two device pixels per CSS pixel wants an 800-pixel file.

What each band of the slider keeps

These bands are what I expect to see when I walk a photograph down the slider and check it at its final size. A hedge fails several bands earlier than a face against a plain wall.

Quality band What survives What is gone
95 to 100 Everything the source had, grain included, at any size you care to inspect Nothing you can see; the file can be as large as a PNG of the same picture
85 to 94 Fine texture and clean edges at display size, with grain largely intact The very finest hair and noise detail, visible only at 100 percent
75 to 84 Texture at display size; edges clean apart from slight ringing on the hardest ones Film grain and sensor noise; large skies begin to flatten
60 to 74 Shape, colour and broad texture; faces still read as faces Fine texture turns soupy; coloured edges smear; banding appears in wide gradients
45 to 59 Broad shapes and overall colour; usable for a thumbnail Blocking in smooth areas; mosquito noise on every edge; skin loses its texture
below 45 The silhouette and the composition Everything else. Block edges show at display size and every gradient is a staircase

The encoder matters as much as the band, because some tools at 75 are doing what others do at 65. And a picture built on a large smooth gradient bands a band or two earlier than this, while one made of coarse high-contrast shapes holds out longer.

A procedure you can trust

This takes about a minute a picture, and it assumes you are working from the original, because everything depends on comparing against something no encoder has touched.

  1. Resize first, to the slot in device pixels. A resize is a low-pass filter itself, so doing it after compression folds the compression error into the picture.
  2. Start at 85. High enough that the first thing you notice is the first thing that actually broke, rather than a pile of damage you cannot attribute to anything.
  3. Compare at display size, not zoomed. Original and candidate side by side, at the width they will be shown. A 100 percent crop tells you what the encoder did. Display size tells you whether it matters.
  4. Step down by five. 80, then 75, then 70. Watch the file size as well as the picture.
  5. Stop at the first setting where you can see a difference, then go back one. That setting is the edge of the cliff. One step back is solid ground, with margin for the second look, which is harsher than the first, and for whatever re-encodes the file downstream.
  6. Write the number down. Quality 70 for this camera, this slot, this sort of picture. The next one starts at 70 and the search is over after a single comparison.

One run of that, on a twelve-megapixel photograph of a person standing in front of a hedge, for a slot 800 pixels wide. It was resized to 800 by 600 first, then compressed at each setting and compared at that size. At 85 the file was 258 KB and I could not tell it from the original. At 75 it was 172 KB and I still could not. At 65 it was 118 KB and the hedge behind the subject's shoulder had gone soupy, which is texture loss arriving where the first table says it arrives, while the edges of the frame still looked clean. At 50 it was 74 KB and the skin had flattened. The answer was 70, one step above the first failure, which is the margin the procedure asks for.

Take the shape of those numbers rather than the numbers themselves: the first steps down saved tens of kilobytes each, the last ones saved a handful. That is the whole reason for starting at 85 rather than in the middle.

Chroma subsampling is a second dial

Nothing on the quality slider does as much damage to coloured text as the setting almost nobody is shown. JPEG stores one luma channel at full resolution and two chroma channels, and it may store those two at lower resolution. In 4:4:4 every channel gets one sample per pixel. In 4:2:2 the chroma channels get one sample per two pixels across. In 4:2:0 they get one sample for each two by two group of pixels, a quarter of the colour samples the picture started with. Cameras, phones, most save dialogs and canvas exports in browsers default to 4:2:0.

Black text on white keeps its edge under any of the three, because the edge is a change in brightness and luma is untouched. Make the word red and the trouble starts. The letter is still a brightness step, so its outline survives, but the colour now travels on a channel with a quarter of the samples, through its own transform with a table far coarser than the luma one. An eight by eight chroma block covers sixteen by sixteen pixels of the picture, so a single red hairline can have its colour spread across a sixteen-pixel region while its sharpness stays where it was. What you get is a crisp grey line with a soft orange halo.

So the fix is not a number, it is a format. Anything whose value sits in coloured text, thin coloured lines or flat colour should leave as PNG, which is lossless and describes a flat patch in a few bytes rather than as a sum of waves. If it has to be a JPEG, find the chroma setting and take 4:4:4.

Never compress a compressed file

The mistake that does the most damage in practice is not a quality setting. It is running this procedure twice on the same picture, months apart, each time from the last exported JPEG instead of the original. Errors from two encodes do not average out. They add.

The second encoder never sees the first encoder's coefficients. It sees pixels, rounded to whole numbers on the way out of the decoder, so what arrives does not sit on its grid and gets rounded a second time. Where the two encodes use different tables, which is the normal case, the errors are unrelated and add in power rather than cancelling: two passes at a step of 30 land near a combined step of 42, and 42 is what libjpeg gives you at 79. Quality 85 twice is roughly quality 79 once, and worse than that if anything in between cropped or resized the file, because moving the picture a few pixels shifts the block grid and drops the old block edges into the middle of the new blocks.

The generations are not all yours. A content system that resizes on upload, a chat app that re-compresses an attachment and a build step that re-encodes a folder are each one, and none of them stop to ask. Keep one lossless master per picture and export every size and format from it. If a master is too big to keep, keep the largest and best JPEG you have and treat that as the master, knowing it is already a generation old. What you must not do is derive a second export from a first.

Where the tools fit. The Image Compressor runs in your browser and puts the original and the result in one frame with a handle between them, which is the comparison this article keeps asking for: both versions at once, at the size the panel can show, rather than a zoomed crop of one and a memory of the other. Its quality slider covers the range the procedure walks, and its width control is the resize step.

The Image Converter handles the decision that is not a number — JPEG, WebP or PNG, with the quality that goes with the lossy ones. Reach for it when the picture holds coloured text or flat colour, where the answer is a different format rather than a lower setting.

The rule of thumb

Pick the size first, then the number, and not the other way round. Export at 85, step down by five, compare at the size the picture will be shown rather than at 100 percent, and stop one step above the first setting where you saw something. Write the number next to the slot it was for.

And when a picture's value sits in coloured text or flat colour, stop thinking about the slider. No number rescues it. Change the format, and the problem is gone.