diff --git a/apps/imagegen/imagegen/README.txt b/apps/imagegen/imagegen/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..95646af3035fb2f9716bbce3d93f6e82273b55da
--- /dev/null
+++ b/apps/imagegen/imagegen/README.txt
@@ -0,0 +1,2 @@
+Servlets based on the example from the identicon package.
+License: See ../identicon/README.md
diff --git a/apps/imagegen/imagegen/build.xml b/apps/imagegen/imagegen/build.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d16eab6d7ac62e92bf1fe1b8636d589728449590
--- /dev/null
+++ b/apps/imagegen/imagegen/build.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project basedir="." default="all" name="imagegen">
+    <property name="project" value="imagegen" />
+    <property name="jetty" value="../../jetty/" />
+    <property name="lib" value="${jetty}/jettylib" />
+    <path id="cp">
+        <pathelement location="${lib}/javax.servlet.jar" />
+        <pathelement location="../identicon/build/identicon.jar" />
+        <pathelement location="../zxing/build/zxing.jar" />
+        <pathelement location="../../../build/i2p.jar" />
+    </path>
+
+    <target name="all" depends="war" />
+    <target name="build" depends="builddep, war" />
+    <target name="builddep">
+        <!-- run from top level build.xml to get dependencies built -->
+    </target>
+    <condition property="depend.available">
+        <typefound name="depend" />
+    </condition>
+    <target name="depend" if="depend.available">
+        <depend
+            cache="../../../build"
+            srcdir="./webapp/src/main/java" 
+            destdir="./build/obj" >
+        </depend>
+    </target>
+
+    <!-- only used if not set by a higher build.xml -->
+    <property name="javac.compilerargs" value="" />
+    <property name="javac.version" value="1.6" />
+
+    <target name="compile" depends="depend">
+        <mkdir dir="./build" />
+        <mkdir dir="./build/WEB-INF" />
+        <mkdir dir="./build/WEB-INF/classes" />
+        <javac srcdir="./webapp/src/main/java" debug="true" deprecation="on" source="${javac.version}" target="${javac.version}"
+               includeAntRuntime="false"
+ 	       classpathref="cp"
+               destdir="./build/WEB-INF/classes" >
+            <compilerarg line="${javac.compilerargs}" />
+        </javac>
+    </target>
+
+    <target name="listChangedFiles" if="shouldListChanges" >
+        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="list" />
+            <arg value="changed" />
+            <arg value="." />
+        </exec>
+        <!-- \n in an attribute value generates an invalid manifest -->
+        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-s" />
+            <arg value="[:space:]" />
+            <arg value="," />
+        </exec>
+    </target>
+
+    <target name="war" depends="compile, warUpToDate" unless="war.uptodate" > 
+        <!-- set if unset -->
+        <property name="workspace.changes.tr" value="" />
+        <!-- put the identicon and zxing classes in the war -->
+        <copy todir="build/WEB-INF/classes">
+            <fileset dir="../identicon/build/obj"/>
+            <fileset dir="../zxing/build/obj"/>
+        </copy>
+        <war destfile="build/${project}.war" webxml="webapp/src/main/webapp/WEB-INF/web.xml">
+            <fileset dir="build">
+                <include name="WEB-INF/**/*.class"/>
+            </fileset>
+            <fileset dir="webapp/src/main/webapp"/>
+            <manifest>
+                <attribute name="Implementation-Version" value="${full.version}" />
+                <attribute name="Built-By" value="${build.built-by}" />
+                <attribute name="Build-Date" value="${build.timestamp}" />
+                <attribute name="Base-Revision" value="${workspace.version}" />
+                <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+            </manifest>
+        </war>
+    </target>
+
+    <target name="warUpToDate">
+        <uptodate property="war.uptodate" targetfile="${project}.war">
+            <srcfiles dir= "." includes="WEB-INF/web-out.xml WEB-INF/**/*.class images/*.png css.css index.html WEB-INF/classes/${project}.properties" />
+        </uptodate>
+        <condition property="shouldListChanges" >
+            <and>
+                <not>
+                    <isset property="war.uptodate" />
+                </not>
+                <isset property="mtn.available" />
+            </and>
+        </condition>
+    </target>
+    
+    <target name="javadoc">
+        <mkdir dir="./build" />
+        <mkdir dir="./build/javadoc" />
+        <javadoc 
+            sourcepath="./webapp/src/main/java" destdir="./build/javadoc" 
+            packagenames="*" 
+            use="true" 
+            splitindex="true" 
+            windowtitle="imagegen webapp" />
+    </target>
+    <target name="clean">
+        <delete dir="./build" />
+        <delete dir="./buildTest" />
+    </target>
+    <target name="cleandep" depends="clean">
+    </target>
+    <target name="distclean" depends="clean">
+    </target>
+</project>
diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..e722c66d6f86c02e5048bdcfdd23dd21a8f86a9c
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java
@@ -0,0 +1,171 @@
+package net.i2p.imagegen;
+
+import java.awt.image.RenderedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.imageio.ImageIO;
+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.IdenticonCache;
+import com.docuverse.identicon.IdenticonRenderer;
+import com.docuverse.identicon.IdenticonUtil;
+import com.docuverse.identicon.NineBlockIdenticonRenderer2;
+
+import net.i2p.data.Hash;
+import net.i2p.util.ConvertToHash;
+
+
+/**
+ * This servlet generates <i>identicon</i> (visual identifier) images ranging
+ * from 16x16 to 512x512 in size.
+ * 
+ * <h5>Supported Image Formats</h5>
+ * <p>
+ * Currently only PNG is supported because <code>javax.imageio</code> package
+ * does not come with built-in GIF encoder and PNG is the only remaining
+ * reasonable format.
+ * </p>
+ * <h5>Initialization Parameters:</h5>
+ * <blockquote>
+ * <dl>
+ * <dt>inetSalt</dt>
+ * <dd>salt used to generate identicon code with. must be fairly long.
+ * (Required)</dd>
+ * <dt>cacheProvider</dt>
+ * <dd>full class path to <code>IdenticonCache</code> implementation.
+ * (Optional)</dd>
+ * </dl>
+ * </blockquote>
+ * <h5>Request ParametersP</h5>
+ * <blockquote>
+ * <dl>
+ * <dt>code</dt>
+ * <dd>identicon code to render. If missing, requester's IP addresses is used
+ * to generated one. (Optional)</dd>
+ * <dt>size</dt>
+ * <dd>identicon size in pixels. If missing, a 16x16 pixels identicon is
+ * returned. Minimum size is 16 and maximum is 64. (Optional)</dd>
+ * </dl>
+ * </blockquote>
+ * 
+ * @author don
+ * @since 0.9.25
+ */
+public class IdenticonServlet extends HttpServlet {
+
+	private static final long serialVersionUID = -3507466186902317988L;
+	private static final String INIT_PARAM_VERSION = "version";
+	private static final String INIT_PARAM_CACHE_PROVIDER = "cacheProvider";
+	private static final String PARAM_IDENTICON_SIZE_SHORT = "s";
+	private static final String PARAM_IDENTICON_CODE_SHORT = "c";
+	private static final String IDENTICON_IMAGE_FORMAT = "PNG";
+	private static final String IDENTICON_IMAGE_MIMETYPE = "image/png";
+	private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000;
+	private int version = 1;
+	private final IdenticonRenderer renderer = new NineBlockIdenticonRenderer2();
+	private IdenticonCache cache;
+	private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS;
+
+	@Override
+	public void init(ServletConfig cfg) throws ServletException {
+		super.init(cfg);
+
+		// Since identicons cache expiration is very long, version is
+		// used in ETag to force identicons to be updated as needed.
+		// Change veresion whenever rendering codes changes result in
+		// visual changes.
+		if (cfg.getInitParameter(INIT_PARAM_VERSION) != null)
+			this.version = Integer.parseInt(cfg
+					.getInitParameter(INIT_PARAM_VERSION));
+
+		String cacheProvider = cfg.getInitParameter(INIT_PARAM_CACHE_PROVIDER);
+		if (cacheProvider != null) {
+			try {
+				Class cacheClass = Class.forName(cacheProvider);
+				this.cache = (IdenticonCache) cacheClass.newInstance();
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	@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) {
+			response.setStatus(403);
+			return;
+		}
+		String sizeParam = request.getParameter(PARAM_IDENTICON_SIZE_SHORT);
+		int size = 32;
+		if (sizeParam != null) {
+			try {
+				size = Integer.parseInt(sizeParam);
+				if (size < 16)
+					size = 16;
+				else if (size > 512)
+					size = 512;
+			} catch (NumberFormatException nfe) {}
+		}
+
+		String identiconETag = IdenticonUtil.getIdenticonETag(codeParam.hashCode(), size,
+				version);
+		String requestETag = request.getHeader("If-None-Match");
+
+		if (requestETag != null && requestETag.equals(identiconETag)) {
+			response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+		} else {
+			// we try to interpret the codeParam parameter as:
+			// 1) a number
+			// 2) a base32 or base64 hash, which we take the Java hashcode of
+			// 3) a string, which we take the Java hashcode of
+			int code;
+			try {
+				code = Integer.parseInt(codeParam);
+			} catch (NumberFormatException nfe) {
+				Hash h = ConvertToHash.getHash(codeParam);
+				if (h != null)
+					code = Arrays.hashCode(h.getData());
+				else
+					code = codeParam.hashCode();
+			}
+			byte[] imageBytes = null;
+
+			// retrieve image bytes from either cache or renderer
+			if (cache == null
+					|| (imageBytes = cache.get(identiconETag)) == null) {
+				ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+				RenderedImage image = renderer.render(code, size);
+				ImageIO.write(image, IDENTICON_IMAGE_FORMAT, byteOut);
+				imageBytes = byteOut.toByteArray();
+				if (cache != null)
+					cache.add(identiconETag, imageBytes);
+			} else {
+				response.setStatus(403);
+				return;
+			}
+
+			// 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
+			response.setContentType(IDENTICON_IMAGE_MIMETYPE);
+			response.setContentLength(imageBytes.length);
+			response.getOutputStream().write(imageBytes);
+		}
+	}
+}
diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f0321521663c346bfafc98b2d1533d063249789
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java
@@ -0,0 +1,138 @@
+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.IdenticonCache;
+import com.docuverse.identicon.IdenticonUtil;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageConfig;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+
+/**
+ * This servlet generates QR code images.
+ * 
+ * @author modiied from identicon
+ * @since 0.9.25
+ */
+public class QRServlet extends HttpServlet {
+
+	private static final long serialVersionUID = -3507466186902317988L;
+	private static final String INIT_PARAM_VERSION = "version";
+	private static final String INIT_PARAM_CACHE_PROVIDER = "cacheProvider";
+	private static final String PARAM_IDENTICON_SIZE_SHORT = "s";
+	private static final String PARAM_IDENTICON_CODE_SHORT = "c";
+	private static final String IDENTICON_IMAGE_FORMAT = "PNG";
+	private static final String IDENTICON_IMAGE_MIMETYPE = "image/png";
+	private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000;
+	private int version = 1;
+	private IdenticonCache cache;
+	private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS;
+
+	@Override
+	public void init(ServletConfig cfg) throws ServletException {
+		super.init(cfg);
+
+		// Since identicons cache expiration is very long, version is
+		// used in ETag to force identicons to be updated as needed.
+		// Change veresion whenever rendering codes changes result in
+		// visual changes.
+		if (cfg.getInitParameter(INIT_PARAM_VERSION) != null)
+			this.version = Integer.parseInt(cfg
+					.getInitParameter(INIT_PARAM_VERSION));
+
+		String cacheProvider = cfg.getInitParameter(INIT_PARAM_CACHE_PROVIDER);
+		if (cacheProvider != null) {
+			try {
+				Class cacheClass = Class.forName(cacheProvider);
+				this.cache = (IdenticonCache) cacheClass.newInstance();
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	@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) {
+			// TODO 404
+			codeParam="http://stats.i2p/?i2paddresshelper=Okd5sN9hFWx-sr0HH8EFaxkeIMi6PC5eGTcjM1KB7uQ0ffCUJ2nVKzcsKZFHQc7pLONjOs2LmG5H-2SheVH504EfLZnoB7vxoamhOMENnDABkIRGGoRisc5AcJXQ759LraLRdiGSR0WTHQ0O1TU0hAz7vAv3SOaDp9OwNDr9u902qFzzTKjUTG5vMTayjTkLo2kOwi6NVchDeEj9M7mjj5ySgySbD48QpzBgcqw1R27oIoHQmjgbtbmV2sBL-2Tpyh3lRe1Vip0-K0Sf4D-Zv78MzSh8ibdxNcZACmZiVODpgMj2ejWJHxAEz41RsfBpazPV0d38Mfg4wzaS95R5hBBo6SdAM4h5vcZ5ESRiheLxJbW0vBpLRd4mNvtKOrcEtyCvtvsP3FpA-6IKVswyZpHgr3wn6ndDHiVCiLAQZws4MsIUE1nkfxKpKtAnFZtPrrB8eh7QO9CkH2JBhj7bG0ED6mV5~X5iqi52UpsZ8gnjZTgyG5pOF8RcFrk86kHxAAAA";
+			//response.setStatus(403);
+			//return;
+		}
+
+		String sizeParam = request.getParameter(PARAM_IDENTICON_SIZE_SHORT);
+		// very rougly, number of "modules" is about 4 * sqrt(chars)
+		// (assuming 7 bit) default margin each side is 4
+		// assuming level L
+		// min modules is 21x21
+		// shoot for 2 pixels per module
+                int size = Math.max(50, (2 * 4) + (int) (2 * 5 * Math.sqrt(codeParam.length())));
+		if (sizeParam != null) {
+			try {
+				size = Integer.parseInt(sizeParam);
+				if (size < 40)
+					size = 40;
+				else if (size > 512)
+					size = 512;
+			} catch (NumberFormatException nfe) {}
+		}
+
+		String identiconETag = IdenticonUtil.getIdenticonETag(codeParam.hashCode(), size,
+				version);
+		String requestETag = request.getHeader("If-None-Match");
+
+		if (requestETag != null && requestETag.equals(identiconETag)) {
+			response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+		} else {
+			byte[] imageBytes = null;
+
+			// retrieve image bytes from either cache or renderer
+			if (cache == null
+					|| (imageBytes = cache.get(identiconETag)) == null) {
+				ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+				QRCodeWriter qrcw = new QRCodeWriter();
+				BitMatrix matrix;
+				try {
+					matrix = qrcw.encode(codeParam, BarcodeFormat.QR_CODE, size, size);
+				} catch (WriterException we) {
+					throw new IOException("encode failed", we);
+				}
+				MatrixToImageWriter.writeToStream(matrix, IDENTICON_IMAGE_FORMAT, byteOut);
+				imageBytes = byteOut.toByteArray();
+				if (cache != null)
+					cache.add(identiconETag, imageBytes);
+			} else {
+				response.setStatus(403);
+				return;
+			}
+
+			// 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
+			response.setContentType(IDENTICON_IMAGE_MIMETYPE);
+			response.setContentLength(imageBytes.length);
+			response.getOutputStream().write(imageBytes);
+		}
+	}
+}
diff --git a/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml b/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7e6ff2edfed9faf2f15251f68dbe7fd972be2ee9
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+    <display-name>WebAppExample - Identicon</display-name>
+
+    <servlet>
+     <servlet-name>net.i2p.imagegen.IdenticonServlet</servlet-name>
+     <servlet-class>net.i2p.imagegen.IdenticonServlet</servlet-class>
+     <load-on-startup>1</load-on-startup>
+    </servlet>
+
+    <servlet>
+     <servlet-name>net.i2p.imagegen.QRServlet</servlet-name>
+     <servlet-class>net.i2p.imagegen.QRServlet</servlet-class>
+     <load-on-startup>1</load-on-startup>
+    </servlet>
+     
+    <servlet>
+     <servlet-name>net.i2p.imagegen.RandomArtServlet</servlet-name>
+     <servlet-class>net.i2p.imagegen.RandomArtServlet</servlet-class>
+     <load-on-startup>1</load-on-startup>
+    </servlet>
+     
+    <!-- precompiled servlets -->
+    
+    <servlet-mapping> 
+      <servlet-name>net.i2p.imagegen.IdenticonServlet</servlet-name>
+      <url-pattern>/id</url-pattern>
+    </servlet-mapping>
+    
+    <servlet-mapping> 
+      <servlet-name>net.i2p.imagegen.QRServlet</servlet-name>
+      <url-pattern>/qr</url-pattern>
+    </servlet-mapping>
+    
+    <servlet-mapping> 
+      <servlet-name>net.i2p.imagegen.RandomArtServlet</servlet-name>
+      <url-pattern>/ra</url-pattern>
+    </servlet-mapping>
+    
+    <!-- this webapp doesn't actually use sessions or cookies -->
+    <session-config>
+        <session-timeout>
+            30
+        </session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
+    </session-config>
+
+</web-app>
diff --git a/apps/imagegen/imagegen/webapp/src/main/webapp/index.html b/apps/imagegen/imagegen/webapp/src/main/webapp/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e1ecc73ba9be085f205b926a655fc413cfdfc449
--- /dev/null
+++ b/apps/imagegen/imagegen/webapp/src/main/webapp/index.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html
+        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <title>Identicon Canvas Test</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+</head>
+<body>
+<h2>Server-side ID</h2>
+<img src="id?c=-2044886870&amp;s=15" width=15 height=15>
+<img src="id?c=-2044886870&amp;s=21" width=21 height=21>
+<img src="id?c=-2044886870&amp;s=30" width=30 height=30>
+<img src="id?c=-2044886870&amp;s=48" width=48 height=48>
+<img src="id?c=-2044886870&amp;s=64" width=64 height=64>
+<img src="id?c=-2044886870&amp;s=128" width=128 height=128>
+
+<h2>Server-side QR</h2>
+<img src="qr?c=https%3a%2f%2fgeti2p.net%2f&amp;s=128" width=128 height=128>
+<img src="qr?c=https%3a%2f%2fgeti2p.net%2f&amp;s=128" width=128 height=128>
+</body>
+</html>