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
WebP: Fix alpha decoding when source subsampling is used #737
WebP: Fix alpha decoding when source subsampling is used #737
Conversation
Thank you very much! 😀 Code looks reasonable! I'll read though it in more detail and hope to merge it soon. |
I'll merge your PR, as it seems to fix the issue. 👍🏻 However, I would really appreciate if you could also create a test method (in the |
That is great! Thank you!
Sure, I can try to do that. |
I just added an unit test that passes with the new code but fails with the current version.
Unfortunately, none of the existing sample files have alpha transparency and alpha filtering, which is necessary to trigger the issue, so I had to add a new sample file. Let me know if there is another way to avoid adding it. While running the existing unit tests and creating the new one, I found two issues in the code I submitted, fixed now by the commits 3eabc59 and 0a2efb9. |
// Alpha transparency and Alpha filtering | ||
new TestData(getClassLoaderResource("/webp/alpha_filter.webp"), new Dimension(2000, 1447)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have a sample file with smaller dimensions, that would probably be better for test performance. If not, no worries, we can use this one. 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have a sample file with smaller dimensions, that would probably be better for test performance. If not, no worries, we can use this one. 😀
I couldn't find a small sample image, so I replaced by the smallest I found, with ~15% less pixels than before (and a much smaller file in bytes).
PS: There seems to be something odd with the indentation here.
I tried to follow the existing indentation.
It looks fine for me here...
Not sure if is that what called your attention, buy the TestData
above the one I added has multiple dimensions, so its indentation is different, right?
dstRaster.setRect(alphaRaster); | ||
} | ||
else { | ||
VP8LDecoder.copyIntoRasterWithParams(alphaRaster, dstRaster, param); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking maybe we should just allow param
to be null
in the copyIntoRasterWithParams
method? Will be more readable.
Code will also read better with a static import of VP8LDecoder.copyIntoRasterWithParams
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! I made both changes.
@@ -527,7 +527,8 @@ private void readAlpha(BufferedImage destination, ImageReadParam param, final in | |||
System.out.println("compression: " + compression); | |||
} | |||
|
|||
WritableRaster alphaRaster = destination.getAlphaRaster(); | |||
// Alpha raster must have same dimensions as the source, because of gradient filtering. | |||
WritableRaster alphaRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 1, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be able to get away with not creating another full-size temporary raster here. Instead we can do the filtering on the tempRaster
below, by creating a writable child raster on line 544, and do the alpha filtering on that.
Something like this:
WritableRaster alphaRaster = destination.getAlphaRaster();
switch (compression) {
case 0:
readUncompressedAlpha(alphaRaster);
break;
case 1:
// Simulate header
imageInput.seek(imageInput.getStreamPosition() - 5);
WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 4, null);
readVP8Lossless(tempRaster, null, width, height);
// Copy from green (band 1) in temp to alpha in destination
WritableRaster alphaChannel = tempRaster.createWritableChild(0, 0, tempRaster.getWidth(), tempRaster.getHeight(), 0, 0, new int[]{1});
alphaFilter(alphaChannel, filtering);
alphaRaster.setRect(alphaChannel); // TODO: Replace this with copyIntoRasterWithParams(alphaChannel, alphaRaster, param);
break;
default:
processWarningOccurred("Unknown WebP alpha compression: " + compression);
opaqueAlpha(alphaRaster);
break;
}
...and extracting the current alpha filtering to a separate method:
private void alphaFilter(WritableRaster alphaRaster, int filtering) {
if (filtering != AlphaFiltering.NONE) {
for (int y = 0; y < alphaRaster.getHeight(); y++) {
for (int x = 0; x < alphaRaster.getWidth(); x++) {
int predictorAlpha = getPredictorAlpha(alphaRaster, filtering, y, x);
alphaRaster.setSample(x, y, 0, alphaRaster.getSample(x, y, 0) + predictorAlpha % 256);
}
}
}
}
I'm not sure if alpha filtering is supported for uncompressed alpha, and we don't support it anyway (if you have a sample file with uncompressed alpha, please send it to me! 😀 ).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be able to get away with not creating another full-size temporary raster here. Instead we can do the filtering on the tempRaster below, by creating a writable child raster on line 544, and do the alpha filtering on that.
Cool! I made the changes you proposed, and it seems to be working fine.
I'm not sure if alpha filtering is supported for uncompressed alpha, and we don't support it anyway (if you have a sample file with uncompressed alpha, please send it to me! 😀 ).
I checked all WebP files I have here, and couldn't find any with uncompressed alpha.
I will try to collect new ones, and let you know if I can find any of these.
By the way, while looking for an uncompressed alpha sample, I found another WebP file that seems to be valid, but it is triggering an exception (it is single file out of a ~5K test set). This time it seems a much simpler problem. I will open another issue, as it is not related to this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if alpha filtering is supported for uncompressed alpha, and we don't support it anyway (if you have a sample file with uncompressed alpha, please send it to me! 😀 ).
I checked all WebP files I have here, and couldn't find any with uncompressed alpha. I will try to collect new ones, and let you know if I can find any of these.
Looking at a larger set of WebPs (~40K files), I found 3 that are reported as having "alpha compression = 0".
I am not sure if they actually have uncompressed alpha, or are false detections caused by some issue in the files.
They do enter in readUncompressedAlpha()
while decoding.
possibly-uncompressed-alpha-samples.zip
.../imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java
Outdated
Show resolved
Hide resolved
Thanks! |
Co-authored-by: Harald Kuhr <harald.kuhr@gmail.com>
Ok, I think I covered all the requested changes. |
Excellent! You have addressed all my concerns. 👍🏻 |
Analyzing the issue described in #736, I believe it is caused by the way alpha channel may be encoded.
In
readAlpha()
method (referenced below), the alpha channel is decoded into the alpha raster ofdestination
, which can be smaller than the original image, if source subsampling is used.TwelveMonkeys/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
Line 513 in 29f7547
However, in the end of this method, filters may be applied:
TwelveMonkeys/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
Lines 551 to 558 in 29f7547
Inside the
getPredictorAlpha()
function, the values from the previous row and/or column are used to calculate the current alpha value, as shown below.In this case, if subsampling was already applied (as it is today), this function will get a different value (from 2 or more rows/columns before). And as this is an accumulative process, pixels to the right and bottom of the image may have higher errors (as observed in the provided sample images).
TwelveMonkeys/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
Lines 561 to 589 in 29f7547
So I think the solution would be first decoding the alpha channel in a raster with the original dimensions (including applying any filters), then subsample this alpha raster to the destination raster.
As I am not very familiar with TwelveMonkeys' code, probably this can be achieved with a better implementation.
My main goal was to confirm that the proposed approach solves the issue.