2016年8月7日日曜日

画像をメタデータ付きで読み書き

アプリ内にあるDocumentsディレクトリ内の画像を、メタデータ(EXIFやGPSの位置情報など)付きで読み書きする方法。

準備

プロジェクトにメタデータの付いた画像をインストールしておく。(いきなりDocumentディレクトリに画像入れられないから)
名前は一応gazo.jpgで。
最近はAssetCatalogにインストールすることが多いけど、そこの画像にアクセスする方法がよくわからないので、以前のようにXcodeのプロジェクトナビゲータの所に突っ込むこと。

今回は以下の手順を取る。
  1. リソースとしてインストールされた画像gazo.jpgとメタデータを読み込み
  2. Documentディレクトリにメタデータとともに書き込み
  3. 画像とメタデータを読み込んで表示する
また、ImageIO.frameworkをインポートしとくこと。
#import <ImageIO/ImageIO.h>

書き編

//リソースにある画像を読み、メタデータとともにアプリ内のDocumentディレクトリに保存
- (void)writeImageWithMetadata {
    //🌟準備としてリソース画像とmetadataを読む
    //リソースディレクトリへのパスを得る
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *resourcePath = [bundle resourcePath];
    NSString *imgPath = [resourcePath stringByAppendingPathComponent:@"gazo.jpg"];
    NSLog(@"リソースのpath %@",imgPath);
    //リソースの画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:imgPath]), nil);
    NSDictionary *metadata = (NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL));
    
    //🌟画像とmetadataDocumentディレクトリに保存
    //Documentに保存するファイルのpathを作る
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectoryNSUserDomainMaskYES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];
    
    //書き込むための画像データを入れるNSMutableData型変数を作る
    NSMutableData *imageData = [[NSMutableData allocinit];
    //書き込むための情報を入れる変数destRefに、書き込む画像データの変数(imageData)の情報と、データタイプ(JPEGとか?)、画像ファイル中の画像枚数(複数画像を持つフォーマットもあるため)を入れる
    //sourceRefはデータタイプを得る関数で参照しているだけなので、ここでは画像そのものは加えられていない
    CGImageDestinationRef destRef = CGImageDestinationCreateWithData((CFMutableDataRef)imageData, CGImageSourceGetType(sourceRef), 1, nil);
    //引数1destRefに、引数2の画像と引数4のメタデータを追加する。引数30は複数画像がある場合の画像のインデックスらしい
    CGImageDestinationAddImageFromSource(destRef, sourceRef, 0,(CFDictionaryRef)metadata);
    //書き込むための情報destRefをファイナライズして完成
    CGImageDestinationFinalize(destRef);
    //画像データのimageDataの書き込みをすることで、destRefを参照してメタデータともども書き込んでくれるらしい
    [imageData writeToFile:filePath atomically:YES];
    //使用後は解放
    CFRelease(sourceRef);
    CFRelease(destRef);
}

読み編

//Documentディレクトリ内の画像を読み込んで表示&メタデータ取得して表示
- (void)readImageWithMetadata {
    //Documentディレクトリにあるファイルのpath作成
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];

    //画像読み込み
    NSError *err = nil;
    NSData *imageData = [NSData dataWithContentsOfFile:filePath
                                              options:NSDataReadingMappedAlways
                                                error:&err];
    //エラー処理するならこの辺で
    NSLog(@"Error -> %@",err);
    //NSDataからCIImage経由でUIImageに変換
    CIImage *ciImage = [CIImage imageWithData:imageData];
    UIImage *image = [UIImage imageWithCIImage:ciImage];
    //こっから。UIImage作り直し開始
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //ここまで。UIImage作り直し終了
    _myImageView.image = [UIImage imageWithData:imageData];

    //メタデータ取得
    //CGImageSourceCreateWithURLを使う場合
    //Documentディレクトリ内の画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:filePath]), nil);
    //sourceRefの情報からメタデータを取り出す
    NSDictionary *metadata = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, nil));

    //使用後は解放
    CFRelease(sourceRef);

    //確認のためmetadata表示
    NSLog(@"メタデータ %@",metadata);
    //EXIF情報取り出し
    NSDictionary *ExifDictionary = [metadata objectForKey:(NSString*)kCGImagePropertyExifDictionary];
    NSLog(@"EXIFデータ %@",ExifDictionary);
}

各メソッド、関数について

NSBundleはアプリにインストールされてるリソースにアクセスするためのクラス。
resourcePathメソッドでリソースディレクトリへのpathが得られるので、ファイル名を追加。

CGImage〜Ref

CGImageSourceRef、CGImageDestinationRefは画像ファイルに付加されたEXIFなどの情報にアクセスするためのクラスで、ImageIO Frameworkが必要。
  • 画像ファイル読み込み時 
    • CGImageSourceRef 
  • 画像ファイル書き込み時 
    • CGImageDestinationRef

メタデータの中身にアクセス

metadataの辞書からEXIF情報を、キーを使って取り出し表示している。
ちなみにEXIFは入れ子になっている辞書なので、
float ss = [metadata[(NSString *)kCGImagePropertyExifDictionary][(NSString *)kCGImagePropertyExifExposureTimefloatValue];
などとやることで下の階層の情報にアクセスする。

CGImageDestinationCreateWithData

CGImageDestinationRef CGImageDestinationCreateWithDataConsumer ( CGDataConsumerRef consumer,CFStringRef type, size_t count, CFDictionaryRef options );

引数
consumer:書き込みデータのコンシューマー(何じゃそりゃ?)だそうだ。初期化しただけのNSMutableDataを指定してる。多分ここで指定した変数に画像データのsourceを書き込み、さらに結果をdestに返り値を返してるんだと思う。
type:データのタイプ(たとえばJPEGとかPNGとか)らしいが、この場合直接指定せずにCGImageSourceGetType関数でソース画像のタイプを得ている。
count:TIFFなどのように複数の画像を含む画像の場合、何枚あるのかを指定するが、1枚だけなら1でいい。
options:将来のための予約とあるので、nilでよし。

CGImageDestinationAddImageFromSource

void CGImageDestinationAddImageFromSource ( CGImageDestinationRef idst, CGImageSourceRef isrc, size_t index, CFDictionaryRef properties );

引数
idst:画像のdestination
isrc:画像のソース
index:ソース画像で画像の位置を指定するインデックス(0オリジン)。多分複数画像を持つフォーマットの場合に、何枚目の画像かを指定するんじゃないかと思うが、よくわからん。とりあえず1枚だけのフォーマットなら0でいいかと。
properties:ソース画像のプロパティに上書きするか追加するか指定する辞書。プロパティのキーがkCFNullなら画像のdestinationは削除される。わかりづらいが、ここに辞書を指定してやればソース画像(source)+辞書をdestinationに追加してくれるらしい。

変数destは書き込む画像のデータ(imagaData)とメタデータ(metadata)の情報を参照していて、CGImageDestinationFinalizeでファイナライズした後にimagaDataの方をwriteToFileすると、メタデータ付きで書き込まれるのではないか?
最後にsourceとdestはreleaseしてやる必要があるようだ。

destに色々情報を追加しつつ、最後はimageDataを書き込んでるのがわかりづらい。imageDataを書き込むとdestに書かれた参照情報を見てmetadataも一緒に書き込まれるという理解でいいのか?

CGImageにすると向きの情報が消えることがある

読み書き時に止むを得ずCGImageに変換を要する場合があるが、UIImageでは持っている画像の向きの情報であるimageOrientationプロパティがないため、UIImageに復元しても縦長に撮った画像が90度横倒しに表示されてしまうことがあった。
その場合、
[UIImage imageWithCGImage: scale: orientation:]メソッドでorientationを指定しつつ復元してやるが、それでもうまくいかない場合は、
UIGraphicsBeginImageContext()

UIGraphicsEndImageContext()
に挟まれた間で、UIImageを作り直してやるといい。
なにやらめんどくさいがしょうがない。
ちなみに前述のコード中、直接CGImageから変換したのではうまくいかなかったため、一度CIImageを経由した。
なにやら本当にめんどくさい。

謎のメタデータ追加メソッド

メタデータを追加するメソッドとしては
CGImageDestinationAddImageAndMetadata(CGImageDestinationRef idst, CGImageRef image, CGImageMetadataRef metadata, CFDictionaryRef options)
というのもあるようなのだが、APIリファレンス見てもちゃんとした説明が書いてないし、よくわからない。
釈然としないけど、CGImageDestinationAddImageFromSourceでうまくいくんならそれでいんじゃね?

アセットカタログ内の画像にアクセスしてみる

結論から言うとうまくいきませんでした。

NSString *imgSuffix = @"-universal-1x";
NSBundle *bundle = [NSBundle mainBundle];
//Assets Catalogのディレクトリへのパスを作る

NSString *imgPath = [bundle pathForResource:[NSStringstringWithFormat:@"%@%@",@"gazo2",imgSuffix] ofType:@"jpg"];

アセットカタログのjsonファイル(Contents.json)を開くと、
{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "gazo2.jpg",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}
などとなっており、アクセスにはファイル名にidiomやscaleのサフィックスを付けるというような情報を見たのだけど、サフィックスが合ってないのか、そもそも情報自体が間違いなのか、imgPathはnilになってしまい、ダメだった。
メタデータを取るためにアクセスするようなことがなければ、単にUIImageのファイル名で楽にアクセスできるのにねぇ。

感想

CGImageSourceRefCGImageDestinationRefがどういうものかよくわからず、ひたすら悩んで調べまくった。
今もよくわかってないけどね。
引数1に引数2以降の情報を追加するという仕様や、情報を追加した変数(たとえばdestRef)じゃなく画像本体の方を書き込むとdestRefを参照してメタデータも書き込まれるとかはわかりづらい。

今回はリソースとDocumentディレクトリの間だったけど、イメージライブラリとの間だとALAssetLibraryを使って(最近はPhotosFrameworkなの?)また別のやり方になるはず。
とにかくめんどっちー。
UIImageのプロパティとして簡単に扱えるようにならんもんか…。

0 件のコメント:

コメントを投稿