二维码定制
二维码生成服务之深度定制之前写了一篇二维码服务定制的博文,现在则在之前的基础上,再进一步,花样的实现深度定制的需求,我们的目标是二维码上的一切都是可以由用户来随意指定设计1. 技术相关zxing 开源包用于生成二维码springboot 搭建基本web服务,提供http接口awt 用于图片的编辑httpclient 用于从网络下载图片lombok 简化编码2. 目的既然是对二...
二维码生成服务之深度定制
之前写了一篇二维码服务定制的博文,现在则在之前的基础上,再进一步,花样的实现深度定制的需求,我们的目标是二维码上的一切都是可以由用户来随意指定
设计
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
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)