7

I have a whole block of code where the CSS rule filter: invert(0.85) is applied.

Inside this block, I have an image, which of course also follows this CSS rule, and is inverted.

I need to revert this invert() for said image.

Is it possible? I tried invert(1), but the image isn't fully like it was before, it's still a little inverted (due to the first invert being only 0.85 and not 1)

See this example:

body{
  font-size: 0;
}

div{
  display: inline-block;
  width: 50%;
}

.filter{
  filter: invert(0.85);
}

.filter img{
  filter: invert(1);
}
<div class="initial">
  <img src="https://s7d1.scene7.com/is/image/PETCO/cat-category-090617-369w-269h-hero-cutout-d?fmt=png-alpha" alt="">
</div>
<div class="filter">
  <img src="https://s7d1.scene7.com/is/image/PETCO/cat-category-090617-369w-269h-hero-cutout-d?fmt=png-alpha" alt="">
</div>
Richard Chambers
  • 16,643
  • 4
  • 81
  • 106
Zenoo
  • 12,670
  • 4
  • 45
  • 69
  • unlike other filter or property i guess invert cannot be inverted – Temani Afif Feb 11 '18 at 11:49
  • @TemaniAfif There must be a way to achieve that, somehow :x – Zenoo Feb 11 '18 at 12:06
  • i guess you need to combine other filter in order to obtain initial result. well am not experienced with filter :) .. but it's like you apply a matrix transformation ... you cannot simply invert the numbers, you need to invert the whole matrix which can be a bit tricky – Temani Afif Feb 11 '18 at 12:18
  • any progress ? :) ... from my side i studied the other filter and not able to find an accurate thing. – Temani Afif Feb 17 '18 at 14:03
  • @TemaniAfif I'm still trying to understand all this W3C documentation about filters ^^ It's pretty difficult. I'm not giving up though ! – Zenoo Feb 17 '18 at 14:04
  • well i didn't give up and at the end i endup with a JS solution combined with the calculation i have done ;) i thnk you can easily adjust it in order to use it, check my update answer :) – Temani Afif Feb 20 '18 at 10:26

3 Answers3

17

TL;DR

You cannot revert the invert() filter by applying another invert() or a combination of other filters.


First, I am going to start with a basic example and an explanation: the invert() filter is used to invert the colors by specifying the percentage of the inversion (or a value from 0 to 1). If we use the value 1 we invert completely the colors thus it's easy to get back to initial state as we simply have to apply the same invert again:

.container {
 display:flex;
 justify-content:space-around;
}

.inner {
  height: 200px;
  width: 200px;
  background: 
  linear-gradient(to right, rgb(255,0,0) 50%, rgb(0,255,255) 0) 0 0/100% 50% no-repeat, 
  
  linear-gradient(to right, rgb(50,0,60) 50%, rgb(205,255,195) 0) 0 100%/100% 50% no-repeat;
}
<div class="container" style="filter:invert(1)">
  <div class="inner"></div>
  <div class="inner" style="filter:invert(1)"></div>
</div>

From this example we can also understand how the invert is working with colors. We simply do (255 - x) for each value of the RGB.

Now let's consider the invert with another value, let's take the 0.85:

.container {
  display: inline-block;
}

.inner {
  display: inline-block;
  height: 200px;
  width: 200px;
  background: linear-gradient(to right, rgb(255, 0, 0) 50%, rgb(0, 255, 255) 0) 0 0/100% 50% no-repeat, linear-gradient(to right, rgb(50, 0, 60) 50%, rgb(205, 255, 195) 0) 0 100%/100% 50% no-repeat;
}
<div class="container" style="filter:invert(0.85)">
  <div class="inner"></div>
</div>
<div class="inner"></div>

How does it work?

For the first color (rgb(255,0,0)) we obtain this (rgb(38, 217, 217)) so the calculation is done as follow:

255 - [255*(1-0.85) + x*(2*0.85-1)]

So our goal is to invert this formula:

f(x) = 255 - [255*(1-p) + x*(2*p-1)] , p a value in the range [0,1]

You may clearly notice that it's pretty easy when p=0 as we will have f(x)=x and when p=1 we will have f(x)=255-x. Now let's express the value of x using f(x) (I will use it as y here):

x = 1/(2*p-1) * [255 - (255*(1-p) +y)]

Let's try to make it similar to an invert function. Let's havey=(2*p-1)*y' and We will obtain:

x = 1/(2*p-1) * [255 - (255*(1-p) +(2*p-1)*y')]

which is equivalent to

x = 1/(2*p-1) * f(y') ---> x = K * f(K*y) with K = 1/(2*p-1)

Here f is an invert function using the same value of p and K is a constant calculated based on the value p. We can distinguish 3 situations:

  1. When the value of p is equal to 0.5 the function is no defined and thus the invert cannot be inverted (you can by the way try to apply invert(0.5) to see the result)
  2. When the value of p is greater than 0.5 K is a positive value in the range [1,+infinity].
  3. When the value of p is smaller than 0.5 K is a negative value in the range [-infinity,-1].

So if we omit the first case, K can be set in this way K=1/K' where K' is a value in the range [-1,1]/{0} and abs(K') is in the range of ]0,1]. Then our function can be written as follow:

x = (1/K') * f(y/K') where K' in the range [-1,1] define by (2*p - 1)

At this point we expressed our function with an invert function and a multiplication/division with a value that we can easily compute.

Now we need to find which filter apply a multiplication/division. I know there is the brightness and the contrast filter that uses linear transformation.

If we use brightness(p) the forumla is as follow:

f(x) = x*p;

And if we use contrast(p) the formula will look like this:

f(x) = p*(x - 255/2) + 255/2

This will end up here ...

As said intially we cannot revert invert() using other filters. We can probably approximate this for some values but for other it will impossible (like with 0.5). In other words, when applying the invert() we lose some information that we cannot get back. It's like when, for example, you get a coloured image that you transform into a black & white version. You have no way to put back the initial colors.

UPDATE

Here is some JS code to proove that the above calculation is correct and to see some results. I used canvas in order to draw the image again while applying my function:

Unfortunately I cannot use an image in the snippet for security and cross-browser origin issue so I considerd a gradient

var canvas = document.querySelector('canvas');
var img = document.querySelector('img');
var ctx = canvas.getContext('2d');
//we draw the same image on the canvas (here I will draw the same gradient )
//canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var grd = ctx.createLinearGradient(0, 0, 150, 0);
grd.addColorStop(0, "blue");
grd.addColorStop(1, "red");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 150, 150);

//we get the data of the image
var imgData = canvas.getContext('2d').getImageData(0, 0, 150, 150);
var pix = imgData.data;
var p = 0.85;
// Loop over each pixel and apply the function
for (var i = 0, n = pix.length; i < n; i += 4) {
  pix[i + 0] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 0])));
  pix[i + 1] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 1])))
  pix[i + 2] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 2])))
  // i+3 is alpha (the fourth element)
}
//Draw the image again
imgData.data = pix;
canvas.getContext('2d').putImageData(imgData, 0, 0);
body {
  font-size: 0;
}

div,
canvas {
  display: inline-block;
}

.grad {
  height: 150px;
  width: 150px;
  background: linear-gradient(to right, blue, red);
}

.filter {
  filter: invert(0.85);
}
<div class="grad"></div>
<div class="filter">
  <div class="grad"></div>
  <canvas></canvas>
</div>

As we can see we have 3 images: the original one, the inverted one and the one on where we applied our function to make it back to the original one.

We are having a good result here because the value is close to 1. If we use another value more close to 0.5 will have a bad result because we are close to the limit of where the function is defined:

var canvas = document.querySelector('canvas');
var img = document.querySelector('img');
var ctx = canvas.getContext('2d');
//we draw the same image on the canvas (here I will draw the same gradient )
//canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var grd = ctx.createLinearGradient(0, 0, 150, 0);
grd.addColorStop(0, "blue");
grd.addColorStop(1, "red");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 150, 150);

//we get the data of the image
var imgData = canvas.getContext('2d').getImageData(0, 0, 150, 150);
var pix = imgData.data;
var p = 0.6;
// Loop over each pixel and apply the function
for (var i = 0, n = pix.length; i < n; i += 4) {
  pix[i + 0] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 0])));
  pix[i + 1] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 1])))
  pix[i + 2] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 2])))
  // i+3 is alpha (the fourth element)
}
//Draw the image again
imgData.data = pix;
canvas.getContext('2d').putImageData(imgData, 0, 0);
body {
  font-size: 0;
}

div,
canvas {
  display: inline-block;
}

.grad {
  height: 150px;
  width: 150px;
  background: linear-gradient(to right, blue, red);
}

.filter {
  filter: invert(0.6);
}
<div class="grad"></div>
<div class="filter">
  <div class="grad"></div>
  <canvas></canvas>
</div>

You can use this code in order to create a generic function that will allow you to partially revert the invert() filter:

  1. Using some JS you can easily find the value of p used in the filter.
  2. You can use a specific selector to target only specific images on where you want to apply this logic.
  3. As you can see I created a canvas so the idea is to create it and then hide the image to keep only the canvas.

Here is a more interactive demo:

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var init = function() {
  var grd = ctx.createLinearGradient(0, 0, 150, 0);
  grd.addColorStop(0, "blue");
  grd.addColorStop(1, "red");
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, 150, 100);
  var grd2 = ctx.createLinearGradient(0, 0, 0, 150);
  grd2.addColorStop(0, "green");
  grd2.addColorStop(1, "yellow");
  ctx.fillStyle = grd2;
  ctx.fillRect(40, 0, 70, 100);
}
var invert = function(p) {
  //we get the data of the image
  var imgData = canvas.getContext('2d').getImageData(0, 0, 150, 100);
  var pix = imgData.data;
  // Loop over each pixel and apply the function
  for (var i = 0, n = pix.length; i < n; i += 4) {
    pix[i + 0] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 0])));
    pix[i + 1] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 1])))
    pix[i + 2] = Math.round((1 / (2 * p - 1)) * (255 - (255 * (1 - p) + pix[i + 2])))
    // i+3 is alpha (the fourth element)
  }
  //Draw the image again
  imgData.data = pix;
  canvas.getContext('2d').putImageData(imgData, 0, 0);
}
init();
$('input').change(function() {
  var v = $(this).val();
  $('.filter').css('filter', 'invert(' + v + ')');
  init();
  invert(v);
})
p {
  margin: 0;
}

div,
canvas {
  display: inline-block;
}

.grad {
  height: 100px;
  width: 150px;
  background: linear-gradient(to bottom, green, yellow)40px 0/70px 100% no-repeat, linear-gradient(to right, blue, red);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="grad"></div>
<div class="filter">
  <div class="grad"></div>
  <canvas height="100" width="150"></canvas>
</div>
<p>Adjust the value of the invert filter</p>
<input type="range" value="0" min=0 max=1 step=0.05>

From this demo we can also notice that when we are close to the value 0.5 we don't have a good result simply because the filter cannot be inverted at 0.5 and if we refer to previous calculation, the value of K become very big and thus K' is too small .

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • 1
    Wow, that's an in-depth answer ^^. I'm still trying to wrap my head around the `255 - [255*(1-0.85) + x*(2*0.85-1)]` part, I don't understand how you got that formula. – Zenoo Feb 15 '18 at 08:04
  • 1
    @Zenoo well i was helped with some internet search :) i worked before with image processing so i had to refresh my memory by reading few article to understand how the calculation is done and after a simplification i ended with this formula ;) – Temani Afif Feb 15 '18 at 08:11
  • I looked at the [W3C](https://www.w3.org/TR/filter-effects/#invertEquivalent) definition for the `invert()` filter, but I must say I still have no clue how you achieved that ^^ – Zenoo Feb 15 '18 at 08:18
  • @Zenoo well if you insist :) you can see that the invert uses `feComponentTransfer` and you can read about this one [here] (https://www.w3.org/TR/filter-effects/#feComponentTransferElement) and if you continu reading you will end up by this `feFunc*` that is applied to the 3 colors using the [**table** type] (https://www.w3.org/TR/filter-effects/#valdef-type-table) --> with this you can have some clue ;) – Temani Afif Feb 15 '18 at 08:33
  • @Zenoo and if you want more complicated stuffs, here is a question dealing with many filters and some calculation where you can see something about the invert and few formulat also https://stackoverflow.com/a/43960991/8620333 ... then by playing around and testing we endup with a formula we can use :) – Temani Afif Feb 15 '18 at 08:37
  • @Zenoo but i first start looking outside the CSS world ... I checked algorithm of some image processing, as if you simply look within the CSS world you will endup at 90% of the cases by a simple explanation and no complex math behind. – Temani Afif Feb 15 '18 at 08:39
  • Oh boy, this is gonna be a long read ! Thanks for the references, I'll check those out. I hope I'm not biting more than I can chew here ^^ – Zenoo Feb 15 '18 at 08:43
  • @Zenoo yes, not easy stuff, i also keep this working on my side but i wanted to share where i end up and not wait until i find a complete solution so like that maybe you or someone else can continue the investigation ;) – Temani Afif Feb 15 '18 at 08:56
  • Your JS solution is working great ! I will accept your answer. Unfortunately I won't be able to use it, since drawing a canvas for each image in a page, would require too much processing. But still, it could be useful for other people ! – Zenoo Feb 20 '18 at 10:30
  • @Zenoo yes right it consuming, but at least we endup learning new things :) and the canvas idea is to essentially show the result of the investigation i have done and to verify the results ;) – Temani Afif Feb 20 '18 at 10:32
  • @Zenoo and to finish i added another demo to hightlight the fact that the invert cannot be inverted when we use 0.5 as value and we can only have good result when we are far from it ;) which also prove that this is too diffucult and maybe impossible to achieve – Temani Afif Feb 20 '18 at 11:50
  • @TemaniAfif Your reasoning is wrong in multiple ways, but they cancel out to arrive at the correct answer: that you can't invert an `invert`. The simple solution to the problem as you stated it would be to `contrast(1/(1-2*p))` the image and then `invert(p)` the container; those two transforms compose to the identity transform. The real reason this doesn't work is that a. colors are clipped to [0..255] after each transform and b. `invert` always compresses the input range → the `img` transform (which precedes `invert`) has to expand the input range and therefore will run into clipping. – Iris Artin Nov 16 '21 at 04:44
3

You'll need to use a combination of other CSS filter()s to revert the changes. Depending on the initial filter() applied, this could be extremely difficult.

In this particular case, with only invert(0.85) being applied, we can try a few other filter()s to get the image back to normal.

Through some experimentation it appears the following will work:

filter: invert(1) contrast(1.3) saturate(1.4);

This is a rough guess, but visually seems to do the trick.

.flex {
  width: 100%;
  display: flex;
}

img {
  width: 100%;
  display: block;
}

.flex > div{
  flex: 1;
}

.filter {
  filter: invert(0.85);
}

.filter img {
  filter: invert(1) contrast(1.3) saturate(1.4);
}

p {
  color: red;
}
<div class="flex">
  <div class="initial">
    <p>Some red text</p>
    <img src="https://s7d1.scene7.com/is/image/PETCO/cat-category-090617-369w-269h-hero-cutout-d?fmt=png-alpha" alt="">
  </div>
  <div class="filter">
    <p>Some red text</p>
    <img src="https://s7d1.scene7.com/is/image/PETCO/cat-category-090617-369w-269h-hero-cutout-d?fmt=png-alpha" alt="">
  </div>
</div>
Brett DeWoody
  • 59,771
  • 29
  • 135
  • 184
  • My question isn't about how to avoid filtering an image, but to *revert* the filter already applied to a block, only on some elements of this block. The `.filter` HAS the `filter: invert()` rule already applied. I need to revert this for some elements inside. – Zenoo Feb 13 '18 at 13:35
  • 1
    As pointed out, it's isn't possible to undo the `invert`. The closest I can imagine is using a series of other `filter`s to get the image colors somewhat back to their initial, but it will be imprecise. For example, some combination of `saturate`, `invert`, etc. – Brett DeWoody Feb 13 '18 at 13:37
  • No, the `.filter` class has to have the `filter` applied to it directly, since the DOM inside it is very complex, applying the `invert` on `.filter *` would screw up everything. If what @TemaniAfif said was correct, there must be a combination of other filters which would render as the initial image exactly. – Zenoo Feb 13 '18 at 13:40
  • "applying the invert on .filter * would screw up everything" - why? – Brett DeWoody Feb 13 '18 at 13:42
  • [Here's how the `invert` applied to StackOverflow's `body` is rendering](https://zenoo.tinytake.com/sf/MjM1MzgxNV83MTc5OTQ4). It's the look I want to achieve. Now [here's how the `invert` applied to StackOverflow's `body` children is rendering](https://zenoo.tinytake.com/sf/MjM1MzgyMl83MTc5OTU2). All the different `inverts` are overlapping. – Zenoo Feb 13 '18 at 13:53
  • Ah, I see. One sec. – Brett DeWoody Feb 13 '18 at 14:28
  • 1
    Thanks for the approximation ! Is there any way to find the exact math behind all this? I'll go for your answer if there isn't, but I'd really like an exact match. – Zenoo Feb 13 '18 at 16:13
  • I tend to create custom CSS for sites that I use a lot in order to create a dark theme. I like to have filter invert set at 0.87 for the html element and by tuning @BrettDeWoody 's formula a bit for the img elements, it works really well: filter: invert(1) contrast(1.2) saturate(1.3); – Fredrik_Borgstrom Jan 15 '23 at 10:55
2

The other answers show the difficulty achieving this with CSS. Here is a basic javascript approach you may find useful. You detect if .filter contains an image, and if it does, remove the class filter.

var filteredDiv = document.querySelectorAll(".filter"),
  len = filteredDiv.length,
  i;

for (i = 0; i < len; i++) {
  if (filteredDiv[i].getElementsByTagName('img').length > 0) {
    filteredDiv[i].classList.remove('filter');
  }
}
body {
  font-size: 0;
}

.filter {
  filter: invert(.85);
}

div {
  display: inline-block;
  width: 200px;
}

img {
  width: 100%;
  height: auto;
}

div>div {
  background: yellow;
  font-size: 2rem;
}
<div class="filter">
  <img src="https://s7d1.scene7.com/is/image/PETCO/cat-category-090617-369w-269h-hero-cutout-d?fmt=png-alpha" alt="">
</div>
<div class="filter">
  <div>Sample text</div>
</div>
sol
  • 22,311
  • 6
  • 42
  • 59
  • no sure this can be a solution because as commented in the other answer he doesn't want to remove the filter .. the filter should remain applied to the whole block and inside this block he want to revert in for only some element. – Temani Afif Feb 19 '18 at 07:53
  • Thanks, but I can't use Javascript for this issue, plus, your code beats the purpose of the question. The filter *has* to stay on the block. – Zenoo Feb 19 '18 at 07:54
  • @Zenoo Fair enough. I would be surprised if you find a way to do this properly with CSS. Best of luck – sol Feb 19 '18 at 08:22