I recently had to come up with a way to add a dynamic watermark to images served in a Java web application.
This was able to be done pretty easily in a Java Servlet Filter using the regular AWT toolkit.
This sample code adds a diagonally rotated watermark on an image in small text. It covers the entire image – you may want something less obtrusive, but remember that watermarks in isolated parts of the image can be cropped out easily.
The fill has a slight gradient to it, to make painting out the watermark more difficult.
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import javax.imageio.ImageIO;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class WatermarkFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getRequestURL().toString().toLowerCase().endsWith(".jpg")) {
OrigResponseWrapper wrap = new OrigResponseWrapper(resp);
chain.doFilter(req, wrap);
if (wrap.writer != null ) wrap.writer.flush();
wrap.sos.flush();
byte[] imageData = wrap.stream.toByteArray();
BufferedImage bi = ImageIO.read(new ByteArrayInputStream(imageData));
watermark(bi, "random="+(int)(Math.random()*1000d));
byte[] resultData = encodeJPEG(bi, 90);
resp.setContentType("image/jpeg");
resp.setContentLength(resultData.length);
OutputStream os = resp.getOutputStream();
os.write(resultData);
os.close();
} else {
chain.doFilter(request, response);
}
}
public void destroy() {
}
private void watermark(BufferedImage original, String watermarkText) {
// create graphics context and enable anti-aliasing
Graphics2D g2d = original.createGraphics();
g2d.scale(1, 1);
g2d.addRenderingHints(
new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// create watermark text shape for rendering
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
GlyphVector fontGV = font.createGlyphVector(g2d.getFontRenderContext(), watermarkText);
Rectangle size = fontGV.getPixelBounds(g2d.getFontRenderContext(), 0, 0);
Shape textShape = fontGV.getOutline();
double textWidth = size.getWidth();
double textHeight = size.getHeight();
AffineTransform rotate45 = AffineTransform.getRotateInstance(Math.PI / 4d);
Shape rotatedText = rotate45.createTransformedShape(textShape);
// use a gradient that repeats 4 times
g2d.setPaint(new GradientPaint(0, 0,
new Color(0f, 0f, 0f, 0.1f),
original.getWidth() / 2, original.getHeight() / 2,
new Color(1f, 1f, 1f, 0.1f)));
g2d.setStroke(new BasicStroke(0.5f));
// step in y direction is calc'ed using pythagoras + 5 pixel padding
double yStep = Math.sqrt(textWidth * textWidth / 2) + 5;
// step over image rendering watermark text
for (double x = -textHeight * 3; x < original.getWidth(); x += (textHeight * 3)) {
double y = -yStep;
for (; y < original.getHeight(); y += yStep) {
g2d.draw(rotatedText);
g2d.fill(rotatedText);
g2d.translate(0, yStep);
}
g2d.translate(textHeight * 3, -(y + yStep));
}
}
private byte[] encodeJPEG(BufferedImage image, int quality) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream((int) ((float) image.getWidth() * image.getHeight() / 4));
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos);
JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(image);
quality = Math.max(0, Math.min(quality, 100));
param.setQuality((float) quality / 100.0f, false);
encoder.setJPEGEncodeParam(param);
encoder.encode(image);
byte[] result = baos.toByteArray();
baos.close();
return result;
}
private class OrigResponseWrapper extends HttpServletResponseWrapper {
protected final HttpServletResponse origResponse;
protected ServletOutputStream sos = null;
protected ByteArrayOutputStream stream = new ByteArrayOutputStream();
protected PrintWriter writer = null;
public OrigResponseWrapper(HttpServletResponse response) {
super(response);
origResponse = response;
}
public ServletOutputStream createOutputStream() throws IOException {
return sos == null ? new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
stream.write(b);
}
} : sos;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (sos == null) {
sos = createOutputStream();
}
return sos;
}
@Override
public PrintWriter getWriter() throws IOException {
sos = getOutputStream();
if (writer == null) {
writer = new PrintWriter(sos);
}
return writer;
}
}
}
And here's some sample results -
Airplane before:
Airplane after:
Helicopter before:
Helicopter after:
This can be easily tweaked to be bigger/smaller, more/less obtrusive.
Related posts:




First of all – great article! What a great CMS a simple Tomcat can be!
Everything worked just fine when I tried it out. Just make sure to send out the correct content length instead of the content length of the original image which might get set during execution of chain.doFilter(…). Otherwise, images might not be fully loaded on your client. I achieved that by adding one line to the “doFilter” method:
byte[] resultData = encodeJPEG(bi, 90);
resp.setContentType(“image/jpeg”);
// the added line:
resp.setContentLength(resultData.length);
OutputStream os = resp.getOutputStream();
os.write(resultData);
os.close();
@Mick – thanks for the heads up! You’re quite right – I’ve fixed the original listing.