Swift iOS App 的圖片上傳,以 remove.bg & Imgur API 為例

網路上有很多適合練習串接後台的第三方 API,不過如果想上傳圖片,相關的 API 就比較少了。彼得潘研究了一下,發現以下兩個 API 特別適合初學者練習圖片上傳的相關技術。

  • remove.bg

將圖片上傳去背的 API,可練習 multipart/form-data,application/json & application/x-www-form-urlencoded 三種上傳的資料格式,以及 binary,base64 & 圖片網址三種圖片格式。

  • Imgur

將圖片存到雲端的 API,適合製作需要使用者上傳圖片的 App 應用。

接下來就讓我們以 remove.bg & Imgur API 開發 SwiftUI & UIKit App,一步步認識圖片上傳的相關技術吧。

將圖片去背的 remove.bg API

remove.bg 提供強大的圖片去背功能。

我們可從網站上傳圖片,將彼得潘 & 小王子在 B612 星球的合照去背。

如此神奇的去背功能,我們的 App 也可以擁有,因為 remove.bg 提供了去背 API。

註冊帳號

為了使用 API,我們必須先註冊帳號。

取得 API key

有了 API key 才能串接 remove.bg API。請連到 API 的介紹頁面,點選 Get API Key。

點選 API Key 旁的 Show,複製 API Key 的內容。

為什麼需要 API key 呢 ? 因為這些第三方 API 通常需知道是誰使用 API。如下圖所示,remove.bg 免費的帳號一個月可去背 50 張圖片,所以它將利用 API key 做記錄和判斷。

從網頁可了解目前還可以使用的 API 數量

上傳資料的多種格式(Content Type)

上傳資料時,有許多不同的資料格式,比方 multipart/form-dataapplication/json & application/x-www-form-urlencoded 。令人開心的,remove.bg API 三種格式都支援,我們可從 HTTP Header 的 Content-Type 設定,等下我們將一一示範。

上傳的圖片格式

上傳圖片時,圖片要先轉換成特定格式才能上傳,比方變成 binary,base64-encoded string & 網址。令人開心的, remove.bg API 三種格式都支援。實在太幸運了,我們可以一次學到多種方法。

採用 multipart/form-data 上傳時,我們可選擇以下三種圖片格式:

  • binary 格式的圖片,對應的欄位名是 image_file。
  • base64-encoded string 格式的圖片,對應的欄位名是 image_file_b64。
  • 指定圖片的網址,對應的欄位名是 image_url。

採用 application/json 或 application/x-www-form-urlencoded 時,我們只有兩種選擇,上傳圖片的 base64-encoded 字串或指定圖片的網址。

回傳的去背圖片格式(Accept)

API 回傳的資料有 image/* & application/json 兩種格式可選擇,我們可從 HTTP Header 的 Accept 設定。image/* 將回傳 binary 格式的圖片,application/json 則會回傳 base64-encoded string 格式的圖片。

準備去背的圖片

彼得潘 & 小王子在 B612 星球的合照。

經過了前面漫長的準備,終於可以開始寫程式了。接下來就讓我們實驗各種有趣的圖片上傳方法吧。

測試 1: 以 multipart/form-data 上傳 binary 的圖片,然後得到 binary 的圖片,搭配 Alamofire 套件

使用 iOS SDK 內建的 URLSession 上傳 multipart/form-data 的資料需要寫許多程式,比 JSON 格式複雜許多。因此我們先示範使用套件 Alamofire 上傳,待會再示範使用內建的 URLSession。

  • 使用 SPM 安裝 Alamofire。
  • 在 NetworkManager 裡定義取得去背圖片的 function removeImageBg。
import UIKit
import Alamofire

struct NetworkManager {
static let shared = NetworkManager()

func removeImageBg(uiImage: UIImage, completion: @escaping (UIImage?) -> Void) {
let headers: HTTPHeaders = [
"X-Api-Key": "123",
]

AF.upload(multipartFormData: { data in
let imageData = uiImage.jpegData(compressionQuality: 0.9)
data.append(imageData!, withName: "image_file", fileName: UUID().uuidString, mimeType: "image/jpeg")

}, to: "https://api.remove.bg/v1.0/removebg", headers: headers).responseData { response in
if let data = response.data,
let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}
}
}

說明:

https://api.remove.bg/v1.0/removebg

remove.bg 的 API 網址

let headers: HTTPHeaders = [
"X-Api-Key": "123",
]

在 HTTP Header 裡以 X-Api-Key 設定 API key,如此才能順利串接 API 去背圖片。(練習時記得將 API key 改成自己帳號的 API key,範例裡的 API key 是假的)

AF.upload(multipartFormData: { data in
let imageData = uiImage.jpegData(compressionQuality: 0.9)
data.append(imageData!, withName: "image_file", fileName: UUID().uuidString, mimeType: "image/jpeg")

},

利用 AF 的 upload function 搭配參數 multipartFormData 上傳 multipart/form-data 格式的資料。在 closure 裡我們先將 UIImage 變成 Data,然後利用 append function 將它添加到上傳的 data。

if let data = response.data, 
let image = UIImage(data: data) {
completion(image)
}

API 回傳的資料格式預設是 image/*,因此它將回傳 binary 格式的圖片。binary 格式表示 API 回傳給我們的 Data 就是圖片,可以直接用 UIImage(data: data) 生成圖片。

有了 function removeImageBg,接下來只要呼叫它即可取得去背的圖片,以下分別為 SwiftUI & UIKit 的範例:

SwiftUI App 範例

struct ContentView: View {
let originalImage = UIImage(named: "peter")!
@State private var transparentUIImage: UIImage?

var body: some View {
Image(uiImage: transparentUIImage ?? originalImage)
.resizable()
.scaledToFit()
.onTapGesture {
NetworkManager.shared.removeImageBg(uiImage: originalImage) { resultImage in
transparentUIImage = resultImage
}
}
}
}

結果

點選圖片後,圖片變成去背。

UIKit App 範例

class ViewController: UIViewController {
@IBOutlet weak var removeBgImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()

NetworkManager.shared.removeImageBg(uiImage: removeBgImageView.image!) { resultImage in
if let resultImage {
self.removeBgImageView.image = resultImage
}
}

}
}

包含多個資料的 multipart/form-data

multipart/form-data 格式的 multipart 告訴我們它可以包含多個 part,所以除了圖片之外,它還可以包含其它資料,甚至有些 API 還可以在 multipart 裡包含多張圖片。

以下例子的 multipart/form-data 包含了代表圖片的 image_file & 設定圖片背景色的 bg_color,它的內容為顏色的十六進位 RGB 字串,比方 00ffff。

  • 使用 Alamofire
AF.upload(multipartFormData: { data in
let imageData = uiImage.jpegData(compressionQuality: 0.9)
data.append(imageData!, withName: "image_file", fileName: UUID().uuidString, mimeType: "image/jpeg")
let color = "00ffff".data(using: .utf8)!
data.append(color, withName: "bg_color")
}, to: "https://api.remove.bg/v1.0/removebg", headers: headers)
  • 使用 URLSession
let parameters: [String: Any] = [
"image_file": uiImage,
"bg_color": "00ffff"
]
request.createMultipartFormData(parameters: parameters)

結果

測試 2: 以 multipart/form-data 上傳 binary 的圖片,然後得到 binary 的圖片,搭配 URLSession

喜歡自力自強不靠套件的朋友也可以使用 URLSession 上傳 multipart/form-data,只是程式會複雜許多。

  • 在 Extension Data 裡添加 function appendString
extension Data {
mutating func appendString(_ string: String) {
append(string.data(using: .utf8)!)
}
}
  • 在 Extension URLRequest 裡定義建立 multipart/form-data 資料的 function createMultipartFormData
extension URLRequest {

mutating func createMultipartFormData(parameters: [String: Any]) {
let boundary = "Boundary-\(UUID().uuidString)"
var postData = Data()

for (key, value) in parameters {
postData.appendString("--\(boundary)\r\n")
postData.appendString("Content-Disposition:form-data; name=\"\(key)\";")

switch value {
case let value as String:
postData.appendString("\r\n\r\n\(value)\r\n")
case let value as UIImage:
let imageData = value.jpegData(compressionQuality: 0.9)!
let fileName = UUID().uuidString
postData.appendString("filename=\"\(fileName)\"\r\n")
postData.appendString("Content-Type: image/jpeg\r\n\r\n")
postData.append(imageData)
postData.appendString("\r\n")
default:
break
}
}
postData.appendString("--\(boundary)--\r\n")
httpBody = postData
setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
}

}

相較於 Alamofire,以上的程式複雜許多,因為我們要自己組合出 multipart/form-data 的資料,加入 \r\n & boundary 等在 multipart 裡用來分隔資料的字元,在此我們使用 "Boundary-\(UUID().uuidString)"當 boundary。

基本上 boundary 可使用任何字串,因此如果你暗戀某個女生,比方angelababy,你也可以用 angelababy 當 boundary。(ps: 嚴格來說 boundary 還是有一些限制,它不能超過 70 bytes,而且只能包含 7-bit US-ASCII 裡的字元。)

值得注意的,要記得設定 HTTP Header 的 Content-Type,並在裡面包含 boundary,如此後台才會知道我們上傳的格式是 multipart/form-data 和 boundary 的內容。

setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
  • 定義取得去背圖片的 function removeImageBg
struct NetworkManager {
static let shared = NetworkManager()

func removeImageBg(uiImage: UIImage, completion: @escaping (UIImage?) -> Void) {

var request = URLRequest(url: URL(string: "https://api.remove.bg/v1.0/removebg")!)
request.httpMethod = "post"
request.setValue("123", forHTTPHeaderField: "X-Api-Key")
let parameters = [
"image_file": uiImage
]
request.createMultipartFormData(parameters: parameters)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data,
let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}.resume()
}
}

將 multipart/form-data 欲上傳的資料存在 parameters 裡,然後呼叫 createMultipartFormData 將它轉換成 multipart/form-data 格式的 Data,存在 httpBody 裡。

剛剛的程式若要在 UIKit App 測試,呼叫 removeImageBg 取得去背圖片後,記得要切換到 main thread 才能更新 App 畫面。

測試 3: 以 multipart/form-data 上傳 binary 的圖片,然後得到 base64-encoded string 的圖片,搭配 Alamofire 套件

我們也可以設定 HTTP Header 的 Accept ,調整 API 回傳的資料格式。remove.bg 支援 image/*application/json 兩種格式,預設是 image/*。

接下來讓我們試試 application/json。從下圖的說明,我們得知 API 回傳的 JSON 資料裡,result_b64 欄位將帶有 base64-encoded string 格式的圖片

  • 定義 API 回傳的 JSON 對應的 Decodable 型別

雖然 JSON 裡的欄位是包含底線的 result_b64,但是我們宣告成 resultB64,因為待會 JSONDecoder 可以幫我們轉換。

struct ReturnImageData: Decodable {
struct ResultB64: Decodable {
let resultB64: String
}

let data: ResultB64
}
  • 定義取得去背圖片的 function removeImageBg
func removeImageBg(uiImage: UIImage, completion: @escaping (UIImage?) -> Void) {

let headers: HTTPHeaders = [
"X-Api-Key": "123",
"Accept": "application/json"
]
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

AF.upload(multipartFormData: { data in
let imageData = uiImage.jpegData(compressionQuality: 0.9)
data.append(imageData!, withName: "image_file", fileName: UUID().uuidString, mimeType: "image/jpeg")

}, to: "https://api.remove.bg/v1.0/removebg", headers: headers).responseDecodable(of: ReturnImageData.self, queue: .main, decoder: decoder) { response in
switch response.result {
case .success(let imageData):
if let data = Data(base64Encoded: imageData.data.resultB64),
let image = UIImage(data: data) {
completion(image)
}
case .failure(_):
completion(nil)
}
}

}

利用 function responseDecodable 將回傳的 JSON 轉換成自訂的型別 ReturnImageData,然後從 case .success(let imageData) 的參數 imageData 讀取。

字串型別的 imageData.data.resultB64 存著 base64-encoded string 格式的圖片,因此我們先用 Data(base64Encoded: imageData.data.resultB64) 將它變成 Data,然後再將 Data 變成 UIImage。

值得注意的,當圖片以 base64 格式回傳時,會比原本 binary 格式大,大約大了三十幾 %。

binary: 95554 bytes
包含 base64 的 JSON: 127434 bytes

ps:

  1. 對圖片的 base64 字串好奇的朋友,可參考以下圖片的範例。

2. 有興趣的朋友也可以將 base64 字串貼到以下網站測試,它會將 base64 字串變成美美的圖片。

測試 4: 以 application/json 上傳圖片的 base64 字串,然後得到 binary 的圖片

以 application/json 上傳時,在欄位 image_file_b64 指定圖片的 base64-encoded string。

  • 定義上傳的 JSON 資料對應的 Encodable 型別

常數 imageFileB64 將儲存圖片的 base64-encoded string。

struct UploadImageJSON: Encodable {
let imageFileB64: String
}
  • 定義取得去背圖片的 function removeImageBg
func removeImageBg(uiImage: UIImage, completion: @escaping (UIImage?) -> Void) {
let imageData = uiImage.jpegData(compressionQuality: 0.9)
let base64Str = imageData!.base64EncodedString()
let uploadImageJSON = UploadImageJSON(imageFileB64: base64Str)
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase

if let uploadData = try? encoder.encode(uploadImageJSON) {
let url = URL(string: "https://api.remove.bg/v1.0/removebg")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("123", forHTTPHeaderField: "X-Api-Key")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
if let data,
let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}.resume()
}
}

說明。

let imageData = uiImage.jpegData(compressionQuality: 0.9)
let base64Str = imageData!.base64EncodedString()
let uploadImageJSON = UploadImageJSON(imageFileB64: base64Str)

由於 application/json 裡要包含圖片的 base64 字串,因此我們先用 jpegData 將 UIImage 變成 Data,然後用 base64EncodedString 將圖片從 Data 變成 base64 的 String,最後再生成準備上傳的 UploadImageJSON 物件。

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

設定上傳的 Content-Type 為 application/json。

測試 5: 以 application/x-www-form-urlencoded 上傳圖片網址,然後得到 binary 的圖片

以 application/x-www-form-urlencoded 上傳,在欄位 image_url 指定圖片的網址。

比方我們試試帥氣的 spider man 圖片網址。

https://static.highsnobiety.com/thumbor/Q6NeReGHDI1KEJOEs5DGWsFbJOA=/fit-in/480x320/smart/static.highsnobiety.com/wp-content/uploads/2019/08/21111543/tom-holland-spider-man-fan-reacts-01.jpg
  • 定義取得去背圖片的 function removeImageBg
func removeImageBg(urlString: String, completion: @escaping (UIImage?) -> Void) {

let encodeString = "image_url=\(urlString)"

if let uploadData = encodeString.data(using: .utf8) {
let url = URL(string: "https://api.remove.bg/v1.0/removebg")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("123", forHTTPHeaderField: "X-Api-Key")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
if let data,
let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}.resume()
}

}

說明

let encodeString = "image_url=\(urlString)"

if let uploadData = encodeString.data(using: .utf8) {

application/x-www-form-urlencoded 的字串格式如下:

因此我們先組合出字串 encodeString 後,再將它變成以 utf8 編碼的 Data 上傳。

request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

設定上傳的 Content-Type 為 application/x-www-form-urlencoded。

呼叫 function removeImageBg,傳入 spider man 的圖片網址後,順利看到帥氣的去背 spider man。

將圖片存到雲端的 Imgur API

網站 Imgur 佛心地提供免費的空間,讓我們可以自由地上傳圖片存到雲端。

這麼棒的功能,我們的 App 也可以享有,因為 Imgur 提供了圖片上傳的 API。

註冊帳號

為了使用 API,我們必須先註冊帳號。

Register App,取得 Client ID & Client secret

除了註冊帳號,還要註冊 App 得到 Client ID 才能使用 API。

選擇 OAuth 2 authorization without a callback URL。

順利建立 App 後,我們將看到以下的 Client ID & Client secret。

請將 Client ID 記起來,待會發送 API 時會用到。若是不小心忘了,請登入 Imgur 網站後從右上方點選 Settings。

然後切換到 Applications 頁面查詢 App 的 Client ID。

Search 圖片 API

Imgur 提供豐富的 API,除了圖片上傳,它還可以搜尋圖片,建立相簿,留言等。接下來就讓我們柿子挑軟的吃,先從 PostMan 測試 Gallery Search API 吧。

根據上圖的 API 說明,我們在 PostMan 輸入以下網址找尋可愛的貓。

https://api.imgur.com/3/gallery/search?q=cats

記得要在 HTTP Header 的 Authorization 欄位加入 Client ID 才能串接 Imgur API,它的格式為

Client-ID YOUR_CLIENT_ID

請將 YOUR_CLIENT_ID 替換成自己申請的 Client ID。

順利地得到貓咪圖片的 JSON。

比方以下是可愛貓咪的網址。

https://i.imgur.com/oBKoluk.png

上傳圖片

接著就讓我們進入正題,撰寫上傳圖片的程式吧。首先研究下圖的 Image Upload API,發現資料上傳的格式是 multipart/form-data,圖片的欄位名稱是 image,然後先用 PostMan 測試圖片上傳。

成功用 PostMan 上傳圖片後,接著讓我們親手撰寫上傳圖片的程式。

  • 定義 API 回傳的 JSON 對應的 Decodable 型別。

API 回傳的 JSON 範例如下:

我們只在乎圖片的網址,因此我們在 Data 裡只定義 link 欄位。

struct UploadImageResult: Decodable {
struct Data: Decodable {
let link: URL
}
let data: Data
}
  • 加入 Alamofire。

由於 Image Upload API 上傳資料的格式是麻煩的 multipart/form-data,所以我們使用 Alamofire 串接。

import Alamofire
  • 定義上傳圖片的 function uploadImage。
func uploadImage(uiImage: UIImage) {
let headers: HTTPHeaders = [
"Authorization": "Client-ID 123",
]
AF.upload(multipartFormData: { data in
let imageData = uiImage.jpegData(compressionQuality: 0.9)
data.append(imageData!, withName: "image")

}, to: "https://api.imgur.com/3/image", headers: headers).responseDecodable(of: UploadImageResult.self, queue: .main, decoder: JSONDecoder()) { response in
switch response.result {
case .success(let result):
print(result.data.link)
case .failure(let error):
print(error)
}
}
}

API 的網址如下

https://api.imgur.com/3/image

成功上傳時,我們呼叫 responseDecodable 將回傳的 JSON 轉換成自訂的型別 UploadImageResult,然後從 case .success(let result) 的 result 讀取,因此 result.data.link 即為圖片的網址。

結果

成功上傳,彼得潘和小王子合照的網址如下:

https://i.imgur.com/BKiAaiL.jpg

ps: 對使用 URLSession 寫法上傳圖片到 Imgur 有興趣的朋友,也可以從網頁點選 Swift- URLSession 查看範例程式。

其它上傳圖片的 API

  • imgbb

範例程式。

AF.upload(multipartFormData: { data in
let apiKey = "123456"
data.append(apiKey.data(using: .utf8)!, withName: "key")
let imageData = UIImage.fish.jpegData(compressionQuality: 0.9)!
data.append(imageData, withName: "image", fileName: "test.jpg")
}, to: "https://api.imgbb.com/1/upload").responseString { dataResponse in
switch dataResponse.result {
case .success(let responseString):
print("Response: \(responseString)")
case .failure(let error):
print("Error: \(error)")
}
}

作品集

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com