Unit 18 - 上傳圖片到 Server, 並將圖片轉成 base64 格式

4 minute read

JSF 教學

Unit 18

問題

讓使用者上傳單張圖片到 Server 端, 之後將圖片轉成 base64 格式, 以利後續儲存或顯示.

介紹兩種上傳方式:

  • 第一個為一般的提交,選擇檔案後, 按下 submit 按鈕。
  • 第二種為 Ajax 提交, 選擇檔案後, 自動提交

技術原理

相關 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> 上傳檔案.
  • 加入 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) 建立

檔案上傳處理程序

  1. JSF 透過 value binding, 將 file bind 到 某個 Bean property.
  2. Part 物件取得 inputStream, 讀取内容到 byte[]
  3. 使用 Base64.Base64.getEncoder().encodeToString()byte[] 的内容轉成 base64 string format.
  4. 若要在 <img>src 屬性中直接使用 base64 格式圖片内容, 需要加上 media type 及 編碼格式指示: data:image/png;base64, 注意:
    • h:form 的編碼方式要改成 enctype="multipart/form-data"

Upload action method

Action Method 內的程序邏輯:

  1. 使用 Part 物件取得 InputStream 物件, 以讀取圖片內容
  2. 將圖片內容存到 byte []
  3. 使用 Base64 物件對 byte[] 內圖片內容進行 Base64 編碼
  4. 取得圖片的 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

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 編碼字串。

Updated: