3

* EDIT * No I am not trying to convert a .BMP Just simply load a PNG and render it in a tk Canvas (with the transparent bits actually transparent.)

Goal: What to load PNG files with transparency and render them in tkinter Canvas with Python3 (and using Pillow for PIL support)

Problem: When loaded as RGBA, they fail to render in canvas. Converting them to RGB and they will render, but of course have no transparency.

Environment: Mac OSX, Python 3.5 installed and verified Pillow is in the 3.5 path

Notes: The CuriosityRover.png is indeed an RGBA, the print statement confirms, and verified with finder showing the grey background and Photoshop saving web with transparency.

I have tried extracting out just the alpha layer like this:

 alpha = img.convert('RGBA').split()[-1]

Calling alpha.show() does indeed show me in Preview the alpha layer, but it fails to render in the canvas widget.

I also tried inverting the alpha layer using ImageOps.invert(alpha) (Verified via calling .show() after applying the invert and I see the reverse alpha layer in Preview.)

I have tried creating rectangular transparent areas like this: transparent_area = (0, 0, 300, 300) mask = Image.new('L', curosity.size, color=255) draw = ImageDraw.Draw(mask) draw.rectangle(transparent_area, fill=0) img.putalpha(mask)

And that does work in creating a transparent rectangular region, so it seems like the canvas.create_image is working fine with transparency, but somehow I am failing to create a PhotoImage with the transparency data correct.

I have so many stack overflow tabs open through the course of the day I am embarrassed that I cannot figure this out.

This seems like the simplest thing in the world. What am I doing wrong?

import tkinter as tk
from PIL import Image, ImageTk

img = Image.open("./Assets/CuriosityRover.png")
img2 = Image.open("./Assets/CuriosityRover.png").convert('RGB')

img.show()  # Shows the image just fine in preview
print(img.format, img.size, img.mode)

root = tk.Tk()
photo = ImageTk.PhotoImage(img)  # img fails render, img2 works but no alpha

canvas = tk.Canvas(root, width=600, height=600, bg="black")

canvas.create_image((300, 300), image=photo)
canvas.grid(row=0, column=0)

root.mainloop()
Erik Bethke
  • 563
  • 5
  • 17
  • Possible duplicate of [Load RGBA bitmap with PIL](http://stackoverflow.com/questions/10453604/load-rgba-bitmap-with-pil) – Laurent LAPORTE Jan 10 '17 at 19:34
  • No, I am not loading a .BMP, I am loading a PNG – Erik Bethke Jan 10 '17 at 19:41
  • what do you mean "fail to render" ? Do you get error message or what ? I can only say that it works on Linux so problem can be only on Mac. BTW: Did you try only with one image ? – furas Jan 11 '17 at 08:03
  • Thank you for giving it a try on Linux. I apologize, I should have been more clear, it renders, but it draws the transparent section as opaque white. I have tried two different images. – Erik Bethke Jan 11 '17 at 14:08
  • RGB mode will render but with opaque bits, RGBA mode will not render, but the RGBA will .show() – Erik Bethke Jan 11 '17 at 14:39
  • I tried this with Python 3.5 and Pillow 3.3 on Windows, and it worked perfectly. I used `bg='red'` on the canvas, and the transparent areas were pure red while the semi-transparent areas were only partially red. Are you sure the image loaded with mode `'RGBA'`? There are multiple forms on PNG images. – Mark Ransom Jan 14 '17 at 02:54

1 Answers1

9

Woot! I solved this.

TL;DR - YES RGBA* PNGS are unsupported in Pillow/Tkinker - but a work around is below and force the Alpha channel to have values only 0 or 255

*A channel cannot have any values other than 0 or 255, if it does then the whole image is not drawn (even the pixels with 255 in the alpha channel.)

Longer version: It was bugging the crap out of me. Turns out my issue is that photoshop saves out the Alpha channel with 8 bits of information and so my test images had subtle transparencies in ranges that I could not see by eye. They looked either opaque or transparent when I viewed the images.

But by comparing what the actual bytes where after the successful case of the rectangle transparency test case I could see that Image only wants 0 for transparent or 255 for opaque (As Tkinter is dynamically laying out the GUI it doesn't know what the color is of the pixel below to blend in the case of partial transparency.)

So, to fix this, I now run my images through this helper function I created below that flips the 8 bits of information coming in on the alpha channel and forces them to be either 0 or 255. I chose somewhat arbitrarily that in the case of 50 and below I would consider it transparent.

Hopefully someone else sees this before having to figure this out from scratch.

# Fixes the issue when trying to render RBGAs that have 8bits of information in the alpha channel
# Turns your image into 8 bits on RGB and then 1 bit on the A channel
# This will render correctly
# See the example below for how to use

from PIL import Image


def flattenAlpha(img):
    alpha = img.split()[-1]  # Pull off the alpha layer
    ab = alpha.tobytes()  # Original 8-bit alpha

    checked = []  # Create a new array to store the cleaned up alpha layer bytes

    # Walk through all pixels and set them either to 0 for transparent or 255 for opaque fancy pants
    transparent = 50  # change to suit your tolerance for what is and is not transparent

    p = 0
    for pixel in range(0, len(ab)):
        if ab[pixel] < transparent:
            checked.append(0)  # Transparent
        else:
            checked.append(255)  # Opaque
        p += 1

    mask = Image.frombytes('L', img.size, bytes(checked))

    img.putalpha(mask)

    return img

# Run this as a test case.
# Assumes that you have a PNG named "CuriosityRover.png"
# that is an RGBA with varying levels of Alpha in the
# subdirectory assets from your working directory

if __name__ == "__main__":
    from PIL import ImageTk
    import tkinter as tk

    img = Image.open("./Assets/CuriosityRover.png")

    img = flattenAlpha(img)
    root = tk.Tk()

    photo = ImageTk.PhotoImage(img)
    canvas = tk.Canvas(root, width=600, height=600, bg="red")

    canvas.create_image((300, 300), image=photo)
    canvas.grid(row=0, column=0)

    root.mainloop()
Erik Bethke
  • 563
  • 5
  • 17