二維碼定製

二維碼生成服務之深度定製
之前寫了一篇二維碼服務定製的博文,現在則在之前的基礎上,再進一步,花樣的實現深度定製的需求,我們的目標是二維碼上的一切都是可以由用戶來隨意指定

設計
1. 技術相關
zxing 開源包用於生成二維碼
springboot 搭建基本web服務,提供http接口
awt 用於圖片的編輯
httpclient 用於從網絡下載圖片
lombok 簡化編碼
2. 目的
既然是對二維碼服務的深度定製,那我們的目的基本上就是二維碼上面出現的東西,都可以按照我們的需求進行改造

這裏,我們設計兩個目的,一個基礎版,一個進階版

基礎版

二維碼大小
邊距留白指定
添加logo
加背景
進階版

二維碼中前置色和背景色可自由指定顏色
二維碼中前置色(黑白二維碼中的黑色區域)可換成圓點,三角形等其他圖形
前置色可用圖片替換
探測點(三個矩形框就是探測點,也叫做定位點)顏色可配置
探測點可用圖片替換
二維碼樣式(圓角矩形,添加邊框,邊框顏色可指定)
背景支持填充(填充在背景圖片的某個區域)和覆蓋方式(全覆蓋背景圖,二維碼設置透明度)
上面是我們希望達到的目的,下面給幾個實際生成的二維碼瞅瞅最終的效果

(小灰灰blog公衆號,實際測試時,請用微信掃一掃)

3. 前提準備
1.相關博文
在直接進入上面花樣的二維碼生成之前,有必要安利一把zxing的基本使用方式,本篇將不會對如何使用zxing進行說明,有需求瞭解的可以參考下面幾篇相關博文,此篇博文是 《spring-boot & zxing 搭建二維碼服務》 的衍生

java 實現二維碼生成工具類
zxing 二維碼大白邊一步一步修復指南
spring-boot & zxing 搭建二維碼服務
二維碼服務拓展(支持logo,圓角logo,背景圖,顏色配置)
2. 源碼介紹
此外下面直接貼代碼,可能有些地方不太容易理解,下面將簡單對一些輔助類進行必要的功能說明

源碼直通車:quick-media

涉及到的工具類:

QrCodeUtil : 二維碼生成工具類
生成二維碼矩陣
根據二維碼矩陣渲染二維碼圖片
ImageUtil : 圖片處理工具類
加載圖片(支持從本地,網絡獲取圖片)
繪製二維碼logo
圖片圓角化
圖片添加純色邊框
背景繪製
二維碼繪製
QrCodeOptions: 二維碼配置類
BitMatrixEx: 二維碼矩陣信息擴展類
QrCodeGenWrapper: 二維碼生成服務包裝類,與用戶進行交互的主要接口,設置配置信息,生成二維碼,選擇輸出方式,都是通過它來設定
4. 實現說明
第一步,生成矩陣
我們直接利用zxing來生成二維碼矩陣信息,並用來實例我們的矩陣拓展類 BitMatrixEx

在我們的工程中,相關的代碼爲

com.hust.hui.quickmedia.common.util.QrCodeUtil#encode
1
在這裏,只關心下面幾個參數的生成,其他的基本上就是zxing庫的調用了

/**
 * 實際生成二維碼的寬
 */
private int width;


/**
 * 實際生成二維碼的高
 */
private int height;


/**
 * 左白邊大小
 */
private int leftPadding;

/**
 * 上白邊大小
 */
private int topPadding;

/**
 * 矩陣信息縮放比例
 */
private int multiple;

private ByteMatrix byteMatrix;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在理解爲什麼有上面的幾個參數之前,有必要看一下byteMatrix到底是個什麼東西?(自問自答:二維碼矩陣)

下面截出前面二維碼中對應的矩陣信息,在生成一張二維碼時,下面的1表示一個小黑塊,0表示一個小白塊;

 1 1 1 1 1 1 1 0 0 0 0 1 0 0 0 1 1 1 0 0 0 0 0 1 0 1 1 0 0 0 1 1 1 1 1 1 1
 1 0 0 0 0 0 1 0 1 0 1 1 1 1 0 1 1 0 0 1 0 0 1 0 1 0 0 0 1 0 1 0 0 0 0 0 1
 1 0 1 1 1 0 1 0 1 1 0 0 0 0 0 1 1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 0 1 1 1 0 1
 1 0 1 1 1 0 1 0 1 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1 0 0 1 0 1 0 1 0 1 1 1 0 1
 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 1 1 1 0 1
 1 0 0 0 0 0 1 0 1 0 0 1 0 1 0 0 0 1 1 0 1 1 1 0 0 1 0 0 1 0 1 0 0 0 0 0 1
 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1
 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 1 1 1 0 0 0 0 0 0 0 0
 0 0 1 0 0 1 1 1 1 1 0 1 1 0 0 1 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 1 1 1 1 1 0
 0 1 0 0 0 1 0 1 0 1 1 1 0 1 1 0 1 1 0 0 1 0 1 0 1 0 0 1 0 0 1 1 0 1 0 0 1
 1 1 1 1 0 0 1 0 1 0 1 1 1 0 0 1 1 1 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1 0 0 1 1
 1 0 1 1 0 0 0 0 0 1 0 0 0 1 0 1 0 0 1 1 1 1 1 1 1 0 1 0 0 0 0 1 0 0 0 0 1
 1 0 1 1 0 0 1 0 1 0 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 0 1 1
 1 0 1 0 0 0 0 0 0 1 1 0 0 1 1 1 1 0 0 1 0 1 1 1 0 1 0 0 1 1 1 0 0 0 1 0 1
 1 1 1 1 0 0 1 0 0 0 0 0 1 1 1 0 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 1 1 0 1
 1 1 1 0 0 0 0 1 0 1 0 1 1 1 1 1 1 0 0 0 1 0 1 0 1 0 1 0 0 1 0 0 0 1 0 0 0
 0 1 1 0 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 1 0 1 0 0 1 1 0 1 0 0 0 0 1 0
 0 0 1 1 1 0 0 0 0 1 0 1 1 1 0 0 1 0 1 1 1 0 0 0 1 0 0 1 1 1 1 1 0 0 1 0 1
 0 0 1 1 1 1 1 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 1 1 0 1 1 1 1 1 0 1 0 0 1 0 1
 0 0 1 0 1 1 0 1 1 0 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 0 0 1 1 1 0 1 1 1 0 1 1
 0 0 1 0 1 0 1 0 0 1 0 1 1 0 0 1 1 0 0 0 1 1 1 0 0 1 0 0 1 0 1 0 0 1 0 1 0
 1 1 0 1 0 0 0 0 1 0 0 0 0 1 1 0 0 1 1 1 0 0 0 1 1 1 0 1 0 1 1 1 0 1 1 1 1
 1 0 0 0 1 0 1 1 0 1 1 0 1 1 0 0 0 0 1 1 0 0 1 1 1 0 0 1 0 1 1 1 1 0 0 1 1
 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 0 1 0 1 1 1 1 0 0 0 0 1 1 1 1 1 1 0 1 0 1 0
 1 0 0 1 0 1 1 0 1 1 0 0 0 1 1 0 1 0 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 1 1 1
 0 0 1 0 1 1 0 1 0 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 1 0 0 1 0 0 0 1 0 0 1 1 1
 1 1 0 1 1 0 1 1 0 1 0 0 1 0 0 1 1 0 0 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 1 0 1
 0 0 1 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 1 0 1 0 1 0 1 0 0 1 0 1 1
 1 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0
 0 0 0 0 0 0 0 0 1 1 1 0 1 0 1 1 1 0 1 1 1 0 1 1 0 1 0 0 1 0 0 0 1 1 0 1 1
 1 1 1 1 1 1 1 0 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 0 0 1 0 1 0 1 1 1 0 1
 1 0 0 0 0 0 1 0 1 0 0 0 1 0 0 1 0 1 0 0 0 0 0 1 1 0 1 1 1 0 0 0 1 1 0 1 0
 1 0 1 1 1 0 1 0 0 0 0 1 0 0 1 1 1 0 1 0 0 1 0 1 1 0 1 1 1 1 1 1 1 0 0 1 0
 1 0 1 1 1 0 1 0 0 1 0 0 1 1 0 1 0 0 0 1 1 1 0 0 0 0 0 1 1 0 0 1 0 1 1 0 1
 1 0 1 1 1 0 1 0 1 1 0 0 1 1 1 1 0 1 0 0 0 1 1 1 1 1 0 0 1 1 0 0 1 1 0 1 1
 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 0 0 1 1 0 0 0 1 0 1 0 0 0 1 0 0 0 0
 1 1 1 1 1 1 1 0 0 0 1 1 1 0 1 0 0 0 0 0 0 1 1 0 1 0 0 1 1 0 1 0 1 1 0 0 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
當生成了上面的人矩陣之後,最終的二維碼繪製都是根據上面的矩陣來的,將1的地方用我們希望繪製的樣式(如圓點,三角形,圖形等)來替換;

上面的矩陣表示的基本的二維碼信息,最終渲染二維碼圖片時,我們還需要知道最終的圖片大小,四周的留白空間,每個二維碼信息在放射到最終二維碼圖片時放大的倍數,有這些參數之後才能唯一指定最終的輸出結果,所以就有了上面的幾個參數

第二步, 二維碼信息的繪製
根據上面的二維碼矩陣來渲染二維碼圖片,先考慮最簡單的,沒有任何配置時,可以怎麼玩?

下面用到的參數來自BitMatirxEx

繪製整個背景(直接根據給定的寬高繪製矩形背景即可)

g2.setColor(Color.WHITE);
g2.fillRect(0, 0, qrCodeWidth, qrCodeHeight);
1
2
二維碼矩陣中(x,y) == 1的地方繪製小方塊

g2.setColor(Color.BLACK);
g2.fillRect(x+leftPadding, y+topPadding, multiple, multiple);
1
2
根據2可知,整個渲染就是矩陣(二維數組)的遍歷而已
根據上面的生成邏輯,我們可以很清晰的發現,有幾個目標是可以很簡單實現的

二維碼背景色&前置色的指定(就是在1,2步驟中的setColor用指定的顏色替換即可)
替換二維碼黑色小方塊爲其他圖形
這裏是一個小關鍵點了,在具體的實現中,我提供了:
- 三角形,
- 矩形(即二維碼默認格式),
- 五邊形(鑽石),
- 六邊形,
- 八邊形,
- 圓形,
- 圖片

比較遺憾的是五角星沒有支持,沒想到合適的繪製方式

不同的樣式,對應的繪製不同,我們定義了一個枚舉,來定義不同的樣式對應的繪製規則,優勢就是擴展自定義樣式方便,下面給出具體的繪製代碼

/**
 * 繪製二維碼信息的樣式
 */
public enum DrawStyle {
    RECT { // 矩形

        @Override
        public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
            g2d.fillRect(x, y, w, h);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return true;
        }
    },
    CIRCLE {
        // 圓點
        @Override
        public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
            g2d.fill(new Ellipse2D.Float(x, y, w, h));
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return expandType == ExpandType.SIZE4;
        }
    },
    TRIANGLE {
        // 三角形
        @Override
        public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
            int px[] = {x, x + (w >> 1), x + w};
            int py[] = {y + w, y, y + w};
            g2d.fillPolygon(px, py, 3);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return false;
        }
    },
    DIAMOND {
        // 五邊形-鑽石
        @Override
        public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
            int cell4 = size >> 2;
            int cell2 = size >> 1;
            int px[] = {x + cell4, x + size - cell4, x + size, x + cell2, x};
            int py[] = {y, y, y + cell2, y + size, y + cell2};
            g2d.fillPolygon(px, py, 5);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return expandType == ExpandType.SIZE4;
        }
    },
    SEXANGLE {
        // 六邊形
        @Override
        public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
            int add = size >> 2;
            int px[] = {x + add, x + size - add, x + size, x + size - add, x + add, x};
            int py[] = {y, y, y + add + add, y + size, y + size, y + add + add};
            g2d.fillPolygon(px, py, 6);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return expandType == ExpandType.SIZE4;
        }
    },
    OCTAGON {
        // 八邊形
        @Override
        public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
            int add = size / 3;
            int px[] = {x + add, x + size - add, x + size, x + size, x + size - add, x + add, x, x};
            int py[] = {y, y, y + add, y + size - add, y + size, y + size, y + size - add, y + add};
            g2d.fillPolygon(px, py, 8);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return expandType == ExpandType.SIZE4;
        }
    },
    IMAGE {
        // 自定義圖片
        @Override
        public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
            g2d.drawImage(img, x, y, w, h, null);
        }

        @Override
        public boolean expand(ExpandType expandType) {
            return true;
        }
    },;

    private static Map<String, DrawStyle> map;

    static {
        map = new HashMap<>(7);
        for (DrawStyle style : DrawStyle.values()) {
            map.put(style.name(), style);
        }
    }

    public static DrawStyle getDrawStyle(String name) {
        if (StringUtils.isBlank(name)) { // 默認返回矩形
            return RECT;
        }


        DrawStyle style = map.get(name.toUpperCase());
        return style == null ? RECT : style;
    }


    public abstract void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img);


    /**
     * 返回是否支持繪製圖形的擴展
     *
     * @param expandType
     * @return
     */
    public abstract boolean expand(ExpandType expandType);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
上面完成了二維碼樣式的定製,還有一個探測點(或者叫做定位點)的定製,也得在這一步中進行;

普通的二維碼結構如下

探測點就是二維碼中的三個方塊,再看上面的二維碼矩陣,下圖中的兩個紅框內的其實就是上面的兩個探測圖形,外面的那層全0是分割符

兩者一結合,很容易就可以搞定探測圖形的位置,第一行有多少個連續的1就表示探測圖形的size是多大

所以探測圖形的私人定製就比較簡單了,下面是具體的繪製代碼(下面實現圖片繪製,內外框採用不同顏色的實現)

// 設置三個位置探測圖形
if (x < detectCornerSize && y < detectCornerSize // 左上角
      || (x < detectCornerSize && y >= byteH - detectCornerSize) // 左下腳
      || (x >= byteW - detectCornerSize && y < detectCornerSize)) { // 右上角

  if (qrCodeConfig.getDetectOptions().getDetectImg() != null) {
     // 繪製圖片
      g2.drawImage(qrCodeConfig.getDetectOptions().getDetectImg(),
              leftPadding + x * infoSize, topPadding + y * infoSize,
              infoSize * detectCornerSize, infoSize * detectCornerSize, null);

      for (int addX = 0; addX < detectCornerSize; addX++) {
          for (int addY = 0; addY < detectCornerSize; addY++) {
              bitMatrix.getByteMatrix().set(x + addX, y + addY, 0);
          }
      }
      continue;
  }


  if (x == 0 || x == detectCornerSize - 1 || x == byteW - 1 || x == byteW - detectCornerSize
          || y == 0 || y == detectCornerSize - 1 || y == byteH - 1 || y == byteH - detectCornerSize) {
      // 外層的框
      g2.setColor(detectOutColor);
  } else {
      // 內層的框
      g2.setColor(detectInnerColor);
  }

  g2.fillRect(leftPadding + x * infoSize, topPadding + y * infoSize, infoSize, infoSize);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
到此,二維碼主體的定製基本上over了,就最終的實現來看,我們的目標中除了logo和背景外,其他的基本上都是ok的,這裏稍稍拓展了一點,如果連續兩個爲1,或一個小矩形全是1,則將這相同的幾個串在一起,因此纔有了上面的部分圖形較大的情況(當然這個是可選的配置)

下面貼出整個繪製代碼

public static BufferedImage drawQrInfo(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) {
    int qrCodeWidth = bitMatrix.getWidth();
    int qrCodeHeight = bitMatrix.getHeight();
    int infoSize = bitMatrix.getMultiple();
    BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);


    // 繪製的背景色
    Color bgColor = qrCodeConfig.getDrawOptions().getBgColor();
    // 繪製前置色
    Color preColor = qrCodeConfig.getDrawOptions().getPreColor();

    // 探測圖形外圈的顏色
    Color detectOutColor = qrCodeConfig.getDetectOptions().getOutColor();
    // 探測圖形內圈的顏色
    Color detectInnerColor = qrCodeConfig.getDetectOptions().getInColor();


    int leftPadding = bitMatrix.getLeftPadding();
    int topPadding = bitMatrix.getTopPadding();

    Graphics2D g2 = qrCode.createGraphics();
    g2.setComposite(AlphaComposite.Src);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);


    // 直接背景鋪滿整個圖
    g2.setColor(bgColor);
    g2.fillRect(0, 0, qrCodeWidth, qrCodeHeight);

    // 探測圖形的大小
    int detectCornerSize = bitMatrix.getByteMatrix().get(0, 5) == 1 ? 7 : 5;

    int byteW = bitMatrix.getByteMatrix().getWidth();
    int byteH = bitMatrix.getByteMatrix().getHeight();

    boolean row2 = false;
    boolean col2 = false;
    QrCodeOptions.DrawStyle drawStyle = qrCodeConfig.getDrawOptions().getDrawStyle();
    for (int x = 0; x < byteW; x++) {
        for (int y = 0; y < byteH; y++) {
            if (bitMatrix.getByteMatrix().get(x, y) == 0) {
                continue;
            }

            // 設置三個位置探測圖形
            if (x < detectCornerSize && y < detectCornerSize // 左上角
                    || (x < detectCornerSize && y >= byteH - detectCornerSize) // 左下腳
                    || (x >= byteW - detectCornerSize && y < detectCornerSize)) { // 右上角

                if (qrCodeConfig.getDetectOptions().getDetectImg() != null) {
                    g2.drawImage(qrCodeConfig.getDetectOptions().getDetectImg(),
                            leftPadding + x * infoSize, topPadding + y * infoSize,
                            infoSize * detectCornerSize, infoSize * detectCornerSize, null);

                    for (int addX = 0; addX < detectCornerSize; addX++) {
                        for (int addY = 0; addY < detectCornerSize; addY++) {
                            bitMatrix.getByteMatrix().set(x + addX, y + addY, 0);
                        }
                    }
                    continue;
                }


                if (x == 0 || x == detectCornerSize - 1 || x == byteW - 1 || x == byteW - detectCornerSize
                        || y == 0 || y == detectCornerSize - 1 || y == byteH - 1 || y == byteH - detectCornerSize) {
                    // 外層的框
                    g2.setColor(detectOutColor);
                } else {
                    // 內層的框
                    g2.setColor(detectInnerColor);
                }

                g2.fillRect(leftPadding + x * infoSize, topPadding + y * infoSize, infoSize, infoSize);
            } else { // 着色二維碼主題
                g2.setColor(preColor);

                if (!qrCodeConfig.getDrawOptions().isEnableScale()) {
                    drawStyle.draw(g2,
                            leftPadding + x * infoSize,
                            topPadding + y * infoSize,
                            infoSize,
                            infoSize,
                            qrCodeConfig.getDrawOptions().getImg());
                    continue;
                }


                // 支持拓展時
                row2 = rightTrue(bitMatrix.getByteMatrix(), x, y);
                col2 = belowTrue(bitMatrix.getByteMatrix(), x, y);

                if (row2 && col2 && diagonalTrue(bitMatrix.getByteMatrix(), x, y) &&
                        qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.SIZE4)) {
                    // 四個相等
                    bitMatrix.getByteMatrix().set(x + 1, y, 0);
                    bitMatrix.getByteMatrix().set(x + 1, y + 1, 0);
                    bitMatrix.getByteMatrix().set(x, y + 1, 0);
                    drawStyle.draw(g2,
                            leftPadding + x * infoSize,
                            topPadding + y * infoSize,
                            infoSize << 1,
                            infoSize << 1,
                            qrCodeConfig.getDrawOptions().getSize4Img());
                } else if (row2 && qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.ROW2)) { // 橫向相同
                    bitMatrix.getByteMatrix().set(x + 1, y, 0);
                    drawStyle.draw(g2,
                            leftPadding + x * infoSize,
                            topPadding + y * infoSize,
                            infoSize << 1,
                            infoSize,
                            qrCodeConfig.getDrawOptions().getRow2Img());
                } else if (col2 && qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.COL2)) { // 列的兩個
                    bitMatrix.getByteMatrix().set(x, y + 1, 0);
                    drawStyle.draw(g2,
                            leftPadding + x * infoSize,
                            topPadding + y * infoSize,
                            infoSize,
                            infoSize << 1,
                            qrCodeConfig.getDrawOptions().getCol2img());
                } else {
                    drawStyle.draw(g2,
                            leftPadding + x * infoSize,
                            topPadding + y * infoSize,
                            infoSize,
                            infoSize,
                            qrCodeConfig.getDrawOptions().getImg());
                }
            }
        }
    }
    g2.dispose();
    return qrCode;
}


private static boolean rightTrue(ByteMatrix byteMatrix, int x, int y) {
    return x + 1 < byteMatrix.getWidth() && byteMatrix.get(x + 1, y) == 1;
}

private static boolean belowTrue(ByteMatrix byteMatrix, int x, int y) {
    return y + 1 < byteMatrix.getHeight() && byteMatrix.get(x, y + 1) == 1;
}

// 對角是否相等
private static boolean diagonalTrue(ByteMatrix byteMatrix, int x, int y) {
    return byteMatrix.get(x + 1, y + 1) == 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
第三步. logo&背景的繪製
到第二步,其實二維碼就已經繪製完成了,二維碼和背景都是在二維碼這種圖片上做文章,一個是往二維碼上加圖片,一個是將二維碼繪製在另一張圖片上

一個圖片在另一個圖片上繪製沒啥技術含量,稍微特別點的就是logo的圓角和邊框了

《二維碼服務拓展(支持logo,圓角logo,背景圖,顏色配置)》 較清晰的說了如何繪製圓角圖片,圓角邊框

不想看上面博文的沒啥關係,下面直接貼出代碼,算是比較通用的方法了,與二維碼項目本身沒什麼黏合

/**
 * 生成邊框
 *
 * @param image        原圖
 * @param cornerRadius 角度 0表示直角
 * @param color        邊框顏色
 * @return
 */
public static BufferedImage makeRoundBorder(BufferedImage image,
                                                int cornerRadius,
                                                Color color) {
    int size = image.getWidth() / 15;
    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));

    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
    g2.drawImage(image, size >> 1, size >> 1, null);
    g2.dispose();

    return output;
}


/**
 * 生成圓角圖片
 *
 * @param image        原始圖片
 * @param cornerRadius 圓角的弧度大小(根據實測效果,一般建議爲圖片寬度的1/4), 0表示直角
 * @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();
    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));


    g2.setComposite(AlphaComposite.SrcAtop);
    g2.drawImage(image, 0, 0, null);

    g2.dispose();

    return output;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
與上一篇定製博文有一點區別的是,對背景圖的支持進行了擴展,除了支持之前的設置二維碼透明度,全覆蓋背景圖之外,又支持了在背景圖的指定位置處進行繪製二維碼,因爲這一塊確實沒什麼好講的,乾脆貼下代碼好了

/**
 * 繪製背景圖
 *
 * @param source       二維碼圖
 * @param bgImgOptions 背景圖信息
 * @return
 */
public static BufferedImage drawBackground(BufferedImage source, QrCodeOptions.BgImgOptions bgImgOptions) {
    int sW = source.getWidth();
    int sH = source.getHeight();

    // 背景的圖寬高不應該小於原圖
    int bgW = bgImgOptions.getBgW() < sW ? sW : bgImgOptions.getBgW();
    int bgH = bgImgOptions.getBgH() < sH ? sH : bgImgOptions.getBgH();


    // 背景圖縮放
    BufferedImage bg = bgImgOptions.getBgImg();
    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;
    }

    Graphics2D g2d = bg.createGraphics();
    if (bgImgOptions.getBgImgStyle() == QrCodeOptions.BgImgStyle.FILL) {
        // 選擇一塊區域進行填充
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.drawImage(source, bgImgOptions.getStartX(), bgImgOptions.getStartY(), sW, sH, null);
    } else {
        // 覆蓋方式
        int x = (bgW - sW) >> 1;
        int y = (bgH - sH) >> 1;
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, bgImgOptions.getOpacity())); // 透明度, 避免看不到背景
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.drawImage(source, x, y, sW, sH, null);
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
    }
    g2d.dispose();
    bg.flush();
    return bg;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
測試
開發完了之後,就要開始愉快的進行測試了,測試一個全乎的

@Test
public void testGenStyleCodeV2() {
    String msg = "http://weixin.qq.com/r/FS9waAPEg178rUcL93oH";

    try {
        String logo = "logo.jpg";
        String bg = "qrbg.jpg";
        BufferedImage img = QrCodeGenWrapper.of(msg)
                .setW(550)
                .setDrawPreColor(0xff002fa7) // 寶石藍
                .setDetectOutColor(0xff0000ff)
                .setDetectInColor(Color.RED)
                .setDetectImg("detect.png")
                .setPadding(1)
                .setErrorCorrection(ErrorCorrectionLevel.H)
                .setLogo(logo)
                .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
                .setLogoBgColor(0xff00cc00)
                .setLogoRate(15)
                .setDrawStyle(QrCodeOptions.DrawStyle.IMAGE.name())
                .setDrawEnableScale(true)
                .setDrawImg("xhrBase.jpg")
                .setDrawRow2Img("xhrr2.jpeg")
                .setDrawCol2Img("xhrc2.jpeg")
                .setDrawSize4Img("xhrSize4.jpg")
                .setBgStyle(QrCodeOptions.BgImgStyle.FILL)
                .setBgImg(bg)
                .setBgStartX(230)
                .setBgStartY(330)
                .asBufferedImage();


        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(img, "png", outputStream);
        String img64 = Base64Util.encode(outputStream);
        System.out.println("<img src=\"data:image/png;base64," + img64 + "\" />");
    } catch (Exception e) {
        System.out.println("create qrcode error! e: " + e);
        Assert.assertTrue(false);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
演示case:

一個最終定格的二維碼

說明
上面的改造,在實際使用時,建議多測試測試是否可以掃描出來,騰訊系列產品的二維碼掃描特別給力,一般都能很迅速的識別,其他的就不好說了

其他
相關博文

java 實現二維碼生成工具類
zxing 二維碼大白邊一步一步修復指南
spring-boot & zxing 搭建二維碼服務
二維碼服務拓展(支持logo,圓角logo,背景圖,顏色配置)
項目地址: https://github.com/liuyueyi/quick-media

個人博客:一灰的個人博客

公衆號獲取更多:

參考
二維碼基礎原理
點贊 2
————————————————
版權聲明:本文爲CSDN博主「一灰灰blog」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/liuyueyi25/article/details/77131810

發佈了91 篇原創文章 · 獲贊 92 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章