前几天项目上线的时候发现一个问题:通过Hutool工具包生成的二维码在内容较少时无法填满(Margin 已设置为 0)给定大小的图片。因此导致前端在显示二维码时样式异常。
从图片中我们可以看到,相同大小的图片,留白内容是不一样的。其中上半部分的图片是一个短字符串,下半部分的图片是一个长的字符串。因此基于Hutool包进行了裁边和缩放。代码如下:
Maven配置
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
QrCodeConfig.java
import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
/**
* 二维吗配置信息
*/
@Getter
@Setter
@ToString
public class QrCodeConfig {
/**
* 塞入二维码的信息
*/
private String msg;
/**
* 生成二维码的宽
*/
private Integer w;
/**
* 生成二维码的高
*/
private Integer h;
/**
* 生成二维码的颜色
*/
private MatrixToImageConfig matrixToImageConfig;
private Map<EncodeHintType, Object> hints;
@ToString
public static class QrCodeConfigBuilder {
/**
* The message to put into QrCode
*/
private String msg;
/**
* qrcode image width
*/
private Integer w;
/**
* qrcode image height
*/
private Integer h;
/**
* qrcode message's code, default UTF-8
*/
private String code;
/**
* 0 - 4
*/
private Integer padding;
/**
* error level, default H
*/
private ErrorCorrectionLevel errorCorrection;
public String getMsg() {
return msg;
}
public QrCodeConfigBuilder setMsg(String msg) {
this.msg = msg;
return this;
}
public Integer getW() {
return w == null ? (h == null ? 200 : h) : w;
}
public QrCodeConfigBuilder setW(Integer w) {
if (w != null && w < 0) {
throw new IllegalArgumentException("???????????0");
}
this.w = w;
return this;
}
public Integer getH() {
if (w != null && w < 0) {
throw new IllegalArgumentException("???????????0");
}
return h == null ? (w == null ? 200 : w) : h;
}
public QrCodeConfigBuilder setH(Integer h) {
this.h = h;
return this;
}
public String getCode() {
return code == null ? "UTF-8" : code;
}
public QrCodeConfigBuilder setCode(String code) {
this.code = code;
return this;
}
public Integer getPadding() {
if (padding == null) {
return 1;
}
if (padding < 0) {
return 0;
}
if (padding > 4) {
return 4;
}
return padding;
}
public QrCodeConfigBuilder setPadding(Integer padding) {
this.padding = padding;
return this;
}
public ErrorCorrectionLevel getErrorCorrection() {
return errorCorrection == null ? ErrorCorrectionLevel.H : errorCorrection;
}
public QrCodeConfigBuilder setErrorCorrection(ErrorCorrectionLevel errorCorrection) {
this.errorCorrection = errorCorrection;
return this;
}
private void validate() {
if (msg == null || msg.length() == 0) {
throw new IllegalArgumentException("????????????!");
}
}
private QrCodeConfig create() {
this.validate();
QrCodeConfig qrCodeConfig = new QrCodeConfig();
qrCodeConfig.setMsg(getMsg());
qrCodeConfig.setH(getH());
qrCodeConfig.setW(getW());
Map<EncodeHintType, Object> hints = new HashMap<>(3);
hints.put(EncodeHintType.ERROR_CORRECTION, this.getErrorCorrection());
hints.put(EncodeHintType.CHARACTER_SET, this.getCode());
hints.put(EncodeHintType.MARGIN, this.getPadding());
qrCodeConfig.setHints(hints);
qrCodeConfig.setMatrixToImageConfig(new MatrixToImageConfig(new Color(0, 0, 0, 255).getRGB(),
new Color(0, 0, 0, 0).getRGB()));
return qrCodeConfig;
}
/**
* create qrcodeConfig
*
* @return 返回构造的 QrCodeConfig 对象
*/
public QrCodeConfig build() {
return create();
}
}
}
MatrixToImageUtil.java
import com.google.zxing.common.BitMatrix;
import java.awt.*;
import java.awt.image.BufferedImage;
public class MatrixToImageUtil {
public static BufferedImage toBufferedImage(QrCodeConfig qrCodeConfig, BitMatrix bitMatrix) {
int qrCodeWidth = bitMatrix.getWidth();
int qrCodeHeight = bitMatrix.getHeight();
BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_ARGB);
int onColor = qrCodeConfig.getMatrixToImageConfig().getPixelOnColor();
int offColor = qrCodeConfig.getMatrixToImageConfig().getPixelOffColor();
for (int x = 0; x < qrCodeWidth; x++) {
for (int y = 0; y < qrCodeHeight; y++) {
boolean pixelOn = bitMatrix.get(x, y);
int pixelColor = pixelOn ? onColor : offColor;
// 设置透明度
int alpha = pixelOn ? 255 : 0;
Color color = new Color(pixelColor, true);
Color colorWithAlpha = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
qrCode.setRGB(x, y, colorWithAlpha.getRGB());
}
}
// 缩放二维码图片
int realQrCodeWidth = qrCodeConfig.getW();
int realQrCodeHeight = qrCodeConfig.getH();
if (qrCodeWidth != realQrCodeWidth || qrCodeHeight != realQrCodeHeight) {
BufferedImage tmp = new BufferedImage(realQrCodeWidth, realQrCodeHeight, BufferedImage.TYPE_INT_ARGB);
tmp.getGraphics().drawImage(
qrCode.getScaledInstance(realQrCodeWidth, realQrCodeHeight, Image.SCALE_SMOOTH),
0, 0, null);
qrCode = tmp;
}
return qrCode;
}
}
QrCodeGenWrapper.java
import cn.hutool.core.img.ImgUtil;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.qrcode.encoder.ByteMatrix;
import com.google.zxing.qrcode.encoder.Encoder;
import com.google.zxing.qrcode.encoder.QRCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Map;
/**
* 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题,原参考 <a href="https://my.oschina.net/u/566591/blog/872770">...</a>
*/
@Slf4j
public class QrCodeGenWrapper {
private static final Logger logger = LoggerFactory.getLogger(QrCodeGenWrapper.class);
private static final int QUIET_ZONE_SIZE = 4;
/**
* 构造 二维吗配置信息
* @return QrCodeConfig
*/
public static QrCodeConfig.QrCodeConfigBuilder createQrCodeConfig() {
return new QrCodeConfig.QrCodeConfigBuilder();
}
/**
* 生成base64格式二维吗
* @param content 二维吗内容
* @param width 宽度 默认 300
* @param height 高度 默认 300
* @param imageType 图片类型默认 png
* @return 返回base64格式二维码信息
*/
public static String generateAsBase64(String content, Integer width, Integer height, String imageType){
QrCodeConfig qrConfig = QrCodeGenWrapper.createQrCodeConfig()
.setMsg(content)
.setH(width == null? 300 : width)
.setW(height == null? 300 : height)
.setPadding(0)
.setErrorCorrection(ErrorCorrectionLevel.L)
.build();
try {
return ImgUtil.toBase64DataUri(asBufferedImage(qrConfig), StringUtils.isBlank(imageType)? "png" : imageType);
} catch (Exception e) {
log.error("QrCodeGenWrapper.generateAsBase64 error", e);
throw new RuntimeException("QrCodeGenWrapper.generateAsBase64 生成二维码异常");
}
}
public static BufferedImage asBufferedImage(QrCodeConfig qrCodeConfig) throws WriterException, IOException {
BitMatrix bitMatrix = encode(qrCodeConfig);
return MatrixToImageUtil.toBufferedImage(qrCodeConfig, bitMatrix);
}
/**
* 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题
* <p/>
* 源码参考 {@link com.google.zxing.qrcode.QRCodeWriter#encode(String, BarcodeFormat, int, int, Map)}
*/
private static BitMatrix encode(QrCodeConfig qrCodeConfig) throws WriterException {
ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;
int quietZone = 1;
if (qrCodeConfig.getHints() != null) {
if (qrCodeConfig.getHints().containsKey(EncodeHintType.ERROR_CORRECTION)) {
errorCorrectionLevel = ErrorCorrectionLevel.valueOf(qrCodeConfig.getHints().get(EncodeHintType.ERROR_CORRECTION).toString());
}
if (qrCodeConfig.getHints().containsKey(EncodeHintType.MARGIN)) {
quietZone = Integer.parseInt(qrCodeConfig.getHints().get(EncodeHintType.MARGIN).toString());
}
if (quietZone > QUIET_ZONE_SIZE) {
quietZone = QUIET_ZONE_SIZE;
} else if (quietZone < 0) {
quietZone = 0;
}
}
QRCode code = Encoder.encode(qrCodeConfig.getMsg(), errorCorrectionLevel, qrCodeConfig.getHints());
return renderResult(code, qrCodeConfig.getW(), qrCodeConfig.getH(), quietZone);
}
/**
* 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题
* <p/>
* 源码参考
*
* @param code {@link QRCode}
* @param width 高
* @param height 宽
* @param quietZone 取值 [0, 4]
* @return {@link BitMatrix}
*/
private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {
ByteMatrix input = code.getMatrix();
if (input == null) {
throw new IllegalStateException();
}
// xxx 二维码宽高相等, 即 qrWidth == qrHeight
int inputWidth = input.getWidth();
int inputHeight = input.getHeight();
int qrWidth = inputWidth + (quietZone * 2);
int qrHeight = inputHeight + (quietZone * 2);
// 白边过多时, 缩放
int minSize = Math.min(width, height);
int scale = calculateScale(qrWidth, minSize);
if (scale > 0) {
if (logger.isDebugEnabled()) {
logger.debug("qrCode scale enable! scale: {}, qrSize:{}, expectSize:{}x{}", scale, qrWidth, width, height);
}
int padding, tmpValue;
// 计算边框留白
padding = (minSize - qrWidth * scale) / QUIET_ZONE_SIZE * quietZone;
tmpValue = qrWidth * scale + padding;
if (width == height) {
width = tmpValue;
height = tmpValue;
} else if (width > height) {
width = width * tmpValue / height;
height = tmpValue;
} else {
height = height * tmpValue / width;
width = tmpValue;
}
}
int outputWidth = Math.max(width, qrWidth);
int outputHeight = Math.max(height, qrHeight);
int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight);
int leftPadding = (outputWidth - (inputWidth * multiple)) / 2;
int topPadding = (outputHeight - (inputHeight * multiple)) / 2;
BitMatrix output = new BitMatrix(outputWidth, outputHeight);
for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) {
// Write the contents of this row of the barcode
for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) {
if (input.get(inputX, inputY) == 1) {
output.setRegion(outputX, outputY, multiple, multiple);
}
}
}
return output;
}
/**
* 如果留白超过15% , 则需要缩放
* (15% 可以根据实际需要进行修改)
*
* @param qrCodeSize 二维码大小
* @param expectSize 期望输出大小
* @return 返回缩放比例, <= 0 则表示不缩放, 否则指定缩放参数
*/
private static int calculateScale(int qrCodeSize, int expectSize) {
if (qrCodeSize >= expectSize) {
return 0;
}
int scale = expectSize / qrCodeSize;
int abs = expectSize - scale * qrCodeSize;
// 在这里配置超过多少留白,则进行缩放(这里已经把 0.15 改成 0 了)
if (abs < 0) {
return 0;
}
return scale;
}
}
最终效果:
---------------------------------- 只能活一次的人生当然要比谁都炽热,浑浑噩噩谁也可以。 ---------------------------------