Skip to content

cur file saved as a png has black pixels where it needs to be transparent #9637

Description

@mmemoo

its a really easy-to-reproduce issue, just run this code with the file i attached in Pillow 12.1.0 :

from PIL import Image

im = Image.open("aero_unavail.cur")
im.save("aero_unavail.png")

(the file is provided as a txt file since github doesn't allow cur files to be uploaded, just change the extension.)

aero_unavail.txt

Activity

  1. radarhere commented on May 24, 2026

    @radarhere
    Member

    Hi. This has been raised before in #8594. In that issue, the conclusion was that the specification says it should actually be black. However, we added a setting for this. If you set USE_RAW_ALPHA to True, then you will get the result you expect.

    from PIL import Image, BmpImagePlugin
    BmpImagePlugin.USE_RAW_ALPHA = True
    
    im = Image.open("./aero_unavail.cur")
    im.save("aero_unavail.png")
    Image
  2. mmemoo commented on May 25, 2026

    @mmemoo
    Author

    hi, thank you for the help.
    why does this happen for some cur files?

  3. radarhere commented on May 25, 2026

    @radarhere
    Member

    https://learn.microsoft.com/en-us/windows/win32/wmdm/-bitmapinfoheader describes 32 bitcount as

    The bitmap has a maximum of 2^32 colors. If the biCompression member is BI_RGB, the bmiColors member is NULL. Each DWORD in the bitmap array represents the relative intensities of blue, green, and red, respectively, for a pixel. The high byte in each DWORD is not used.

    So the specification says that the fourth channel should not be used. That means that the image should be rendered as RGB, not RGBA, hence the black.

    I can't say why some other software has chosen to ignore this part of the specification.

  4. mmemoo commented on May 25, 2026

    @mmemoo
    Author

    thank you!

  5. reopened this on May 25, 2026
  6. mmemoo commented on May 25, 2026

    @mmemoo
    Author

    sorry to bother again but the issue persists on other files like the one i attached
    (attached as txt instead of .cur, again)

    default_arrow.txt

  7. radarhere commented on May 25, 2026

    @radarhere
    Member

    Your latest file appears to have a different issue. I still need to investigate this further, but it appears that it contains two images, and one is a mask for the other.

    If you would like an immediate fix, this code

    from PIL import Image, ImageOps, BmpImagePlugin
    
    def read(path):
      BmpImagePlugin.USE_RAW_ALPHA = True
    
      im = Image.open(path)
      if im.mode == "1":
        out = Image.new("LA", im.size)
        im._size = im.width, im.height * 2
        im.tile = [im.tile[0]._replace(extents=(0, 0)+im.size)]
        
        and_mask = im.crop((0, out.height, out.width, im.height))
        xor_mask = ImageOps.invert(im.crop((0, 0) + out.size))
        out.paste(and_mask, mask=xor_mask)
        return out
      else:
        return im
    
    read("./default_arrow.cur").save("default_arrow.png")
    read("./aero_unavail.cur").save("aero_unavail.png")

    gives
    Image
    Image

  8. mmemoo commented on May 25, 2026

    @mmemoo
    Author

    by "it has another image which is a mask" you mean the single image in the .cur file has a proper and mask no? if that's not the case when i inspect the file with imagemagick i see only 1 image

  9. radarhere commented on May 25, 2026

    @radarhere
    Member

    it contains two images, and one is a mask for the other.

    This sentence was just me describing a technical detail.

    If you're not curious about how data is arranged within an image, feel free to completely ignore it.

    If you are curious, then I meant that within the image, there is a black-and-white section, and then another black-and-white section. One section actually does render as black-and-white, in the other, the 'white' pixels re the ones that are shown, and the 'black' pixels are the ones that become transparent. It's how the creators of CUR decided to assemble a black/white/transparent image out of only black-and-white data.

    In the end, yes, there is only one image that the user should ever see.

  10. mmemoo commented on May 25, 2026

    @mmemoo
    Author

    i actually somewhat know about the structure of cur files and i guess you are talking about xor, and masks.

    plus i have some questions afaik, the and mask is used as the transparency mask but your code says otherwise, why?
    and when this cur file is used, is there any xor operation which involves the xor mask or is it just used as pixel data?

    also a little off-topic but, does bmp's with any bpp store images as and + xor masks or does it work differently

  11. radarhere commented on May 27, 2026

    @radarhere
    Member

    plus i have some questions afaik, the and mask is used as the transparency mask but your code says otherwise, why?

    I expect my use of ImageOps.invert() has things less clear. I would put together an example with logical_xor(), but that method only accepts 1 mode images, which wouldn't give the transparent/black/white result we want.

    does bmp's with any bpp store images as and + xor masks or does it work differently

    No, I see nothing about AND and XOR masks at https://en.wikipedia.org/wiki/BMP_file_format or in our current BmpImagePlugin. It does use bit masks, but those are filters used to look at the same set of bytes, rather than two different sets of bytes being combined.

  12. mmemoo commented on May 27, 2026

    @mmemoo
    Author

    this is going a little off-topic but can you tell me a little bit about what the code snippet you sent is actually doing and what those bit masks are for when they are present.

    btw this site says that the pixel data is actually xor and and masks atleast when bpp is 1 (it could be wrong idk) https://www.daubnet.com/en/file-format-cur

  13. radarhere commented on May 30, 2026

    @radarhere
    Member

    I've created #9641. Is default_arrow an image that we can include in our test suite, and distribute under the Pillow license?

    You're asking for an explanation of how BmpImagePlugin handles bit masks? It was added in #1125.

    Bit masks are used as a form of compression

    (16, (0xF800, 0x7E0, 0x1F)): "BGR;16",

    means that applying bitwise AND 0xF800 against the data gives one band's values, 0x7E0 another and 0x1F the last.

    Or they are simply a way to rearrange bands, as these examples use the full range of each band, just in a different order.

    (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
    (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
    (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
    (32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",

    Pillow isn't handing any series of masks arbitrarily though. These entries are matching possible mask values to our 'raw modes', which are then used to unpack the data into more common image modes.

  14. mmemoo commented on May 30, 2026

    @mmemoo
    Author

    https://www.rw-designer.com/cursor-set/windows-xp-complete-edition the file was taken from here, and the license says the author should be asked for permission if it will be used for purposes other than non-commercial personal usage.

    thanks for the explanation of bit masks, i understood how they work. i still don't really get how your code works, you and wikipedia said bmps dont have xor and and masks but your code says otherwise and you are using an inverted xor mask where there should be an inverted and mask.

  15. radarhere commented on May 31, 2026

    @radarhere
    Member

    you and wikipedia said bmps dont have xor and and masks but your code says otherwise

    BMP and CUR images are not the same. Wikipedia describes CUR as a 'Container for' the BMP format. In Pillow terms, we read the first 4 bytes to determine that it is a CUR file,

    https://www.daubnet.com/en/file-format-cur

    Reserved | 2 byte | =0
    Type | 2 byte | =2

    the next 2 bytes to find out how many cursors are in the file,

    Count | 2 byte | Number of Cursors in this file

    and read the header information from the largest cursor, a 'Variant of BMP InfoHeader'. Parsing that part of the header is where Pillow uses code from our BmpImagePlugin.

    After that, CUR images have the palette, the XOR mask and then the AND mask.

  16. mmemoo commented on May 31, 2026

    @mmemoo
    Author

    okay, thanks for the explanation.

  17. radarhere commented on May 31, 2026

    @radarhere
    Member

    you are using an inverted xor mask where there should be an inverted and mask.

    You are correct. The fact that 'Pixels are stored bottom-up' confused me, and meant that I had the AND and XOR masks mislabelled - I had found the right visual result, but my variable names were wrong. I've updated my PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions