二維碼服務拓展(支持logo,圓角logo,背景圖,顏色配置)

二維碼的基礎服務拓展

zxing 提供了二維碼一些列的功能,在平常生活中,能夠發現不少二維碼並不只僅是簡單的黑白矩形塊,有的添加了文字,加了logo,定製顏色,背景等,本片博文則着手於此,進行基礎服務的拓展html

本片博文拓展的功能點:java

  • 支持在二維碼中間添加logo
  • logo樣式選擇:支持圓角/直角logo,支持logo的邊框選擇
  • 二維碼顏色選擇(可自由將原來的黑白色進行替換)
  • 支持背景圖片
  • 支持探測圖形的前置色選擇

一個包含上面全部功能點的二維碼以下圖git

http://s2.mogucdn.com/mlcdn/c45406/170728_45a54147f26eh3lf1aiek04c1620h_300x300.png

準備

因爲以前有一篇博文《spring-boot & zxing 搭建二維碼服務》 較爲消息的介紹了設計一個二維碼服務的過程,所以這篇則再也不總體設計上多作說明,主要的功能點將集中在以上幾個功能點設計與實現上github

源碼地址: https://github.com/liuyueyi/quick-mediaspring

這篇博文,將不對二維碼生成的細節進行說明,某些地方若有疑惑(如二維碼生成時的一些參數,渲染邏輯等)請直接查看代碼,or百度谷歌,或者私聊也可。數組

下面簡單說明一下這個工程中與二維碼相關的幾個類的做用app

1. QrCodeOptions.java

二維碼的各類配置參數spring-boot

2. QrCodeGenWrapper.java

封裝了二維碼的參數設置和處理方法,一般來說對於使用者而言,只須要使用這個類中的方法便可實現二維碼的生成,如生成上面的二維碼測試代碼以下工具

@Test
public void testGenColorCode() {
    String msg = "https://my.oschina.net/u/566591/blog/1359432";
    // 根據本地文件生成待logo的二維碼, 從新着色位置探測圖像
    try {
        String logo = "logo.jpg";
        String bg = "bg.png";
        BufferedImage img = QrCodeGenWrapper.of(msg)
                .setW(300)
                .setPreColor(0xff0000ff)
                .setBgColor(0xffFFFF00)
                .setDetectCornerPreColor(0xffff0000)
                .setPadding(2)
                .setLogo(logo)
                .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
                .setLogoBgColor(0xff00cc00)
                .setBackground(bg)
                .asBufferedImage();


        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(img, "png", outputStream);
        System.out.println(Base64Util.encode(outputStream));
    } catch (Exception e) {
        System.out.println("create qrcode error! e: " + e);
        Assert.assertTrue(false);
    }
}

3.QrCodeUtil.java

二維碼工具類,包括生成二維碼矩陣信息,二維碼圖片渲染,輸出BufferedIamge對象等源碼分析

4. ImageUtil.java

圖片處理輔助類,實現圖片圓角化,添加邊框,插入logo,繪製背景圖等


設計與實現

1. 二維碼顏色可配置

二維碼顏色的選擇,主要在將二維碼矩陣轉換成圖的時候,選擇不一樣的顏色進行渲染便可,咱們主要的代碼將放在 com.hust.hui.quickmedia.common.util.QrCodeUtil#toBufferedImage 方法中

先看一下實現邏輯

/**
 * 根據二維碼配置 & 二維碼矩陣生成二維碼圖片
 *
 * @param qrCodeConfig
 * @param bitMatrix
 * @return
 * @throws IOException
 */
public static BufferedImage toBufferedImage(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) throws IOException {
    int qrCodeWidth = bitMatrix.getWidth();
    int qrCodeHeight = bitMatrix.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,
                        bitMatrix.get(x, y) ?
                                qrCodeConfig.getMatrixToImageConfig().getPixelOnColor() :
                            qrCodeConfig.getMatrixToImageConfig().getPixelOffColor());
        }
    }
    
    ...
}

注意

BitMatrixExcom.google.zxing.common.BitMatrix 的拓展,後面說明爲何這麼作,

此處知曉 com.hust.hui.quickmedia.common.qrcode.BitMatrixEx#get 等同於 com.google.zxing.common.BitMatrix#get便可

說明

  • 上面的邏輯比較清晰,先建立一個置頂大小的圖像,而後遍歷 bitMatrix,對圖像進行着色

  • bitMatrix.get(x, y) == true 表示該處爲二維碼的有效信息(這個是在二維碼生成時決定,zxing的二維碼生成邏輯負責生成BitMatrix對象,原理此處省略,由於我也沒仔細研究),而後塗上配置的前置色;不然表示空白背景,塗上背景色便可

2. 位置探測圖行可配置

位置探測圖形就是二維碼的左上角,右上角,左下角的三個矩形框(前面途中的三個紅框),用於定位二維碼使用,這裏的實現確保它的顏色能夠與二維碼的前置色不一樣

通過上面的二維碼顏色渲染,很容易就能夠想到,在二維碼的最終渲染時,對位置探測圖形採用不一樣的顏色進行渲染便可,因此渲染代碼以下

/**
 * 根據二維碼配置 & 二維碼矩陣生成二維碼圖片
 *
 * @param qrCodeConfig
 * @param bitMatrix
 * @return
 * @throws IOException
 */
public static BufferedImage toBufferedImage(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) throws IOException {
    int qrCodeWidth = bitMatrix.getWidth();
    int qrCodeHeight = bitMatrix.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++) {
            if (bitMatrix.isDetectCorner(x, y)) { // 着色位置探測圖形
                qrCode.setRGB(x, y,
                        bitMatrix.get(x, y) ?
                                qrCodeConfig.getDetectCornerColor().getPixelOnColor() :
                                qrCodeConfig.getDetectCornerColor().getPixelOffColor());
            } else { // 着色二維碼主題
                qrCode.setRGB(x, y,
                        bitMatrix.get(x, y) ?
                                qrCodeConfig.getMatrixToImageConfig().getPixelOnColor() :
                                qrCodeConfig.getMatrixToImageConfig().getPixelOffColor());
            }
        }
    }
    
    ....
}

相比較與以前,在遍歷邏輯中,多了一個是否爲位置探測圖形的分支判斷

if (bitMatrix.isDetectCorner(x, y)) { // 着色位置探測圖形
  qrCode.setRGB(x, y,
      bitMatrix.get(x, y) ?
        qrCodeConfig.getDetectCornerColor().getPixelOnColor() :
        qrCodeConfig.getDetectCornerColor().getPixelOffColor());
}

因此咱們的問題就是如何判斷(x,y)座標對應的位置是否爲位置探測圖形?

位置探測圖形斷定

這個斷定的邏輯,就須要深刻到二維碼矩陣的生成邏輯中,直接給出對應代碼位置

// Embed basic patterns
// The basic patterns are:
// - Position detection patterns
// - Timing patterns
// - Dark dot at the left bottom corner
// - Position adjustment patterns, if need be
com.google.zxing.qrcode.encoder.MatrixUtil#embedBasicPatterns


// 肯定位置探測圖形的方法
com.google.zxing.qrcode.encoder.MatrixUtil#embedPositionDetectionPatternsAndSeparators

// 自適應調整矩陣的方法
com.google.zxing.qrcode.encoder.MatrixUtil#maybeEmbedPositionAdjustmentPatterns

直接看代碼,會發現位置探測圖形的二維數組以下

private static final int[][] POSITION_DETECTION_PATTERN =  {
    {1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 1, 0, 1},
    {1, 0, 1, 1, 1, 0, 1},
    {1, 0, 1, 1, 1, 0, 1},
    {1, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1},
};

private static final int[][] POSITION_ADJUSTMENT_PATTERN = {
    {1, 1, 1, 1, 1},
    {1, 0, 0, 0, 1},
    {1, 0, 1, 0, 1},
    {1, 0, 0, 0, 1},
    {1, 1, 1, 1, 1},
};

到這裏,咱們的判斷就比較清晰了,位置探測圖形有兩種規格,5 or 7

在看具體的斷定邏輯以前,先看 BitMatrixEx加強類,能夠斷定(x,y)座標處是否爲位置探測圖形,內部斷定邏輯和 BitMatrix中是否爲二維碼有效信息的斷定一致

@Getter
@Setter
public class BitMatrixEx {
    private final int width;
    private final int height;
    private final int rowSize;
    private final int[] bits;


    private BitMatrix bitMatrix;

    public BitMatrixEx(BitMatrix bitMatrix) {
        this(bitMatrix.getWidth(), bitMatrix.getHeight());
        this.bitMatrix = bitMatrix;

    }

    private BitMatrixEx(int width, int height) {
        if (width < 1 || height < 1) {
            throw new IllegalArgumentException("Both dimensions must be greater than 0");
        }

        this.width = width;
        this.height = height;
        this.rowSize = (width + 31) / 32;
        bits = new int[rowSize * height];
    }



    public void setRegion(int left, int top, int width, int height) {
        int right = left + width;
        int bottom = top + height;

        for (int y = top; y < bottom; y++) {
            int offset = y * rowSize;
            for (int x = left; x < right; x++) {
                bits[offset + (x / 32)] |= 1 << (x & 0x1f);
            }
        }
    }


    public boolean get(int x, int y) {
        return bitMatrix.get(x, y);
    }


    public boolean isDetectCorner(int x, int y) {
        int offset = y * rowSize + (x / 32);
        return ((bits[offset] >>> (x & 0x1f)) & 1) != 0;
    }
}

位置斷定邏輯

位置斷定邏輯在 com.hust.hui.quickmedia.common.util.QrCodeUtil#renderResult 方法中,簡單說一下這個方法的做用

直接看斷定邏輯

// 獲取位置探測圖形的size,根據源碼分析,有兩種size的可能
// {@link com.google.zxing.qrcode.encoder.MatrixUtil.embedPositionDetectionPatternsAndSeparators}
ByteMatrix input = qrCode.getMatrix();
// 由於位置探測圖形的下一位必然是0,因此下面的一行能夠斷定選擇的是哪一種規格的位置斷定
int detectCornerSize = input.get(0, 5) == 1 ? 7 : 5;

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


        // 設置三個位置探測圖形
        if (inputX < detectCornerSize && inputY < detectCornerSize // 左上角
                || (inputX < detectCornerSize && inputY >= inputHeight - detectCornerSize) // 左下腳
                || (inputX >= inputWidth - detectCornerSize && inputY < detectCornerSize)) { // 右上角
            res.setRegion(outputX, outputY, multiple, multiple);
        }
    }
}

3. 背景圖支持

前面兩個涉及到二維碼自己的修改,接下來的背景 & logo則基本上無二維碼無關,只是圖片的操做而已,背景圖支持,即將背景圖做爲圖層,將二維碼渲染在正中間便可

對於圖片的覆蓋,直接借用 java.awt 包下的工具類便可實現

/**
 * 繪製背景圖
 *
 * @param source     原圖
 * @param background 背景圖
 * @param bgW        背景圖寬
 * @param bgH        背景圖高
 * @return
 * @throws IOException
 */
public static BufferedImage drawBackground(BufferedImage source, String background, int bgW, int bgH) throws IOException {
    int sW = source.getWidth();
    int sH = source.getHeight();


    // 背景的圖寬高不該該小於原圖
    if (bgW < sW) {
        bgW = sW;
    }

    if (bgH < sH) {
        bgH = sH;
    }


    // 獲取背景圖
    BufferedImage bg = getImageByPath(background);
    if (bg.getWidth() != bgW || bg.getHeight() != bgH) { // 須要縮放
        BufferedImage temp = new BufferedImage(bgW, bgH, BufferedImage.TYPE_INT_ARGB);
        temp.getGraphics().drawImage(bg.getScaledInstance(bgW, bgH, Image.SCALE_SMOOTH)
                , 0, 0, null);
        bg = temp;
    }


    // 繪製背景圖
    int x = (bgW - sW) >> 1;
    int y = (bgH - sH) >> 1;
    Graphics2D g2d = bg.createGraphics();
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f)); // 透明度, 避免看不到背景
    g2d.drawImage(source, x, y, sW, sH, null);
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
    g2d.dispose();
    bg.flush();
    return bg;
}

簡單說一下上面的實現邏輯

  • 獲取背景圖
  • 根據置頂的背景圖大小,對原背景圖進行縮放
  • 將目標圖片(二維碼)繪製在背景圖正中間

其中,咱們對二維碼的覆蓋設置了透明度爲0.8,確保不會徹底覆蓋背景圖,致使徹底看不到背景是什麼,此處若有其餘的需求場景能夠進行可配置化處理

4. logo支持

其實logo的支持和背景的支持邏輯基本沒什麼差異,都是將一個圖繪製在另外一個圖上

具體的實現以下, 先無視logo樣式的選擇問題

/**
 * 在圖片中間,插入圓角的logo
 *
 * @param qrCode      原圖
 * @param logo        logo地址
 * @param logoStyle   logo 的樣式 (圓角, 直角)
 * @param logoBgColor logo的背景色
 * @throws IOException
 */
public static void insertLogo(BufferedImage qrCode,
                              String logo,
                              QrCodeOptions.LogoStyle logoStyle,
                              Color logoBgColor) throws IOException {
    int QRCODE_WIDTH = qrCode.getWidth();
    int QRCODE_HEIGHT = qrCode.getHeight();

    // 獲取logo圖片
    BufferedImage bf = getImageByPath(logo);
    int boderSize = bf.getWidth() / 15;
    // 生成圓角邊框logo
    bf = makeRoundBorder(bf, logoStyle, boderSize, logoBgColor); // 邊距爲二維碼圖片的1/15

    // logo的寬高
    int w = bf.getWidth() > QRCODE_WIDTH * 2 / 10 ? QRCODE_WIDTH * 2 / 10 : bf.getWidth();
    int h = bf.getHeight() > QRCODE_HEIGHT * 2 / 10 ? QRCODE_HEIGHT * 2 / 10 : bf.getHeight();

    // 插入LOGO
    Graphics2D graph = qrCode.createGraphics();

    int x = (QRCODE_WIDTH - w) >> 1 ;
    int y = (QRCODE_HEIGHT - h) >> 1;

    graph.drawImage(bf, x, y, w, h, null);
    graph.dispose();
    bf.flush();
}

上面的主要邏輯,其實沒啥區別,接下來主要關心的則是圓角圖形生成以及邊框的支持

5. 圓角圖形

生成圓角圖片是一個很是常見的需求

先借用new RoundRectangle2D.Float(0, 0, w, h, cornerRadius, cornerRadius)繪製一個圓角的畫布出來

將原圖繪製在畫布上便可

/**
 * 生成圓角圖片
 *
 * @param image        原始圖片
 * @param cornerRadius 圓角的弧度
 * @return 返回圓角圖
 */
public static BufferedImage makeRoundedCorner(BufferedImage image,
                                              int cornerRadius) {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage output = new BufferedImage(w, h,
            BufferedImage.TYPE_INT_ARGB);

    Graphics2D g2 = output.createGraphics();

    // This is what we want, but it only does hard-clipping, i.e. aliasing
    // g2.setClip(new RoundRectangle2D ...)

    // so instead fake soft-clipping by first drawing the desired clip shape
    // in fully opaque white with antialiasing enabled...
    g2.setComposite(AlphaComposite.Src);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(Color.WHITE);
    g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
            cornerRadius));

    // ... then compositing the image on top,
    // using the white shape from above as alpha source
    g2.setComposite(AlphaComposite.SrcAtop);
    g2.drawImage(image, 0, 0, null);

    g2.dispose();

    return output;
}

6. 圓角邊框的圖片

上面實現圓角圖片以後,再考慮生成一個帶圓角邊框的圖片就很簡單了,直接繪製一個大一號的存色邊框,而後將圓角圖片繪製上去便可

/**
 * <p>
 * 生成圓角圖片 & 圓角邊框
 *
 * @param image     原圖
 * @param logoStyle 圓角的角度
 * @param size      邊框的邊距
 * @param color     邊框的顏色
 * @return 返回帶邊框的圓角圖
 */
public static BufferedImage makeRoundBorder(BufferedImage image,
                                            QrCodeOptions.LogoStyle logoStyle,
                                            int size, Color color) {
    // 將圖片變成圓角
    int cornerRadius = 0;
    if (logoStyle == QrCodeOptions.LogoStyle.ROUND) {
        cornerRadius = image.getWidth() / 4;
        image = makeRoundedCorner(image, cornerRadius);
    }

    int w = image.getWidth() + size;
    int h = image.getHeight() + size;
    BufferedImage output = new BufferedImage(w, h,
            BufferedImage.TYPE_INT_ARGB);

    Graphics2D g2 = output.createGraphics();
    g2.setComposite(AlphaComposite.Src);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(color == null ? Color.WHITE : color);
    g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
            cornerRadius));

    // ... then compositing the image on top,
    // using the white shape from above as alpha source
//        g2.setComposite(AlphaComposite.SrcAtop);
    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
    g2.drawImage(image, size / 2, size / 2, null);
    g2.dispose();

    return output;
}

測試

上面分別對每個點進行了實現並加以簡單說明,最後就是須要將上面的都串起來進行測試了,由於咱們的工程是在前面已經搭建好的二維碼服務上進行的,因此測試代碼也比較簡單,以下

@Test
public void testGenColorCode() {
    String msg = "https://my.oschina.net/u/566591/blog/1359432";
    // 根據本地文件生成待logo的二維碼, 從新着色位置探測圖像
    try {
        String logo = "logo.jpg";
        String bg = "bg.png";
        BufferedImage img = QrCodeGenWrapper.of(msg)
                .setW(300)
                .setPreColor(0xff0000ff)
                .setBgColor(0xffFFFF00)
                .setDetectCornerPreColor(0xffff0000)
                .setPadding(2)
                .setLogo(logo)
                .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
                .setLogoBgColor(0xff00cc00)
                .setBackground(bg)
                .asBufferedImage();


        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(img, "png", outputStream);
        System.out.println(Base64Util.encode(outputStream));
    } catch (Exception e) {
        System.out.println("create qrcode error! e: " + e);
        Assert.assertTrue(false);
    }
}

測試執行示意圖

http://s2.mogucdn.com/mlcdn/c45406/170728_2lebbba9b47037cc0g03hd42hf6ga_1224x639.gif


其餘

項目源碼: https://github.com/liuyueyi/quick-media

相關博文:

我的博客:一灰的我的博客

公衆號獲取更多:

https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg

相關文章
相關標籤/搜索