package net.i2p.imagegen;
/* contains code adapted from jrobin: */
/*******************************************************************************
* Copyright (c) 2001-2005 Sasa Markovic and Ciaran Treanor.
* Copyright (c) 2011 The OpenNMS Group, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*******************************************************************************/
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.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;
import net.i2p.util.SystemVersion;
/**
* 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 PARAM_IDENTICON_TEXT_SHORT = "t";
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;
// TODO the fonts all look terrible. See also the rendering hints below, nothing helps
private static final String DEFAULT_FONT_NAME = SystemVersion.isWindows() ?
"Lucida Sans Typewriter" : Font.SANS_SERIF;
private static final Font DEFAULT_LARGE_FONT = new Font(DEFAULT_FONT_NAME, Font.BOLD, 16);
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.getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (request.getCharacterEncoding() == null)
request.setCharacterEncoding("UTF-8");
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);
// 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);
}
String text = request.getParameter(PARAM_IDENTICON_TEXT_SHORT);
if (text != null) {
BufferedImage bi = MatrixToImageWriter.toBufferedImage(matrix);
Graphics2D g = bi.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
Font font = DEFAULT_LARGE_FONT;
g.setFont(font);
// doesn't work
Color color = Color.RED;
g.setColor(color);
int width = bi.getWidth();
int height = bi.getHeight();
double swidth = font.getStringBounds(text, 0, text.length(),
g.getFontRenderContext()).getBounds().getWidth();
int x = (width - (int) swidth) / 2;
int y = height - 10;
g.drawString(text, x, y);
if (!ImageIO.write(bi, IDENTICON_IMAGE_FORMAT, byteOut))
throw new IOException("ImageIO.write() fail");
} else {
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.setHeader("X-Content-Type-Options", "nosniff");
response.setContentLength(imageBytes.length);
response.getOutputStream().write(imageBytes);
}
}
}