diff --git a/apps/imagegen/identicon/README.md b/apps/imagegen/identicon/README.md new file mode 100644 index 000000000..d596b9bd3 --- /dev/null +++ b/apps/imagegen/identicon/README.md @@ -0,0 +1,46 @@ +Identicon +========= + +###License + + + (The MIT License) + + Copyright (c) 2007-2014 Don Park + Contributor 2014-2014 Paulo Miguel Almeida Rodenas + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + 'Software'), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +###Compiling and Running it + ++ On root path run this: + + mvn clean install + ++ To run the example webapp run this in /webappexample folder + + mvn clean compile package tomcat7:run -Ptomcat + ++ Example Urls + + http://localhost:8080/9block?code=-2034984870&size=64 + http://localhost:8080/9block?code=-2034954870&size=64 + http://localhost:8080/9block?code=-2034894870&size=64 + diff --git a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonCache.java b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonCache.java new file mode 100644 index 000000000..0a4a93cfa --- /dev/null +++ b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonCache.java @@ -0,0 +1,11 @@ +package com.docuverse.identicon; + +public interface IdenticonCache { + public byte[] get(String key); + + public void add(String key, byte[] imageData); + + public void remove(String key); + + public void removeAll(); +} diff --git a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonRenderer.java b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonRenderer.java new file mode 100644 index 000000000..af6f7a80c --- /dev/null +++ b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonRenderer.java @@ -0,0 +1,36 @@ +package com.docuverse.identicon; + +import java.awt.image.BufferedImage; +import java.math.BigInteger; + +/** + * Identicon renderer interface. + * + * @author don + * + */ +public interface IdenticonRenderer { + + /** + * Returns rendered identicon image for given identicon code encoded as a + * 32-bit signed integer. + * + * @param code + * identicon code + * @param size + * image size + * @return identicon image + */ + public BufferedImage render(int code, int size); + + /** + * Returns rendered identicon image for given identicon code. + * + * @param code + * identicon code + * @param size + * image size + * @return identicon image + */ + public BufferedImage render(BigInteger code, int size); +} diff --git a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonUtil.java b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonUtil.java new file mode 100644 index 000000000..cfbef5c54 --- /dev/null +++ b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/IdenticonUtil.java @@ -0,0 +1,169 @@ +package com.docuverse.identicon; + +import java.net.InetAddress; +import java.security.MessageDigest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility methods useful for implementing identicon functionality. Methods are + * class methods for convenience. + *

+ * Key method of interest is {@link getIdenticonCode} which converts IP address + * into identicon code.
+ * IMPORTANT: inetSalt value must be set to + * reasonably long random string prior to invoking this method. + *

+ * + * @author don + */ +public class IdenticonUtil { + private static final Log log = LogFactory.getLog(IdenticonUtil.class); + + private static final int DEFAULT_IDENTICON_SIZE = 16; + + private static final int MINIMUM_IDENTICON_SIZE = 15; + + private static final int MAXIMUM_IDENTICON_SIZE = 64; + + private static final int DEFAULT_INET_MASK = 0xffffffff; + + private static int inetMask = DEFAULT_INET_MASK; + + private static String inetSalt; + + /** + * Returns current IP address mask. Default is 0xffffffff. + * + * @return current IP address mask + */ + public static int getInetMask() { + return inetMask; + } + + /** + * Sets current IP address mask. Default is 0xffffffff. + * + * @param inetMask + */ + public static void setInetMask(int inetMask) { + IdenticonUtil.inetMask = inetMask; + } + + /** + * Returns current inetSalt value. + * + * @return + */ + public static String getInetSalt() { + return inetSalt; + } + + /** + * Sets current inetSalt value. + * + * @param inetSalt + */ + public static void setInetSalt(String inetSalt) { + IdenticonUtil.inetSalt = inetSalt; + } + + /** + * Returns identicon code for given IP address. + *

+ * Current implementation uses first four bytes of SHA1(int(mask(ip))+salt) + * where mask(ip) uses inetMask to remove unwanted bits from IP address. + * Also, since salt is a string for convenience sake, int(mask(ip)) is + * converetd into a string and combined with inetSalt prior to hashing. + *

+ * + * @param inetAddr + * IP address + * @return identicon code for inetAddr + * @throws Exception + */ + public static int getIdenticonCode(InetAddress inetAddr) throws Exception { + if (inetSalt == null) + throw new Exception( + "inetSalt must be set prior to retrieving identicon code"); + + byte[] ip = inetAddr.getAddress(); + int ipInt = (((ip[0] & 0xFF) << 24) | ((ip[1] & 0xFF) << 16) + | ((ip[2] & 0xFF) << 8) | (ip[3] & 0xFF)) + & inetMask; + StringBuilder s = new StringBuilder(); + s.append(ipInt); + s.append('+'); + s.append(inetSalt); + MessageDigest md; + md = MessageDigest.getInstance("SHA1"); + byte[] hashedIp = md.digest(s.toString().getBytes("UTF-8")); + int code = ((hashedIp[0] & 0xFF) << 24) | ((hashedIp[1] & 0xFF) << 16) + | ((hashedIp[2] & 0xFF) << 8) | (hashedIp[3] & 0xFF); + return code; + } + + /** + * Returns identicon code specified as an input parameter or derived from an + * IP address. + *

+ * This method is a convenience method intended to be used by servlets like + * below: + *

+ * + *
+	 * int code = IdenticonUtil.getIdenticonCode(request.getParameter("code"), request
+	 * 		.getRemoteAddr());
+	 * 
+ * + * @param codeParam + * code parameter, if null remoteAddr parameter + * will be used to determine the value. + * @param remoteAddr + * HTTP requester's IP address. Optional if code was specified. + * @return + */ + public static int getIdenticonCode(String codeParam, String remoteAddr) { + int code = 0; + try { + if (codeParam != null) { + code = Integer.parseInt(codeParam); + } else { + code = IdenticonUtil.getIdenticonCode(InetAddress + .getByName(remoteAddr)); + } + } catch (Exception e) { + log.error(e); + } + return code; + } + + public static int getIdenticonSize(String param) { + int size = DEFAULT_IDENTICON_SIZE; + try { + String sizeParam = param; + if (sizeParam != null) { + size = Integer.parseInt(sizeParam); + if (size < MINIMUM_IDENTICON_SIZE) + size = MINIMUM_IDENTICON_SIZE; + else if (size > MAXIMUM_IDENTICON_SIZE) + size = MAXIMUM_IDENTICON_SIZE; + } + } catch (Exception e) { + log.error(e); + } + return size; + } + + public static String getIdenticonETag(int code, int size, int version) { + StringBuilder s = new StringBuilder("W/\""); + s.append(Integer.toHexString(code)); + s.append('@'); + s.append(size); + s.append('v'); + s.append(version); + s.append('\"'); + return s.toString(); + } +} diff --git a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java new file mode 100644 index 000000000..0cc69cb79 --- /dev/null +++ b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java @@ -0,0 +1,325 @@ +package com.docuverse.identicon; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.math.BigInteger; + +/** + * 9-block Identicon renderer. + * + *

+ * Current implementation uses only the lower 32 bits of identicon code. + *

+ * + * @author don + * + */ +public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { + + /* + * Each patch is a polygon created from a list of vertices on a 5 by 5 grid. + * Vertices are numbered from 0 to 24, starting from top-left corner of the + * grid, moving left to right and top to bottom. + */ + + private static final int PATCH_GRIDS = 5; + + private static final float DEFAULT_PATCH_SIZE = 20.0f; + + private static final byte PATCH_SYMMETRIC = 1; + + private static final byte PATCH_INVERTED = 2; + + private static final int PATCH_MOVETO = -1; + + private static final byte[] patch0 = { 0, 4, 24, 20 }; + + private static final byte[] patch1 = { 0, 4, 20 }; + + private static final byte[] patch2 = { 2, 24, 20 }; + + private static final byte[] patch3 = { 0, 2, 20, 22 }; + + private static final byte[] patch4 = { 2, 14, 22, 10 }; + + private static final byte[] patch5 = { 0, 14, 24, 22 }; + + private static final byte[] patch6 = { 2, 24, 22, 13, 11, 22, 20 }; + + private static final byte[] patch7 = { 0, 14, 22 }; + + private static final byte[] patch8 = { 6, 8, 18, 16 }; + + private static final byte[] patch9 = { 4, 20, 10, 12, 2 }; + + private static final byte[] patch10 = { 0, 2, 12, 10 }; + + private static final byte[] patch11 = { 10, 14, 22 }; + + private static final byte[] patch12 = { 20, 12, 24 }; + + private static final byte[] patch13 = { 10, 2, 12 }; + + private static final byte[] patch14 = { 0, 2, 10 }; + + private static final byte[] patchTypes[] = { patch0, patch1, patch2, + patch3, patch4, patch5, patch6, patch7, patch8, patch9, patch10, + patch11, patch12, patch13, patch14, patch0 }; + + private static final byte patchFlags[] = { PATCH_SYMMETRIC, 0, 0, 0, + PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, + PATCH_SYMMETRIC + PATCH_INVERTED }; + + private static int centerPatchTypes[] = { 0, 4, 8, 15 }; + + private float patchSize; + + private GeneralPath[] patchShapes; + + // used to center patch shape at origin because shape rotation works + // correctly. + private float patchOffset; + + private Color backgroundColor = Color.WHITE; + + /** + * Constructor. + * + */ + public NineBlockIdenticonRenderer2() { + setPatchSize(DEFAULT_PATCH_SIZE); + } + + /** + * Returns the size in pixels at which each patch will be rendered before + * they are scaled down to requested identicon size. + * + * @return + */ + public float getPatchSize() { + return patchSize; + } + + /** + * Set the size in pixels at which each patch will be rendered before they + * are scaled down to requested identicon size. Default size is 20 pixels + * which means, for 9-block identicon, a 60x60 image will be rendered and + * scaled down. + * + * @param size + * patch size in pixels + */ + public void setPatchSize(float size) { + this.patchSize = size; + this.patchOffset = patchSize / 2.0f; // used to center patch shape at + float patchScale = patchSize / 4.0f; + // origin. + this.patchShapes = new GeneralPath[patchTypes.length]; + for (int i = 0; i < patchTypes.length; i++) { + GeneralPath patch = new GeneralPath(GeneralPath.WIND_NON_ZERO); + boolean moveTo = true; + byte[] patchVertices = patchTypes[i]; + for (int j = 0; j < patchVertices.length; j++) { + int v = (int) patchVertices[j]; + if (v == PATCH_MOVETO) + moveTo = true; + float vx = ((v % PATCH_GRIDS) * patchScale) - patchOffset; + float vy = ((float) Math.floor(((float) v) / PATCH_GRIDS)) + * patchScale - patchOffset; + if (!moveTo) { + patch.lineTo(vx, vy); + } else { + moveTo = false; + patch.moveTo(vx, vy); + } + } + patch.closePath(); + this.patchShapes[i] = patch; + } + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public BufferedImage render(BigInteger code, int size) { + return renderQuilt(code.intValue(), size); + } + + /** + * Returns rendered identicon image for given identicon code. + * + *

+ * Size of the returned identicon image is determined by patchSize set using + * {@link setPatchSize}. Since a 9-block identicon consists of 3x3 patches, + * width and height will be 3 times the patch size. + *

+ * + * @param code + * identicon code + * @param size + * image size + * @return identicon image + */ + public BufferedImage render(int code, int size) { + return renderQuilt(code, size); + } + + protected BufferedImage renderQuilt(int code, int size) { + // ------------------------------------------------- + // PREPARE + // + + // decode the code into parts + // bit 0-1: middle patch type + // bit 2: middle invert + // bit 3-6: corner patch type + // bit 7: corner invert + // bit 8-9: corner turns + // bit 10-13: side patch type + // bit 14: side invert + // bit 15: corner turns + // bit 16-20: blue color component + // bit 21-26: green color component + // bit 27-31: red color component + int middleType = centerPatchTypes[code & 0x3]; + boolean middleInvert = ((code >> 2) & 0x1) != 0; + int cornerType = (code >> 3) & 0x0f; + boolean cornerInvert = ((code >> 7) & 0x1) != 0; + int cornerTurn = (code >> 8) & 0x3; + int sideType = (code >> 10) & 0x0f; + boolean sideInvert = ((code >> 14) & 0x1) != 0; + int sideTurn = (code >> 15) & 0x3; + int blue = (code >> 16) & 0x01f; + int green = (code >> 21) & 0x01f; + int red = (code >> 27) & 0x01f; + + // color components are used at top of the range for color difference + // use white background for now. + // TODO: support transparency. + Color fillColor = new Color(red << 3, green << 3, blue << 3); + + // outline shapes with a noticeable color (complementary will do) if + // shape color and background color are too similar (measured by color + // distance). + Color strokeColor = null; + if (getColorDistance(fillColor, backgroundColor) < 32.0f) + strokeColor = getComplementaryColor(fillColor); + + // ------------------------------------------------- + // RENDER + // + + BufferedImage targetImage = new BufferedImage(size, size, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = targetImage.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + g.setBackground(backgroundColor); + g.clearRect(0, 0, size, size); + + float blockSize = size / 3.0f; + float blockSize2 = blockSize * 2.0f; + + // middle patch + drawPatch(g, blockSize, blockSize, blockSize, middleType, 0, + middleInvert, fillColor, strokeColor); + + // side patchs, starting from top and moving clock-wise + drawPatch(g, blockSize, 0, blockSize, sideType, sideTurn++, sideInvert, + fillColor, strokeColor); + drawPatch(g, blockSize2, blockSize, blockSize, sideType, sideTurn++, + sideInvert, fillColor, strokeColor); + drawPatch(g, blockSize, blockSize2, blockSize, sideType, sideTurn++, + sideInvert, fillColor, strokeColor); + drawPatch(g, 0, blockSize, blockSize, sideType, sideTurn++, sideInvert, + fillColor, strokeColor); + + // corner patchs, starting from top left and moving clock-wise + drawPatch(g, 0, 0, blockSize, cornerType, cornerTurn++, cornerInvert, + fillColor, strokeColor); + drawPatch(g, blockSize2, 0, blockSize, cornerType, cornerTurn++, + cornerInvert, fillColor, strokeColor); + drawPatch(g, blockSize2, blockSize2, blockSize, cornerType, + cornerTurn++, cornerInvert, fillColor, strokeColor); + drawPatch(g, 0, blockSize2, blockSize, cornerType, cornerTurn++, + cornerInvert, fillColor, strokeColor); + + g.dispose(); + + return targetImage; + } + + private void drawPatch(Graphics2D g, float x, float y, float size, + int patch, int turn, boolean invert, Color fillColor, + Color strokeColor) { + assert patch >= 0; + assert turn >= 0; + patch %= patchTypes.length; + turn %= 4; + if ((patchFlags[patch] & PATCH_INVERTED) != 0) + invert = !invert; + + Shape shape = patchShapes[patch]; + double scale = ((double) size) / ((double) patchSize); + float offset = size / 2.0f; + + // paint background + g.setColor(invert ? fillColor : backgroundColor); + g.fill(new Rectangle2D.Float(x, y, size, size)); + + AffineTransform savet = g.getTransform(); + g.translate(x + offset, y + offset); + g.scale(scale, scale); + g.rotate(Math.toRadians(turn * 90)); + + // if stroke color was specified, apply stroke + // stroke color should be specified if fore color is too close to the + // back color. + if (strokeColor != null) { + g.setColor(strokeColor); + g.draw(shape); + } + + // render rotated patch using fore color (back color if inverted) + g.setColor(invert ? backgroundColor : fillColor); + g.fill(shape); + + g.setTransform(savet); + } + + /** + * Returns distance between two colors. + * + * @param c1 + * @param c2 + * @return + */ + private float getColorDistance(Color c1, Color c2) { + float dx = c1.getRed() - c2.getRed(); + float dy = c1.getGreen() - c2.getGreen(); + float dz = c1.getBlue() - c2.getBlue(); + return (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + /** + * Returns complementary color. + * + * @param color + * @return + */ + private Color getComplementaryColor(Color color) { + return new Color(color.getRGB() ^ 0x00FFFFFF); + } +}