2
votes

I have a Java application where I need to draw text on top of an image. The text, the font, and the image are all determined at run time. The text needs to look nice, yet be readable (sufficiently contrastive) on top of the image.

To meet these requirements, I create a drop shadow. This is done by drawing the text in opaque black, on a blank/transparent BufferedImage, then applying a Gaussian blur filter. I then draw the text again, in opaque white, on top of the drop shadow. So I have opaque white text, with a black blurred shadow around it that quickly fades to full transparency. I can then draw this image on top of the background image.

The problem I'm trying to solve is that the drop shadow seems too transparent. So against bright, busy backgrounds, it doesn't give the white text enough separation.

So how to increase the opacity of the shadow? I've tried increasing the radius of the gaussian blur, and that makes the shadow wider, but doesn't make it more opaque.

The code I'm using is based on this DropShadowDemo by Romain Guy. I use his createDropShadow() and gaussianBlurFilter(). But instead of painting the drop shadow and the text separately during paintComponent(), I draw them both onto a BufferedImage in advance; and I draw this single BufferedImage on top of the background during paintComponent(). Maybe that's my problem? But I don't see how that would decrease the opacity of the shadow. I'm not using g2.setComposite() during paintComponent().

I've looked at adjusting the opacity of the drop shadow using some kind of BufferedImageOp, such as a LookupOp. But it seems like a lot of work for a simple adjustment (creating four arrays of numbers, I guess). I don't think a RescaleOp would work, since I want the result alpha to fall in the same range (0 to 1) as the source alpha. If I could specify a BufferedImageOp that sets new alpha = sqrt(old alpha), or something like that, that would be ideal. But I don't know an easy way to do that.

Details of the code can be seen here:

I would include relevant code blocks here, but it seems like the relevant amount of code is too big (wall of code)... might as well just give links to the source files.

Update

It looks like Change the alpha value of a BufferedImage? would be a way to change the opacity of the drop shadow... basically recalculating the alpha value of each pixel, one by one. TBD: whether it's portable (to 64-bit machines, e.g.), and whether it's fast enough. If I do a = sqrt(a) or a = sin(a * pi * 0.5) on every pixel (thinking of a in the range 0 to 1), will that be slow? I would be happy to know if there's a simpler way that takes advantage of available optimizations, like the built-in BufferedImageOps presumably do. Maybe the answer is building arrays for LookupOp after all. Anybody know of some example code for that?

Final update

Solved using a LookupOp; see code below.

1
Have a look on this question. It's nearly the same question as yours. Instead of setting alpha to a certain value multiply it with a factor.IchBinKeinBaum
You could modify the coefficients of your filter so that it has a sharper drop-off. It would no longer be Gaussian, but I don't imagine that's an issue.Oliver Charlesworth
@OliCharlesworth: how would that make the shadow more opaque? Wouldn't that just be like reducing the blur radius? Maybe I'm not getting what you mean.LarsH
@IchBinKeinBaum: good catch. Sounds like we both found the same related question independently.LarsH

1 Answers

0
votes

Below is the code I ended up with to make a BufferedImage more opaque. I decided to go ahead and use a LookupOp, rather than a potentially unportable and slow loop over getRGB / setRGB on each pixel. The work to set up Lookup arrays wasn't so bad.

/* Make alpha channel more opaque.
 * Modify the alpha (opacity) channel so that values are higher, but still
 * continuous and monotonically increasing.
 */
private static void adjustOpacity(BufferedImage shadowImage) {
    // Use a lookup table with four arrays;
    // the three for RGB are identity arrays (no change).
    byte identityArray[] = new byte[256];
    for (int i=0; i < 256; i++)
        identityArray[i] = (byte)i;

    byte alphaArray[] = new byte[256];
    // map the range (0..256] to (0..pi/2]
    double mapTo90Deg = Math.PI / 2.0 / 256.0;
    for (int i=0; i < 256; i++) {
        alphaArray[i] = (byte)(Math.sin(i * mapTo90Deg) * 256);
    }

    byte[][] tables = {
            identityArray,
            identityArray,
            identityArray,
            alphaArray
    };
    ByteLookupTable blut = new ByteLookupTable(0, tables);
    LookupOp op = new LookupOp(blut, null);

    // With LookupOp, it's ok for src and dest to be the same image.
    op.filter(shadowImage,  shadowImage);
}

It seems to work (though I haven't taken before-and-after screenshots for comparison).