JAI and masking operations
I just spent a few hours tearing my hair out at an peculiar behavior with JAI, and thought to briefly document it in case anybody ran into a similar situation.
I started off from the code on this relatively old article, adapting it to my needs on Groovy. The intention was to:
- Get an image
- Crop it to a square
- Generate a thumbnail
- Add a mask to the thumbnail
The original test code we wrote work perfectly. The mask function was (with control output added):
public applyMask()
{
println("ON APPLY MASK")
println("Mask ${mask.getWidth()} x ${mask.getHeight()}")
println("Alpha ${alpha.getWidth()} x ${alpha.getHeight()}")
println("Image ${image.width} x ${image.height}")
ParameterBlock params = new ParameterBlock();
params.addSource(mask);
params.addSource(image);
params.add(alpha);
params.add(null);
params.add(new Boolean(false));
result = JAI.create("composite", params, null);
println("Result ${result.width} x ${result.height}")
}
Thumbnails were generated with:
def d = new ImageUtils()
d.load("cropped.jpg")
[178, 133, 69].each {
d.thumbnail(it)
println "Created thumbnail"
d.writeResult("Thumb_${it}.jpg", "JPEG")
}
And having generated them, we iterated through them with:
def e = new ImageUtils()
[178, 133, 69].each {
e.load("Thumb_${it}.jpg")
e.loadMask("Mask_${it}.jpg")
e.loadAlpha("Alpha_${it}.jpg")
e.applyMask()
println "Created Masked thumbnail"
e.writeResult("Masked_${it}.jpg", "JPEG")
}
to apply the corresponding masks.
Turns out that when the code was optimized to just handle everything in memory, without an intermediate save, the resulting image ended up missing a few pixels in all sizes. Executing
[178, 133, 69].each {
c.thumbnail(it)
println "Created thumbnail"
c.swapSource() // Assigns the result RenderedOp to copy to work from
c.loadMask("Mask_${it}.jpg")
c.loadAlpha("Alpha_${it}.jpg")
c.applyMask()
println "Created Masked thumbnail"
c.writeResult("Masked_${it}.jpg", "JPEG")
c.restoreOriginal()
}
gave us results like:
Mask 133 x 133 Alpha 133 x 133 Image 133 x 133 Result 133 x 128
This seems to be a JAI bug when working completely from memory RenderedOp sources. Saving a temporary thumbnail and loading it again before applying the mask works just fine.
[178, 133, 69].each {
c.thumbnail(it)
println "Created thumbnail"
c.writeResult("Thumb_${it}.jpg", "JPEG")
c.load("Thumb_${it}.jpg")
c.loadMask("Mask_${it}.jpg")
c.loadAlpha("Alpha_${it}.jpg")
c.applyMask()
}