Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect transparency after converting indexed image to RGB #7288

Closed
nedbat opened this issue Jul 16, 2023 · 5 comments · Fixed by #7289
Closed

Incorrect transparency after converting indexed image to RGB #7288

nedbat opened this issue Jul 16, 2023 · 5 comments · Fixed by #7289
Labels

Comments

@nedbat
Copy link

nedbat commented Jul 16, 2023

What did you do?

I use Pillow to convert images on my web site on-demand to .webp format. It works great! Except for one of two images on this page: https://nedbatchelder.com/text/cardcube.html The first image is strangely pixellated. The second looks perfect.

What did you expect to happen?

The webp conversion should produce the same pixels as the original .png files.

What actually happened?

Unfortunate graininess.

What are your OS, Python and Pillow versions?

  • OS: MacOS
  • Python: 3.9.17
  • Pillow: 10.0.0

I tried converting the two .png files into a variety of formats. The .gif and .bmp look identical to the .png. The .webp shows the results I see on my site:

from pathlib import Path
from PIL import Image

for n in [1, 2]:
    for fmt in ["WEBP", "GIF", "BMP"]:
        inpath = Path(f"cardcube_step{n}.png")
        outpath = inpath.with_suffix("." + fmt.lower())
        im = Image.open(inpath)
        im.save(outpath, fmt, lossless=True, quality=80, method=6)

Here are the two .png files:

cardcube_step1
cardcube_step2

Here are the .webp files I get:

webp.zip

@homm
Copy link
Member

homm commented Jul 16, 2023

The error is not related to webp, this is because of transparency chunk in the original PNG file:

from pathlib import Path
from PIL import Image

for n in [1, 2]:
    inpath = Path(f"cardcube_step{n}.png")
    im = Image.open(inpath)
    print(im, im.info)
    for mode in ["RGB", "L"]:
        outpath = inpath.with_suffix(f".{mode}.png")
        im.convert(mode).save(outpath)
<PIL.PngImagePlugin.PngImageFile image mode=P size=473x267 at 0x104D3DB80> {'transparency': 0, 'dpi': (999.9472, 999.9472)}
<PIL.PngImagePlugin.PngImageFile image mode=P size=473x300 at 0x104D3DC10> {'dpi': (999.9725999999999, 999.9725999999999)}

@homm homm added the Palette label Jul 16, 2023
@homm
Copy link
Member

homm commented Jul 16, 2023

Some research about the image:

In [1]: from PIL import Image

In [2]: im = Image.open('cardcube_step1.png')

In [3]: im.info
Out[3]: {'transparency': 0, 'dpi': (999.9472, 999.9472)}

In [4]: im.palette.mode, len(im.palette.palette)
Out[4]: ('RGB', 90)

In [6]: " ".join(map(str, im.histogram()))
Out[6]: '0 492 169 255 261 392 10906 193 368 340 259 429 232 85 489 193 488 181 304 289 420 226 397 271 287 338 105299 243 297 330 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1858'

In [7]: im.convert('RGB').info
Out[7]: {'transparency': (0, 0, 0), 'dpi': (999.9472, 999.9472)}

So, this image with palette of 30 elements. The index of transparent pixels is 0, but there are no pixels in the image with such values. Instead, there are 1858 pixels with values outside of the palette (value 255), which interpreted as black.

The problem is that after converting the image from P to RGB mode, pixels outside of the palette turns black, but transparent item in the im.info is also converted to black, since the first element in the palette is also black.

There is no easy fix since after converting the image to RGB we can't distinguish true black pixels from transparent black pixels. One of the possible solutions could be deny transparency for RGB mode at all since this mode doesn't mean any transparency, although I don't know how this applicable and backward compatible.

@nedbat
Copy link
Author

nedbat commented Jul 16, 2023

Thanks for the analysis. Why does it convert properly to gif or bmp, but not to webp?

@homm homm changed the title Poor-quality webp conversion Incorrect transparency after converting indexed image to RGB Jul 16, 2023
@homm
Copy link
Member

homm commented Jul 16, 2023

It's because it doesn't convert anything for bmp or gif, those formats support indexed images, like png.

@radarhere
Copy link
Member

Instead, there are 1858 pixels with values outside of the palette (value 255), which interpreted as black.

It's interpreted as black by other viewers because it is outside the palette, but from my investigation, this is not the case with Pillow. Pillow just retrieves the value from the palette regardless, which is still set to the values we initialise it with.

I've created PR #7289 as a possible solution, setting the unused parts of the palette to black.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants