Java 工具箱 | 圖片-Base64 互轉

前言

最近真的被圖片上傳的功能給煩惱了。在web的項目中,咱們常常會有上傳圖片的業務場景,最典型的是上傳頭像。爲了解決頭像上能夠有以下的實現:javascript

  1. 使用 multipart/form-data 上傳用戶信息和頭像,也便是使用html裏面的<form></form>。如 gitlab中修改用戶信息的頭像。
  2. 先將圖片上傳到圖片服務,並獲取圖片鏈接,以後再用這個圖片鏈接修改用戶信息。
  3. 直接上傳圖片的Base64編碼信息,做爲圖片的數據,後臺再將編碼轉化爲圖片文件。

這裏將討論的是第三中實現方法的中的圖片與Base64編碼互轉。 css

在網頁中,會有以下兩種處理圖片的方式,一種是直接src="/avatar/avatar.jpg",另外一種則是 src="data:image/jpeg;base64,xxxxxx="的方式。第二種方式就是前端將發給後臺的內容,數據由[數據描述],[數據Base64]組成,[數據描述]將告知咱們該圖片的類別,能夠從中分析出圖片的拓展名,[數據Base64]爲圖片Base64編碼以後的數據,爲圖片文件完整的數據。爲了可以完整地回覆圖片的內容和拓展名,須要前端發送[數據描述],[數據Base64]到後臺。該格式實際上是Data URI Scheme,完整後面再作講解。html

<!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:java

  1. 將圖片文件讀取爲數據流,並轉化爲byte數組
  2. 將byte數組進行Base64編碼,並轉化爲字符串
  3. 根據文件的拓展名添加數據描述前綴

Base64轉圖片:ios

  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.*:git

<!-- 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轉圖片的代碼其實就只要以下的幾行代碼。web

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/4AAQSkxxxxxxxapache

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

相關文章
相關標籤/搜索