Your theme renders a post image in a slot that is 720 pixels wide. So you export the photograph at 720 pixels wide, upload it, and on your own monitor it looks correct. On a phone, on a MacBook, on any screen that packs two device pixels into one CSS pixel, it looks faintly soft, and there is nothing obvious to point at. The file is not compressed badly. It is simply half the size the screen asked for.
The second version of the problem is shape. The design wants 16:9. The photograph is 4:3. Something has to be removed, and if you do not decide what, the crop happens from the centre by default and takes the top of somebody’s head with it.
Both problems are arithmetic, and neither is hard arithmetic. They are just fiddly enough that most people guess, and the guess costs either a soft image or a four megabyte upload that the browser scales down to a fifth of its width anyway.
The number you are actually looking for
There is one multiplication at the centre of this. Take the CSS width of the slot the image sits in, multiply by the device pixel ratio you intend to serve, and you have the intrinsic width the file needs. Everything else, the height, the ratio, the crop, the megapixels, falls out of that number and the shape you have chosen.
Measuring the CSS width takes ten seconds. Open the page, right click the image, inspect it, and the browser shows the rendered box next to the element, typically as something like 720 × 405. Widen and narrow the window and watch that number move; the one you want is the largest it reaches, because that is the worst case the file has to satisfy. In Chrome the element tooltip also reports the intrinsic size separately, which is the first place most people notice that the two numbers are not the same thing.
The aspect ratio calculator below does the rest. It runs entirely in the browser, so there is no upload and nothing is sent anywhere: the arithmetic is plain integers in the page.
Aspect ratio calculator
Resize to a ratio, see the reduced ratio and the megapixels, work out a centre crop, and check which srcset widths WordPress would really generate. Pure arithmetic in this tab: nothing is uploaded and no numbers leave the page.
| Size | Width | Height | What happens |
|---|
CSS pixels, intrinsic pixels, and the gap between them
An <img> on a page has two widths that almost never agree. The intrinsic width is what the file contains, the number stored in the JPEG or WebP header. The rendered width is how many CSS pixels of layout the browser gives it. If those numbers differ, the browser resamples, and resampling upward is where softness comes from.
Device pixel ratio is the multiplier between CSS pixels and the physical pixels in the panel. A standard desktop monitor runs at 1. A Retina MacBook runs at 2. Most current phones report 2 or 3. At a ratio of 2, a 720 CSS pixel slot is 1440 physical pixels wide, so a 720 pixel file gets one image pixel stretched across two hardware pixels in each direction. The result is not blurry in the way a badly saved JPEG is blurry. It is soft in a way people register as cheap without being able to name it.
Two is the practical target. Going to 3 doubles the byte count again for a difference that almost nobody can see on a phone held at arm’s length, and it pushes you past the sizes WordPress generates by default. Design at the CSS width, upload at twice it, and let the browser pick something smaller when the visitor is on a plain monitor. If your images already look wrong and you are not sure whether this is the cause, the diagnostic order in why your WordPress images look blurry separates the resolution problem from the compression problem and the CSS problem.
Cropping is subtraction
An aspect ratio is width divided by height, written as two whole numbers with the common factors taken out. 1920 by 1080 reduces by a greatest common divisor of 120 to 16:9. That reduction is honest arithmetic, not pattern matching, which is why 1601 by 900 reduces to 1601:900 and stays there. It is not 16:9. It is 0.06 percent away from 16:9, which does not matter for a photograph and matters enormously if the file is going into a slot with a fixed aspect-ratio in CSS and you are wondering why there is a one pixel line of background showing.
The important property of ratios is that changing one is always a subtraction. There is no way to move a 4:3 photograph to 16:9 without discarding pixels, because the largest 16:9 rectangle that fits inside a 4:3 frame is narrower in one dimension than the frame itself. A 4000 by 3000 photograph becomes 4000 by 2250, and 750 rows of pixels stop existing. Split evenly, that is 375 from the top and 375 from the bottom, and three of the original twelve megapixels are gone.
Turn the same photograph on its side and the cost changes character. A 3000 by 4000 portrait cropped to 16:9 keeps 3000 by 1687, which throws away 2313 rows, well over half the image. That is not a rounding error, it is a different photograph. When the numbers come out like that, the answer is usually to pick a different ratio for that slot rather than to crop, or to shoot the frame knowing where the 16:9 band will sit.
The only real decision is which edge pays. A centre crop splits the loss evenly and gives the odd leftover pixel to the bottom or the right, which is what every image editor does when you do not tell it otherwise. That is fine for landscapes and wrong for portraits of people, product shots with the label near an edge, and anything where the subject is deliberately off centre. The calculator states the crop as pixels off the top, bottom, left and right precisely so you can see whether the default is acceptable before you commit to it, and it never pads: it only ever fits the largest rectangle of the target ratio inside what you already have.
What WordPress generates, and what it quietly refuses to
A default install registers six sizes. Four come from the options table: thumbnail at 150 by 150 with thumbnail_crop set to 1, medium at 300 by 300, medium_large at 768 by 0, and large at 1024 by 1024. Two more are added in code by _wp_add_additional_image_sizes() in wp-includes/media.php: 1536x1536 and 2048x2048, which exist to serve the 768 pixel and 1024 pixel layouts at 2x.
With the single exception of the thumbnail, those numbers are boxes, not dimensions. The file is fitted inside the box and keeps its ratio, so the binding constraint on a portrait photograph is its height, not its width. A 1200 by 1600 upload produces a large of 768 by 1024, not 1024 of anything. People who assume large means “1024 pixels wide” and size their layout around it get a surprise the first time a portrait photograph lands in the slot. The full breakdown of which file lands where is in WordPress image sizes explained.
The second rule is that WordPress does not upscale. image_resize_dimensions() refuses to produce a size when the destination is not smaller than the source, but the exact test is more subtle than most write-ups admit. For a box with both dimensions set, the size is skipped only when the original is smaller than the box in both directions. A 900 by 1400 portrait therefore still gets a large, at 658 by 1024, even though it is under 1024 pixels wide. For medium_large, whose height is zero, only the width is compared, so a 700 pixel wide upload gets no medium_large at all.
// wp-includes/media.php, image_resize_dimensions(), condensed
if ( empty( $dest_h ) ) {
if ( $orig_w < $dest_w ) { return false; }
} elseif ( empty( $dest_w ) ) {
if ( $orig_h < $dest_h ) { return false; }
} else {
if ( $orig_w < $dest_w && $orig_h < $dest_h ) { return false; }
}
The third rule belongs to the thumbnail alone. It is hard cropped, which means it does not fit the file into a 150 by 150 box, it cuts a 150 by 150 square out of it. From that 3000 by 4000 portrait it takes the centre 3000 by 3000 square and discards 500 rows off the top and 500 off the bottom. A standing figure loses their head and their feet, and nobody chose that. It is also the size most likely to appear in an archive grid, which is where theme authors reach for it, and is a large part of why featured image size is decided by the theme rather than by core.
srcset can only offer candidates that exist
All of this converges on the responsive markup. wp_calculate_image_srcset() walks the sizes recorded in the attachment metadata and builds a candidate list from them. It cannot invent a width. If no generated size is 2400 pixels wide, no browser will ever be offered 2400 pixels, and a 1200 pixel slot on a Retina screen gets served whatever the widest available candidate happens to be.
Two filters thin the list further. Any size whose aspect ratio does not match the source within one pixel is dropped, which is exactly why the hard cropped 150 by 150 thumbnail never joins the srcset of a rectangular photograph, and does join for a square one. And any candidate wider than max_srcset_image_width, default 2048, is removed unless it is the src itself. So a 4000 pixel original contributes its full size to the candidate list only when it is the image the markup already points at. The mechanics, including the sizes attribute that decides which candidate wins, are covered in how core builds srcset and where it goes wrong.
The practical consequence is a ceiling. Between the 2048 pixel cap and the fact that 2048x2048 is the largest size a default install generates, a stock WordPress site comfortably serves layouts up to about 1024 CSS pixels at 2x and starts running out above that. A full width hero in a 1400 pixel container needs a custom size registered, or it will be soft on every Retina laptop that visits.
A procedure instead of a guess
- Inspect the slot in a browser and write down the widest rendered width it reaches.
- Double it. That is your target intrinsic width.
- Choose the ratio the design wants, then read the crop the calculator gives you and decide whether the centre is the right place to take it from.
- Check the WordPress table underneath. If the size you need is flagged as never generated, the original is too small and no amount of uploading will fix it.
- Export once, at that width, in that ratio, and upload that file.
The last step matters more than it sounds. Uploading the 6000 pixel camera original and letting WordPress sort it out gives you six derivative files, a full size nobody serves, and a media library that grows faster than the site. Uploading at exactly the display width gives you a soft image on half the devices. The target sits between those two, and it is a specific number, not a range.
The File you upload toggle exists for this. Switch it to the target size and the table models the file you are about to export; switch it to the original and it models what would happen if you uploaded the camera file untouched. The difference between the two tables is usually the argument for cropping and resizing before the upload rather than after.
Where the calculator stops
It does not read image files. There is no upload, no canvas and no drag and drop, which is the point of it running fully in the browser, and it also means it cannot tell you the dimensions of a photograph sitting on your disk. Get those from the file properties in your operating system, or from the attachment details panel in the media library, and type them in.
It models a default install only. A theme that calls add_image_size() adds rows the table does not know about, the big image threshold that scales very large uploads down on arrival is not represented, and plugins that switch sizes off will make the real output shorter than the modelled one. Treat the WordPress section as what a typical install would produce, then check your own Settings → Media screen if the numbers matter to a pixel.
The ratio field is deliberately forgiving and deliberately quiet when it fails. It takes 16:9, 3/2, 3x2, free text such as 1.91:1, and a bare decimal such as 1.5, which it reads as 1.5:1. Anything it cannot parse turns the field amber with a hint and leaves your target numbers exactly where they were, because guessing at a malformed ratio is worse than doing nothing. The preset chips write into that same field, so typing a value that matches one selects it and anything else selects Custom, and the Original chip fills in the original’s own reduced ratio and follows it if you change the source dimensions.
One more limit worth stating: the calculator gives widths, not markup. It will not write your sizes attribute, and knowing the number does not retrofit it onto the two thousand images already in your library. That is a batch job, and bulk resizing without breaking the library covers what has to happen to the metadata when you do it.
None of this is difficult once the two numbers are separated in your head. The CSS width belongs to the design and is fixed by the layout. The intrinsic width belongs to the file and has to be larger, by a factor of two, because screens stopped being one device pixel per CSS pixel more than a decade ago. Everything people call an image quality problem on a WordPress site is usually one of those two numbers being read as the other.
The ratio question is separate and simpler. Pixels come off, the only decision is from which edge, and the moment you look at the actual count of discarded rows the question of whether this photograph belongs in this slot tends to answer itself. Half an image thrown away for a 16:9 band is a sign the crop is wrong, not a sign the crop needs to be more centred.
What WordPress adds is a set of hard edges around the answer. Sizes are only generated when the original beats the box, boxes constrain the long side of a portrait rather than the width, the hard cropped thumbnail cuts without asking, and srcset can only hand the browser a candidate that was written to disk. Work out the target once, before the export, and the rest of the pipeline stops being a source of surprises.