関連
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情報を取得してみる。 セルの選択イベントはUICollectionViewDelegate
のdidSelectItemAt
を実装すればよい。
アセットに対して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)
}
})
}
}
シミュレータ内にある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.dataTask
をresume
してレスポンスをクロージャ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()