[Swift] 相片日記-上

看到 Leica LUX 的 APP(https://www.gq.com.tw/article/leica-lux-app-%E4%BA%AE%E9%BB%9E)可以為相片打上 EXIF 的標籤,並加上 Leica的 Logo 在左下角,也想做一個 Canon 版本的,但哪個廠牌其實就是換張圖片而已,就連其他廠牌的也順便做一做了,目前有支援:

  • Canon
  • Apple
  • Nikon
  • Sony
  • Fuji
  • Leica

目前測試 Canon 與 Sony 的單眼相機拍的照片,都能成功讀取 EXIF 資訊,用我目前的 Apple 手機相機拍攝的讀不到,不知道是不是 .HEIF 格式的關係,之前用 iPhone XS 拍攝的 .JPG 照片有成功讀取到 EXIF。

Demo

Demo

這次 StoryBoard 也很簡單,一個 TabBar 而已,剩下的元件全部都用程式碼產生,並且在程式碼中設定 Auto Layout,以適應不同大小的手機螢幕:

StoryBoard
ScreenShot

圖分為上半部的相片(imageView)與下半部白底的資訊(infoView),infoView 中含有一張圖跟四個 Label。

程式碼中各 Function 要做的事情大致如下:

uploadButtonTapped():按下按鈕後要做的事
imagePickerController():使用者選好照片以後要做的事
updateInfoView():將 EXIF 資訊更新到 Label 當中
updateBrandImageView():更新 infoView 中的相機品牌圖片
resizeImage():縮小圖片
updateImageViewConstraints():更新 imageView 的 Auto Layout
extractExifInfo():取得相片之 EXIF
imageTapped():點擊相片後要做的事
saveImageToAlbum():儲存相片
convertDecimalToFraction():轉換快門速度,從小數到分數
gcd():找最大公因數,配合 convertDecimalToFraction() 使用
showAlert():跳出通知視窗

Upload 頁面中要拉一個按鈕,按了之後可以從相簿選取圖片,所以要取得權限,在 Info.plist 中增加兩個 Privacy(如圖)。
Fonts provided by application 是我從 Google Fonts 上下載的字體 Exo2(https://fonts.google.com/specimen/Exo+2)。

Info.plist

使用者點擊頁面上的按鈕後,會執行 uploadButtonTapped(),帶使用者去選照片:

@objc func uploadButtonTapped() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}

當使用者選好照片以後,會執行 imagePickerController():

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
originalImage = selectedImage
let resizedImage = resizeImage(selectedImage)
imageView.image = resizedImage
updateImageViewConstraints(for: resizedImage)
updateInfoView(for: selectedImage, info: info)
infoView.isHidden = false
}
dismiss(animated: true, completion: nil)
}

originalImage 儲存原始解析度的照片,會再將照片使用 resizeImage() 縮圖後儲存到 resizedImage 這個變數中,才顯示在畫面上:

func resizeImage(_ image: UIImage) -> UIImage {
let isLandscape = image.size.width > image.size.height
let targetSize: CGSize

if isLandscape {
let aspectRatio = image.size.height / image.size.width
targetSize = CGSize(width: 2048, height: 2048 * aspectRatio)
} else {
let aspectRatio = image.size.width / image.size.height
targetSize = CGSize(width: 1365 * aspectRatio, height: 1365)
}
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
}

其他應該沒啥好講的,下載的圖片是另外渲染的,因為有存原始解析度的照片,所以照片的部分不用額外處理,要注意的是 infoView 的大小及裡面的圖片與文字,原本是根據螢幕去設定的,而下載是下載原始解析度的照片,所以 infoView 中的大小要跟著等比例放大後,才能被丟進 renderer 中:

@objc func imageTapped() {
guard let originalImage = self.originalImage else {
print("No original image available")
return
}
let infoViewSize = infoView.frame.width
let scale = originalImage.size.width / infoViewSize
let size = CGSize(width: originalImage.size.width, height: originalImage.size.height + infoView.bounds.height*scale)
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: size, format: format)
// make final image that will be saved to user's album
let finalImage = renderer.image { ctx in
originalImage.draw(in: CGRect(origin: .zero, size: originalImage.size))
ctx.cgContext.saveGState()
ctx.cgContext.translateBy(x: 0, y: originalImage.size.height)
ctx.cgContext.scaleBy(x: scale, y: scale)
infoView.layer.render(in: ctx.cgContext)
ctx.cgContext.restoreGState()
}
saveImageToAlbum(finalImage)
}

Todo:

點擊照片後,會從下面滑出選單,可以選「儲存至相簿」或是「儲存至日記」,若是選擇儲存至日記,會將照片儲存至使用者的手機硬碟中,並且讓使用者寫下這張照片的標題、敘述。
之後可以在第二頁 Album 中,看到所有儲存過的照片,照片會顯示標題、敘述、日期時間與 EXIF 資訊。

GitHub:

Reference:

https://leica-camera.com/zh-Hant/photography/leica-apps/leica-lux

--

--