使用zxing生成二维码时, 某些场景下,即使指定
padding
参数为0,依然有很大的白边,本篇博文主要分析产生这个的缘由,以及如何修复这个问题java
首先抛出一个源码传送门 二维码生成java工具类git
写个测试类以下,其中 genQrCode
方法调用zxing的库,生成二维码,并输出为java的 BufferedImage
对象数组
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);
, 依然没有什么用,接下来咱们就须要分析这个问题怎么产生的, 为何会有这样的问题以及如何解决这个问题ide
在开始以前,简单了解下二维码的生成原理,详情可参考连接http://cli.im/news/10601工具
简单来说,将数据字符转换为位流,每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 的正方形ui
version肯定了最终输出的二维码矩阵大小,如今咱们假设下,生成一个 200x200的二维码图片,若version的值为 40, 即二维码矩阵为 177x177, 那么剩下的23x23就须要白边来填充了; 而version若是为2,由于二维码矩阵为 25x25, 放大8倍, 正好 200x200, 白边就不须要了this
那么如今的问题就是 version
这个东西怎么肯定的, 在上面的测试中咱们并无指定versiongoogle
最简单的,直接到源码里面去看,怎么肯定的version, 首先从源头出发,调用 com.google.zxing.qrcode.QRCodeWriter#encode(java.lang.String, com.google.zxing.BarcodeFormat, int, int, java.util.Map<com.google.zxing.EncodeHintType,?>)
生成的二维码矩阵,那么就进入这个方法查看.net
@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的计算原理,要解决这个大白边的问题,咱们最容易想到的就是找到合适的version就能够了,仔细想一想这个思路,好像并无那么容易
再好的version,也没法保证100%的无白边,好比生成300x300的二维码,只有 verson=2才刚好知足 怎么样的version才是知足需求的很差确认
既然从version这一角度出发很差处理,不妨换个角度,着手于渲染阶段,咱们先看如今的渲染逻辑
肯定生成二维码矩阵的基本大小 根据输出尺寸进行最大规模的放大(即再上面的基础上 xN 小于输出尺码, x(N-1) 大于输出尺码) 剩余的用白边填充
实现代码以下
// 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 进行扩展, 解决白边过多的问题 * <p/> * 源码参考 {@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; }
至此,二维码大白边的问题就解决了, 实际测试以下