前言

最近真的被图片上传的功能给烦恼了。在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,完整后面再做讲解。

<!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,分别记录数据描述映射到拓展名和拓展名映射到数据描述,从而方便数据描述和拓展名的获取。

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数组到文件,如果保存的文件路径提供完整的文件名称,则无需所得拓展名,否则使用所得拓展名作为图片文件拓展名
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.*:

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
获取文件拓展名
int lastPointIndex = filename.lastIndexOf(".");
String extension = lastPointIndex < 0 ? "" : getScheme(filename.substring(lastPointIndex + 1));
// 简化
String extension = FilenameUtils.getExtension(filename);
文件转 byte[]
File image = new File("avatar.jpg");
IOUtils.toByteArray(new FileInputStream(image));
// 或者
byte[] bytes = FileUtils.readFileToByteArray(image);
byte[] 保存为文件
File image = new File("avatar.jpg")
FileUtils.writeByteArrayToFile(image, bytes);

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

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

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 转化工具:


donespeak
125 声望14 粉丝

Let the Work That I've Done Speak for Me.