/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ofbiz.common.qrcode; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.io.IOException; import java.lang.Integer; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.FileUtil; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.common.image.ImageTransform; import org.apache.ofbiz.service.DispatchContext; import org.apache.ofbiz.service.ServiceUtil; import com.google.zxing.BarcodeFormat; import com.google.zxing.ChecksumException; import com.google.zxing.DecodeHintType; import com.google.zxing.EncodeHintType; import com.google.zxing.FormatException; import com.google.zxing.MultiFormatWriter; import com.google.zxing.NotFoundException; import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.common.DecoderResult; import com.google.zxing.common.DetectorResult; import com.google.zxing.qrcode.decoder.Decoder; import com.google.zxing.qrcode.detector.Detector; import freemarker.template.utility.StringUtil; /** * Services for QRCode. */ public class QRCodeServices { public static final String module = QRCodeServices.class.getName(); public static final String QRCODE_DEFAULT_WIDTH = UtilProperties.getPropertyValue("qrcode", "qrcode.default.width", "200"); public static final String QRCODE_DEFAULT_HEIGHT = UtilProperties.getPropertyValue("qrcode", "qrcode.default.height", "200"); public static final String QRCODE_DEFAULT_FORMAT = UtilProperties.getPropertyValue("qrcode", "qrcode.default.format", "jpg"); public static final String QRCODE_FORMAT_SUPPORTED = UtilProperties.getPropertyValue("qrcode", "qrcode.format.supported", "jpg|png|bmp"); public static final String QRCODE_DEFAULT_LOGOIMAGE = UtilProperties.getPropertyValue("qrcode", "qrcode.default.logoimage"); public static BufferedImage defaultLogoImage; public static final String[] FORMAT_NAMES = StringUtil.split(QRCODE_FORMAT_SUPPORTED, '|'); public static final List<String> FORMATS_SUPPORTED = Arrays.asList(FORMAT_NAMES); public static final int MIN_SIZE = 20; public static final int MAX_SIZE = 500; private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; static { if (UtilValidate.isNotEmpty(QRCODE_DEFAULT_LOGOIMAGE)) { try { Map<String, Object> logoImageResult = ImageTransform.getBufferedImage(FileUtil.getFile(QRCODE_DEFAULT_LOGOIMAGE).getAbsolutePath(), Locale.getDefault()); defaultLogoImage = (BufferedImage) logoImageResult.get("bufferedImage"); if (UtilValidate.isEmpty(defaultLogoImage)) { Debug.logError("Your logo image file(" + QRCODE_DEFAULT_LOGOIMAGE + ") cannot be read by javax.imageio.ImageIO. Please use png, jpeg formats instead of ico and etc.", module); } } catch (IllegalArgumentException e) { defaultLogoImage = null; } catch (IOException e) { defaultLogoImage = null; } } } /** Streams QR Code to the result. */ public static Map<String, Object> generateQRCodeImage(DispatchContext ctx,Map<String, Object> context) { Locale locale = (Locale) context.get("locale"); String message = (String) context.get("message"); Integer width = (Integer) context.get("width"); Integer height = (Integer) context.get("height"); String format = (String) context.get("format"); String encoding = (String) context.get("encoding"); Boolean verifyOutput = (Boolean) context.get("verifyOutput"); String logoImage = (String) context.get("logoImage"); Integer logoImageMaxWidth = (Integer) context.get("logoImageMaxWidth"); Integer logoImageMaxHeight = (Integer) context.get("logoImageMaxHeight"); if (UtilValidate.isEmpty(message)) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ParameterCannotEmpty", new Object[] { "message" }, locale)); } if (width == null) { width = Integer.parseInt(QRCODE_DEFAULT_WIDTH); } if (width.intValue() < MIN_SIZE || width.intValue() > MAX_SIZE) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "SizeOutOfBorderError", new Object[] {"width", String.valueOf(width), String.valueOf(MIN_SIZE), String.valueOf(MAX_SIZE)}, locale)); } if (height == null) { height = Integer.parseInt(QRCODE_DEFAULT_HEIGHT); } if (height.intValue() < MIN_SIZE || height.intValue() > MAX_SIZE) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "SizeOutOfBorderError", new Object[] { "height", String.valueOf(height), String.valueOf(MIN_SIZE), String.valueOf(MAX_SIZE) }, locale)); } if (UtilValidate.isEmpty(format)) { format = QRCODE_DEFAULT_FORMAT; } if (!FORMATS_SUPPORTED.contains(format)) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ErrorFormatNotSupported", new Object[] { format }, locale)); } Map<EncodeHintType, Object> encodeHints = null; if (UtilValidate.isNotEmpty(encoding)) { encodeHints = new EnumMap<>(EncodeHintType.class); encodeHints.put(EncodeHintType.CHARACTER_SET, encoding); } try { BitMatrix bitMatrix = new MultiFormatWriter().encode(message, BarcodeFormat.QR_CODE, width, height, encodeHints); BufferedImage bufferedImage = toBufferedImage(bitMatrix, format); BufferedImage logoBufferedImage = null; if (UtilValidate.isNotEmpty(logoImage)) { Map<String, Object> logoImageResult; try { logoImageResult = ImageTransform.getBufferedImage(FileUtil.getFile(logoImage).getAbsolutePath(), locale); logoBufferedImage = (BufferedImage) logoImageResult.get("bufferedImage"); } catch (IllegalArgumentException e) { // do nothing } catch (IOException e) { // do nothing } } if (UtilValidate.isEmpty(logoBufferedImage)) { logoBufferedImage = defaultLogoImage; } BufferedImage newBufferedImage = null; if (UtilValidate.isNotEmpty(logoBufferedImage)) { if (UtilValidate.isNotEmpty(logoImageMaxWidth) && UtilValidate.isNotEmpty(logoImageMaxHeight) && (logoBufferedImage.getWidth() > logoImageMaxWidth.intValue() || logoBufferedImage.getHeight() > logoImageMaxHeight.intValue())) { Map<String, String> typeMap = new HashMap<String, String>(); typeMap.put("width", logoImageMaxWidth.toString()); typeMap.put("height", logoImageMaxHeight.toString()); Map<String, Map<String, String>> dimensionMap = new HashMap<String, Map<String, String>>(); dimensionMap.put("QRCode", typeMap); Map<String, Object> logoImageResult = ImageTransform.scaleImage(logoBufferedImage, (double) logoBufferedImage.getWidth(), (double) logoBufferedImage.getHeight(), dimensionMap, "QRCode", locale); logoBufferedImage = (BufferedImage) logoImageResult.get("bufferedImage"); } BitMatrix newBitMatrix = bitMatrix.clone(); newBufferedImage = toBufferedImage(newBitMatrix, format); Graphics2D graphics = newBufferedImage.createGraphics(); graphics.drawImage(logoBufferedImage, new AffineTransformOp(AffineTransform.getTranslateInstance(1, 1), null), (newBufferedImage.getWidth() - logoBufferedImage.getWidth())/2, (newBufferedImage.getHeight() - logoBufferedImage.getHeight())/2); graphics.dispose(); } if (UtilValidate.isNotEmpty(verifyOutput) && verifyOutput.booleanValue()) { Decoder decoder = new Decoder(); Map<DecodeHintType, Object> decodeHints = new EnumMap<>(DecodeHintType.class); decodeHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); if (UtilValidate.isNotEmpty(encoding)) { decodeHints.put(DecodeHintType.CHARACTER_SET, encoding); } DetectorResult detectorResult = null; if (UtilValidate.isNotEmpty(newBufferedImage)) { BitMatrix newBitMatrix = createMatrixFromImage(newBufferedImage); DecoderResult result = null; try { detectorResult = new Detector(newBitMatrix).detect(decodeHints); result = decoder.decode(detectorResult.getBits(), decodeHints); } catch (ChecksumException e) { // do nothing } catch (FormatException e) { // do nothing } catch (NotFoundException e) { // do nothing } if (UtilValidate.isNotEmpty(result) && !result.getText().equals(message)) { detectorResult = new Detector(bitMatrix).detect(decodeHints); result = decoder.decode(detectorResult.getBits(), decodeHints); if (!result.getText().equals(message)) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "GeneratedTextNotMatchOriginal", new Object[]{result.getText(), message}, locale)); } } else { bufferedImage = newBufferedImage; } } else { detectorResult = new Detector(bitMatrix).detect(decodeHints); DecoderResult result = decoder.decode(detectorResult.getBits(), decodeHints); if (!result.getText().equals(message)) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "GeneratedTextNotMatchOriginal", new Object[]{result.getText(), message}, locale)); } } } else if (UtilValidate.isNotEmpty(newBufferedImage)) { bufferedImage = newBufferedImage; } Map<String, Object> result = ServiceUtil.returnSuccess(); result.put("bufferedImage", bufferedImage); return result; } catch (WriterException e) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ErrorGenerateQRCode", new Object[] { e.toString() }, locale)); } catch (ChecksumException e) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ErrorVerifyQRCode", new Object[] { e.toString() }, locale)); } catch (FormatException e) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ErrorVerifyQRCode", new Object[] { e.toString() }, locale)); } catch (NotFoundException e) { return ServiceUtil.returnError(UtilProperties.getMessage("QRCodeUiLabels", "ErrorVerifyQRCode", new Object[] { e.toString() }, locale)); } } /** * Renders a {@link BitMatrix} as an image, where "false" bits are rendered * as white, and "true" bits are rendered as black. * * This is to replace MatrixToImageWriter.toBufferedImage(bitMatrix) if you * find the output image is not right, you can change BufferedImage image = * new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); to * BufferedImage image = new BufferedImage(width, height, * BufferedImage.TYPE_INT_RGB); or others to make it work correctly. */ private static BufferedImage toBufferedImage(BitMatrix matrix, String format) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = null; String osName = System.getProperty("os.name").toLowerCase(); image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); if (osName.startsWith("mac os") && format.equals("png")) { image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } private static BitMatrix createMatrixFromImage(BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); int[] pixels = new int[width * height]; image.getRGB(0, 0, width, height, pixels, 0, width); BitMatrix matrix = new BitMatrix(width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int pixel = pixels[y * width + x]; int luminance = (306 * ((pixel >> 16) & 0xFF) + 601 * ((pixel >> 8) & 0xFF) + 117 * (pixel & 0xFF)) >> 10; if (luminance <= 0x7F) { matrix.set(x, y); } } } return matrix; } }