package com.universalbits.conorganizer.badger.control; import com.universalbits.conorganizer.badger.model.BadgeInfo; import com.universalbits.conorganizer.common.ISettings; import org.apache.batik.bridge.*; import org.apache.batik.dom.svg.SAXSVGDocumentFactory; import org.apache.batik.gvt.GraphicsNode; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.ImageTranscoder; import org.apache.batik.transcoder.image.PNGTranscoder; import org.apache.batik.util.XMLResourceDescriptor; import org.apache.xerces.impl.dv.util.Base64; import org.json.JSONObject; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.svg.SVGDocument; import org.w3c.dom.svg.SVGLength; import org.w3c.dom.svg.SVGSVGElement; import javax.print.PrintService; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Dimension2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.print.*; import java.io.*; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; /** * * * Problems: * - SVG Version problems. Workaround: remove version attribute from svg tag. * - Empty space in SVG is cut off. Workaround: add viewbox attribute to svg tag. */ public class BadgePrinter { private static final Logger LOGGER = Logger.getLogger(BadgePrinter.class.getName()); public static final String XLINK_NAMESPACE_URI = "http://www.w3.org/1999/xlink"; public static final String ATTRIBUTE_HREF = "href"; public static final String BADGE_DATA_DIR = "badgedata"; public static final String PROPERTY_FIELDS = "fields"; public static final String PROPERTY_PAGE_WIDTH = "pageWidth"; public static final String PROPERTY_PAGE_HEIGHT = "pageHeight"; public static final String PROPERTY_X_SCALE = "xScale"; public static final String PROPERTY_X_TRANSLATE = "xTranslate"; public static final String PROPERTY_Y_SCALE = "yScale"; public static final String PROPERTY_Y_TRANSLATE = "yTranslate"; public static final double DEFAULT_X_SCALE = 1;//0.975; public static final double DEFAULT_X_TRANSLATE = 0;//12.0; public static final double DEFAULT_Y_SCALE = 1;//0.975; public static final double DEFAULT_Y_TRANSLATE = 0;//18.0; public static final double DEFAULT_PAGE_WIDTH = 4.133;//4.1 public static final double DEFAULT_PAGE_HEIGHT = 6.147;//6.15 private long t = 0; private int widthInInches = 4; private int heightInInches = 6; private int dpi = 300; private boolean stopped = false; private ISettings settings; public BadgePrinter(ISettings settings) { this.settings = settings; } private void t(String event) { long n = System.currentTimeMillis(); long d = t == 0 ? 0 : n - t; t = n; LOGGER.info(event + " " + d); } private File badgeDataDir; private File getBadgeFile(String name) { if (badgeDataDir == null) { badgeDataDir = new File(BADGE_DATA_DIR); if (!badgeDataDir.exists() || !badgeDataDir.isDirectory()) { throw new RuntimeException("badgedata directory not found"); } } return new File(badgeDataDir, name); } private File getBadgeImage(String name) { File image = getBadgeFile(name + ".png"); if (!image.exists()) { image = getBadgeFile(name + ".jpg"); } return image; } private String loadImage(File picFile) throws IOException { String picBase64 = null; t("begin loading " + picFile); byte[] picData = toByteArray(picFile); if (picFile.getName().endsWith(".png")) { picBase64 = "data:image/png;base64," + Base64.encode(picData); } else if (picFile.getName().endsWith(".jpg")) { picBase64 = "data:image/jpeg;base64," + Base64.encode(picData); } t("end loading " + picFile); return picBase64; } private String getImage(final String picture, final String userId) throws IOException { String picBase64 = null; if (userId != null) { File uniquePicFile = getBadgeImage(userId); if (uniquePicFile.exists()) { picBase64 = loadImage(uniquePicFile); } else { LOGGER.log(Level.INFO, "No unique picture for member " + userId); } } // if we didn't find a unique picture for this member if (picBase64 == null) { final File picFile = getBadgeImage(picture); picBase64 = loadImage(picFile); } return picBase64; } private SVGDocument getSVGTemplate(String template) throws IOException, URISyntaxException { template = template + ".svg"; final String uri = getBadgeFile(template).toURI().toString(); t("begin loading " + uri); final String parser = XMLResourceDescriptor.getXMLParserClassName(); final SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); final SVGDocument doc = f.createSVGDocument(uri); t("end loading " + uri); return doc; } private Properties getProperties(String type) throws IOException, InterruptedException { final File propsFileUTF8 = getBadgeFile(type + ".properties"); final Properties props = new Properties(); if (propsFileUTF8.exists() && propsFileUTF8.canRead()) { final Reader propsReader = new InputStreamReader(new FileInputStream(propsFileUTF8), "UTF-8"); props.load(propsReader); } return props; } private void printBadge(final BufferedImage image, PrintService ps) throws IOException, InterruptedException { try { final PrinterJob printJob = PrinterJob.getPrinterJob(); printJob.setPrintService(ps); final PageFormat pf = printJob.defaultPage(); final Paper paper = new Paper(); double pageWidth = settings.getPropertyDouble(PROPERTY_PAGE_WIDTH, DEFAULT_PAGE_WIDTH) * 72; double pageHeight = settings.getPropertyDouble(PROPERTY_PAGE_HEIGHT, DEFAULT_PAGE_HEIGHT) * 72; System.out.println("Setting paper size to " + pageWidth + "x" + pageHeight); paper.setSize(pageWidth, pageHeight); paper.setImageableArea(0.0, 0.0, paper.getWidth(), paper.getHeight()); pf.setPaper(paper); System.out.println("Buffered Image is " + image.getWidth() + "x" + image.getHeight()); System.out.println("Printer Page width=" + pf.getWidth() + " height=" + pf.getHeight()); System.out.println("Printer Page Imageable x=" + pf.getImageableX() + " y=" + pf.getImageableY() + " width=" + pf.getImageableWidth() + " height=" + pf.getImageableHeight()); printJob.setPrintable(new Printable() { public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { Graphics2D g2 = (Graphics2D) graphics; final double xScale = settings.getPropertyDouble(PROPERTY_X_SCALE, DEFAULT_X_SCALE); final double xTranslate = settings.getPropertyDouble(PROPERTY_X_TRANSLATE, DEFAULT_X_TRANSLATE); final double yScale = settings.getPropertyDouble(PROPERTY_Y_SCALE, DEFAULT_Y_SCALE); final double yTranslate = settings.getPropertyDouble(PROPERTY_Y_TRANSLATE, DEFAULT_Y_TRANSLATE); final double widthScale = (pageFormat.getWidth() / image.getWidth()) * xScale; final double heightScale = (pageFormat.getHeight() / image.getHeight()) * yScale; System.out.println("Setting scale to " + widthScale + "x" + heightScale); final AffineTransform at = AffineTransform.getScaleInstance(widthScale, heightScale); System.out.println("Setting translate to " + xTranslate + "x" + yTranslate); at.translate(xTranslate, yTranslate); if (pageIndex != 0) { return NO_SUCH_PAGE; } g2.drawRenderedImage(image, at); return PAGE_EXISTS; } }, pf); printJob.print(); } catch (Exception e) { throw new RuntimeException("Error printing", e); } } public void stop() { this.stopped = true; } public void printBadges(BadgeSource badgeSource, PrintService printService, File outDir) { while (!stopped) { BadgeInfo badgeInfo = badgeSource.getBadgeToPrint(); String status = "ERROR - UNKNOWN"; if (badgeInfo == null) { return; } try { final SVGDocument doc = generateBadge(badgeInfo); final BufferedImage image = generateImage(doc); printBadge(image, printService); saveBadgeInfo(badgeInfo, outDir); badgeSource.reportDone(badgeInfo); status = "OK"; } catch (Exception e) { badgeInfo.put(BadgeInfo.ERROR, e.getMessage()); badgeSource.reportProblem(badgeInfo); status = "ERROR - " + e.getMessage(); LOGGER.log(Level.SEVERE, "Error while printing badge " + badgeInfo, e); } finally { final Object context = badgeInfo.getContext(); if (context != null && context instanceof BadgeStatusListener) { ((BadgeStatusListener) context).notifyBadgeStatus(badgeInfo, status); } } } } public void generateBadgePNGs(BadgeSource badgeSource, File outDir) { while (!stopped) { String status = "ERROR - UNKNOWN"; BadgeInfo badgeInfo = badgeSource.getBadgeToPrint(); if (badgeInfo == null) { return; } try { SVGDocument doc = generateBadge(badgeInfo); generatePNG(badgeInfo, doc, outDir); saveBadgeInfo(badgeInfo, outDir); badgeSource.reportDone(badgeInfo); status = "OK"; } catch (Exception e) { badgeInfo.put(BadgeInfo.ERROR, e.getMessage()); badgeSource.reportProblem(badgeInfo); status = "ERROR: " + e.getMessage(); LOGGER.log(Level.SEVERE, "Error while printing badge " + badgeInfo, e); } finally { final Object context = badgeInfo.getContext(); if (context != null && context instanceof BadgeStatusListener) { ((BadgeStatusListener) context).notifyBadgeStatus(badgeInfo, status); } } } } public SVGDocument generateBadge(BadgeInfo badgeInfo) throws Exception { Element e; final String type = badgeInfo.get(BadgeInfo.TYPE); // add the type specific defaults as needed final Properties props = getProperties(type); for (Object key : props.keySet()) { final String stringKey = key.toString(); //if the badge info doesn't contain this property, set it if (!badgeInfo.containsKey(stringKey)) { badgeInfo.put(stringKey, props.getProperty(stringKey)); } } final SVGDocument doc = getSVGTemplate(badgeInfo.get(BadgeInfo.TEMPLATE)); t("parse"); for (String key : badgeInfo.keySet()) { for (int i = 0; i < 5; i++) { final String numberedKey = (i > 0) ? key + "_" + i : key; e = doc.getElementById(numberedKey); if (e != null) { final String tag = e.getTagName(); if ("image".equals(tag)) { switch (key) { case BadgeInfo.QRCODE: e = doc.getElementById(BadgeInfo.QRCODE); final String qrCodeURL = badgeInfo.get(BadgeInfo.QRCODE); if (e != null && qrCodeURL != null) { final int qrCodeWidth = Math.round(Float.parseFloat(e.getAttribute("width"))) * 7; final String qrCodeData = "data:image/png;base64," + BarcodeGenerator.qrCodePNGBase64(qrCodeURL, qrCodeWidth); t("gen qrcode"); e.setAttributeNS(XLINK_NAMESPACE_URI, ATTRIBUTE_HREF, qrCodeData); t("set qrcode"); } break; case BadgeInfo.BARCODE: e = doc.getElementById(BadgeInfo.BARCODE); final String barcodeValue = badgeInfo.get(BadgeInfo.BARCODE); if (e != null && barcodeValue != null) { if (e.getTagName().equals("image")) { final int barcodeWidth = Math.round(Float.parseFloat(e.getAttribute("width"))) * 7; final int barcodeHeight = Math.round(Float.parseFloat(e.getAttribute("height"))) * 7; final String barcodeData = "data:image/png;base64," + BarcodeGenerator.code128PNGBase64(barcodeValue, barcodeWidth, barcodeHeight); t("gen barcode"); e.setAttributeNS(XLINK_NAMESPACE_URI, ATTRIBUTE_HREF, barcodeData); t("set barcode"); } } break; case BadgeInfo.PICTURE: e = doc.getElementById(BadgeInfo.PICTURE); if (e != null) { final String picture = badgeInfo.get(BadgeInfo.PICTURE); final String userId = badgeInfo.get(BadgeInfo.ID_USER); final String picBase64 = getImage(picture, userId); e.setAttributeNS(XLINK_NAMESPACE_URI, ATTRIBUTE_HREF, picBase64); t("set pic"); } break; } } else if ("text".equals(tag)) { // Inkscape often puts a tspan element inside the text element for formatting. We need to preserve it. final NodeList children = e.getChildNodes(); if (children.getLength() > 0) { Node eChild = children.item(0); String nodeName = eChild.getNodeName(); if (eChild instanceof Element && nodeName.equals("tspan")) { e = (Element) eChild; } } e.setTextContent(badgeInfo.get(key)); } } } } t("get elements"); // Save modified XML /* * TransformerFactory tFactory = TransformerFactory.newInstance(); * Transformer transformer = tFactory.newTransformer(); DOMSource source * = new DOMSource(doc); FileOutputStream xmlOut = new * FileOutputStream(memberRoleID + ".svg"); StreamResult result = new * StreamResult(xmlOut); transformer.transform(source, result); * xmlOut.close(); t("save xml"); */ return doc; } private static Point2D.Double getScaledSize(double currentWidth, double currentHeight, double maxWidth, double maxHeight) { double ratioX = maxWidth / currentWidth; double ratioY = maxHeight / currentHeight; double newWidth; double newHeight; if (ratioX > ratioY){ newWidth = currentWidth * ratioY; newHeight = currentHeight * ratioY; } else { newWidth = currentWidth * ratioX; newHeight = currentHeight * ratioX; } return new Point2D.Double(newWidth, newHeight); } private void setupTranscoder(ImageTranscoder t, SVGDocument doc) throws TranscoderException { UserAgent userAgent = new UserAgentAdapter(); DocumentLoader loader = new DocumentLoader(userAgent); BridgeContext ctx = new BridgeContext(userAgent, loader); ctx.setDynamicState(BridgeContext.DYNAMIC); GVTBuilder builder = new GVTBuilder(); GraphicsNode rootGN = builder.build(ctx, doc); Rectangle2D bounds = rootGN.getBounds(); double pageWidth = settings.getPropertyDouble(PROPERTY_PAGE_WIDTH, DEFAULT_PAGE_WIDTH) * dpi; double pageHeight = settings.getPropertyDouble(PROPERTY_PAGE_HEIGHT, DEFAULT_PAGE_HEIGHT) * dpi; Point2D.Double scaledSize = getScaledSize(bounds.getWidth(), bounds.getHeight(), pageWidth, pageHeight); System.out.println("Target size = " + pageWidth + "x" + pageHeight); System.out.println("Scaled size = " + scaledSize.getX() + "x" + scaledSize.getY()); t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, (float) scaledSize.getX()); t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, (float)scaledSize.getY()); t.addTranscodingHint(PNGTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER, 25.4f / 300.0f); } private String getBadgeFilename(BadgeInfo badgeInfo, String ext) { String fileName = ""; final String userId = badgeInfo.get(BadgeInfo.ID_USER); final String badgeId = badgeInfo.get(BadgeInfo.ID_BADGE); final String type = badgeInfo.get(BadgeInfo.TYPE); if (userId != null && badgeId != null) { fileName = userId + "-" + badgeId + "-" + type + ext; } else if (badgeId != null) { fileName = badgeId + "-" + type + ext; } else if (userId != null) { fileName = userId + "-" + type + ext; } else { fileName = System.currentTimeMillis() + "-" + type + ext; } return fileName; } private void saveBadgeInfo(BadgeInfo badgeInfo, File outDir) { String fileName = getBadgeFilename(badgeInfo, ".json"); File file = new File(outDir, fileName); JSONObject json = badgeInfo.toJsonObject(); try { FileOutputStream out = new FileOutputStream(file); byte[] jsonBytes = json.toString().getBytes(Charset.forName("UTF-8")); out.write(jsonBytes); out.close(); } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "Error saving json", ioe); } } private File generatePNG(BadgeInfo badgeInfo, SVGDocument doc, File outDir) throws TranscoderException, IOException { String fileName = getBadgeFilename(badgeInfo, ".png"); PNGTranscoder t = new PNGTranscoder(); setupTranscoder(t, doc); // Set the transcoder input and output. TranscoderInput input = new TranscoderInput(doc); File outFile = new File(outDir, fileName); LOGGER.info("Saving badge as " + outFile.getAbsolutePath()); OutputStream outStream = new FileOutputStream(outFile); TranscoderOutput output = new TranscoderOutput(outStream); // Perform the transcoding. t.transcode(input, output); outStream.flush(); outStream.close(); t("save png"); return outFile; } private BufferedImage generateImage(SVGDocument doc) throws IOException, TranscoderException { BufferedImageTranscoder t = new BufferedImageTranscoder(); setupTranscoder(t, doc); TranscoderInput input = new TranscoderInput(doc); t.transcode(input, null); BufferedImage image = t.getBufferedImage(); t("generate image"); return image; } private static byte[] toByteArray(File file) throws IOException { int length = (int) file.length(); byte[] array = new byte[length]; InputStream in = new FileInputStream(file); int offset = 0; while (offset < length) { int count = in.read(array, offset, (length - offset)); offset += count; } in.close(); return array; } private static class BufferedImageTranscoder extends ImageTranscoder { @Override public BufferedImage createImage(int w, int h) { return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); } @Override public void writeImage(BufferedImage img, TranscoderOutput output) { this.img = img; } public BufferedImage getBufferedImage() { return img; } private BufferedImage img = null; } }