diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArt.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArt.java
new file mode 100644
index 0000000000000000000000000000000000000000..342da1228fb585d0a8c485c4b78f73ada98e7abc
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArt.java
@@ -0,0 +1,288 @@
+package net.i2p.imagegen;
+
+/*
+ *
+ * Translated to Java and modified from:
+ *
+ * gnutls lib/extras/randomart.c
+ *
+ */
+
+/* $OpenBSD: key.c,v 1.98 2011/10/18 04:58:26 djm Exp $ */
+/*
+ * Copyright (c) 2000, 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2008 Alexander von Gernler.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import net.i2p.data.DataHelper;
+
+/**
+ * Draw an ASCII-Art representing the fingerprint so human brain can
+ * profit from its built-in pattern recognition ability.
+ * This technique is called "random art" and can be found in some
+ * scientific publications like this original paper:
+ *
+ * "Hash Visualization: a New Technique to improve Real-World Security",
+ * Perrig A. and Song D., 1999, International Workshop on Cryptographic
+ * Techniques and E-Commerce (CrypTEC '99)
+ * sparrow.ece.cmu.edu/~adrian/projects/validation/validation.pdf
+ *
+ * The subject came up in a talk by Dan Kaminsky, too.
+ *
+ * If you see the picture is different, the key is different.
+ * If the picture looks the same, you still know nothing.
+ *
+ * The algorithm used here is a worm crawling over a discrete plane,
+ * leaving a trace (augmenting the field) everywhere it goes.
+ * Movement is taken from dgst_raw 2bit-wise.  Bumping into walls
+ * makes the respective movement vector be ignored for this turn.
+ * Graphs are not unambiguous, because circles in graphs can be
+ * walked in either direction.
+ *
+ * @since 0.9.25
+ */
+public class RandomArt {
+
+    /*
+     * Field sizes for the random art.  Have to be odd, so the starting point
+     * can be in the exact middle of the picture, and FLDBASE should be >=8 .
+     * Else pictures would be too dense, and drawing the frame would
+     * fail, too, because the key type would not fit in anymore.
+     */
+    private static final int	FLDBASE		= 8;
+    private static final int	FLDSIZE_Y	= FLDBASE + 1;
+    private static final int	FLDSIZE_X	= FLDBASE * 2 + 1;
+    /*
+     * Chars to be used after each other every time the worm
+     * intersects with itself.  Matter of taste.
+     */
+    private static final String A_augmentation_string = " .o+=*BOX@%&#/^SE";
+    // https://en.wikipedia.org/wiki/Miscellaneous_Symbols
+    private static final String U_augmentation_string = " \u2600\u2601\u2602\u2603" +
+                                                         "\u2604\u2605\u2606\u2607" +
+                                                         "\u2608\u2609\u260a\u260b" +
+                                                         "\u260c\u260d\u260e\u260f";
+
+    private static final char A_BOX_TOP = '-';
+    private static final char A_BOX_BOTTOM = '-';
+    private static final char A_BOX_LEFT = '|';
+    private static final char A_BOX_RIGHT = '|';
+    private static final char A_BOX_TL = '+';
+    private static final char A_BOX_TR = '+';
+    private static final char A_BOX_BL = '+';
+    private static final char A_BOX_BR = '+';
+
+    // https://en.wikipedia.org/wiki/Box-drawing_characters
+    // these are the thin singles
+    private static final char U_BOX_TOP = '\u2500';
+    private static final char U_BOX_BOTTOM = '\u2500';
+    private static final char U_BOX_LEFT = '\u2502';
+    private static final char U_BOX_RIGHT = '\u2502';
+    private static final char U_BOX_TL = '\u250c';
+    private static final char U_BOX_TR = '\u2510';
+    private static final char U_BOX_BL = '\u2514';
+    private static final char U_BOX_BR = '\u2518';
+
+    private static final int BASE = 0x778899;
+
+    /**
+     *  @param dgst_raw the data to be visualized, recommend 64 bytes or less
+     *  @param key_type output in the first line, recommend 6 chars or less
+     *  @param key_size output in the first line
+     *  @param prefix if non-null, prepend to every line
+     */
+    public static String gnutls_key_fingerprint_randomart(final byte[] dgst_raw,
+					final String key_type,
+					final int key_size,
+					final String prefix,
+					final boolean unicode,
+					final boolean html)
+    {
+        final String augmentation_string = unicode ? U_augmentation_string : A_augmentation_string;
+        final char BOX_TOP = unicode ? U_BOX_TOP : A_BOX_TOP;
+        final char BOX_BOTTOM = unicode ? U_BOX_BOTTOM : A_BOX_BOTTOM;
+        final char BOX_LEFT = unicode ? U_BOX_LEFT : A_BOX_LEFT;
+        final char BOX_RIGHT = unicode ? U_BOX_RIGHT : A_BOX_RIGHT;
+        final char BOX_TL = unicode ? U_BOX_TL : A_BOX_TL;
+        final char BOX_TR = unicode ? U_BOX_TR : A_BOX_TR;
+        final char BOX_BL = unicode ? U_BOX_BL : A_BOX_BL;
+        final char BOX_BR = unicode ? U_BOX_BR : A_BOX_BR;
+        final String NL = System.getProperty("line.separator");
+
+	final int dgst_raw_len = dgst_raw.length;
+	final byte[][] field = new byte[FLDSIZE_X][FLDSIZE_Y];
+	final byte[][] color = new byte[FLDSIZE_X][FLDSIZE_Y];
+	final int len = augmentation_string.length() - 1;
+	int prefix_len = 0;
+
+	if (prefix != null)
+		prefix_len = prefix.length();
+
+	int x = FLDSIZE_X / 2;
+	int y = FLDSIZE_Y / 2;
+
+	/* process raw key */
+	for (int i = 0; i < dgst_raw_len; i++) {
+		int input;
+		/* each byte conveys four 2-bit move commands */
+		input = dgst_raw[i];
+		for (int b = 0; b < 4; b++) {
+			/* evaluate 2 bit, rest is shifted later */
+			x += ((input & 0x1) != 0) ? 1 : -1;
+			y += ((input & 0x2) != 0) ? 1 : -1;
+
+			/* assure we are still in bounds */
+			x = Math.max(x, 0);
+			y = Math.max(y, 0);
+			x = Math.min(x, FLDSIZE_X - 1);
+			y = Math.min(y, FLDSIZE_Y - 1);
+
+			/* augment the field */
+			if ((field[x][y] & 0xff) < len - 2)
+				field[x][y]++;
+			color[x][y] = (byte) i;
+			input = input >> 2;
+		}
+	}
+
+	/* mark starting point and end point */
+	field[FLDSIZE_X / 2][FLDSIZE_Y / 2] = (byte) (len - 1);
+	field[x][y] = (byte) len;
+
+	final String size_txt;
+	if (key_size > 0)
+		size_txt = String.format(" %4d", key_size);
+	else
+		size_txt = "";
+
+	/* fill in retval */
+	StringBuilder retval = new StringBuilder(1024);
+        long base = 0;
+        if (html) {
+		// Pick a color base. We use the first 3 bytes of the data,
+		// but normalize by 75% since we're designed for a white background.
+		int clen = Math.min(3, dgst_raw_len);
+		byte[] cbase = new byte[clen];
+		for (int i = 0; i < clen; i++) {
+			cbase[i] = (byte) ((dgst_raw[i] & 0xff) * 5 / 8);
+		}
+		base = DataHelper.fromLong(cbase, 0, clen);
+		retval.append("<font color=\"#")
+		      .append(getColor(base, 0))
+		      .append("\"><pre>\n");
+	}
+	if (prefix_len > 0)
+		retval.append(String.format("%s" + BOX_TL + BOX_TOP + BOX_TOP + "[%4s%s]",
+			 prefix, key_type, size_txt));
+	else
+		retval.append(String.format("" + BOX_TL + BOX_TOP + BOX_TOP + "[%4s%s]", key_type,
+			 size_txt));
+
+	/* output upper border */
+	for (int i = 0; i < FLDSIZE_X - Math.max(key_type.length(), 4) - 9; i++)
+		retval.append(BOX_TOP);
+	retval.append(BOX_TR);
+	retval.append(NL);
+
+	if (prefix_len > 0) {
+		retval.append(prefix);
+	}
+
+	/* output content */
+	for (y = 0; y < FLDSIZE_Y; y++) {
+		retval.append(BOX_LEFT);
+		for (x = 0; x < FLDSIZE_X; x++) {
+			int idx = Math.min(field[x][y], len);
+			if (html && idx != 0)
+				retval.append("<span style=\"color: #")
+				      .append(getColor(base, color[x][y] & 0xff))
+				      .append("\">");
+			retval.append(augmentation_string.charAt(idx));
+			if (html && idx != 0)
+				retval.append("</span>");
+                }
+		retval.append(BOX_RIGHT);
+		retval.append(NL);
+
+		if (prefix_len > 0) {
+			retval.append(prefix);
+		}
+	}
+
+	/* output lower border */
+	retval.append(BOX_BL);
+	for (int i = 0; i < FLDSIZE_X; i++)
+		retval.append(BOX_BOTTOM);
+	retval.append(BOX_BR);
+	retval.append(NL);
+        if (html)
+		retval.append("</pre></font>\n");
+
+	return retval.toString();
+    }
+
+    private static String getColor(long base, int mod) {
+	if (mod != 0) {
+		//base += mod * 16;
+		//base += mod * 16 * 256;
+		base += mod * 5 * 256 * 256;
+	}
+	if (base > 0xffffff || base < 0)
+		base &= 0xffffff;
+	return String.format("%06x", base);
+    }
+
+    public static void main(String[] args) {
+        try {
+            boolean uni = true;
+            boolean html = true;
+            if (html)
+                System.out.println("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body>");
+            byte[] b = new byte[16];
+            net.i2p.util.RandomSource.getInstance().nextBytes(b);
+            String art = gnutls_key_fingerprint_randomart(b, "SHA", 128, null, uni, html);
+            System.out.println(art);
+            System.out.println("");
+            b = new byte[32];
+            for (int i = 0; i < 5; i++) {
+                net.i2p.util.RandomSource.getInstance().nextBytes(b);
+                art = gnutls_key_fingerprint_randomart(b, "XSHA", 256, null, uni, html);
+                System.out.println(art);
+                System.out.println("");
+            }
+            b = new byte[48];
+            net.i2p.util.RandomSource.getInstance().nextBytes(b);
+            art = gnutls_key_fingerprint_randomart(b, "XXSHA", 384, null, uni, html);
+            System.out.println(art);
+            System.out.println("");
+            b = new byte[64];
+            net.i2p.util.RandomSource.getInstance().nextBytes(b);
+            art = gnutls_key_fingerprint_randomart(b, "XXXSHA", 512, null, uni, html);
+            System.out.println(art);
+            if (html)
+                System.out.println("</body></html>");
+        } catch (RuntimeException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArtServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArtServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..a11c2a3a0e22568b8ab490e38b4405dcc6387aaf
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/RandomArtServlet.java
@@ -0,0 +1,77 @@
+package net.i2p.imagegen;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.docuverse.identicon.IdenticonUtil;
+
+import net.i2p.data.Hash;
+import net.i2p.util.ConvertToHash;
+
+/**
+ * This servlet generates random art (visual identifier) images.
+ * 
+ * @author modiied from identicon
+ * @since 0.9.25
+ */
+public class RandomArtServlet extends HttpServlet {
+
+	private static final long serialVersionUID = -3507466186902317988L;
+	private static final String PARAM_IDENTICON_CODE_SHORT = "c";
+	private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000;
+	private int version = 1;
+	private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS;
+
+	@Override
+	protected void doGet(HttpServletRequest request,
+			HttpServletResponse response) throws ServletException, IOException {
+
+		String codeParam = request.getParameter(PARAM_IDENTICON_CODE_SHORT);
+		boolean codeSpecified = codeParam != null && codeParam.length() > 0;
+		if (!codeSpecified)
+			codeParam="stats.i2p";
+		String identiconETag = IdenticonUtil.getIdenticonETag(codeParam.hashCode(), 0,
+				version);
+		String requestETag = request.getHeader("If-None-Match");
+
+		if (requestETag != null && requestETag.equals(identiconETag)) {
+			response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+		} else {
+			Hash h = ConvertToHash.getHash(codeParam);
+			if (h == null) {
+				response.setStatus(403);
+			} else {
+				boolean html = true;
+				StringBuilder buf = new StringBuilder(512);
+				if (html) {
+					response.setContentType("text/html");
+					buf.append("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body>");
+				} else {
+					response.setContentType("text/plain");
+				}
+				buf.append(RandomArt.gnutls_key_fingerprint_randomart(h.getData(), "SHA", 256, "", true, html));
+				if (html)
+					buf.append("</body></html>");
+
+				// set ETag and, if code was provided, Expires header
+				response.setHeader("ETag", identiconETag);
+				if (codeSpecified) {
+					long expires = System.currentTimeMillis()
+							+ identiconExpiresInMillis;
+					response.addDateHeader("Expires", expires);
+				}
+	
+				// return image bytes to requester
+				byte[] imageBytes = buf.toString().getBytes("UTF-8");
+				response.setContentLength(imageBytes.length);
+				response.getOutputStream().write(imageBytes);
+			}
+		}
+	}
+}