iOS開発メモ8 (MediaUploaderの開発4)

関連
iOS開発メモ7 (MediaUploaderの開発3)
iOS開発メモ6 (MediaUploaderの開発2)
iOS開発メモ5 (MediaUploaderの開発1)
iOS開発メモ4 (MediaUploaderの設計)

今回は、写真ロールのイメージからExif情報を取得する。

Exifメタ情報は、写真ロールの画像がもっており、いくつかの基本情報はExif情報にアクセスしなくとも、UIImageのプロパティでも保持しているが、すべてのExifは写真からCIImageというイメージデータを作成すると、そのプロパティとして取得できるようである。

処理の流れとしては、アセット情報から画像ファイルのURL(ファイルパス)を取得し、そのURLでCIImageを作成、そしてExifメタ情報取得という順番になる。

セルを選択して、写真のExifメタ情報を取得

サンプルとして、セルの選択イベントを取得し、選択されたセルに該当する画像データのExif情報を取得してみる。 セルの選択イベントはUICollectionViewDelegatedidSelectItemAtを実装すればよい。

アセットに対してrequestContentEditingInputを実行すると、クロージャ内で、選択した画像のURLが取得できる。URLからCIImageを作成して、プロパティがDictionaryとして取得される。

extension ViewController : UICollectionViewDelegate{
    func collectionView(_ collectionView: UICollectionView,
        didSelectItemAt indexPath: IndexPath){
        
        print("[selected] idx = " + String(indexPath.row))
        let asset = fetchResult.object(at: indexPath.item)
        
        let options = PHContentEditingInputRequestOptions()
        options.isNetworkAccessAllowed = true

        asset.requestContentEditingInput(with: options, completionHandler: {
            ( input, info ) in
            if let input = input, let url = input.fullSizeImageURL {
                // CIImage取得
                let ciImg = CIImage( contentsOf:url )!
                // メタ情報(Exif)を取得
                let meta = ciImg.properties as CFDictionary
                print(meta)
            }
        })
    }
}

ss--2017-10-27-1.51.31-1

シミュレータ内にある1つ目の写真を選択して、取得したExifメタ情報をダンプprint(meta)してみた。2012年にNIKON D800Eで撮影したことがわかる。

{
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 1;
    PixelHeight = 2002;
    PixelWidth = 3000;
    ProfileName = "sRGB IEC61966-2.1";
    "{ExifAux}" =     {
        ImageNumber = 11035;
        LensID = 163;
        LensInfo =         (
            16,
            35,
            4,
            4
        );
        LensModel = "16.0-35.0 mm f/4.0";
        SerialNumber = 6001440;
    };
    "{Exif}" =     {
        ApertureValue = "6.643855776306108";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            1,
            2,
            3,
            0
        );
        Contrast = 0;
        CustomRendered = 0;
        DateTimeDigitized = "2012:08:08 14:55:30";
        DateTimeOriginal = "2012:08:08 14:55:30";
        DigitalZoomRatio = 1;
        ExifVersion =         (
            2,
            3
        );
        ExposureBiasValue = 0;
        ExposureMode = 1;
        ExposureProgram = 1;
        ExposureTime = 20;
        FNumber = 10;
        FileSource = 3;
        Flash = 16;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 16;
        FocalLength = 16;
        FocalPlaneResolutionUnit = 4;
        FocalPlaneXResolution = "204.840206185567";
        FocalPlaneYResolution = "204.840206185567";
        GainControl = 0;
        ISOSpeedRatings =         (
            200
        );
        LightSource = 0;
        MaxApertureValue = 4;
        MeteringMode = 5;
        PixelXDimension = 3000;
        PixelYDimension = 2002;
        Saturation = 0;
        SceneCaptureType = 0;
        SensingMethod = 2;
        Sharpness = 0;
        ShutterSpeedValue = "-4.321927997619756";
        SubjectDistRange = 0;
        SubsecTimeDigitized = 4;
        SubsecTimeOriginal = 4;
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = "107.4666666666667";
        GPSVersion =         (
            2,
            3,
            0,
            0
        );
        ImgDirection = "265.8132992327366";
        ImgDirectionRef = T;
        Latitude = "63.5314";
        LatitudeRef = N;
        Longitude = "19.5112";
        LongitudeRef = W;
        MapDatum = "WGS-84";
        Speed = "2.053334425692282";
        SpeedRef = K;
    };
    "{IPTC}" =     {
        Byline =         (
            "Nicolas Cornet"
        );
        City = "Eyvindarh\U00f3lar";
        CopyrightNotice = "Nicolas Cornet";
        "Country/PrimaryLocationName" = Iceland;
        DateCreated = 20120808;
        DigitalCreationDate = 20120808;
        DigitalCreationTime = 145530;
        "Province/State" = South;
        TimeCreated = 145530;
    };
    "{JFIF}" =     {
        DensityUnit = 1;
        JFIFVersion =         (
            1,
            0,
            1
        );
        XDensity = 72;
        YDensity = 72;
    };
    "{TIFF}" =     {
        Artist = "Nicolas Cornet";
        Copyright = "Nicolas Cornet";
        DateTime = "2012:08:08 14:55:30";
        Make = "NIKON CORPORATION";
        Model = "NIKON D800E";
        Orientation = 1;
        ResolutionUnit = 2;
        Software = "Aperture 3.4.5";
        XResolution = 72;
        YResolution = 72;
    };
}

ネットワーク転送時にExifメタ情報が削除される?どうやってExif付きで転送するのか?

Webアプリなどで、ファイルアップロード機能を作成して、iPhoneのSafafiでアクセスして、画像を選択・アップロードする。そしてサーバー側で受け取った画像を見てみると、個人情報保護への配慮なのかExif情報はなくなっている

USBケーブルでiPhoneをPC接続して、写真を抜き出すとちゃんとExif情報が残っているので、プログラムで画像データを転送するときに一工夫が必要になることはわかっていた。

いろいろ調べてみると、転送するときに、画像のピクセルデータからバイト列を作成する必要があり、このタイミングであらかじめ取得しておいたExif情報を付与すれば良いようである。

// CIImage取得
let ciImg = CIImage( contentsOf:url )!
// メタ情報(Exif)を取得
let meta = ciImg.properties as CFDictionary
print(meta)

// CIImage => CGImage
let context = CIContext(options: nil)
let cgImg = context.createCGImage(ciImg, from: ciImg.extent )

// テンポラリに一旦書き出してから加工する
// DestinationにCGImageとメタ(Exif情報)を書き出す
//let tmpName = ProcessInfo.processInfo.globallyUniqueString
let tmpFilePath = NSTemporaryDirectory() + imgUrl.lastPathComponent
let tmpUrl = NSURL.fileURL(withPath: tmpFilePath ) as CFURL
let dest = CGImageDestinationCreateWithURL(tmpUrl, kUTTypeJPEG, 1, nil)
CGImageDestinationAddImage( dest!, cgImg!, meta )
CGImageDestinationFinalize( dest! )

// ネットワーク転送可能なデータを作成する
var imgData :Data?
do{
    let newUrl = URL(fileURLWithPath: tmpFilePath )
    imgData = try Data( contentsOf: newUrl, options: .mappedIfSafe )
}catch{
    print("exception!")
    imgData = nil
}

CIImageからCGImageを作成し、このデータをCGImageDestinationAddImageでファイル出力する際にExifメタ情報を付与する。テンポラリに作成したファイルのURLからネットワーク転送可能なData型のデータを作成する。

iPhoneでファイルアップロード処理の実装方法

CollectionViewで対象ファイルを選択して、TableViewに列挙して順番にネットワーク転送する処理の流れを考えており、それは次回以降の内容になる。話が前後してしまうが、データ作成手順のついでにこのデータをiOSではどうやって転送するのかを検証した。

アップロードサーバー

転送先のサーバーについて、とりあえず以下の仕様でサーバーを構築する。

項目
アップロードURL http://192.168.1.99:8080/test_upload
転送メソッド POST
name image

サーバー側は、Node.jsで実装してみた。multerを使うと、ファイルのアップロード処理が簡単になる。image名でアップロードされたバイナリデータをimagesサブフォルダへ移動している。

// config.json

{
	"public_dir":"./public/"
	,"upload_cache_dir":"./uploads/"
	,"view_dir":"./views"
	
}
// app.js

'use strict';

// -------------------------
// initialize
// -------------------------
var conf = require('./config.json');

// Express
var express = require( 'express' );
var app = express();
// httpサーバー
var http = require( 'http' );
var server = http.createServer( app );
var port = 8080;

// util
var fs = require( 'fs' );
var path = require( 'path' );
var logger = require('morgan');
// post
var body_parser = require( 'body-parser' );
var method_override = require( 'method-override' );
// upload
var multer = require( 'multer' );
var upload = multer({ dest: path.join( __dirname, conf.upload_cache_dir) });


// -------------------------
// configuration
// -------------------------
app.use( body_parser.json() );
app.use( body_parser.urlencoded( { limit:'50mb', extended : true, parameterLimit: 1000000 } ) );
app.use( method_override() );
app.use( express.static( path.join( __dirname, conf.public_dir ) ) );
// view engine setup
app.set('views', path.join( __dirname, conf.view_dir ));
app.set('view engine', 'jade');
// log
//app.use( logger( 'dev' ) );
// basic authentication
var basic_auth = require('basic-auth-connect');
//app.use( basic_auth( 'test', 'test' ) );

// -------------------------
// routing
// -------------------------

// authentication
var auth = function(user, password){
    return ( user === conf.user_id && password === conf.user_pass );
}
app.all( '/test_upload', upload.array('image'), function( request, response ) {
    console.log("--- test_upload ---");
    var data;
    for( var i = 0; i < request.files.length; i ++ ){
        data = request.files[i];
        //data.path
        //data.originalnamei
        //data.size
        //console.log( request.files[i].originalname );
        // move ( "images" directory )
        console.log( data );
        fs.renameSync( data.path, path.join( "images", data.originalname ) );
    }

    response.status(200).end();
});


//--------------------------------
// error 
server.on('error', function (e) {
  // Handle your error here
  console.log(" !!! server error.");
  console.log(e);
});
console.log( "server timeout = " + server.timeout );
server.timeout = 30* 60 * 1000;
console.log( "server timeout = " + server.timeout );

// start server
server.listen( port );
console.log("script root : "+__dirname);
console.log( 'app server listening on port %d in %s mode ',
   server.address().port, app.settings.env
);

iOS側の転送処理

iOS側では、NSMutableURLRequestのインスタンスを生成して、リクエストデータを作成する。またNSMutableDataに画像データやバウンダリを書き込んで、最後にこのデータをリクエストに追加する。

URLSession.shared.dataTaskresumeしてレスポンスをクロージャresponseHandlerで受理する。転送処理が完了したら、テンポラリに残っている生成したファイルを削除する。


// 前述した方法でデータとファイルパスを作成する
let imgData : Data = ...
let filePath : String = ...

// 通信のリクエスト生成.
let uniqueId = ProcessInfo.processInfo.globallyUniqueString
let boundary:String = "------------\(uniqueId)"

let url = NSURL(string: "http://192.168.1.99:8080/test_upload")
let req:NSMutableURLRequest = NSMutableURLRequest( url: url! as URL)
req.httpMethod = "POST"
req.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

let body: NSMutableData = NSMutableData()
let nspath = filePath as NSString
var postStr = ""
postStr += "--\(boundary)\r\n"
postStr += "Content-Disposition: form-data; name=\"image\"; filename=\"\(nspath.lastPathComponent)\"\r\n"
postStr += "Content-Type: image/jpeg\r\n\r\n"

body.append( postStr.data(using: String.Encoding.utf8)! )
body.append( imgData! )

postStr = ""
postStr += "\r\n"
postStr += "\r\n--\(boundary)--\r\n"

body.append( postStr.data(using: String.Encoding.utf8)! )

req.httpBody = body as Data

// URLSession.dataTaskのレスポンス処理
// completionHandlerで指定した関数がレスポンス処理として実行される
func responseHandler( d: Data?, r: URLResponse?, e: Error?)->Void{
    // 転送終了後のテンポラリファイルは削除
    try? FileManager.default.removeItem( atPath: filePath )

    if e != nil {
        print("error=\(String(describing: e))")
        return
    }
    print("delete file = "+filePath)
}

// データアップロード
let task = URLSession.shared.dataTask(
    with: req as URLRequest, completionHandler: responseHandler )
task.resume()