转自:开源中国 小灰灰Blog 发布于 2017/04/03 12:10
二维码边距修复
使用zxing生成二维码时, 某些场景下,即便指定 padding 参数为0,依然有很大的白边,本篇博文主要分析产生这个的原因,以及如何修复这个问题 |
---|
首先抛出一个源码传送门二维码生成java工具类
private BufferedImage genQrCode(String content, Integer size) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>(3);
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 0);
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, size, size, hints);
return MatrixToImageWriter.toBufferedImage(bitMatrix);
}
@Test
public void testGenCode() {
String content = "使用zxing生成二维码时, 某些场景下,即便指定
`padding` 参数为0,依然有很大的白边,本篇博文主要分析产生这个的原因,
以及如何修复这个问题使用zxing生成二维码时,
某些场景下,即便指定 `padding` 参数为0,依然有很大的白边,
本篇博文主要分析产生这个的原因,以及如何修复这个问题";
int size = 300;
try {
BufferedImage bufferedImage = this.genQrCode(content, size);
System.out.println("---");
} catch (WriterException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
我们debug下,测试二维码的输出,如下图,四周的白边超级大, 即便我们在生成二维码的时候设置了padding参数 hints.put(EncodeHintType.MARGIN, 0);, 依然没有什么用,接下来我们就需要分析这个问题怎么产生的, 为什么会有这样的问题以及如何解决这个问题
简单来讲,将数据字符转换为位流,每8位一个码字,输出渲染时,根据对应值为1还是0,来判定输出小黑快还是小白块;当然为了读取二维码信息,还规定了一些其他的参数,我们主要关注下 Version 这个参数
二维码一共有40个尺寸。官方叫版本Version。Version 1是21 x 21的矩阵,Version 2是 25 x 25的矩阵,Version 3是29的尺寸,每增加一个version,就会增加4的尺寸,公式是:(V-1)*4 + 21(V是版本号) 最高Version 40,(40-1)*4+21 = 177,所以最高是177 x 177 的正方形 |
---|
version确定了最终输出的二维码矩阵大小,现在我们假设下,生成一个 200x200的二维码图片,若version的值为 40, 即二维码矩阵为 177x177, 那么剩下的23x23就需要白边来填充了; 而version如果为2,因为二维码矩阵为 25x25, 放大8倍, 正好 200x200, 白边就不需要了
那么现在的问题就是 version 这个东西怎么确定的, 在上面的测试中我们并没有指定version
@Override
public BitMatrix encode(String contents,
BarcodeFormat format,
int width,
int height,
Map<EncodeHintType,?> hints) throws WriterException {
if (contents.isEmpty()) {
throw new IllegalArgumentException("Found empty contents");
}
if (format != BarcodeFormat.QR_CODE) {
throw new IllegalArgumentException("Can only encode QR_CODE, but got " + format);
}
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' +
height);
}
ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;
int quietZone = QUIET_ZONE_SIZE;
if (hints != null) {
if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) {
errorCorrectionLevel = ErrorCorrectionLevel.valueOf(hints.get(EncodeHintType.ERROR_CORRECTION).toString());
}
if (hints.containsKey(EncodeHintType.MARGIN)) {
quietZone = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString());
}
}
// 二维码生成
QRCode code = Encoder.encode(contents, errorCorrectionLevel, hints);
// 输出渲染
return renderResult(code, width, height, quietZone);
}
上面的方法, 主要关注最后两行,一个生成二维码, 一个对生成的二维码进行渲染, 进入 Encoder.encode 这个方法,就可以看到里面正好有个version变量,而这个就是我们的目标,过滤掉我们不关心的参数,下面提出versin的初始化过程
public static QRCode encode(String content,
ErrorCorrectionLevel ecLevel,
Map<EncodeHintType,?> hints) throws WriterException {
// ...
Version version;
if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) {
int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION).toString());
version = Version.getVersionForNumber(versionNumber);
int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, version);
if (!willFit(bitsNeeded, version, ecLevel)) {
throw new WriterException("Data too big for requested version");
}
} else {
version = recommendVersion(ecLevel, mode, headerBits, dataBits);
}
// ...
}
我们的设置中,没有指定version, 所以最终进入的 else 逻辑, 通过debug,我们看下上面测试中,计算出来的version为21, 生成的方块为 101x101, (21-1) * 4 + 21 = 101, 最终我们要生成300x300的二维码,所以白边为 98x98 (300 - 101x2)
分析上面生成version的原理, 第一个是计算信息填充需要的空间, databytes为二维码内容转换的bit数组; 第二个是选择可能满足的version, 从方法的实现也可以看出, 是遍历40个版本, 看哪个版本能容下这些数据,返回第一个匹配的; 接着就是再次确认这个版本是否满足需求
private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException {
for (int versionNum = 1; versionNum <= 40; versionNum++) {
Version version = Version.getVersionForNumber(versionNum);
if (willFit(numInputBits, version, ecLevel)) {
return version;
}
}
throw new WriterException("Data too big");
}
至此version就计算出来了, 但是白边改怎么处理,按照上面的逻辑,我们如何才能选择一个白边小,且满足需求的version呢?
再好的version,也无法保证100%的无白边,比如生成300x300的二维码,只有 verson=2才恰好满足。怎么样的version才是满足需求的不好确认 |
---|
既然从version这一角度出发不好处理,不妨换个角度,着手于渲染阶段,我们先看现在的渲染逻辑
实现代码如下
// Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses
// 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap).
private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {
ByteMatrix input = code.getMatrix();
if (input == null) {
throw new IllegalStateException();
}
int inputWidth = input.getWidth();
int inputHeight = input.getHeight();
int qrWidth = inputWidth + (quietZone * 2);
int qrHeight = inputHeight + (quietZone * 2);
int outputWidth = Math.max(width, qrWidth);
int outputHeight = Math.max(height, qrHeight);
int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight);
// Padding includes both the quiet zone and the extra white pixels to accommodate the requested
// dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.
// If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will
// handle all the padding from 100x100 (the actual QR) up to 200x160.
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;
}
从上面的debug信息也可以看出这点,看到这里,我们的一个想法就是,如果白边太大,我们就不这么玩,直接n倍放大,如上面的输入条件, 生成一个 303x303的二维码矩阵, 再最后输出二维码图片的时候, 缩放下,压缩为 300x300的二维码图片,这样白边问题就解决了
修改之后渲染代码如下
/**
* 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题
*
* 源码参考 {@link com.google.zxing.qrcode.QRCodeWriter#renderResult(QRCode, int, int, int)}
*
* @param code
* @param width
* @param height
* @param quietZone 取值 [0, 4]
* @return
*/
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;
if (abs < expectSize * 0.15) {
return 0;
}
return scale;
}
渲染改了之后,输出的地方也需要修改,不然生成的二维码图片大小就不是需求的大小了
public static BufferedImage toBufferedImage(BitMatrix matrix,
int width,
int height,
MatrixToImageConfig config) throws IOException {
int qrCodeWidth = matrix.getWidth();
int qrCodeHeight = matrix.getHeight();
BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < qrCodeWidth; x++) {
for (int y = 0; y < qrCodeHeight; y++) {
qrCode.setRGB(x, y, matrix.get(x, y) ? config.getPixelOnColor() : config.getPixelOffColor());
}
}
// 若二维码的实际宽高和预期的宽高不一致, 则缩放
if (qrCodeWidth != width || qrCodeHeight != height) {
BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
tmp.getGraphics().drawImage(
qrCode.getScaledInstance(width, height,
java.awt.Image.SCALE_SMOOTH), 0, 0, null);
qrCode = tmp;
}
return qrCode;
}