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

Transparency is not applied to RGBA palette when opening PNG #6348

Closed
JensRestemeier opened this issue Jun 5, 2022 · 9 comments · Fixed by #6352
Closed

Transparency is not applied to RGBA palette when opening PNG #6348

JensRestemeier opened this issue Jun 5, 2022 · 9 comments · Fixed by #6352
Labels

Comments

@JensRestemeier
Copy link

What did you do?

I need to get a palette with transparency information from a PNG image. When saving this is correctly applied to the output image, when loading only an opaque palette is returned.

What did you expect to happen?

I expect that image.info["transparency"] is applied to a returned RGBA palette, i.e. alpha cleared for a single transparent colour, or alpha replaced with the array of transparency values.

What actually happened?

A completely opaque palette is returned

What are your OS, Python and Pillow versions?

  • OS: Windows 10
  • Python: 3.9.5 (64 bit)
  • Pillow: 9.1.1
from PIL import Image

img = Image.new("P", (256, 256))
pixel = []
for y in range(256):
    for x in range(256):
        v = ((y // 16) & 1) ^ ((x//16) & 1)
        pixel.append(v)
img.putdata(pixel)
pal = []
for i in range(256):
    if i == 0:
        pal.extend([0,0,0])
    else:
        pal.extend([255,255,255])
img.putpalette(pal, "RGB")

img.save("test_A.png", transparency=0)
img.save("test_B.png", transparency=bytes([0] + [255] * 255))

pal = []
for i in range(256):
    if i == 0:
        pal.extend([0,0,0,0])
    else:
        pal.extend([255,255,255,255])
img.putpalette(pal, "RGBA")
img.save("test_C.png")

for f in ["test_A.png", "test_B.png", "test_C.png"]:
    img = Image.open(f)
    pal = img.getpalette("RGBA")
    print (img.info, img.mode, pal[0:8])

This sets the transparency in 3 different ways and saves images to the current directory, then loads them back.
The images show the expected transparency if opened in a program like GIMP.

Output is:

{'transparency': 0} P [0, 0, 0, 255, 255, 255, 255, 255]
{'transparency': 0} P [0, 0, 0, 255, 255, 255, 255, 255]
{'transparency': 0} P [0, 0, 0, 255, 255, 255, 255, 255]

Expected output should be something like:

{'transparency': 0} P [0, 0, 0, 0, 255, 255, 255, 255]
{'transparency': 0} P [0, 0, 0, 0, 255, 255, 255, 255]
{'transparency': 0} P [0, 0, 0, 0, 255, 255, 255, 255]
@radarhere
Copy link
Member

Hi. As you have said, the transparency information is in the info dictionary, but not in the palette.

I'm not convinced this is a bug. I don't think it should be in both, but in one or the other. I don't like the idea of having two sources of truth about what the transparency is. The image is being opened with an RGB palette. When you are requesting an RGBA palette, the RGB palette is just being converted.

If I ask Pillow to open and re-save test_A.png, test_B.png and test_C.png, the transparency is retained, so there's no information loss occurring.

If you need the transparency index to become part of the palette for reasons external to Pillow, it should be straightforward for you to apply it in your code.

@JensRestemeier
Copy link
Author

I do actually have the code to apply transparency from before Pillow supported RGBA palettes, I was hoping to remove it.

My reasoning is:

  • palette transparency is set up the file format loader - you have to dig through the documentation to find how to get and set it. This took some time when I started to use Pillow.
  • The PNG loader supports two forms of transparency (index or array), you need to handle both in your code. RGBA could hide this difference.
  • The P->RGBA conversion handles transparency, so getting an RGBA palette should handle this as well.
  • From a single source of truth perspective: All code needs to know where to look for the transparency, and that the one in the RGBA palette may not be correct.

Obviously one problem is existing code that expects transparency in the info dictionary. But that code probably uses an RGB palette as well.

@radarhere
Copy link
Member

How about this idea - rather than changing the default behaviour, what if we added an include_transparency argument to getpalette()?

@JensRestemeier
Copy link
Author

I would expect this to be the default if I request an RGBA palette over an RGB palette, also another argument will change the API which may cause problems in the future if you want to change it.
I am happy if you want to leave it like it is for now, until there are other reasons change it.

@radarhere
Copy link
Member

Pillow values backwards compatibility, and this is not explicitly a bug - at the moment, I think getpalette() is correctly casting the RGB palette of the image to an RGBA palette, because you requested the palette be in RGBA format.

@JensRestemeier
Copy link
Author

JensRestemeier commented Jun 6, 2022

What about a separate "expand palette" function, that internally updates the palette from RGB to RGBA and applies transparency. It would do nothing if the palette is already RGBA.
A companion could be "compact palette" that would reduce the internal palette to RGB, and update the transparency index (or transparency array) in the info header?

@radarhere
Copy link
Member

I see, one function that takes an image with an RGB palette and "transparency" info and converts it to image with an RGBA palette without "transparency" info. I've created PR #6352 for this.

Your second idea is a function that turns an image with an RGBA palette without "transparency" info into an image with an RGB palette and "transparency" info. Why would you consider this to be helpful?

@JensRestemeier
Copy link
Author

Only for symmetry, and you may have the function already for saving, so it would just be some refactoring.

@radarhere
Copy link
Member

PngImagePlugin achieves this fairly neatly when saving

if im.mode == "P" and im.im.getpalettemode() == "RGBA":
alpha = im.im.getpalette("RGBA", "A")
alpha_bytes = colors
chunk(fp, b"tRNS", alpha[:alpha_bytes])

If it's not something helpful to users, I'm inclined not to make that operation part of the public API.

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.

2 participants