WordPress Images

Image Quality Curve: Why Every Recommended Setting Is Wrong

Quality 80 means one thing in JPEG, another in WebP, and something different again on a screenshot than on a photograph. Here is what the number actually sets, what SSIM can and cannot tell you, and how to find the point where your own file stops getting meaningfully smaller and starts getting visibly worse.

Image Quality Curve: Why Every Recommended Setting Is Wrong

Someone in a forum thread says use 80. You set 80. The photograph on the homepage loses two thirds of its weight and nobody notices a thing. The screenshot in your documentation comes back with grey fuzz around every letter, a faint halo down the edge of the sidebar, and a red heading that has gone slightly muddy. Same encoder, same number, two completely different outcomes.

The number is not a percentage. Nothing in the file is eighty percent of anything. In JPEG it is an index into a formula that scales a quantisation table. In WebP it drives a quantiser and a set of encoder decisions that have no relationship to the JPEG one. WordPress core is quietly explicit about this: WP_Image_Editor::get_default_quality() returns 82 for image/jpeg and 86 for image/webp. Two different numbers, because the same number would not have produced comparable output.

So the useful question is not which quality setting is best. It is this: for this image, at the size it will actually be displayed, where does the file stop getting meaningfully smaller and start getting visibly worse? That has an answer, and finding it takes about fifteen seconds.

What lossy compression discards first

JPEG splits the image into one luma channel and two chroma channels, usually throws away three quarters of the chroma resolution outright, then cuts the luma into 8 by 8 blocks and runs a discrete cosine transform on each block. Sixty four pixels become sixty four coefficients, ordered from flat to fine. Each coefficient is divided by an entry in a quantisation table and rounded to a whole number. The fine detail entries are the largest divisors, so fine detail is what rounds away to zero first.

On a photograph, fine detail is leaf texture, skin pores, sand grain and sensor noise. Deleting a good part of it changes nothing you could put into words. On a screenshot, fine detail is the content: a one pixel border, the stems of the letters, the hard step between two flat fills. A cosine transform has no compact way to describe a step edge, so what comes back is ringing beside it, and the discarded chroma turns coloured text into a smear. That is the whole reason one recommended number cannot serve both files.

The way out is to stop arguing about the number and measure it. Drop a file into the image quality comparison tool below and it encodes your image across a sweep of quality values in JPEG and WebP, decodes every result again, and scores each one against the original. All of it runs in your browser: nothing is uploaded, and the page makes no network request with your image in it.

Image quality curve

Drop one image and get a measured answer to the question the quality slider never answers: which number is right for this image. It is encoded across a sweep of quality values in JPEG and WebP, every result is decoded again, and each decoded result is compared with the original by SSIM over the luma channel, computed here in the tab. SSIM is a structural similarity measure, not a perceptual model like Butteraugli, and it does not know what your eye forgives. Nothing is uploaded: every byte is written and read inside this browser tab.

Drop one image here or press Enter to pick a file. JPEG, PNG, WebP or GIF, one at a time.

No image yet. The two sample images are drawn in this tab, not downloaded, and they behave very differently under a quality sweep.

The sweep

Eight quality values from 30 to 100 in steps of 10, in two formats.

No image yet
Bytes and similarity against quality

Nothing measured yet. Drop an image above, or press Try a sample photo to watch the curve appear.

Solid lines are bytes on the left axis, dashed lines are SSIM on the right. The ring marks the knee, the last point where more bytes still buy a real gain in similarity. SSIM compares local means, variances and covariance of the luma channel through a sliding window. It is a structural measure, not Butteraugli and not an eye: a file can sit at 0.99 and still show a visible ring around a hard edge, which is what the difference view underneath is for.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

The quality scale is a knob, not a measurement

Inside libjpeg, the number you type is converted to a scaling factor by two lines of arithmetic. At 50 and above, the factor is 200 - 2q. Below 50, it is 5000 / q. Every entry in the standard quantisation table is then multiplied by that factor, divided by a hundred, rounded, and clamped to a minimum of 1. Quality 50 leaves the table exactly as the standard defines it. Quality 75 halves it. Quality 90 divides it by five, and quality 100 flattens every divisor to 1.

Two things follow that surprise people. The scale is violently non-linear: moving from 50 to 55 changes the factor from 100 to 90, while moving from 90 to 95 halves it from 20 to 10. And quality 100 is still lossy, because chroma subsampling and the rounding in the transform itself do not go away when the divisors reach 1. There is no setting on the JPEG slider that gives you your pixels back.

WebP’s number is a different knob on a different machine. It sets a quantiser, and it also moves the filter strength, the number of analysis passes and the segmentation the encoder is willing to do. Chromium’s canvas writer goes further and switches to lossless mode at quality 100, which is why a WebP written at 100 can be a smaller file than the same image at 90 when the artwork is flat. The sweep in the tool runs its recommendation over the Pareto frontier rather than the raw list for exactly this reason, and when that inversion happens it says so: the higher number is also the smaller file.

Table mapping JPEG quality values to the libjpeg scaling factor applied to the standard quantisation table, from 100 at zero percent through the WordPress default of 82 at 36 percent down to 25 at 200 percent, with two panels comparing what quality 80 means in JPEG and in WebP.

One consequence worth stating plainly. The encoder in your browser is not the encoder on your server. Chromium’s JPEG writer is not mozjpeg, and its WebP writer is not cwebp at method 6. The absolute byte counts you measure here will not match what GD or Imagick writes during an upload. The shape of the curve and the position of the knee do transfer, because both are properties of the image, not of the writer.

Why a photograph and a screenshot disagree

Run the same sweep on a landscape photograph and on a screenshot of an admin screen and the two curves do not even have the same shape. The photograph gives up bytes cheaply for a long stretch, because most of what it loses is noise it never needed. Its failure mode at the bottom is soft and gradual: texture goes waxy, skies start to band, then blocks appear.

The screenshot fails differently and much earlier. Loss concentrates exactly where you are looking, along type and hard edges, and it is visible as a defect rather than as softness. Switch the tool’s right panel to the difference view and this is obvious in a second: on the photograph the error is spread through the grass and the foliage, on the screenshot it traces every glyph and every border like a highlighter. Flat artwork with text usually has no useful lossy setting at all, and the right move is to keep it lossless. Which format that means is a separate question, covered in the guide to picking a format for WordPress, and this article does not repeat it.

The awkward cases are the mixed ones: a product photographed on a pure white background, a chart with a photograph inside it, a screenshot with a gradient behind it. Those have a photograph’s tolerance in one region and a screenshot’s intolerance in another, and the single averaged number hides the disagreement. This is the case where actually looking at the difference view matters more than reading the score.

What SSIM measures

SSIM does not compare pixels to pixels. It slides a small window across both images and, at every position, compares three statistics of the two patches: their means, which is local brightness, their standard deviations, which is local contrast, and their covariance, which is the extent to which they vary together. That third term is the structural one and it carries most of the weight. Two stabilising constants derived from the 0 to 255 range, 6.5025 and 58.5225, keep the ratios from exploding where a patch is nearly flat. Each window produces a number that reaches 1 only when the patches are identical, and the score for the whole image is the average over all windows.

The reason this beats a plain error measure is that it cares how the error is arranged. Scatter a given amount of error randomly through a photograph and SSIM barely moves, because the local statistics survive. Concentrate the same amount of error into a ring along one edge and SSIM drops hard, because the covariance in those windows collapses. That is a good match for what a reader notices. The tool offers the original paper’s window, 11 by 11 with a Gaussian weighting at sigma 1.5, and a faster 8 by 8 uniform one that is slightly harsher on edges. Both are averaged over the valid interior only, so no invented border pixels contaminate the result.

Now the limits, because a number with unstated limits is worse than no number. This implementation works on the Rec. 601 luma channel, weighting red, green and blue at 0.299, 0.587 and 0.114. It is therefore close to blind to a pure colour shift: desaturate every red in the picture and luma hardly moves, so SSIM will report that almost nothing happened. It is not a perceptual model in the sense that Butteraugli or SSIMULACRA are. And the scores are not comparable between images. A 0.98 on a noisy photograph and a 0.98 on a screenshot are not the same experience, so compare points within one curve and never across two files.

Reading the curve, and finding the knee

Plot bytes on one axis and SSIM on the other against quality and the picture is always the same in outline. Coming down from 100, the file size falls off a cliff while the similarity line stays nearly flat. Then, at some point that depends entirely on the image, the two trade places: similarity starts dropping steeply and the file size barely moves. The bend where that happens is the knee, and it is the answer you came for. Above it you are paying bytes for detail nobody perceives. Below it you are destroying structure and saving very little.

The table under the chart makes this concrete with a column most quality comparisons leave out: what the last step cost, in bytes saved per unit of SSIM given up. Read down that column and the knee announces itself, because the figure falls off a cliff the moment you pass it. For ordinary photographs at web sizes the bend usually sits somewhere in the seventies or low eighties for JPEG and a little lower for WebP, which holds structure better at low bit rates. For flat graphics it often does not exist: similarity is already sliding at 90, which is the curve’s way of telling you the file wants a lossless format.

If you have a house rule, set it and let it override the geometry. The floor control (“never go below 0.95, 0.97, 0.98 or 0.99”) always beats the knee when the two disagree, and if nothing in the sweep reaches the floor you asked for, the tool tells you that instead of quietly returning the best of a bad set. That failure is information: it means the image cannot survive lossy encoding at the size you are measuring.

Flow diagram of the measurement loop: decode and scale the source, encode at each quality, decode the result again, compute SSIM on the luma channel over the valid interior, drop points dominated on both bytes and similarity, then report the knee, with the window size, sigma, luma weights and sweep cap listed underneath.

One file, two answers

The same photograph does not have one correct quality setting, because it is not displayed at one size. This is what the “measure at” control is for. Encode at a long edge of 1440 and every artefact lands at full size in front of the reader. Encode the same source at 512 and half of the damage disappears into the downsample before anyone sees it, so the knee moves down and a lower setting becomes defensible. A hero image and a thumbnail cut from the same original genuinely want different numbers.

WordPress makes that concrete, because a single upload becomes a whole set of files at different dimensions, as the breakdown of WordPress image sizes lays out. Every subsize is its own encode, and by default they all get the same quality number. Since WordPress 6.8 the wp_editor_set_quality filter receives the dimensions of the size being written, so you can finally split the difference instead of picking one compromise for everything.

add_filter( 'wp_editor_set_quality', function ( $quality, $mime_type, $size ) {
    if ( 'image/webp' !== $mime_type ) {
        return $quality;   // leave JPEG at core's 82
    }

    $width = isset( $size['width'] ) ? (int) $size['width'] : 0;

    // Large renditions carry the visual weight, small ones do not.
    return $width >= 1200 ? 84 : 76;
}, 10, 3 );

Numbers like 84 and 76 in that snippet are placeholders until you have measured your own library, which is the point of the whole exercise. Note also that the two ends of the range matter for different reasons: the large rendition is usually the Largest Contentful Paint element and its byte count is a direct page speed problem, while forty thumbnails on an archive page are mostly a request count problem where a few kilobytes each is not where your time should go.

Where the measurement stops being valid

Several things are deliberately outside the scope, and knowing them keeps you from over trusting a number. There is no batch mode and no server side encoder: one image at a time, and no mozjpeg, no cwebp tuning, no encoder effort setting. If your browser cannot write one of the two formats, that is detected from the data URL the canvas hands back and the format is dropped with a note rather than counted as a PNG in disguise. Two copies of the tool on one page are not supported.

The canvas also strips things on the way in. Colour profiles and EXIF do not survive it, so an image tagged Display P3 is measured after conversion and the tool cannot warn you about the colour shift that causes, which is a separate failure with a separate fix for washed out images. An animated GIF is measured on its first frame only. An image smaller than the SSIM window has no valid interior to average, so the tool falls back to every pixel and reports a margin of zero rather than pretending the usual guarantee still holds.

One detail in the arithmetic is worth understanding, because it changes what the saving column means. When the image was not scaled for measurement, saving is calculated against your original file, which is the comparison you want. When it was scaled, comparing a 1024 pixel encode against a 4000 pixel original would flatter every result, so the reference becomes a measured lossless PNG of the working image instead. The percentages then describe what the encoder achieved, not what the resize achieved.

From one measurement to a setting you can keep

Measure three or four images that genuinely represent your library rather than the one you happen to have open: a busy photograph, a clean one with large flat areas like a sky or a studio background, a screenshot, and whatever your most common product or hero shot looks like. You will get four different knees, and the spread between them tells you more than any single result. Set your default from the photographs, because they are the bulk of the bytes, and route the flat artwork away from lossy encoding entirely instead of trying to find a number that covers it.

Then apply it once, in the place where uploads pass through, rather than per file by hand. A filter like the one above sets it for everything WordPress writes from now on, and existing files need a pass with a batch converter such as the image processor to bring them in line. What you should not do is re-encode an already lossy file at a higher quality number, which adds bytes and repairs nothing: the detail was discarded the first time, and generation loss only accumulates.

The reason every recommended quality setting is wrong is not that the people recommending them are careless. It is that the number describes the encoder, not the picture, and the picture is the half that decides. Once you have watched the curve bend for your own files, the recommendation stops being a number someone gave you and becomes a decision you can defend, which is the difference between a setting you keep and one you change again next month.

Image Quality Curve: Why Every Recommended Setting Is Wrong

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

SEO & Structured Data

Permalink Structure Simulator: What That One Field Decides

The permalink field decides thirteen URL shapes at once, and most of them fail quietly when you change it. What each structure tag costs, what verbose page rules actually do, which collisions core resolves first, and which old URLs its canonical redirect will never rescue.

Photo Editing

Take the scratches and dust out of a scanned family photograph

A scratch is thin and disagrees with its surroundings in almost every direction at once. A real edge disagrees in one or two. That single difference finds the damage with plain arithmetic, and an inpainting model fills what you agree to. All of it in your browser.

WordPress Development

Why Should a Design Become a File at All?

Every static graphic on a page is a snapshot of what was true when somebody exported it. There is a whole category of quiet wrongness that follows from that.

WordPress Images

WordPress Media Library Folders: Why There Are None, and What Actually Works

WordPress stores every upload as a post, not as a file in a folder, which is why the media library has no directories to organise. Here is what the uploads folder really is, why moving files by hand breaks images everywhere, and how taxonomy-based folders solve it without touching the disk.

Photo Editing

Spend your JPEG bits where the eye looks, not evenly across the frame

An encoder gives the hedge behind your subject as many bits as the subject. A saliency model knows better, but a browser will not let you set quality per region, so the encoder is not steered: its input is prepared instead. On the sample that was 41 per cent off the file at an unchanged quality setting.

Developer Tools

Hashes and Encoding: Getting Back Into Your Own WordPress

The reset mail never arrives and you are locked out of a site you own. The way back in runs through one column: user_pass in wp_users. Here is what is actually stored there, checked line by line against wp-includes/pluggable.php, plus a browser-only tool that builds both the bare MD5 and the modern $wp bcrypt value.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.