文档章节

zxing 二维码大白边一步一步修复指南

小灰灰Blog
 小灰灰Blog
发布于 2017/04/03 12:10
字数 2381
阅读 1646
收藏 6

二维码边距修复

使用zxing生成二维码时, 某些场景下,即便指定 padding 参数为0,依然有很大的白边,本篇博文主要分析产生这个的原因,以及如何修复这个问题

首先抛出一个源码传送门 二维码生成java工具类

问题重现

写个测试类如下,其中 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);, 依然没有什么用,接下来我们就需要分析这个问题怎么产生的, 为什么会有这样的问题以及如何解决这个问题

原因探究

1. 背景

在开始之前,简单了解下二维码的生成原理,详情可参考链接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 的正方形

version确定了最终输出的二维码矩阵大小,现在我们假设下,生成一个 200x200的二维码图片,若version的值为 40, 即二维码矩阵为 177x177, 那么剩下的23x23就需要白边来填充了; 而version如果为2,因为二维码矩阵为 25x25, 放大8倍, 正好 200x200, 白边就不需要了

那么现在的问题就是 version 这个东西怎么确定的, 在上面的测试中我们并没有指定version

2. version 指定探究

最简单的,直接到源码里面去看,怎么确定的version, 首先从源头出发,调用 com.google.zxing.qrcode.QRCodeWriter#encode(java.lang.String, com.google.zxing.BarcodeFormat, int, int, java.util.Map<com.google.zxing.EncodeHintType,?>) 生成的二维码矩阵,那么就进入这个方法查看

@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;
    }

至此,二维码大白边的问题就解决了, 实际测试如下

源码传送门

http://git.oschina.net/liuyueyi/quicksilver

© 著作权归作者所有

共有 人打赏支持
小灰灰Blog
粉丝 195
博文 200
码字总数 355998
作品 0
武汉
程序员
私信 提问
加载中

评论(2)

小灰灰Blog
小灰灰Blog

引用来自“蓝萝卜blu”的评论

分析的太好了...直击问题根源..

回复@蓝萝卜blu : 老博客都被翻出来了,感谢赞同
蓝萝卜blu
蓝萝卜blu
分析的太好了...直击问题根源..
zxing 如何识别反转二维码

说起二维码扫描,估计很多人用的是 zxing 吧。 然而 zxing 虽然好用,但是却有一些坑。 这边分析一下自己实际项目遇到的一个坑。 什么坑呢? 下面举个栗子你就懂了。 这边生成二维码使用的是...

AndroidTraveler
2018/07/23
0
0
基于zxing-lib三步实现Android二维码应用

只需要基于开源项目zxing-lib,便可轻松创建独立二维码App,也可将二维码功能集成进App内。本文也是zxing-lib的使用文档。同时抛砖引玉,欢迎大家一起共建zxing-lib。 项目地址:https://git...

yuminw
2014/08/23
0
8
ZXing读写二维码,桌面和手机的不同用法

虽然ZXing是用Java实现的Barcode开源库,但是并不代表桌面上实现的Barcode应用在手机上也可以直接使用。因为Android的Java接口有很多是不同的。这里分享下Java Barcode生成和读取的不同用法。...

yushulx
2015/08/18
0
0
生成二维码的开源工具对比(附源码了呀!)

原文:生成二维码的开源工具对比(附源码了呀!) 某天发现生成二维码的工具(zxing)运行的很慢,于是乎上网上寻找生成二维码的工具,发现常见的开源工具有如下三种: Zxing(zxing.dll) Th...

杰克.陈
2018/07/03
0
0
利用iText和zxing生成和读pdf417二维码

前面的一些博文中已经提到了zxing这个开源工具生成和读取二维码图片,仅从学习的角度来看,可以告一个段落。在实际的生产环境中,应用zxing生成和读取二维码,却存在一些问题: 使用扫描枪读...

彭苏云
2014/03/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

2019 年最好的 7 款虚拟私人网络服务

糟糕的数据安全会带来极大的代价,特别是对企业而言。它会大致大规模的破坏并影响你的品牌声誉。尽管有些企业可以艰难地收拾残局,但仍有一些企业无法从事故中完全恢复。不过现在,你很幸运地...

linuxCool
22分钟前
1
0
OSChina 周一乱弹 —— 加油,还有11个小时就下班了

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @_全村的希望 :吴亦凡把大碗面正儿八经做成单曲了,你别说,还挺好听 《大碗宽面》- 吴亦凡 手机党少年们想听歌,请使劲儿戳(这里) @tom_t...

小小编辑
44分钟前
147
8
C++ vector和list的区别

1.vector数据结构 vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。 因此能高效的进行随机存取,时间复杂度为o(1); 但因为内存空间是连续的,所以在进行插入和删除操作时,会造...

shzwork
今天
7
0
Spring之invokeBeanFactoryPostProcessors详解

Spring的refresh的invokeBeanFactoryPostProcessors,就是调用所有注册的、原始的BeanFactoryPostProcessor。 相关源码 public static void invokeBeanFactoryPostProcessors(Configu......

cregu
昨天
6
0
ibmcom/db2express-c_docker官方使用文档

(DEPRECIATED) Please check DB2 Developer-C Edition for the replacement. What is IBM DB2 Express-C ? ``IBM DB2 Express-C``` is the no-charge community edition of DB2 server, a si......

BG2KNT
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部