Unit 18 - 上傳圖片到 Server, 並將圖片轉成 base64 格式
Unit 18
問題
讓使用者上傳單張圖片到 Server 端, 之後將圖片轉成 base64 格式, 以利後續儲存或顯示.
介紹兩種上傳方式:
- 第一個為一般的提交,選擇檔案後, 按下 submit 按鈕。
- 第二種為 Ajax 提交, 選擇檔案後, 自動提交
技術原理
- Data url
- URLs prefixed with the data: scheme, allow content creators to embed small files inline in documents.
- Ref: Data URLs
- base64 格式
- Base64 is a group of binary-to-text encoding schemes that represent binary data in an ASCII string format by translating it into a radix-64 representation. The term Base64 originates from a specific MIME content transfer encoding.
- Ref: Base64 - Wikipedia
相關技術文件
- Unit 06 - 上傳及下載檔案
- 17.16 The fileupload Example Application with Servlet 3.0 - Java Platform, Enterprise Edition: The Java EE Tutorial (Release 7)
相關 API
Java 9
- java.util.Base64
- java.io.InputStream
Demo Source Code
Demo Code: t11 directory @ jsf_under_training_codes - Bitbucket
實作 1
專案設定
Project Dependency
- 使用 Primeface 8.0 及 lombok 1.8.12
- 部分
pom.xml
内容
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.primefaces/primefaces -->
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>8.0</version>
</dependency>
- 因爲使用了 lombok project 自動產生 getter 及 setter, Maven compiler plugin 要額外設定.
- Maven compiler plugin 的設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
<!--Set the lombok for the compiler plugin-->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
PrimeFaces 設定
- 使用了 PrimeFaces 的
<p:fileUpload>
上傳檔案.- 此 tag 可使用 Servlet 3.0 或者 Apache Commons, 需要額外設定.
- 參考 PrimeFaces Documentation
- 加入
web.xml
的設定内容:
1
2
3
4
<context-param>
<param-name>primefaces.UPLOADER</param-name>
<param-value>auto</param-value>
</context-param>
Web Components (CDI Beans) 建立
檔案上傳處理程序
- JSF 透過 value binding, 將 file bind 到 某個 Bean property.
- 該 Bean property 的資料型態為
javax.servlet.http.Part
- 該 Bean property 的資料型態為
- 由
Part
物件取得inputStream
, 讀取内容到byte[]
- 使用
Base64.Base64.getEncoder().encodeToString()
將byte[]
的内容轉成 base64 string format. - 若要在
<img>
的src
屬性中直接使用 base64 格式圖片内容, 需要加上 media type 及 編碼格式指示:data:image/png;base64,
注意:h:form
的編碼方式要改成enctype="multipart/form-data"
Upload action method
Action Method 內的程序邏輯:
- 使用
Part
物件取得InputStream
物件, 以讀取圖片內容 - 將圖片內容存到
byte []
- 使用
Base64
物件對byte[]
內圖片內容進行 Base64 編碼 - 取得圖片的 Base64 内容編碼之後, 將其值設定到 bean property
base64Photo
, 以利後續使用<img>
顯示圖片
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
...
private Part uploadPart;
/**
* Save file to the file system in the server-side.
* @return null (Stay in the same page).
*/
public String uploadAction() {
if (uploadPart == null) {
return null;
}
//Get the input string from the Part Object
InputStream is = null;
try {
is = uploadPart.getInputStream();
// Convert the input stream to the byte array
// Ref: https://magiclen.org/java-base64/
byte[] bytes = is.readAllBytes();
// Convert the base64 string to data uri
// Ref: https://blog.gtwang.org/web-development/minimizing-http-request-using-data-uri/
// Schema: data:[<media type>][;base64],<data>
String base64Str = Base64.getEncoder().encodeToString(bytes);
this.base64Photo = "data:image/png;base64," + base64Str;
} catch (IOException ex) {
Logger.getLogger(UploadBean.class.getName()).log(Level.SEVERE, null, ex);
}
//Set the file name property
filename = uploadPart.getSubmittedFileName();
return null; // Stay in the same page
}
JSF Page
Form to upload the file
- 表單的 enctype 要設定成 multipart/form-data
- enctype 資料上傳到伺服器時瀏覽器使用的編碼型別
- multipart/form-data 會將表單内的每個欄位各自編碼
- 使用
<h:inputFile>
上傳檔案, 其value
屬性 binding 的 property 的型態需要為javax.servlet.http.Part
1
2
3
4
5
6
7
8
<h2>File upload with html tag</h2>
<h:form enctype="multipart/form-data">
<h:panelGrid columns="2">
<h:outputLabel for="selectFile">Choose a file</h:outputLabel>
<h:inputFile id="selectFile" value="#{uploadBean.uploadPart}"/>
<h:commandButton value="Upload" action="#{uploadBean.uploadAction}"/>
</h:panelGrid>
</h:form>
Element to display the image using base64 format
1
2
3
<h2>Uploaded Photo</h2>
<p>Filename: #{uploadBean.filename}</p>
<img jsf:id="display" src="#{uploadBean.base64Photo}" />
實作 2: 使用 Ajax 的方式, 選擇圖片後, 自動上傳圖片
工作原理
- 修改實作 1 的程式碼
- 監聽上傳圖片的
<h:inputFile>
的valueChange
ajax event - 當
valueChange
ajax event 被觸發時, 提出請求(request), 只將<h:inputFile>
元素送往後端 Server 處理 - Server 端請求處理完畢後, 回應 Browser 處理結果, 用以更新 HTML 的 DOM 模型。我們指定更新顯示上傳圖片的元素。
修改步驟
JSF 頁面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<h2>File upload with html tag and Ajax</h2>
<h:form enctype="multipart/form-data">
<h:panelGrid columns="2">
<h:outputLabel>Field 1 </h:outputLabel>
<h:inputText value="#"></h:inputText>
<h:outputLabel for="selectFile">Choose a file</h:outputLabel>
<h:inputFile id="selectFile" value="#{uploadBean.uploadPart}">
<!-- #1 -->
<f:ajax event="valueChange" execute="@this" render=":display"
listener="#{uploadBean.handleFileUploadAjaxListener}"/>
</h:inputFile>
</h:panelGrid>
</h:form>
<h2>Uploaded Photo</h2>
<p>Filename: #{uploadBean.filename}</p>
<!--passthrough element; HTML tag friendly-->
<img jsf:id="display" src="#{uploadBean.base64Photo}" />
<br />
說明: 標示 #1:
- 使用
<f:ajax>
使元素具備 Ajax 的能力 - 屬性
event="valueChange"
: 監聽上傳圖片的<h:inputFile>
的valueChange
ajax event - 屬性
execute="@this"
: 事件觸發後, 將<f:ajax>
的父元素送交後端 Server 處理 - 屬性
listener="#{uploadBean.handleFileUploadAjaxListener}"
: 事件觸發後, 要執行的 bean method - 屬性
render=":display"
: 請求處理後, 重新 render id 為display
的元素
Ajax 事件的 event listener
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
@Named(value = "uploadBean")
@SessionScoped
// Use lombok to generate the getters and setters. See: https://kucw.github.io/blog/2020/3/java-lombok/
@Getter
@Setter
public class UploadBean implements Serializable {
private Part uploadPart;
private String filename = "";
private String base64Photo;
// #1
public void handleFileUploadAjaxListener(AjaxBehaviorEvent event) {
this.base64Photo = toBase64Str(uploadPart);
this.filename = uploadPart.getSubmittedFileName();
}
// #2
private String toBase64Str(Part part){
String result ="";
try {
InputStream is = uploadPart.getInputStream();
// Convert the input stream to the byte array
// Ref: https://magiclen.org/java-base64/
byte[] bytes = is.readAllBytes();
// Convert the base64 string to data uri
// Ref: https://blog.gtwang.org/web-development/minimizing-http-request-using-data-uri/
// Schema: data:[<media type>][;base64],<data>
String base64Str = Base64.getEncoder().encodeToString(bytes);
result = "data:image/png;base64," + base64Str;
} catch (IOException ex) {
Logger.getLogger(UploadBean.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}
標示 #1 說明:
- Ajax 事件觸發後, 圖片檔案會先儲存到 bean property
uploadPart
中, 之後再執行 Ajax event listener - Ajax event listener 方法的簽名:
1
public void ajaxListenerName(AjaxBehaviorEvent event)
標示 #2 說明:
- 給與一個
Part
物件,傳回該物件的 Base64 編碼字串。