Troubleshooting

WordPress Images Washed Out: The Colour Profile Fix

The photograph looked right in your editor and flat on the site. Here is where WordPress and its plugins drop the colour profile, and the export habit that ends it.

WordPress Images Washed Out: The Colour Profile Fix

You export a photograph, it looks right in Lightroom, it looks right in Preview, and then you put it on the site and the reds have gone brick, the sky has gone grey, and the whole picture looks like someone pulled the saturation down by fifteen percent.

Nothing was compressed badly. The file is not blurry. Open the full size file directly by its URL and it often looks fine, which makes the whole thing feel like a rendering bug in the theme.

It is not a bug. The file arrived carrying a colour profile, something in the upload pipeline threw that profile away, and the browser then did the only sensible thing left to it: it assumed sRGB. The pixel values never changed. Their meaning did.

Reports of WordPress images washed out after upload almost always come down to that one mechanism. What follows is exactly where in the pipeline the profile gets lost, how to prove it in about two minutes with read only commands, and the one export habit that ends the problem permanently.

A number is not a colour

When a file says a pixel is (220, 40, 60), that is not a colour. It is a recipe: give me 220 parts of your red, 40 of your green, 60 of your blue. Every display has different reds. A cheap laptop panel and a wide gamut monitor mean genuinely different things by “full red”, so the same three numbers land as different colours on different screens.

A colour profile is the small block of data that says which red, green and blue those numbers refer to. Numbers plus profile equal a defined colour. Numbers alone are a recipe with the units missing. When the profile is absent, every browser and operating system falls back to the same guess, sRGB, because that guess is right often enough and there is no better option available.

The case behind almost every report

Two sources produce nearly all of these tickets, and both of them are modern, correct, well behaved software doing exactly what it was told.

The first is a phone. Recent iPhones and most flagship Android devices capture in Display P3, a space noticeably wider than sRGB in the reds and greens. The second is a photographer’s export. Lightroom and Capture One will happily export in Adobe RGB, which is wider than sRGB in a different direction and is still the default in plenty of print oriented workflows.

Both files are correct on disk. Both carry an embedded profile that explains their numbers. Strip that profile and the numbers get read as sRGB instead, and because sRGB is the narrower space, every value now describes a less saturated colour than it did a moment ago. Vivid reds turn dull. Deep greens turn olive. Skin tones go slightly grey. Nothing looks broken enough to be obviously broken, which is why the complaint always arrives as “it just looks a bit off”.

The direction of the error is the giveaway. A lost profile almost never makes an image look more saturated. It makes it look flatter, because you are reading wide gamut numbers with a narrow gamut ruler.

Table comparing sRGB, Display P3 and Adobe RGB, showing the typical source of each file and what happens to the picture when the embedded colour profile is lost.

What WordPress does when it builds the sub sizes

One upload does not become one file. WordPress re-encodes your image into a set of registered sizes, and the size your theme actually renders is almost never the file you uploaded. If that split is new to you, the way one upload becomes many files is worth reading first, because this whole problem hides inside that mechanism.

Every one of those sub sizes is written by an image editor class. wp_get_image_editor() hands the choice to _wp_image_editor_choose(), which runs the wp_image_editors filter over a default of array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ). Imagick first, GD as the fallback. The two backends do not behave the same way here, and that difference is the whole story.

The Imagick path protects the profile

In wp-includes/class-wp-image-editor-imagick.php, resizing runs through thumbnail_image(), which calls strip_meta() unless the image_strip_meta filter says otherwise. The name is alarming, but read what strip_meta() actually does: it walks every profile in the image and removes the ones that are not on a protected list. That list is icc, icm, iptc, exif and xmp, and core’s own comment explains why the first two are on it: “Color profile information”.

So on an Imagick host, the ICC profile survives the resize by design. Core strips the bulky junk and keeps the block that gives your pixels their meaning.

The GD path cannot keep it

GD is a different story, and core is blunt about it. The docblock on the image_strip_meta filter reads: “This filter only applies when resizing using the Imagick editor since GD always strips profiles by default.”

That is not a policy decision, it is a limitation. In wp-includes/class-wp-image-editor-gd.php the resize is wp_imagecreatetruecolor() followed by imagecopyresampled(): a brand new blank canvas, pixels copied into it, then written out. There is no ICC profile anywhere in that sequence because GD has no concept of one. The string “profile” does not appear in that file at all. The output is genuinely untagged, and the browser genuinely has nothing to work with.

Which backend does your site use? It depends entirely on whether the PHP Imagick extension is available to the web server, and plenty of shared hosts do not install it. Check rather than assume, because the answer decides whether your fix is a setting or a habit. On the server this blog runs on, for the record, there is no Imagick extension and the active editor is WP_Image_Editor_GD.

# Which editor class does WordPress choose for an image?
wp eval 'echo get_class( wp_get_image_editor( ABSPATH . "wp-admin/images/wordpress-logo.png" ) ), PHP_EOL;'

Treat that as an indication rather than a verdict. WP-CLI runs on the command line build of PHP, which is often a different build with different extensions from the one serving your site. The authoritative answer is in the admin under Tools, Site Health, Info, Media Handling: the field labelled Active editor prints the result of _wp_image_editor_choose() in the same PHP process that handles your uploads.

One thing neither backend does is convert between colour spaces. There is no call to Imagick’s profileImage() or transformImageColorspace() anywhere in the resize path. WordPress either carries your profile along or drops it. It will not rewrite your pixels into sRGB for you, and no amount of configuration will make it start.

It does change file formats, though, and that catches phone photographs specifically. Since WordPress 6.7 the default value of the image_editor_output_format filter maps image/heic and image/heif to image/jpeg, so a HEIC straight off an iPhone is re-encoded by your image editor before anything else touches it. On a GD host that single step is enough to lose the profile on the main file, not just the sub sizes. Which format your uploads end up in is worth knowing for its own sake, but it is also the first place a profile can vanish.

The original survives, the size on the page does not

This is why the problem feels so inconsistent. In wp-admin/includes/image.php, core only re-saves the uploaded file itself when one of two conditions is true: the image is larger than the big_image_size_threshold value (2560 pixels by default, applied to width or height), or its format is being converted. If neither applies, the file you uploaded stays exactly as it was, byte for byte, and it keeps whatever profile it came with.

When the threshold is crossed, core writes a new “full” size with a -scaled suffix through the same image editor that writes every other size, and records the untouched upload under the original_image key in the attachment metadata, reachable with wp_get_original_image_path(). That file survives on disk with its profile intact, and nothing on the front end ever links to it. The -scaled file your theme calls “full” went through the editor like everything else.

The result is a library where one photograph exists as several files that disagree about what their numbers mean. Here is a real example from this site, which runs the GD backend. The 1280 pixel wide original is under the threshold, so core never touched it, and a head -c 65536 against it finds an ICC_PROFILE marker. Both of its sub sizes, written by GD, have no such marker anywhere. Same photograph, three files, two different answers to the question of what the pixels mean.

That inconsistency is the most useful diagnostic in this article. Right click the washed out image, copy its URL, and open it directly. If it still looks flat you are looking at the sub size. Now edit the URL to remove the -1024x576 style suffix so you are requesting the full file, and reload. If the colour snaps back, the profile was lost during sub size generation and nowhere else.

Diagram following one upload through the WordPress pipeline on a GD host, with a table showing the full size original carrying an ICC segment while both generated sub sizes carry none.

Optimisation plugins strip metadata on purpose

Even on an Imagick host where core protects the profile, something downstream can still remove it, and usually something does. Almost every image optimisation plugin has a setting somewhere in the region of “remove EXIF data”, “strip metadata” or “remove image metadata”, and it is generally on by default.

That default is not stupid. Metadata is real weight: camera make and model, lens, capture settings, editing history, embedded thumbnails, and often an ICC profile of several kilobytes. On a page with twenty images, stripping all of it is a measurable saving, and for most uploads, screenshots, icons and product shots exported straight from a design tool, nothing of value is lost.

The trouble is that a colour profile is metadata. Most of these plugins do not distinguish between “the GPS coordinates of my house” and “the block of data that tells the browser what these pixels mean”. You are handed one checkbox that covers both. This site runs Imagify, and something in that chain rewrites files after upload: one original here is recorded in its attachment metadata as 404,572 bytes and measures 202,258 bytes on disk. In that particular case the ICC marker survived the pass, which is the point. The behaviour is a setting, not a defect, and it is common to the whole category, so the honest advice is not “switch plugins”, it is “go and read what your optimiser’s metadata option actually removes”.

Conversion to WebP or AVIF is a second place profiles evaporate, whether it is done by a plugin, by your CDN or by core. That is a fresh encode by a different encoder, and whether the profile is carried across depends on the encoder and the flags it was handed.

On an Imagick host you can tell core to keep everything, though it is a blunt instrument. Dropping this in a small site specific plugin, or in your child theme’s functions.php, preserves the profile and also preserves every other embedded field:

// Keep embedded metadata when Imagick writes sub sizes.
// No effect on GD hosts: GD cannot write an ICC profile at all.
add_filter( 'image_strip_meta', '__return_false' );

Use it with your eyes open. That filter keeps EXIF and XMP as well, which means camera serial numbers, editing history and, on phone photographs, GPS coordinates that get published along with the file. If your uploads come from a phone, this cure is worse than the disease. Fix the export instead.

Convert to sRGB, do not assign it

Here is the rule that ends the confusion for good: convert your images to sRGB before they reach WordPress. Not afterwards, and not with a setting. On export, in whatever tool made the file.

This needs spelling out because serious image software offers two operations with confusingly similar names, and picking the wrong one produces exactly the symptom you were trying to cure.

Convert to Profile recalculates every pixel value so that the colour you see stays the same in the new space. The numbers change, the appearance is preserved. A vivid red that was (237, 28, 36) in Adobe RGB becomes some other triplet in sRGB, chosen so that it still looks like that red.

Assign Profile changes the label and leaves the numbers alone. The appearance changes, because you have just told the software that those same numbers now mean something different. Assigning sRGB to an Adobe RGB file produces, pixel for pixel, the identical washed out image that a stripped profile produces. The difference is that now it is baked into the file and no browser can recover it.

Two panels comparing Convert to Profile, which recalculates pixel values and preserves appearance, with Assign Profile, which changes the label and changes appearance.

In practice: in Photoshop use Edit, Convert to Profile and pick sRGB IEC61966-2.1, or use Export As with “Convert to sRGB” ticked. In Lightroom and Capture One, set the export colour space to sRGB. In Affinity Photo it sits under Document, Convert Format / ICC Profile. On a Mac, the Tools, Assign Profile menu in Preview is the one to leave alone unless you know precisely why you are reaching for it.

Do this and the profile question stops mattering. An sRGB file that loses its profile still looks correct, because sRGB is exactly what every browser assumes when there is nothing to go on. The stripping still happens, it just stops having consequences. That is the real fix, and it sits upstream of WordPress entirely. Re-encoding a pile of images that were uploaded before you knew any of this is a batch job rather than a per file one, and WunderPaint’s image processor will run a whole selection through in one pass, though the profile decision itself still belongs in your export settings.

Checking a file instead of guessing

Do not diagnose this by eye. Read the file. Any one of these will tell you whether a profile is present and, in the first two cases, which one it is:

# Profile name and colour space, if exiftool is available
exiftool -ProfileDescription -ColorSpace photo.jpg

# The same question through ImageMagick
identify -verbose photo.jpg | grep -i -m 3 -E "profile|colorspace"

# Nothing installed at all: does this JPEG carry an ICC segment?
head -c 65536 photo.jpg | grep -a -c ICC_PROFILE

All three are read only. The last one is crude, it only works on JPEG and it reports presence rather than identity, but it needs nothing installed and it answers the only question that matters at three in the morning: is the profile there or not. A 1 means yes, a 0 means no.

Run it against the pair of files, not one of them. Check the original in your uploads folder, then check the sub size your theme is actually rendering. If the original reports Display P3 or Adobe RGB and the sub size reports nothing, you have your answer and you know which step in the pipeline to go and look at. Across eight originals sampled from this site’s own library, six carried an ICC segment and two did not, which is precisely the sort of inconsistency that makes this problem so annoying to reproduce on demand.

The second useful comparison is the same image in a colour managed desktop application against the same image in a browser. Open the file in Preview or Photoshop, then open the file URL in a browser tab, and put the two windows side by side on one screen. If the desktop application shows rich colour and the browser shows the flat version, the file has a profile the browser is not receiving, which usually means a caching or delivery layer in between is rewriting it on the way out.

Guessing is optional here. Drop the file below and the inspector reads the colour profile straight out of the bytes: the APP2 segment in a JPEG, the iCCP or sRGB chunk in a PNG, the ICCP chunk in a WebP.

It classifies the profile by its primaries rather than by its name, which matters, because a profile labelled sRGB that carries Adobe RGB primaries is exactly the case behind most washed out images. Next to it you get the same picture rendered as if the profile had been thrown away, which is what a stripped sub size looks like.

Colour profile inspector

Reads the colour profile straight out of the bytes of a JPEG, PNG or WebP, names the colour space from its primaries rather than from its label, and shows what the picture turns into once the profile is thrown away. The file is read in this browser tab and never uploaded.

Your own file
Drop a JPEG, PNG or WebP here
or press Enter to pick one. Only the bytes are read, and only in this tab.
Or a built in example

Nothing read yet.

Nothing read yet

Drop a file, or pick one of the examples.

What the file says
Colorants, measured against the expected space
ChannelMeasured X Y ZExpected X Y Z
With the profile, and with it thrown away
Profile appliedWhat the browser shows you.
Profile discardedThe same numbers read as sRGB.

GD, the library most hosts give WordPress by default, drops the profile when it writes the resized copies. ImageMagick usually keeps it. Every intermediate size inherits whichever library made it, so a site can serve a correct full size file and washed out thumbnails from the same upload.

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.

When it is not the profile

Before you rebuild your entire export workflow, rule out the three things that look identical and are far easier to fix.

A CSS filter is the most common false alarm. Open your browser’s element inspector, select the img tag and look at the computed styles for filter, opacity and mix-blend-mode. Theme demo content and page builder presets love a filter: saturate(0.8) or a brightness(1.1), and once that sits in a global style it applies to every image on the site without ever showing up in the editor.

A theme overlay is the second. Hero sections, card grids and cover blocks routinely paint a semi transparent colour layer over the image so that white text stays readable. That layer is a separate element sitting above the picture, so it survives every export change you make and it will keep washing the image out forever. Inspect the parent element and look for a pseudo element with a background colour and an alpha value.

The third is your own screen. An uncalibrated panel, a night mode or blue light filter still running, or a bright window behind the monitor will shift your perception more than any of the differences discussed here. Turn the filters off, check on a second device, and only then decide something is wrong. And if the image looks soft as well as flat you may be chasing two problems at once, because blurriness has its own set of six causes and none of them are colour related.

Symptom to cause

The full size file looks right, the version on the page looks flat. The sub size was re-encoded without the profile. Either the host is running the GD backend, which cannot write one, or an optimisation plugin removed it afterwards.

Every size looks flat, including the full one. The file left your editor in a wide space and was never converted, or the upload crossed the 2560 pixel threshold and the -scaled file you are looking at was written by the image editor too. Check the true original with exiftool before you blame WordPress.

It looks fine on your Mac and wrong on a Windows laptop. A classic untagged wide gamut file. Your Mac’s display is close enough to P3 that the uncorrected numbers happen to look plausible on it. The other screen is not.

Only images uploaded after a certain date are affected. Something changed in the pipeline on that date. An optimisation plugin was activated, its metadata setting was switched on, or the host moved you to a server without Imagick.

The colours are not flat, they are tinted, and the greens have gone yellow. Usually a CMYK JPEG that reached the web by accident, or an unusual embedded profile. Convert to sRGB and re-upload.

Everything in that section looks dull, not just the photograph. Not a profile problem. A CSS filter or a semi transparent overlay is sitting on top of the whole block.

Photographs from an iPhone are the only ones affected. If those files arrive as HEIC, core re-encodes them to JPEG on the way in, which puts them through the image editor no matter how small they are.

The habit that replaces all of this

Colour management on the web sounds like a specialism and turns out to be one decision, made once, at export time. Wide colour spaces exist for capture and editing, where the extra headroom is genuinely useful. They are not delivery formats. The moment an image leaves your editor for a website, its numbers should already mean sRGB, because sRGB is the only thing the rest of the chain is guaranteed to understand.

Everything else in this article is a workaround for having skipped that step. You can protect the profile through the Imagick backend, you can turn off metadata stripping in your optimiser, you can persuade a CDN to stop rewriting files, and each of those is one more thing that will quietly break the next time the stack changes. Converting on export breaks nothing, costs nothing, and survives every plugin update and server migration you will ever go through. It is also the only fix available to you if your host runs GD, which you now know how to check.

If you take one line away from this, take the difference between converting and assigning. Convert recalculates the numbers and preserves the appearance. Assign relabels the numbers and changes the appearance. They sit next to each other in the same menu, they sound like synonyms, and choosing the wrong one produces precisely the flat, grey, slightly wrong photograph you came here to fix.

WordPress Images Washed Out: The Colour Profile Fix

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.

Photo Editing

Turn a phone photo of a document into something that looks scanned

A photographed page is tilted, tapered, grey and shadowed, and all three faults are completely determined. Four corners give eight equations, a heavy blur estimates the lighting so it can be divided out, and Otsu picks the threshold. No model, no download, and it finishes before you let go of the button.

Photo Editing

Get a depth map from any photo and use it for real lens blur

A cutout blur draws a line and blurs one side of it, which is a thing no lens has ever done. A depth model gives every pixel a distance instead, so sharpness can fall away gradually the way it does optically. The map is a downloadable asset in its own right, and none of it leaves your browser.

Design Fundamentals

How to Choose a Color Palette Without Learning Color Theory

Palette generators hand you five swatches of equal weight, which is the one thing a real palette never is. Here is the ten minute version that works.

Security & Privacy

Black out every piece of text in a screenshot before you share it

A tool that guesses which text is sensitive will miss the one that mattered. This one covers every text field it finds and lets you click back what can stay. It also refuses to default to a blur, because blurring and pixelation can be undone, and there is published work showing exactly how.

Photo Editing

Content Aware Resize: Changing a Photo’s Shape Without Squashing Anyone

One photograph has to be a wide header, a square thumbnail and a tall story card. Cropping loses the edges, stretching lies about proportions, letterboxing buys space it never uses. Seam carving spends the quiet parts of the frame instead: what a seam is, why the search needs a table, and exactly where the method falls apart.

Troubleshooting

WordPress Maximum Upload Size: Where the Limit Really Lives

The media library says 8 MB, the file is 12 MB, and nothing you edit in WordPress moves the number. It is not a WordPress setting at all: core reads two PHP directives and prints the smaller one. Which limit is really stopping the upload, why post_max_size is usually the culprit, how to raise the limits in the order that works, and why a photograph almost never needs a bigger ceiling.

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.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.