DoneSpeak

Java 工具箱 | 图片-Base64 互转

字数: 2.8k时长: 12 min
2019/06/05 Share

前言

最近真的被图片上传的功能给烦恼了。在web的项目中,我们经常会有上传图片的业务场景,最典型的是上传头像。为了解决头像上可以有如下的实现:

  1. 使用 multipart/form-data 上传用户信息和头像,也即是使用html里面的<form></form>。如 gitlab中修改用户信息的头像。
  2. 先将图片上传到图片服务,并获取图片连接,之后再用这个图片连接修改用户信息。
  3. 直接上传图片的Base64编码信息,作为图片的数据,后台再将编码转化为图片文件。

这里将讨论的是第三中实现方法的中的图片与Base64编码互转。

在网页中,会有如下两种处理图片的方式,一种是直接src="/avatar/avatar.jpg",另一种则是 src="data:image/jpeg;base64,xxxxxx="的方式。第二种方式就是前端将发给后台的内容,数据由[数据描述],[数据Base64]组成,[数据描述]将告知我们该图片的类别,可以从中分析出图片的拓展名,[数据Base64]为图片Base64编码之后的数据,为图片文件完整的数据。为了能够完整地回复图片的内容和拓展名,需要前端发送[数据描述],[数据Base64]到后台。该格式其实是Data URI Scheme,完整后面再做讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<p>Data URLs Image:</p>
<!-- 常见的方式 -->
<img src="/avatar/avatar.jpg">
<!-- 将 img.src 的内容复制粘贴到浏览器的输入框中,可以看到图片的内容 -->
<img src="data:image/jpeg;base64,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" />
</body>
</html>

从 [数据描述] 判断图片拓展名

具体实现

数据描述与拓展名的映射

这里利用两个map,分别记录数据描述映射到拓展名和拓展名映射到数据描述,从而方便数据描述和拓展名的获取。

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
import java.util.HashMap;
import java.util.Map;
import java.io.File;

public class ImageDataURISchemeMapper {
private static Map<String, String> scheme2Extension = new HashMap<String, String>();
private static Map<String, String> extension2Scheme = new HashMap<String, String>();

static {
initSchemeSupported();
}

public static String getScheme(String imageExtension) {
if (imageExtension == null || imageExtension.isEmpty()) {
return "";
}
String result = extension2Scheme.get(imageExtension.toLowerCase());
return result == null ? "" : result;
}

public static String getScheme(File image) {
if (image == null) {
return "";
}
String name = image.getName();
int lastPointIndex = name.lastIndexOf(".");
return lastPointIndex < 0 ? "" : getScheme(name.substring(lastPointIndex + 1));
}

public static String getExtension(String dataUrlScheme) {
return scheme2Extension.get(dataUrlScheme);
}

public static String getExtensionFromImageBase64(String imageBase64, String defaultExtension) {
int firstComma = imageBase64.indexOf(",");
if(firstComma < 0) {
return defaultExtension;
}
return scheme2Extension.get(imageBase64.subSequence(0, firstComma + 1));
}

private static void initSchemeSupported() {
addScheme("jpg", "data:image/jpg;base64,");
addScheme("jpeg", "data:image/jpeg;base64,");
addScheme("png", "data:image/png;base64,");
addScheme("gif", "data:image/gif;base64,");
addScheme("icon", "data:image/x-icon;base64,");
}

private static void addScheme(String extension, String dataUrl) {
scheme2Extension.put(dataUrl, extension);
extension2Scheme.put(extension, dataUrl);
}
}

图片转Base64及Base64转图片

图片转Base64:

  1. 将图片文件读取为数据流,并转化为byte数组
  2. 将byte数组进行Base64编码,并转化为字符串
  3. 根据文件的拓展名添加数据描述前缀

Base64转图片:

  1. 将Base64字符串分成数据描述和数据Base64两个部分
  2. 通过数据描述部分获得图片拓展名
  3. 将数据Base64进行Base64解码,得到byte数组
  4. 保存byte数组到文件,如果保存的文件路径提供完整的文件名称,则无需所得拓展名,否则使用所得拓展名作为图片文件拓展名
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
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.IOUtils;

/**
* 目标处理的图片类别有:png,jpg,jpeg
*
* 参考:
* <ul>
* <li>[浅析data:image/png;base64的应用](https://www.cnblogs.com/ECJTUACM-873284962/p/9245474.html)</li>
* <li>[Base64](https://zh.wikipedia.org/wiki/Base64)</li>
* <li>[Data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
* </ul>
*
* @author Donespeak
* @date 2019/06/26
*/
public class ImageConvertBase64 {

/**
* 将图片文件转化为 byte 数组
*
* @param image
* 待处理图片文件
* @return 图片文件转化为的byte数组
*/
public static byte[] toBytes(File image) {
try (FileInputStream input = new FileInputStream(image)) {
// InputStream 的 available() 返回的值是该InputStream 在不被阻塞的情况下,一次可以读取到的数据长度。
// byte[] imageBytes = new byte[input.available()];
// input.read(imageBytes);
return IOUtils.toByteArray(input);
} catch (IOException e) {
return null;
}
}

public static String toBase64(byte[] bytes) {
return bytesEncode2Base64(bytes);
}

/**
* 将图片转化为 base64 的字符串
*
* @param image
* 待处理图片文件
* @return 图片文件转化出来的 base64 字符串
*/
public static String toBase64(File image) {
return toBase64(image, false);
}

/**
* 将图片转化为 base64 的字符串。如果<code>appendDataURLScheme</code>的值为true,则为图片的base64字符串拓展Data URL scheme。
* @param image 图片文件的路径
* @param appendDataURLScheme 是否拓展 Data URL scheme 前缀
* @return 图片文件转化为的base64字符串
*/
public static String toBase64(File image, boolean appendDataURLScheme) {
String imageBase64 = bytesEncode2Base64(toBytes(image));
if(appendDataURLScheme) {
imageBase64 = ImageDataURISchemeMapper.getScheme(image) + imageBase64;
}
return imageBase64;
}

private static String bytesEncode2Base64(byte[] bytes) {
return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
}

private static byte[] base64Decode2Bytes(String base64) {
return Base64.getDecoder().decode(base64);
}

/**
* 将byte数组恢复为图片文件
*
* @param imageBytes
* 图片文件的 byte 数组
* @param imagePath
* 恢复的图片文件的保存地址
* @return 如果生成成功,则返回生成的文件路径,此时结果为参数的<code>imagePath</code>。否则返回 null
*/
public static File toImage(byte[] imageBytes, File imagePath) {
if (!imagePath.getParentFile().exists()) {
imagePath.getParentFile().mkdirs();
}
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(imagePath))) {
bos.write(imageBytes);
return imagePath;
} catch (IOException e) {
return null;
}
}

/**
* 将base64字符串恢复为图片文件
*
* @param imageBase64
* 图片文件的base64字符串
* @param imagePath
* 恢复的图片文件的保存地址
* @return 如果生成成功,则返回生成的文件路径,此时结果为参数的<code>imagePath</code>。。否则返回 null
*/
public static File toImage(String imageBase64, File imagePath) {
// base64 字符串中没有 ","
int firstComma = imageBase64.indexOf(",");
if(firstComma >= 0) {
imageBase64 = imageBase64.substring(firstComma + 1);
}
return toImage(base64Decode2Bytes(imageBase64), imagePath);
}

/**
* 保存 imageBase64 到指定文件中。如果<code>fileName</code>含有拓展名,则直接使用<code>fileName</code>的拓展名。
* 否则,如果 <code>imageBase64</code> 为Data URLs,则更具前缀的来判断拓展名。如果无法判断拓展名,则使用“png”作为默认拓展名。
* @param imageBase64 图片的base64编码字符串
* @param dir 保存图片的目录
* @param fileName 图片的名称
* @return 如果生成成功,则返回生成的文件路径。否则返回 null
*/
public static File toImage(String imageBase64, File dir, String fileName) {
File imagePath = null;
if(fileName.indexOf(".") < 0) {
String extension = ImageDataURISchemeMapper.getExtensionFromImageBase64(imageBase64, "png");
imagePath = new File(dir, fileName + "." + extension);
} else {
imagePath = new File(dir, fileName);
}
return toImage(imageBase64, imagePath);
}
}

利用第三方工具类简化代码。

这里使用的工具类为 org.apache.commons.io.*:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

获取文件拓展名

1
2
3
4
int lastPointIndex = filename.lastIndexOf(".");
String extension = lastPointIndex < 0 ? "" : getScheme(filename.substring(lastPointIndex + 1));
// 简化
String extension = FilenameUtils.getExtension(filename);

文件转 byte[]

1
2
3
4
File image = new File("avatar.jpg");
IOUtils.toByteArray(new FileInputStream(image));
// 或者
byte[] bytes = FileUtils.readFileToByteArray(image);

byte[] 保存为文件

1
2
File image = new File("avatar.jpg")
FileUtils.writeByteArrayToFile(image, bytes);

实现图片转Base64和Base64转图片的代码其实就只要如下的几行代码。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {
File image = new File("src/test/resources/imageConvertBase64.jpeg");
File newImage = new File("src/test/resources/new-imageConvertBase64.jpeg");

// 编码为 Base64 字符串
byte[] bytes = FileUtils.readFileToByteArray(image);
String base64 = new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
System.out.println(base64);
// Base64 保存为图片
bytes = Base64.getDecoder().decode(base64);
FileUtils.writeByteArrayToFile(newImage, bytes);
}

拓展知识

Data URLs

Data URLs, URLs prefixed with the data: scheme, allow content creators to embed small files inline in documents.

Data URLs @Mozilla

Data URLs 由如下四个部分组成,如文章开篇的 data:image/jpeg;base64,/9j/4AAQSkxxxxxxx

1
data:[<mediatype>][;base64],<data>

  • data: : 协议固定前缀。
  • [<mediatype>] : 是一个 MIME type,比如 image/jpeg,你也可以在 “Incomplete list of MIME types” 中找到一些类型。
  • [;base64] : 是编码方式。 这里用的 base64
  • <data> : 编码后的字符串。

Data URL schema 是在 RFC2397 中被定义的URL scheme。其有如下的优缺点:

优点

  • 减少请求
  • 当外部资源受限是可以使用

缺点

  • 无法重复使用
  • 无法独立缓存 (可以利用css的background-image和css文件一起缓存)
  • base64会比原始文件增加 1/3 的大小

目前Data URL schema支持的类型有:

类型 描述
data:, 文本数据
data:text/plain, 文本数据
data:text/html, HTML代码
data:text/html;base64, base64编码的HTML代码
data:text/css, CSS代码
data:text/css;base64, base64编码的CSS代码
data:text/javascript, Javascript代码
data:text/javascript;base64, base64编码的Javascript代码
data:image/gif;base64, base64编码的gif图片数据
data:image/png;base64, base64编码的png图片数据
data:image/jpeg;base64, base64编码的jpeg图片数据
data:image/x-icon;base64, base64编码的icon图片数据

更加详细的内容可以看:浅析data:image/png;base64的应用 @Angel_Kitty

Base64

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。任何数据都是二进制数据,也就是说Base64可以表示任何数据。Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括MIME的电子邮件及XML的一些复杂数据。其主要的主要作用不在于安全,而在于让数据在网络中无错传输。

由于 2^6=64,所以每6个bit为一个单元,对应某个可打印字符。3个字节(byte)有24个bit,对应于4个Base64单元(24/6=4),即3个字节可由4个可打印字符来表示。这就导致编码后的数据比原始数据增加了1/3的长度。每6个bit的取值按照 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ 进行选择。如原数据长度不是3的倍数,则剩下一个输入字符,添加一个=,两个输入字符,编码结果则增加一个=

编码「Man」

base64-encode-string-man.png

在最后的一位(A)或者两位字符(BC)时进行编码
base64-decode.png

解码

编码的逆过程,将 = 转化为 0 即可,在 ASCII 码中,0字符为空字符。

以上的内容基本来自 维基百科 Base64。由于+/的特殊性,为了适应不同的场景,会使用不同的字符替换掉原来算法中的 +/,而形成新的算法。

在Java 8中,JDK提供了Base64的编解码工具。提供了 Basic编码 URL编码 MIME编码 以及 对流的封装,文章 Java 8实现BASE64编解码 中对 Base64 工具做了不错的介绍。

Base64 转化工具:

CATALOG
  1. 1. 前言
  2. 2. 从 [数据描述] 判断图片拓展名
  3. 3. 具体实现
    1. 3.1. 数据描述与拓展名的映射
    2. 3.2. 图片转Base64及Base64转图片
    3. 3.3. 利用第三方工具类简化代码。
  4. 4. 拓展知识
    1. 4.1. Data URLs
    2. 4.2. Base64