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 000000000..342da1228
--- /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("\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("");
+ retval.append(augmentation_string.charAt(idx));
+ if (html && idx != 0)
+ retval.append("");
+ }
+ 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("
\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("