zxing 二維碼大白邊一步一步修復指南

二維碼邊距修復

使用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

緣由探究

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 的正方形ui

version肯定了最終輸出的二維碼矩陣大小,如今咱們假設下,生成一個 200x200的二維碼圖片,若version的值爲 40, 即二維碼矩陣爲 177x177, 那麼剩下的23x23就須要白邊來填充了; 而version若是爲2,由於二維碼矩陣爲 25x25, 放大8倍, 正好 200x200, 白邊就不須要了this

那麼如今的問題就是 version 這個東西怎麼肯定的, 在上面的測試中咱們並無指定versiongoogle

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,?>) 生成的二維碼矩陣,那麼就進入這個方法查看.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;
    }

至此,二維碼大白邊的問題就解決了, 實際測試以下

源碼傳送門

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

相關文章
相關標籤/搜索