前言 最近真的被图片上传的功能给烦恼了。在web的项目中,我们经常会有上传图片的业务场景,最典型的是上传头像。为了解决头像上可以有如下的实现:
使用 multipart/form-data
上传用户信息和头像,也即是使用html里面的<form></form>
。如 gitlab中修改用户信息的头像。
先将图片上传到图片服务,并获取图片连接,之后再用这个图片连接修改用户信息。
直接上传图片的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 ="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 :
将图片文件读取为数据流,并转化为byte数组
将byte数组进行Base64编码,并转化为字符串
根据文件的拓展名添加数据描述前缀
Base64转图片 :
将Base64字符串分成数据描述和数据Base64两个部分
通过数据描述部分获得图片拓展名
将数据Base64进行Base64解码,得到byte数组
保存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;public class ImageConvertBase64 { public static byte [] toBytes(File image) { try (FileInputStream input = new FileInputStream(image)) { return IOUtils.toByteArray(input); } catch (IOException e) { return null ; } } public static String toBase64 (byte [] bytes) { return bytesEncode2Base64(bytes); } public static String toBase64 (File image) { return toBase64(image, false ); } 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); } 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 ; } } public static File toImage (String imageBase64, File imagePath) { int firstComma = imageBase64.indexOf("," ); if (firstComma >= 0 ) { imageBase64 = imageBase64.substring(firstComma + 1 ); } return toImage(base64Decode2Bytes(imageBase64), imagePath); } 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 <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" ); byte [] bytes = FileUtils.readFileToByteArray(image); String base64 = new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); System.out.println(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 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」
在最后的一位(A)或者两位字符(BC)时进行编码
解码
编码的逆过程,将 =
转化为 0
即可,在 ASCII 码中,0字符为空字符。
以上的内容基本来自 维基百科 Base64 。由于+/
的特殊性,为了适应不同的场景,会使用不同的字符替换掉原来算法中的 +/
,而形成新的算法。
在Java 8中,JDK提供了Base64的编解码工具。提供了 Basic编码
URL编码
MIME编码
以及 对流的封装
,文章 Java 8实现BASE64编解码 中对 Base64
工具做了不错的介绍。
Base64 转化工具: