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

BMP: Support fake alpha in BMPImageReader #727

Open
tmccombs opened this issue Jan 11, 2023 · 6 comments
Open

BMP: Support fake alpha in BMPImageReader #727

tmccombs opened this issue Jan 11, 2023 · 6 comments

Comments

@tmccombs
Copy link

Is your feature request related to a use case or a problem you are working on? Please describe.
Some user-supplied BMP images use fake alpha. Meaning that even though they use RGB compression,

Describe the solution you'd like
Use a heuristic to determine if a BMP is using fake alpha. In particular, if any of the unused bits are non-zero for any pixel when using 32 bpp and a compression method of RGB (no bitfields), then assume it is used as alpha.

Describe alternatives you've considered
Another alternative would be to unconditionally assume that if there are 32 bpp it is using fake alpha. But that isn't necessarily a good assumption, and I don't even know if it is more common for BMPs with 32 bpp to use fake alpha or not.

** Possible implementations **

This is a little tricky because you can't know if the image is using an alpha channel until you have read the pixels.

I can think of a few ways to do this:

  1. Have getRawImageType do a pass over the pixels if the bitCount is 32 and hasMasks is false to determine if any unused bits are used
  2. Have getRawImageType return TYPE_INT_ARGB if the bitCount is 32 and hasMasks is false, then in read( we keep track of whether we have encountered any pixels with a non-zero alpha. Then if all pizels have an alpha of zero, change them all to have an alpha of 255. And possibly if we encounter a pizel with an alpha of zero, but non-zero RGB, then convert all previous pixels to be opaque, and switch into a mode where we set all future pixels to be opaque. (or maybe there at the end we just create a new BufferedImage with a different colormodel that ignores the alpha?).
  3. Similar to 2, but reversed. We start assuming that the extra bits aren't used for alpha, and set the alpha to fully opaque, until we find a nonzero value in the extra bits, then we change all previous pixels to have an alpha of 0, and switch into a mode where we assume the unused bits contain the alpha value.

Additional context
See the q/rgb32fakealpha.bmp in the bmp suite

tmccombs added a commit to tmccombs/TwelveMonkeys that referenced this issue Jan 11, 2023
@haraldk
Copy link
Owner

haraldk commented Jan 18, 2023

@tmccombs

Thank you for your work and the PR!

I think I understand your use case, and unfortunately transparency in BMPs is a feature that seems to be a bit of an afterthought. Causing different implementations, which doesn't adhere to spec... I guess the fact that formats like Microsofts own ICO will put alpha values in the BMP (DIB) structure just like this "fake" alpha, without writing BI_BITFIELDS or BI_ALPHA_BITFIELDS doesn't help either...

Anyway, I think that q/rgb32fakealpha.bmp in the bmp suite is currently decoded as it should according to the spec. Changing the behavior, like using a heuristic, might just cause unexpected results for other samples which might have rubbish data in the extra bits.

However, the extra bits (alpha values) are kept in the data, even if they aren't displayed as transparency. They are just ignored. You can convert it back to transparency like this (quick and dirty implementation, will only work for 32 bit packed with the normal channel ordering);

int[] rgba = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();

BufferedImage tmp = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
tmp.setRGB(0, 0, image.getWidth(), image.getHeight(), rgba, 0, image.getWidth());

image = tmp;

I think the best/cleanest way to implement a way to "force" the decoder to treat the extra bits as transparency would be to do it using the ImageReadParam, either by an explicit force32BitTransparency setting (preferably with a better name 😉 ), or looking at the destination/destination type, and treating the data as transparency if the destination contains transparency.

@tmccombs
Copy link
Author

Is there a way to determine if the original BMP used 32 bpp?

@haraldk
Copy link
Owner

haraldk commented Jan 19, 2023

Is there a way to determine if the original BMP used 32 bpp?

Yes. If the bitmap info header says the bit count is 32, then... it's 32 bits/pixel. If that's what you mean?

If the bit count is 24, then there is obviously no ambiguity. And if the bit count is 32 and masks are present (ie. BI_BITFIELDS or BI_ALPHA_BITFIELDS) there is no ambiguity; masks decide. However, when the bit count is 32 and there are no masks, the pixel format is supposed to be RGBx, where x must be 0 for all pixels.

The ambiguity starts when we have 32 bits/pixel, no masks and an x > 0... This is BMP "fake alpha". And the problem is that we (the decoder) can't know the intention of the values (could be alpha channel, could be garbage from an uninitialized buffer or just random noise of some sort).

Of course, if you are also "the encoder" of this file or know its origin, you could know what the intention is, and then force the values to become transparency if you like. So I think it makes the most sense to allow that as an option.

@tmccombs
Copy link
Author

Sorry, I should have been more clear. I meant, is there a way to get that information from the final BufferedImage returned by ImageIO.read?

I think that

image.getType() == BufferedImage.TYPE_INT_RGB

would probably work?
Or is there another case where that would be true?

@tmccombs
Copy link
Author

This is what I ended up with:

if (image.getType() == BufferedImage.TYPE_INT_RGB) {
  int[] bandMasks = {0xff0000, 0xff00, 0xff, 0xff000000};
  WritableRaster raster = Raster.createPackedRaster(
      image.getRaster().getDataBuffer(),
      image.getWidth(),
      image.getHeight(),
      image.getWidth(),
      bandMasks,
      null
      );
    image = new BufferedImage(ColorModel.getRGBdefault(), raster, false, null);
}

@haraldk
Copy link
Owner

haraldk commented Jan 19, 2023

👍🏻That should be fairly safe. It’s also much faster than my initial suggestion, as you don’t duplicate the data buffer/copy pixel values.

If you want to be really sure, you could also scan the pixels for non-zero alpha values as you outlined above in the initial request, to avoid a completely transparent image.

@haraldk haraldk changed the title Support fake alpha in BMPImageReader BMP: Support fake alpha in BMPImageReader Aug 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants