├── src └── com │ └── docuverse │ └── identicon │ ├── IdenticonCache.java │ ├── IdenticonRenderer.java │ ├── MemoryIdenticonCache.java │ ├── IdenticonUtil.java │ ├── IdenticonServlet.java │ ├── NineBlockIdenticonRenderer.java │ └── NineBlockIdenticonRenderer2.java ├── README.md └── identicon-canvas ├── identicon_canvas_test.htm └── identicon_canvas.js /src/com/docuverse/identicon/IdenticonCache.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | public interface IdenticonCache { 4 | public byte[] get(String key); 5 | 6 | public void add(String key, byte[] imageData); 7 | 8 | public void remove(String key); 9 | 10 | public void removeAll(); 11 | } 12 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/IdenticonRenderer.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.math.BigInteger; 5 | 6 | /** 7 | * Identicon renderer interface. 8 | * 9 | * @author don 10 | * 11 | */ 12 | public interface IdenticonRenderer { 13 | 14 | /** 15 | * Returns rendered identicon image for given identicon code encoded as a 16 | * 32-bit signed integer. 17 | * 18 | * @param code 19 | * identicon code 20 | * @param size 21 | * image size 22 | * @return identicon image 23 | */ 24 | public BufferedImage render(int code, int size); 25 | 26 | /** 27 | * Returns rendered identicon image for given identicon code. 28 | * 29 | * @param code 30 | * identicon code 31 | * @param size 32 | * image size 33 | * @return identicon image 34 | */ 35 | public BufferedImage render(BigInteger code, int size); 36 | } 37 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/MemoryIdenticonCache.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | 6 | import com.whirlycott.cache.Cache; 7 | import com.whirlycott.cache.CacheException; 8 | import com.whirlycott.cache.CacheManager; 9 | 10 | public class MemoryIdenticonCache implements IdenticonCache { 11 | private static final Log log = LogFactory 12 | .getLog(MemoryIdenticonCache.class); 13 | 14 | private Cache cache; 15 | 16 | public MemoryIdenticonCache() { 17 | try { 18 | cache = CacheManager.getInstance().getCache("identicon"); 19 | } catch (CacheException e) { 20 | log.error(e); 21 | } 22 | } 23 | 24 | public byte[] get(String key) { 25 | if (cache != null) 26 | return (byte[]) cache.retrieve(key); 27 | else 28 | return null; 29 | } 30 | 31 | public void add(String key, byte[] imageData) { 32 | if (cache != null) 33 | cache.store(key, imageData); 34 | } 35 | 36 | public void remove(String key) { 37 | if (cache != null) 38 | cache.remove(key); 39 | } 40 | 41 | public void removeAll() { 42 | if (cache != null) 43 | cache.clear(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### License 2 | ``` 3 | (The MIT License) 4 | 5 | Copyright (c) 2007-2012 Don Park 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | ``` 26 | 27 | ### Alternate Versions 28 | 29 | This project exists for archival purpose. 30 | To see the code running, I recommend [Mavenized version](https://github.com/PauloMigAlmeida/identicon) by [@PauloMigAlmeida](https://github.com/PauloMigAlmeida). 31 | -------------------------------------------------------------------------------- /identicon-canvas/identicon_canvas_test.htm: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | Identicon Canvas Test 8 | 9 | 10 | 11 | 12 |

Client-side Canvas-based Identicons

13 | 14 | 15 | 16 | 17 |

Server-side Rend and Scale Identicons

18 | 19 | 20 | 21 | 22 |

Client-side Canvas-based Identicons with Fallback to Server-Side *

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

* Safari will display both because it ignores canvas end tags

36 | 37 | 38 | -------------------------------------------------------------------------------- /identicon-canvas/identicon_canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | Client-side Canvas tag based Identicon rendering code 3 | 4 | @author Don Park 5 | @version 0.2 6 | @date January 21th, 2007 7 | */ 8 | 9 | var patch0 = new Array( 0, 4, 24, 20 ); 10 | var patch1 = new Array( 0, 4, 20 ); 11 | var patch2 = new Array( 2, 24, 20 ); 12 | var patch3 = new Array( 0, 2, 20, 22 ); 13 | var patch4 = new Array( 2, 14, 22, 10 ); 14 | var patch5 = new Array( 0, 14, 24, 22 ); 15 | var patch6 = new Array( 2, 24, 22, 13, 11, 22, 20 ); 16 | var patch7 = new Array( 0, 14, 22 ); 17 | var patch8 = new Array( 6, 8, 18, 16 ); 18 | var patch9 = new Array( 4, 20, 10, 12, 2 ); 19 | var patch10 = new Array( 0, 2, 12, 10 ); 20 | var patch11 = new Array( 10, 14, 22 ); 21 | var patch12 = new Array( 20, 12, 24 ); 22 | var patch13 = new Array( 10, 2, 12 ); 23 | var patch14 = new Array( 0, 2, 10 ); 24 | var patchTypes = new Array( patch0, patch1, patch2, patch3, patch4, 25 | patch5, patch6, patch7, patch8, patch9, patch10, patch11, 26 | patch12, patch13, patch14, patch0 ); 27 | var centerPatchTypes = new Array(0, 4, 8, 15); 28 | 29 | function render_identicon_patch(ctx, x, y, size, patch, turn, invert, foreColor, backColor) { 30 | patch %= patchTypes.length; 31 | turn %= 4; 32 | if (patch == 15) 33 | invert = !invert; 34 | 35 | var vertices = patchTypes[patch]; 36 | var offset = size / 2; 37 | var scale = size / 4; 38 | 39 | ctx.save(); 40 | 41 | // paint background 42 | ctx.fillStyle = invert ? foreColor : backColor; 43 | ctx.fillRect(x, y, size, size); 44 | 45 | // build patch path 46 | ctx.translate(x + offset, y + offset); 47 | ctx.rotate(turn * Math.PI / 2); 48 | ctx.beginPath(); 49 | ctx.moveTo((vertices[0] % 5 * scale - offset), (Math.floor(vertices[0] / 5) * scale - offset)); 50 | for (var i = 1; i < vertices.length; i++) 51 | ctx.lineTo((vertices[i] % 5 * scale - offset), (Math.floor(vertices[i] / 5) * scale - offset)); 52 | ctx.closePath(); 53 | 54 | // offset and rotate coordinate space by patch position (x, y) and 55 | // 'turn' before rendering patch shape 56 | 57 | // render rotated patch using fore color (back color if inverted) 58 | ctx.fillStyle = invert ? backColor : foreColor; 59 | ctx.fill(); 60 | 61 | // restore rotation 62 | ctx.restore(); 63 | } 64 | 65 | function render_identicon(node, code, size) { 66 | if (!node || !code || !size) return; 67 | 68 | var patchSize = size / 3; 69 | var middleType = centerPatchTypes[code & 3]; 70 | var middleInvert = ((code >> 2) & 1) != 0; 71 | var cornerType = (code >> 3) & 15; 72 | var cornerInvert = ((code >> 7) & 1) != 0; 73 | var cornerTurn = (code >> 8) & 3; 74 | var sideType = (code >> 10) & 15; 75 | var sideInvert = ((code >> 14) & 1) != 0; 76 | var sideTurn = (code >> 15) & 3; 77 | var blue = (code >> 16) & 31; 78 | var green = (code >> 21) & 31; 79 | var red = (code >> 27) & 31; 80 | var foreColor = "rgb(" + (red << 3) + "," + (green << 3) + "," + (blue << 3) + ")"; 81 | var backColor = "rgb(255,255,255)"; 82 | 83 | var ctx = node.getContext("2d"); 84 | 85 | // middle patch 86 | render_identicon_patch(ctx, patchSize, patchSize, patchSize, middleType, 0, middleInvert, foreColor, backColor); 87 | // side patchs, starting from top and moving clock-wise 88 | render_identicon_patch(ctx, patchSize, 0, patchSize, sideType, sideTurn++, sideInvert, foreColor, backColor); 89 | render_identicon_patch(ctx, patchSize * 2, patchSize, patchSize, sideType, sideTurn++, sideInvert, foreColor, backColor); 90 | render_identicon_patch(ctx, patchSize, patchSize * 2, patchSize, sideType, sideTurn++, sideInvert, foreColor, backColor); 91 | render_identicon_patch(ctx, 0, patchSize, patchSize, sideType, sideTurn++, sideInvert, foreColor, backColor); 92 | // corner patchs, starting from top left and moving clock-wise 93 | render_identicon_patch(ctx, 0, 0, patchSize, cornerType, cornerTurn++, cornerInvert, foreColor, backColor); 94 | render_identicon_patch(ctx, patchSize * 2, 0, patchSize, cornerType, cornerTurn++, cornerInvert, foreColor, backColor); 95 | render_identicon_patch(ctx, patchSize * 2, patchSize * 2, patchSize, cornerType, cornerTurn++, cornerInvert, foreColor, backColor); 96 | render_identicon_patch(ctx, 0, patchSize * 2, patchSize, cornerType, cornerTurn++, cornerInvert, foreColor, backColor); 97 | } 98 | 99 | function render_identicon_canvases(prefix) { 100 | var canvases = document.getElementsByTagName("canvas"); 101 | var n = canvases.length; 102 | for (var i = 0; i < n; i++) { 103 | var node = canvases[i]; 104 | if (node.title && node.title.indexOf(prefix) == 0) { 105 | if (node.style.display == 'none') node.style.display = "inline"; 106 | var code = node.title.substring(prefix.length) * 1; 107 | var size = node.width; 108 | render_identicon(node, code, size); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/IdenticonUtil.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import java.net.InetAddress; 4 | import java.security.MessageDigest; 5 | 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | 9 | /** 10 | * Utility methods useful for implementing identicon functionality. Methods are 11 | * class methods for convenience. 12 | *

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

18 | * 19 | * @author don 20 | */ 21 | public class IdenticonUtil { 22 | private static final Log log = LogFactory.getLog(IdenticonUtil.class); 23 | 24 | private static final int DEFAULT_IDENTICON_SIZE = 16; 25 | 26 | private static final int MINIMUM_IDENTICON_SIZE = 15; 27 | 28 | private static final int MAXIMUM_IDENTICON_SIZE = 64; 29 | 30 | private static final int DEFAULT_INET_MASK = 0xffffffff; 31 | 32 | private static int inetMask = DEFAULT_INET_MASK; 33 | 34 | private static String inetSalt; 35 | 36 | /** 37 | * Returns current IP address mask. Default is 0xffffffff. 38 | * 39 | * @return current IP address mask 40 | */ 41 | public static int getInetMask() { 42 | return inetMask; 43 | } 44 | 45 | /** 46 | * Sets current IP address mask. Default is 0xffffffff. 47 | * 48 | * @param inetMask 49 | */ 50 | public static void setInetMask(int inetMask) { 51 | IdenticonUtil.inetMask = inetMask; 52 | } 53 | 54 | /** 55 | * Returns current inetSalt value. 56 | * 57 | * @return 58 | */ 59 | public static String getInetSalt() { 60 | return inetSalt; 61 | } 62 | 63 | /** 64 | * Sets current inetSalt value. 65 | * 66 | * @param inetSalt 67 | */ 68 | public static void setInetSalt(String inetSalt) { 69 | IdenticonUtil.inetSalt = inetSalt; 70 | } 71 | 72 | /** 73 | * Returns identicon code for given IP address. 74 | *

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

80 | * 81 | * @param inetAddr 82 | * IP address 83 | * @return identicon code for inetAddr 84 | * @throws Exception 85 | */ 86 | public static int getIdenticonCode(InetAddress inetAddr) throws Exception { 87 | if (inetSalt == null) 88 | throw new Exception( 89 | "inetSalt must be set prior to retrieving identicon code"); 90 | 91 | byte[] ip = inetAddr.getAddress(); 92 | int ipInt = (((ip[0] & 0xFF) << 24) | ((ip[1] & 0xFF) << 16) 93 | | ((ip[2] & 0xFF) << 8) | (ip[3] & 0xFF)) 94 | & inetMask; 95 | StringBuilder s = new StringBuilder(); 96 | s.append(ipInt); 97 | s.append('+'); 98 | s.append(inetSalt); 99 | MessageDigest md; 100 | md = MessageDigest.getInstance("SHA1"); 101 | byte[] hashedIp = md.digest(s.toString().getBytes("UTF-8")); 102 | int code = ((hashedIp[0] & 0xFF) << 24) | ((hashedIp[1] & 0xFF) << 16) 103 | | ((hashedIp[2] & 0xFF) << 8) | (hashedIp[3] & 0xFF); 104 | return code; 105 | } 106 | 107 | /** 108 | * Returns identicon code specified as an input parameter or derived from an 109 | * IP address. 110 | *

111 | * This method is a convenience method intended to be used by servlets like 112 | * below: 113 | *

114 | * 115 | *
116 | 	 * int code = IdenticonUtil.getIdenticonCode(request.getParameter("code"), request
117 | 	 * 		.getRemoteAddr());
118 | 	 * 
119 | * 120 | * @param codeParam 121 | * code parameter, if null remoteAddr parameter 122 | * will be used to determine the value. 123 | * @param remoteAddr 124 | * HTTP requester's IP address. Optional if code was specified. 125 | * @return 126 | */ 127 | public static int getIdenticonCode(String codeParam, String remoteAddr) { 128 | int code = 0; 129 | try { 130 | if (codeParam != null) { 131 | code = Integer.parseInt(codeParam); 132 | } else { 133 | code = IdenticonUtil.getIdenticonCode(InetAddress 134 | .getByName(remoteAddr)); 135 | } 136 | } catch (Exception e) { 137 | log.error(e); 138 | } 139 | return code; 140 | } 141 | 142 | public static int getIdenticonSize(String param) { 143 | int size = DEFAULT_IDENTICON_SIZE; 144 | try { 145 | String sizeParam = param; 146 | if (sizeParam != null) { 147 | size = Integer.parseInt(sizeParam); 148 | if (size < MINIMUM_IDENTICON_SIZE) 149 | size = MINIMUM_IDENTICON_SIZE; 150 | else if (size > MAXIMUM_IDENTICON_SIZE) 151 | size = MAXIMUM_IDENTICON_SIZE; 152 | } 153 | } catch (Exception e) { 154 | log.error(e); 155 | } 156 | return size; 157 | } 158 | 159 | public static String getIdenticonETag(int code, int size, int version) { 160 | StringBuilder s = new StringBuilder("W/\""); 161 | s.append(Integer.toHexString(code)); 162 | s.append('@'); 163 | s.append(size); 164 | s.append('v'); 165 | s.append(version); 166 | s.append('\"'); 167 | return s.toString(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/IdenticonServlet.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import java.awt.image.RenderedImage; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | 7 | import javax.imageio.ImageIO; 8 | import javax.servlet.ServletConfig; 9 | import javax.servlet.ServletException; 10 | import javax.servlet.http.HttpServlet; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import org.apache.commons.logging.Log; 15 | import org.apache.commons.logging.LogFactory; 16 | 17 | /** 18 | * This servlet generates identicon (visual identifier) images ranging 19 | * from 16x16 to 64x64 in size. 20 | * 21 | *
Supported Image Formats
22 | *

23 | * Currently only PNG is supported because javax.imageio package 24 | * does not come with built-in GIF encoder and PNG is the only remaining 25 | * reasonable format. 26 | *

27 | *
Initialization Parameters:
28 | *
29 | *
30 | *
inetSalt
31 | *
salt used to generate identicon code with. must be fairly long. 32 | * (Required)
33 | *
cacheProvider
34 | *
full class path to IdenticonCache implementation. 35 | * (Optional)
36 | *
37 | *
38 | *
Request ParametersP
39 | *
40 | *
41 | *
code
42 | *
identicon code to render. If missing, requester's IP addresses is used 43 | * to generated one. (Optional)
44 | *
size
45 | *
identicon size in pixels. If missing, a 16x16 pixels identicon is 46 | * returned. Minimum size is 16 and maximum is 64. (Optional)
47 | *
48 | *
49 | * 50 | * @author don 51 | */ 52 | public class IdenticonServlet extends HttpServlet { 53 | 54 | private static final long serialVersionUID = -3507466186902317988L; 55 | 56 | private static final Log log = LogFactory.getLog(IdenticonServlet.class); 57 | 58 | private static final String INIT_PARAM_VERSION = "version"; 59 | 60 | private static final String INIT_PARAM_INET_SALT = "inetSalt"; 61 | 62 | private static final String INIT_PARAM_CACHE_PROVIDER = "cacheProvider"; 63 | 64 | private static final String PARAM_IDENTICON_SIZE = "size"; 65 | 66 | private static final String PARAM_IDENTICON_SIZE_SHORT = "s"; 67 | 68 | private static final String PARAM_IDENTICON_CODE = "code"; 69 | 70 | private static final String PARAM_IDENTICON_CODE_SHORT = "c"; 71 | 72 | // private static final String PARAM_IDENTICON_TYPE = "type"; 73 | // 74 | // private static final String PARAM_IDENTICON_TYPE_SHORT = "t"; 75 | 76 | private static final String IDENTICON_IMAGE_FORMAT = "PNG"; 77 | 78 | private static final String IDENTICON_IMAGE_MIMETYPE = "image/png"; 79 | 80 | private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000; 81 | 82 | private int version = 1; 83 | 84 | private IdenticonRenderer renderer = new NineBlockIdenticonRenderer2(); 85 | 86 | private IdenticonCache cache; 87 | 88 | private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS; 89 | 90 | @Override 91 | public void init(ServletConfig cfg) throws ServletException { 92 | super.init(cfg); 93 | 94 | // Since identicons cache expiration is very long, version is 95 | // used in ETag to force identicons to be updated as needed. 96 | // Change veresion whenever rendering codes changes result in 97 | // visual changes. 98 | if (cfg.getInitParameter(INIT_PARAM_VERSION) != null) 99 | this.version = Integer.parseInt(cfg 100 | .getInitParameter(INIT_PARAM_VERSION)); 101 | 102 | String inetSalt = cfg.getInitParameter(INIT_PARAM_INET_SALT); 103 | if (inetSalt != null && inetSalt.length() > 0) 104 | IdenticonUtil.setInetSalt(inetSalt); 105 | else 106 | throw new ServletException(INIT_PARAM_INET_SALT 107 | + " init parameter not set"); 108 | 109 | String cacheProvider = cfg.getInitParameter(INIT_PARAM_CACHE_PROVIDER); 110 | if (cacheProvider != null) { 111 | try { 112 | Class cacheClass = Class.forName(cacheProvider); 113 | this.cache = (IdenticonCache) cacheClass.newInstance(); 114 | } catch (Exception e) { 115 | log.error(e); 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | protected void doGet(HttpServletRequest request, 122 | HttpServletResponse response) throws ServletException, IOException { 123 | 124 | String codeParam = request.getParameter(PARAM_IDENTICON_CODE_SHORT); 125 | if (codeParam == null) 126 | codeParam = request.getParameter(PARAM_IDENTICON_CODE); 127 | boolean codeSpecified = codeParam != null && codeParam.length() > 0; 128 | int code = IdenticonUtil.getIdenticonCode(codeParam, request 129 | .getRemoteAddr()); 130 | 131 | String sizeParam = request.getParameter(PARAM_IDENTICON_SIZE_SHORT); 132 | if (sizeParam == null) 133 | sizeParam =request.getParameter(PARAM_IDENTICON_SIZE); 134 | int size = IdenticonUtil.getIdenticonSize(sizeParam); 135 | 136 | // String typeParam = request.getParameter(PARAM_IDENTICON_TYPE_SHORT); 137 | // if (typeParam == null) 138 | // typeParam = request.getParameter(PARAM_IDENTICON_TYPE); 139 | 140 | String identiconETag = IdenticonUtil.getIdenticonETag(code, size, 141 | version); 142 | String requestETag = request.getHeader("If-None-Match"); 143 | 144 | if (requestETag != null && requestETag.equals(identiconETag)) { 145 | response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 146 | } else { 147 | byte[] imageBytes = null; 148 | 149 | // retrieve image bytes from either cache or renderer 150 | if (cache == null 151 | || (imageBytes = cache.get(identiconETag)) == null) { 152 | ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 153 | RenderedImage image = renderer.render(code, size); 154 | ImageIO.write(image, IDENTICON_IMAGE_FORMAT, byteOut); 155 | imageBytes = byteOut.toByteArray(); 156 | if (cache != null) 157 | cache.add(identiconETag, imageBytes); 158 | } 159 | 160 | // set ETag and, if code was provided, Expires header 161 | response.setHeader("ETag", identiconETag); 162 | if (codeSpecified) { 163 | long expires = System.currentTimeMillis() 164 | + identiconExpiresInMillis; 165 | response.addDateHeader("Expires", expires); 166 | } 167 | 168 | // return image bytes to requester 169 | response.setContentType(IDENTICON_IMAGE_MIMETYPE); 170 | response.setContentLength(imageBytes.length); 171 | response.getOutputStream().write(imageBytes); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/NineBlockIdenticonRenderer.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.Polygon; 6 | import java.awt.RenderingHints; 7 | import java.awt.Shape; 8 | import java.awt.geom.AffineTransform; 9 | import java.awt.image.BufferedImage; 10 | import java.math.BigInteger; 11 | 12 | /** 13 | * 9-block Identicon renderer. 14 | * 15 | *

16 | * Current implementation uses only the lower 32 bits of identicon code. 17 | *

18 | * 19 | * @author don 20 | * 21 | */ 22 | public class NineBlockIdenticonRenderer implements IdenticonRenderer { 23 | private static final int DEFAULT_PATCH_SIZE = 20; 24 | 25 | /* 26 | * Each patch is a polygon created from a list of vertices on a 5 by 5 grid. 27 | * Vertices are numbered from 0 to 24, starting from top-left corner of the 28 | * grid, moving left to right and top to bottom. 29 | */ 30 | 31 | private static final int PATCH_CELLS = 4; 32 | 33 | private static final int PATCH_GRIDS = PATCH_CELLS + 1; 34 | 35 | private static final byte PATCH_SYMMETRIC = 1; 36 | 37 | private static final byte PATCH_INVERTED = 2; 38 | 39 | private static final byte[] patch0 = { 0, 4, 24, 20, 0 }; 40 | 41 | private static final byte[] patch1 = { 0, 4, 20, 0 }; 42 | 43 | private static final byte[] patch2 = { 2, 24, 20, 2 }; 44 | 45 | private static final byte[] patch3 = { 0, 2, 20, 22, 0 }; 46 | 47 | private static final byte[] patch4 = { 2, 14, 22, 10, 2 }; 48 | 49 | private static final byte[] patch5 = { 0, 14, 24, 22, 0 }; 50 | 51 | private static final byte[] patch6 = { 2, 24, 22, 13, 11, 22, 20, 2 }; 52 | 53 | private static final byte[] patch7 = { 0, 14, 22, 0 }; 54 | 55 | private static final byte[] patch8 = { 6, 8, 18, 16, 6 }; 56 | 57 | private static final byte[] patch9 = { 4, 20, 10, 12, 2, 4 }; 58 | 59 | private static final byte[] patch10 = { 0, 2, 12, 10, 0 }; 60 | 61 | private static final byte[] patch11 = { 10, 14, 22, 10 }; 62 | 63 | private static final byte[] patch12 = { 20, 12, 24, 20 }; 64 | 65 | private static final byte[] patch13 = { 10, 2, 12, 10 }; 66 | 67 | private static final byte[] patch14 = { 0, 2, 10, 0 }; 68 | 69 | private static final byte[] patchTypes[] = { patch0, patch1, patch2, 70 | patch3, patch4, patch5, patch6, patch7, patch8, patch9, patch10, 71 | patch11, patch12, patch13, patch14, patch0 }; 72 | 73 | private static final byte patchFlags[] = { PATCH_SYMMETRIC, 0, 0, 0, 74 | PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, 75 | PATCH_SYMMETRIC + PATCH_INVERTED }; 76 | 77 | private static int centerPatchTypes[] = { 0, 4, 8, 15 }; 78 | 79 | private int patchSize; 80 | 81 | private Shape[] patchShapes; 82 | 83 | // used to center patch shape at origin because shape rotation works 84 | // correctly. 85 | private int patchOffset; 86 | 87 | private Color backgroundColor = Color.WHITE; 88 | 89 | /** 90 | * Constructor. 91 | * 92 | */ 93 | public NineBlockIdenticonRenderer() { 94 | setPatchSize(DEFAULT_PATCH_SIZE); 95 | } 96 | 97 | /** 98 | * Returns the size in pixels at which each patch will be rendered before 99 | * they are scaled down to requested identicon size. 100 | * 101 | * @return 102 | */ 103 | public int getPatchSize() { 104 | return patchSize; 105 | } 106 | 107 | /** 108 | * Set the size in pixels at which each patch will be rendered before they 109 | * are scaled down to requested identicon size. Default size is 20 pixels 110 | * which means, for 9-block identicon, a 60x60 image will be rendered and 111 | * scaled down. 112 | * 113 | * @param size 114 | * patch size in pixels 115 | */ 116 | public void setPatchSize(int size) { 117 | this.patchSize = size; 118 | this.patchOffset = patchSize / 2; // used to center patch shape at 119 | // origin. 120 | int scale = patchSize / PATCH_CELLS; 121 | this.patchShapes = new Polygon[patchTypes.length]; 122 | for (int i = 0; i < patchTypes.length; i++) { 123 | Polygon patch = new Polygon(); 124 | byte[] patchVertices = patchTypes[i]; 125 | for (int j = 0; j < patchVertices.length; j++) { 126 | int v = (int) patchVertices[j]; 127 | int vx = (v % PATCH_GRIDS * scale) - patchOffset; 128 | int vy = (v / PATCH_GRIDS * scale) - patchOffset; 129 | patch.addPoint(vx, vy); 130 | } 131 | this.patchShapes[i] = patch; 132 | } 133 | } 134 | 135 | public Color getBackgroundColor() { 136 | return backgroundColor; 137 | } 138 | 139 | public void setBackgroundColor(Color backgroundColor) { 140 | this.backgroundColor = backgroundColor; 141 | } 142 | 143 | public BufferedImage render(BigInteger code, int size) { 144 | return renderQuilt(code.intValue(), size); 145 | } 146 | 147 | /** 148 | * Returns rendered identicon image for given identicon code. 149 | * 150 | *

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

155 | * 156 | * @param code 157 | * identicon code 158 | * @param size 159 | * image size 160 | * @return identicon image 161 | */ 162 | public BufferedImage render(int code, int size) { 163 | return renderQuilt(code, size); 164 | } 165 | 166 | protected BufferedImage renderQuilt(int code, int size) { 167 | // ------------------------------------------------- 168 | // PREPARE 169 | // 170 | 171 | // decode the code into parts 172 | // bit 0-1: middle patch type 173 | // bit 2: middle invert 174 | // bit 3-6: corner patch type 175 | // bit 7: corner invert 176 | // bit 8-9: corner turns 177 | // bit 10-13: side patch type 178 | // bit 14: side invert 179 | // bit 15: side turns 180 | // bit 16-20: blue color component 181 | // bit 21-26: green color component 182 | // bit 27-31: red color component 183 | int middleType = centerPatchTypes[code & 0x3]; 184 | boolean middleInvert = ((code >> 2) & 0x1) != 0; 185 | int cornerType = (code >> 3) & 0x0f; 186 | boolean cornerInvert = ((code >> 7) & 0x1) != 0; 187 | int cornerTurn = (code >> 8) & 0x3; 188 | int sideType = (code >> 10) & 0x0f; 189 | boolean sideInvert = ((code >> 14) & 0x1) != 0; 190 | int sideTurn = (code >> 15) & 0x3; 191 | int blue = (code >> 16) & 0x01f; 192 | int green = (code >> 21) & 0x01f; 193 | int red = (code >> 27) & 0x01f; 194 | 195 | // color components are used at top of the range for color difference 196 | // use white background for now. 197 | // TODO: support transparency. 198 | Color fillColor = new Color(red << 3, green << 3, blue << 3); 199 | 200 | // outline shapes with a noticeable color (complementary will do) if 201 | // shape color and background color are too similar (measured by color 202 | // distance). 203 | Color strokeColor = null; 204 | if (getColorDistance(fillColor, backgroundColor) < 32.0f) 205 | strokeColor = getComplementaryColor(fillColor); 206 | 207 | // ------------------------------------------------- 208 | // RENDER AT SOURCE SIZE 209 | // 210 | 211 | int sourceSize = patchSize * 3; 212 | BufferedImage sourceImage = new BufferedImage(sourceSize, sourceSize, 213 | BufferedImage.TYPE_INT_RGB); 214 | Graphics2D g = sourceImage.createGraphics(); 215 | 216 | // middle patch 217 | drawPatch(g, patchSize, patchSize, middleType, 0, middleInvert, 218 | fillColor, strokeColor); 219 | 220 | // side patchs, starting from top and moving clock-wise 221 | drawPatch(g, patchSize, 0, sideType, sideTurn++, sideInvert, fillColor, 222 | strokeColor); 223 | drawPatch(g, patchSize * 2, patchSize, sideType, sideTurn++, 224 | sideInvert, fillColor, strokeColor); 225 | drawPatch(g, patchSize, patchSize * 2, sideType, sideTurn++, 226 | sideInvert, fillColor, strokeColor); 227 | drawPatch(g, 0, patchSize, sideType, sideTurn++, sideInvert, fillColor, 228 | strokeColor); 229 | 230 | // corner patchs, starting from top left and moving clock-wise 231 | drawPatch(g, 0, 0, cornerType, cornerTurn++, cornerInvert, fillColor, 232 | strokeColor); 233 | drawPatch(g, patchSize * 2, 0, cornerType, cornerTurn++, cornerInvert, 234 | fillColor, strokeColor); 235 | drawPatch(g, patchSize * 2, patchSize * 2, cornerType, cornerTurn++, 236 | cornerInvert, fillColor, strokeColor); 237 | drawPatch(g, 0, patchSize * 2, cornerType, cornerTurn++, cornerInvert, 238 | fillColor, strokeColor); 239 | 240 | g.dispose(); 241 | 242 | // ------------------------------------------------- 243 | // SCALE TO TARGET SIZE 244 | // 245 | // Bicubic algorithm is used for quality scaling 246 | 247 | BufferedImage targetImage = new BufferedImage(size, size, 248 | BufferedImage.TYPE_INT_RGB); 249 | g = targetImage.createGraphics(); 250 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 251 | RenderingHints.VALUE_INTERPOLATION_BICUBIC); 252 | g.drawImage(sourceImage, 0, 0, size, size, null); 253 | g.dispose(); 254 | 255 | return targetImage; 256 | } 257 | 258 | private void drawPatch(Graphics2D g, int x, int y, int patch, int turn, 259 | boolean invert, Color fillColor, Color strokeColor) { 260 | assert patch >= 0; 261 | assert turn >= 0; 262 | patch %= patchTypes.length; 263 | turn %= 4; 264 | if ((patchFlags[patch] & PATCH_INVERTED) != 0) 265 | invert = !invert; 266 | 267 | // paint background 268 | g.setBackground(invert ? fillColor : backgroundColor); 269 | g.clearRect(x, y, patchSize, patchSize); 270 | 271 | // offset and rotate coordinate space by patch position (x, y) and 272 | // 'turn' before rendering patch shape 273 | AffineTransform saved = g.getTransform(); 274 | g.translate(x + patchOffset, y + patchOffset); 275 | g.rotate(Math.toRadians(turn * 90)); 276 | 277 | // if stroke color was specified, apply stroke 278 | // stroke color should be specified if fore color is too close to the 279 | // back color. 280 | if (strokeColor != null) { 281 | g.setColor(strokeColor); 282 | g.draw(patchShapes[patch]); 283 | } 284 | 285 | // render rotated patch using fore color (back color if inverted) 286 | g.setColor(invert ? backgroundColor : fillColor); 287 | g.fill(patchShapes[patch]); 288 | 289 | // restore rotation 290 | g.setTransform(saved); 291 | } 292 | 293 | /** 294 | * Returns distance between two colors. 295 | * 296 | * @param c1 297 | * @param c2 298 | * @return 299 | */ 300 | private float getColorDistance(Color c1, Color c2) { 301 | float dx = c1.getRed() - c2.getRed(); 302 | float dy = c1.getGreen() - c2.getGreen(); 303 | float dz = c1.getBlue() - c2.getBlue(); 304 | return (float) Math.sqrt(dx * dx + dy * dy + dz * dz); 305 | } 306 | 307 | /** 308 | * Returns complementary color. 309 | * 310 | * @param color 311 | * @return 312 | */ 313 | private Color getComplementaryColor(Color color) { 314 | return new Color(color.getRGB() ^ 0x00FFFFFF); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/com/docuverse/identicon/NineBlockIdenticonRenderer2.java: -------------------------------------------------------------------------------- 1 | package com.docuverse.identicon; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.RenderingHints; 6 | import java.awt.Shape; 7 | import java.awt.geom.AffineTransform; 8 | import java.awt.geom.GeneralPath; 9 | import java.awt.geom.Rectangle2D; 10 | import java.awt.image.BufferedImage; 11 | import java.math.BigInteger; 12 | 13 | /** 14 | * 9-block Identicon renderer. 15 | * 16 | *

17 | * Current implementation uses only the lower 32 bits of identicon code. 18 | *

19 | * 20 | * @author don 21 | * 22 | */ 23 | public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { 24 | 25 | /* 26 | * Each patch is a polygon created from a list of vertices on a 5 by 5 grid. 27 | * Vertices are numbered from 0 to 24, starting from top-left corner of the 28 | * grid, moving left to right and top to bottom. 29 | */ 30 | 31 | private static final int PATCH_GRIDS = 5; 32 | 33 | private static final float DEFAULT_PATCH_SIZE = 20.0f; 34 | 35 | private static final byte PATCH_SYMMETRIC = 1; 36 | 37 | private static final byte PATCH_INVERTED = 2; 38 | 39 | private static final int PATCH_MOVETO = -1; 40 | 41 | private static final byte[] patch0 = { 0, 4, 24, 20 }; 42 | 43 | private static final byte[] patch1 = { 0, 4, 20 }; 44 | 45 | private static final byte[] patch2 = { 2, 24, 20 }; 46 | 47 | private static final byte[] patch3 = { 0, 2, 20, 22 }; 48 | 49 | private static final byte[] patch4 = { 2, 14, 22, 10 }; 50 | 51 | private static final byte[] patch5 = { 0, 14, 24, 22 }; 52 | 53 | private static final byte[] patch6 = { 2, 24, 22, 13, 11, 22, 20 }; 54 | 55 | private static final byte[] patch7 = { 0, 14, 22 }; 56 | 57 | private static final byte[] patch8 = { 6, 8, 18, 16 }; 58 | 59 | private static final byte[] patch9 = { 4, 20, 10, 12, 2 }; 60 | 61 | private static final byte[] patch10 = { 0, 2, 12, 10 }; 62 | 63 | private static final byte[] patch11 = { 10, 14, 22 }; 64 | 65 | private static final byte[] patch12 = { 20, 12, 24 }; 66 | 67 | private static final byte[] patch13 = { 10, 2, 12 }; 68 | 69 | private static final byte[] patch14 = { 0, 2, 10 }; 70 | 71 | private static final byte[] patchTypes[] = { patch0, patch1, patch2, 72 | patch3, patch4, patch5, patch6, patch7, patch8, patch9, patch10, 73 | patch11, patch12, patch13, patch14, patch0 }; 74 | 75 | private static final byte patchFlags[] = { PATCH_SYMMETRIC, 0, 0, 0, 76 | PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, 77 | PATCH_SYMMETRIC + PATCH_INVERTED }; 78 | 79 | private static int centerPatchTypes[] = { 0, 4, 8, 15 }; 80 | 81 | private float patchSize; 82 | 83 | private GeneralPath[] patchShapes; 84 | 85 | // used to center patch shape at origin because shape rotation works 86 | // correctly. 87 | private float patchOffset; 88 | 89 | private Color backgroundColor = Color.WHITE; 90 | 91 | /** 92 | * Constructor. 93 | * 94 | */ 95 | public NineBlockIdenticonRenderer2() { 96 | setPatchSize(DEFAULT_PATCH_SIZE); 97 | } 98 | 99 | /** 100 | * Returns the size in pixels at which each patch will be rendered before 101 | * they are scaled down to requested identicon size. 102 | * 103 | * @return 104 | */ 105 | public float getPatchSize() { 106 | return patchSize; 107 | } 108 | 109 | /** 110 | * Set the size in pixels at which each patch will be rendered before they 111 | * are scaled down to requested identicon size. Default size is 20 pixels 112 | * which means, for 9-block identicon, a 60x60 image will be rendered and 113 | * scaled down. 114 | * 115 | * @param size 116 | * patch size in pixels 117 | */ 118 | public void setPatchSize(float size) { 119 | this.patchSize = size; 120 | this.patchOffset = patchSize / 2.0f; // used to center patch shape at 121 | float patchScale = patchSize / 4.0f; 122 | // origin. 123 | this.patchShapes = new GeneralPath[patchTypes.length]; 124 | for (int i = 0; i < patchTypes.length; i++) { 125 | GeneralPath patch = new GeneralPath(GeneralPath.WIND_NON_ZERO); 126 | boolean moveTo = true; 127 | byte[] patchVertices = patchTypes[i]; 128 | for (int j = 0; j < patchVertices.length; j++) { 129 | int v = (int) patchVertices[j]; 130 | if (v == PATCH_MOVETO) 131 | moveTo = true; 132 | float vx = ((v % PATCH_GRIDS) * patchScale) - patchOffset; 133 | float vy = ((float) Math.floor(((float) v) / PATCH_GRIDS)) 134 | * patchScale - patchOffset; 135 | if (!moveTo) { 136 | patch.lineTo(vx, vy); 137 | } else { 138 | moveTo = false; 139 | patch.moveTo(vx, vy); 140 | } 141 | } 142 | patch.closePath(); 143 | this.patchShapes[i] = patch; 144 | } 145 | } 146 | 147 | public Color getBackgroundColor() { 148 | return backgroundColor; 149 | } 150 | 151 | public void setBackgroundColor(Color backgroundColor) { 152 | this.backgroundColor = backgroundColor; 153 | } 154 | 155 | public BufferedImage render(BigInteger code, int size) { 156 | return renderQuilt(code.intValue(), size); 157 | } 158 | 159 | /** 160 | * Returns rendered identicon image for given identicon code. 161 | * 162 | *

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

167 | * 168 | * @param code 169 | * identicon code 170 | * @param size 171 | * image size 172 | * @return identicon image 173 | */ 174 | public BufferedImage render(int code, int size) { 175 | return renderQuilt(code, size); 176 | } 177 | 178 | protected BufferedImage renderQuilt(int code, int size) { 179 | // ------------------------------------------------- 180 | // PREPARE 181 | // 182 | 183 | // decode the code into parts 184 | // bit 0-1: middle patch type 185 | // bit 2: middle invert 186 | // bit 3-6: corner patch type 187 | // bit 7: corner invert 188 | // bit 8-9: corner turns 189 | // bit 10-13: side patch type 190 | // bit 14: side invert 191 | // bit 15: corner turns 192 | // bit 16-20: blue color component 193 | // bit 21-26: green color component 194 | // bit 27-31: red color component 195 | int middleType = centerPatchTypes[code & 0x3]; 196 | boolean middleInvert = ((code >> 2) & 0x1) != 0; 197 | int cornerType = (code >> 3) & 0x0f; 198 | boolean cornerInvert = ((code >> 7) & 0x1) != 0; 199 | int cornerTurn = (code >> 8) & 0x3; 200 | int sideType = (code >> 10) & 0x0f; 201 | boolean sideInvert = ((code >> 14) & 0x1) != 0; 202 | int sideTurn = (code >> 15) & 0x3; 203 | int blue = (code >> 16) & 0x01f; 204 | int green = (code >> 21) & 0x01f; 205 | int red = (code >> 27) & 0x01f; 206 | 207 | // color components are used at top of the range for color difference 208 | // use white background for now. 209 | // TODO: support transparency. 210 | Color fillColor = new Color(red << 3, green << 3, blue << 3); 211 | 212 | // outline shapes with a noticeable color (complementary will do) if 213 | // shape color and background color are too similar (measured by color 214 | // distance). 215 | Color strokeColor = null; 216 | if (getColorDistance(fillColor, backgroundColor) < 32.0f) 217 | strokeColor = getComplementaryColor(fillColor); 218 | 219 | // ------------------------------------------------- 220 | // RENDER 221 | // 222 | 223 | BufferedImage targetImage = new BufferedImage(size, size, 224 | BufferedImage.TYPE_INT_RGB); 225 | Graphics2D g = targetImage.createGraphics(); 226 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 227 | RenderingHints.VALUE_ANTIALIAS_ON); 228 | 229 | g.setBackground(backgroundColor); 230 | g.clearRect(0, 0, size, size); 231 | 232 | float blockSize = size / 3.0f; 233 | float blockSize2 = blockSize * 2.0f; 234 | 235 | // middle patch 236 | drawPatch(g, blockSize, blockSize, blockSize, middleType, 0, 237 | middleInvert, fillColor, strokeColor); 238 | 239 | // side patchs, starting from top and moving clock-wise 240 | drawPatch(g, blockSize, 0, blockSize, sideType, sideTurn++, sideInvert, 241 | fillColor, strokeColor); 242 | drawPatch(g, blockSize2, blockSize, blockSize, sideType, sideTurn++, 243 | sideInvert, fillColor, strokeColor); 244 | drawPatch(g, blockSize, blockSize2, blockSize, sideType, sideTurn++, 245 | sideInvert, fillColor, strokeColor); 246 | drawPatch(g, 0, blockSize, blockSize, sideType, sideTurn++, sideInvert, 247 | fillColor, strokeColor); 248 | 249 | // corner patchs, starting from top left and moving clock-wise 250 | drawPatch(g, 0, 0, blockSize, cornerType, cornerTurn++, cornerInvert, 251 | fillColor, strokeColor); 252 | drawPatch(g, blockSize2, 0, blockSize, cornerType, cornerTurn++, 253 | cornerInvert, fillColor, strokeColor); 254 | drawPatch(g, blockSize2, blockSize2, blockSize, cornerType, 255 | cornerTurn++, cornerInvert, fillColor, strokeColor); 256 | drawPatch(g, 0, blockSize2, blockSize, cornerType, cornerTurn++, 257 | cornerInvert, fillColor, strokeColor); 258 | 259 | g.dispose(); 260 | 261 | return targetImage; 262 | } 263 | 264 | private void drawPatch(Graphics2D g, float x, float y, float size, 265 | int patch, int turn, boolean invert, Color fillColor, 266 | Color strokeColor) { 267 | assert patch >= 0; 268 | assert turn >= 0; 269 | patch %= patchTypes.length; 270 | turn %= 4; 271 | if ((patchFlags[patch] & PATCH_INVERTED) != 0) 272 | invert = !invert; 273 | 274 | Shape shape = patchShapes[patch]; 275 | double scale = ((double) size) / ((double) patchSize); 276 | float offset = size / 2.0f; 277 | 278 | // paint background 279 | g.setColor(invert ? fillColor : backgroundColor); 280 | g.fill(new Rectangle2D.Float(x, y, size, size)); 281 | 282 | AffineTransform savet = g.getTransform(); 283 | g.translate(x + offset, y + offset); 284 | g.scale(scale, scale); 285 | g.rotate(Math.toRadians(turn * 90)); 286 | 287 | // if stroke color was specified, apply stroke 288 | // stroke color should be specified if fore color is too close to the 289 | // back color. 290 | if (strokeColor != null) { 291 | g.setColor(strokeColor); 292 | g.draw(shape); 293 | } 294 | 295 | // render rotated patch using fore color (back color if inverted) 296 | g.setColor(invert ? backgroundColor : fillColor); 297 | g.fill(shape); 298 | 299 | g.setTransform(savet); 300 | } 301 | 302 | /** 303 | * Returns distance between two colors. 304 | * 305 | * @param c1 306 | * @param c2 307 | * @return 308 | */ 309 | private float getColorDistance(Color c1, Color c2) { 310 | float dx = c1.getRed() - c2.getRed(); 311 | float dy = c1.getGreen() - c2.getGreen(); 312 | float dz = c1.getBlue() - c2.getBlue(); 313 | return (float) Math.sqrt(dx * dx + dy * dy + dz * dz); 314 | } 315 | 316 | /** 317 | * Returns complementary color. 318 | * 319 | * @param color 320 | * @return 321 | */ 322 | private Color getComplementaryColor(Color color) { 323 | return new Color(color.getRGB() ^ 0x00FFFFFF); 324 | } 325 | } 326 | --------------------------------------------------------------------------------