Blog About Contact

Adding a Dynamic Watermark to an Image - Java Web Application

Published Wed, 8 Sep 2010 • 4 comments

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 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();
            byte[] imageData =;

            BufferedImage bi = ByteArrayInputStream(imageData));
            watermark(bi, "random="+(int)(Math.random()*1000d));
            byte[] resultData = encodeJPEG(bi, 90);
            OutputStream os = resp.getOutputStream();
        } 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);
                new RenderingHints(RenderingHints.KEY_ANTIALIASING,

        // 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.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);
        byte[] result = baos.toByteArray();
        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) {
            origResponse = response;

        public ServletOutputStream createOutputStream() throws IOException {
            return sos == null ? new ServletOutputStream() {
                public void write(int b) throws IOException {
            } : sos;

        public ServletOutputStream getOutputStream() throws IOException {
            if (sos == null) {
                sos = createOutputStream();
            return sos;

        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.

About the Author

Richard Nichols is an Australian software engineer with a passion for making things.

Follow him on twitter or subscribe by RSS or email.

You might also enjoy reading -

Discuss / Comment

There are 4 comments.

Add a comment

  • {{e.error}}

Thanks for your comment!/

Valid email address required.
Posting message, please wait...