popup mlv

作ったり、買ったり、遊んだり。

PENTAX K-70のWi-Fiを使ってRAWをiPhoneと同期させる話

f:id:popupin0x0:20171101192602j:plain

keenaiがサービス終了するという話なのでiPhonePENTAX k-70のSDカード内にあるRAWファイルを同期させるアプリを作った話です。

はじめに

Pythonの方がなれているのですがiosアプリということでSwiftを使って開発しました。PENTAXのカメラから写真を選択して読み込むアプリは公式も含めていくつかあります。keenaiアプリはそれとは違いすべての写真を自動同期することができました。しかしK-70では有料登録しなければ同期できませんでした。そこで自分でアプリを作ってしまおうということです。

Image Sync

Image Sync

  • RICOH IMAGING COMPANY,LTD
  • 写真/ビデオ
  • 無料

PK読み込み

PK読み込み

  • YUN YOUNG LEE
  • 写真/ビデオ
  • 無料

アプリについて

iphonepentaxwifiに接続してjsonをダウンロードします。同期ボタンを押せば自動でPhotosにカメラのモデル名を読み取って(PENTAX K-70)アルバムを作りそこにRAWやらJPEGやらをダウンロードします。同期した「フォルダ/ファイル名」はiphone上に保存されているので2回目以降も同じファイルをダウンロードすることはありません。

非同期処理などは書いていないので同期を始めると終わるまで操作できません。必要最低限書いているだけです。シリアル番号も読み取れるのでコードを追加すればあるていど正確に同期できるようになるでしょう。もしApple Developer Programに入っているならNetworkExtensionが使えるのでWi-Fiに自動接続したりすることができるでしょう。

PENTAXWi-Fiに対応しているKPやK-1でも使えると思います。持っていないので試したことはありません。

UI

Storyboardです。textviewとボタン3つのシンプルな画面です。

f:id:popupin0x0:20180913091524p:plain:h200

実行中の画面です。RAWやJPEGは写真のアルバムに保存されます。

f:id:popupin0x0:20180913091651j:plain:h200 f:id:popupin0x0:20180913091705j:plain:h200

コード

//
//  ViewController.swift
//  Copyright © 2018年 popupmlv. All rights reserved.
//

import UIKit
import Foundation
import Photos

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    // 永続化
    let userDefaults = UserDefaults.standard
    
    // copy code json
    struct Props: Decodable{
        let errCode: Int
        let errMsg: String
        let model: String
        let serialNo: String
    }
    struct Photos: Decodable{
        let errCode: Int
        let errMsg: String
        let dirs: [Dir]
    }
    struct Dir: Decodable{
        let name: String
        let files: [String]
    }
    
    // ダウンロードするlist
    var downloadPentaxArry:[(model:String, dir: String, file: String)] = []
    
    // button wifiへ移動
    @IBAction func button5(_ sender: Any) {
        // コントローラーの実装
        let alertController = UIAlertController(
            title: "接続エラー",
            message: "カメラのWi-Fiへ接続されていません。接続してください。",
            preferredStyle: UIAlertControllerStyle.alert
        )
        // OKボタン
        let openAction = UIAlertAction(
            title: "設定を開く",
            style: UIAlertActionStyle.default){
                (action: UIAlertAction) in
                let url = NSURL(string: "App-Prefs:root=WIFI")! as URL
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
        // TODO: リトライ
        let retryAction = UIAlertAction(
            title: "リトライ",
            style: UIAlertActionStyle.default){
                (action: UIAlertAction) in
                // json ファイルを読み取る
                print("get json")
        }
        alertController.addAction(openAction)
        alertController.addAction(retryAction)
        present(alertController,animated: true,completion: nil)
    }
    
    func getPropsStruct() -> Props {
        // props jsonファイルを読み取る
        let url = URL(string: "http://192.168.0.1/v1/props")!
        let data = try? Data(contentsOf: url)
        //let contents = try? String(contentsOf: url!)
        let props = try? JSONDecoder().decode(Props.self, from: data!)
        return props!
    }
    
    func getPhotosStruct() -> Photos {
        // photos jsonファイルを読み取る
        let url = URL(string: "http://192.168.0.1/v1/photos")!
        let data = try? Data(contentsOf: url)
        let contents = try? String(contentsOf: url)
        print(contents!)
        let photos = try? JSONDecoder().decode(Photos.self, from: data!)
        return photos!
    }
    
    // 起動時にpentaxに接続しているか確認
    
    // button jsonファイルを読み取る
    @IBOutlet weak var textview: UITextView!
    @IBAction func Button(_ sender: Any) {
        let photos = getPhotosStruct()
        let props = getPropsStruct()
        print("err code: \(photos.errCode)")
        print("err msg: \(photos.errMsg)")
        
        // pentax sd にデータがある物を出力
        var pentaxsd:[(model:String, dir: String, file: String)] = []
        for dir in photos.dirs {
            for file in dir.files {
                pentaxsd.append((props.model, dir.name, file))
                //pentaxSd.append("\(dir.name)/\(file)")
            }
        }
        // 永続化があればそこから読み取って差分を出す new, dup
        var textarry:[String] = []
        if (self.userDefaults.object(forKey: "loadimagearry") != nil) {
            print("データ有り")
            let loadImageArry:[String] = self.userDefaults.array(forKey: "loadimagearry") as! [String]
            var newarry:[String] = ["NEW"]
            var duparry:[String] = ["", "DUP"]
            for i in pentaxsd {
                for j in loadImageArry {
                    if ("\(i.dir)/\(i.file)" != j) {
                        newarry.append("\(i.dir)/\(i.file)")
                        // 新規だけダウンロード
                        self.downloadPentaxArry.append(i)
                    } else {
                        duparry.append(j)
                    }
                }
            }
            textarry = newarry + duparry
        } else {
            print("データなし")
            var newarry:[String] = ["NEW"]
            for i in pentaxsd {
                newarry.append("\(i.dir)/\(i.file)")
                // 新規だけダウンロード
                self.downloadPentaxArry.append(i)
            }
            textarry = newarry
        }
        textview.text = textarry.joined(separator: "\n")
    }
    // button write jpg test img
    // write img
    @IBAction func Button3(_ sender: Any) {
        /*
         let url = URL(string: "http://192.168.0.1/v1/photos/100PENTX/IMGP0244.JPG?size=view")
         let data = try? Data(contentsOf: url!)
         let image = UIImage(data: data!)
         let props = getPropsStruct()
         let dir = "102PENTX"
         let file = "IMGP0371.PEF" //IMGP0255.JPG IMGP0307.DNG IMGP0371.PEF
        */
        for i in self.downloadPentaxArry {
            print(i)
            savePentaxImage(model: i.model, dirName: i.dir, fileName: i.file)
        }
    }
    /*
     Photos.frameworkを使ってアルバムに画像を保存
     アルバムがなければ自動でつくる
     */
    func saveImage(_ imageData: NSData, toAlbum albumName: String, fileName: String, completionBlock: @escaping (String?) -> Void, failureBlock: @escaping (Error?) -> Void) {
        // filenameをDNGからdngへ置き換える。syncでraw内のjpegを誤って読み込まない様にする為
        let TempFilePath = "\(NSTemporaryDirectory())\(fileName)".replacingOccurrences(of: ".DNG", with: ".dng")
        var imageID: String? = nil
        
        findOrCreatePhotoAlbum(name: albumName) { (album, error) in
            // 画像データを一時ファイルとして保存
            let fileURL = URL(fileURLWithPath: TempFilePath)
            try? imageData.write(to: fileURL, options: .atomic)
            
            if let album = album, FileManager.default.fileExists(atPath: TempFilePath) {
                PHPhotoLibrary.shared().performChanges({
                    let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)!
                    let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
                    let placeHolder = assetRequest.placeholderForCreatedAsset
                    albumChangeRequest?.addAssets([placeHolder!] as NSArray)
                    imageID = assetRequest.placeholderForCreatedAsset?.localIdentifier
                }) { (isSuccess, error) in
                    if isSuccess {
                        // 保存した画像にアクセスする為のimageIDを返却
                        completionBlock(imageID)
                    } else {
                        failureBlock(error)
                    }
                    _ = try? FileManager.default.removeItem(atPath: TempFilePath)
                }
            } else {
                failureBlock(error)
                _ = try? FileManager.default.removeItem(atPath: TempFilePath)
            }
        }
    }
    private func findOrCreatePhotoAlbum(name: String, completion: @escaping (PHAssetCollection?, Error?) -> Void) {
        var assetCollection: PHAssetCollection?
        var assetCollectionPlaceholder: PHObjectPlaceholder?
        
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", name)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
        if collection.firstObject != nil {
            assetCollection = collection.firstObject
            completion(assetCollection, nil)
        } else {
            PHPhotoLibrary.shared().performChanges({
                let createRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name)
                assetCollectionPlaceholder = createRequest.placeholderForCreatedAssetCollection
            }) { (isSuccess, error) in
                if isSuccess {
                    let refetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [assetCollectionPlaceholder!.localIdentifier], options: nil)
                    assetCollection = refetchResult.firstObject
                    completion(assetCollection, nil)
                } else {
                    completion(nil, error)
                }
            }
        }
    }
    // データを送ってダウンロードする
    func savePentaxImage(model: String, dirName: String, fileName: String) {
        if let url = URL(string: "http://192.168.0.1/v1/photos/\(dirName)/\(fileName)?size=full"),
            let urlData = NSData(contentsOf: url) {
            saveImage(urlData, toAlbum: model, fileName: fileName,
                      completionBlock: {(imgid) in
                        // dir file model imgid を受けとってjsonで保存
                        print("succes write img")
                        if (self.userDefaults.object(forKey: "loadimagearry") != nil) {
                            print("データ有り")
                            var loadImageArry:[String] = self.userDefaults.array(forKey: "loadimagearry") as! [String]
                            loadImageArry.append("\(dirName)/\(fileName)")
                            self.userDefaults.set(loadImageArry, forKey: "loadimagearry")
                        } else {
                            print("データなし")
                            self.userDefaults.set(["\(dirName)/\(fileName)"], forKey: "loadimagearry")
                        }
                        self.userDefaults.synchronize()
            },
                      failureBlock: {(error) in
                        print("err img")
            })}
    }
}

課題

写真アプリに保存したあとPCとResilio Syncを使って同期することを考えていたのですがいつの間にかDNGに対応しなくなったこと、サムネイル画像をバックアップしてしまうことから思ったように同期できなくなりました。アプリ内で.dng(小文字)にすると同期を回避することができます。iCloudAmazon PhotosからはRAWを同期することができます。P2P方式ではないので同期が遅いこと、ギガが足りないということになるかもしれません。

FileからResilio Syncのフォルダに送った場合はRAWを適切に同期することができます。何回かのボタン操作が必要でめんどくさいでしょう。

最後に

本当はデザインもまともにしてアプリをApp Storeに公開しようと思う気持ちもあったのですが、飽きたのでここで公開することにしました。個人で無料のライセンスの場合ビルドしてから1週間は使えます。それ以上はアップルの審査などがあるので面倒かと思います。

ググって先人の知恵を借りました。

参考

Photos.frameworkを使ってアルバムに画像を保存(+取得)する

GitHub - tateisu/FADownloader at kp-downloader