ラベル NSBitmapImageRep の投稿を表示しています。 すべての投稿を表示
ラベル NSBitmapImageRep の投稿を表示しています。 すべての投稿を表示

2011年6月20日月曜日

イメージを描画する(9)

あなたの瞳は世界をどんなふうに見るの?奇麗な瞳をみられたら写真家にもそんな気持ちになるときがあると思います。というわけで、こんなものをつくります。


Photoshop(CS3)では ”色の校正” プレビュー(5.0.3)では ”プロファイルを使ってソフトプルーフ” 、つまりソフトプルーフ機能を単独で実装します。

NSBitmapImageRep
表示だけのシステムなので、なにかないか探してみますとこんなものがありました。

- (NSBitmapImageRep *)bitmapImageRepByConvertingToColorSpace:(NSColorSpace *)targetSpace
                                                           renderingIntent:(NSColorRenderingIntent)renderingIntent

すてき。長いです。画像を描画意図(rendering intent)で別の色空間に変換してくれます。ここに色校正に使うディスプレイ・プロファイルとアウトプット・プロファイルからカラースペースを作って渡してやればよさそうです。なお 10.6 以降です。

NSColorSpace
NSColorSpace にはプロファイルのクラスごとにカラースペースを生成してくれるメソッドがないので

- (id)initWithColorSyncProfile:(void *)prof

を使ってColorSync プロファイルから作成してやれば良さそうです。引数が (void *)prof となっていますが、リファレンスは CMProfileRef わたすべしとなっています。なるほど。

ColorSync Manager API
目的は CMProfileRef を取得することです。

CMError CMOpenProfile (
   CMProfileRef *prof,
   const CMProfileLocation *theProfile
);

第1引数は戻ったときにプロファイルがはいります。なので第2引数の CMProfileLocation を取得して渡してやればよいですが、長旅になりそうな予感!

少し寄り道 CMIterateColorSyncFolder
インストースされているプロファイルの情報を提供してくれる API はないものでしょうか。そんな僕のために CMIterateColorSyncFolder 関数がありました。すべての利用可能なプロファイルからプロファイルごとの情報を確認できます。

CMError CMIterateColorSyncFolder (
   CMProfileIterateUPP proc,
   UInt32 *seed,
   UInt32 *count,
   void *refCon
);

第1引数は CMIterateColorSyncFolder ルーチンのなかで呼出されるコールバック関数のポインタになります。

第2引数はキャッシュの設定です。CMIterateColorSyncFolder 初めて呼出すときは0をセットしたポインタを渡せとあります。内部の seed と一致しなかったな場合はプロファイルごとに一度のコールバックを呼出すとあります。

第3引数は戻ってきたときに利用可能なプロファイルの数が入れられます。

第4引数は任意のデータへのポインタを指定します。この値は第1引数のコールバックが呼出されるたびに渡されます。なのでコールバック内で参照する必要のあるデータがある場合は設定します。

今度は CMProfileIterateUPP!
CMIterateColorSyncFolder の第1引数 CMProfileIterateUPP は

typedef CMProfileIterateProcPtr CMProfileIterateUPP;

となっていて CMProfileIterateProcPtr は

typedef OSErr (*CMProfileIterateProcPtr)
(
   CMProfileIterateData * iterateData,
   void * refCon
);

です。第1引数にはプロファイルに関連するデータ構造体です。第2引数にはCMIterateColorSyncFolder の第4引数で設定したデータへのポインタがそのままやってきます。

CMProfileLocation を求めて
CMProfileIterateData はどうなっているのでしょうか。

struct CMProfileIterateData {
   UInt32 dataVersion;
   CM2Header header;
   ScriptCode code;
   Str255 name;
   CMProfileLocation location;
   UniCharCount uniCodeNameCount;
   UniChar * uniCodeName;
   unsigned char * asciiName;
   CMMakeAndModel * makeAndModel;
   CMProfileMD5 * digest;
};
typedef struct CMProfileIterateData CMProfileIterateData;

CMProfileLocation ありました。これを CMOpenProfile に渡してやれば CMProfileRef が取得できます。さらに CM2Header 構造体の

struct CM2Header {
   UInt32 size;
   OSType CMMType;
   UInt32 profileVersion;
   OSType profileClass;
   OSType dataColorSpace;
   OSType profileConnectionSpace;
   CMDateTime dateTime;
   OSType CS2profileSignature;
   OSType platform;
   UInt32 flags;
   OSType deviceManufacturer;
   UInt32 deviceModel;
   UInt32 deviceAttributes[2];
   UInt32 renderingIntent;
   CMFixedXYZColor white;
   OSType creator;
   char reserved[44];
};
typedef struct CM2Header CM2Header;

OSType profileClass でプロファイルのクラスの情報があります。

enum {
   cmInputClass = 'scnr',
   cmDisplayClass = 'mntr',
   cmOutputClass = 'prtr',
   cmLinkClass = 'link',
   cmAbstractClass = 'abst',
   cmColorSpaceClass = 'spac',
   cmNamedColorClass = 'nmcl'
};

cmDisplayClass と cmOutputClass を使ってプロファイルのクラスを選別できそうです。

それでは
戻り値のチェックをしていません。
DrawingImage_9_AppDelegate.h
#import <Cocoa/Cocoa.h>

@class DISourceImageView, DIProofImageView;

@interface DrawingImage_9_AppDelegate : NSObject  {
    NSWindow *window;
    
    DISourceImageView *srcView;
    DIProofImageView *proofView;
    
    NSTextField *srcColorSpaceField;
    
    NSBitmapImageRep *srcImage;
}

#pragma mark Accessor Method
@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet DISourceImageView *srcView;
@property (assign) IBOutlet DIProofImageView *proofView;
@property (assign) IBOutlet NSTextField *srcColorSpaceField;

#pragma mark Action Method
- (IBAction)openImage:(id)sender;
@end

DrawingImage_9_AppDelegate.m
#import "DrawingImage_9_AppDelegate.h"
#import "DISourceImageView.h"
#import "DIProofImageView.h"

@implementation DrawingImage_9_AppDelegate

#pragma mark init & dealloc
- (void) dealloc
{
    [srcImage release];
    [super dealloc];
}

#pragma mark Accessor Method
@synthesize window, srcView, proofView, srcColorSpaceField;

#pragma mark Inner Method
- (void)setViewsWithImage:(NSBitmapImageRep *)bitmapImage
{
    if (bitmapImage) {
        NSColorSpace *srcSpace = [bitmapImage colorSpace];
        [srcColorSpaceField setStringValue:[srcSpace localizedName]];
    }
    else {
        [srcColorSpaceField setStringValue:@""];
    }
    
    [srcView setImage:bitmapImage];
    [proofView setImage:bitmapImage];
}

#pragma mark Action Method
-(IBAction)openImage:(id)sender
{
    NSOpenPanel *opPanel = [NSOpenPanel openPanel];
    
    [opPanel setCanChooseFiles:YES];
    [opPanel setCanChooseDirectories:NO];
    [opPanel setAllowsMultipleSelection:NO];
    
    [opPanel beginSheetModalForWindow:window
                    completionHandler:^(NSInteger result){
                        
                        if (result == NSFileHandlingPanelOKButton) {
                            
                            if (srcImage != nil) {
                                [srcImage release];
                            }
                            
                            NSData *data = [NSData dataWithContentsOfURL:[opPanel URL]];
                            srcImage = [[NSBitmapImageRep alloc] initWithData:data];
                            
                            [self setViewsWithImage:srcImage];
                            
                        }
                        else {
                            // do nothing; 
                        }
                        
                    }];
}
@end

DISourceImageView.h
#import <Cocoa/Cocoa.h>

@interface DISourceImageView : NSView {

    NSBitmapImageRep *image;

}

#pragma mark Accessor Method
- (void)setImage:(NSBitmapImageRep *)bitmapImage;
@end

DISourceImageView.m
#import "DISourceImageView.h"

@implementation DISourceImageView

#pragma mark init & dealloc
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

#pragma mark Drawing Method
- (void)drawRect:(NSRect)dirtyRect {
    
    if (image) {
        
        // ビューのサイズを取得
        CGSize viewSize = CGSizeMake([self bounds].size.width,
                                     [self bounds].size.height);
        
        // NSBitmapImageRep から CGImageRef を取得
        CGImageRef imageRep = [image CGImage];
        
        // 現在のグラフィクス・コンテキストを取得、変更するのでグラフィック状態を保存
        CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
        CGContextSaveGState(context);
        
        //描画意図を Absolute Colorimetric(絶対的な色域)に設定
        CGContextSetRenderingIntent(context, kCGRenderingIntentAbsoluteColorimetric);
        
        // 体裁を整えてイメージを描画
        CGFloat imageWidth = CGImageGetWidth(imageRep);
        CGFloat imageHeight = CGImageGetHeight(imageRep);
        
        CGFloat scale = imageHeight / imageWidth;
        
        BOOL isWide = (imageWidth >= imageHeight) ? YES : NO;
        
        if (isWide == YES) {
            
            CGFloat translate;
            translate = (viewSize.height - (viewSize.width * scale)) / 2.0;
            
            CGContextTranslateCTM(context, 0.0, translate);
            CGContextScaleCTM(context, 1.0, scale);
        }
        else {
            CGFloat translate;
            translate = (viewSize.width - (viewSize.height * (1.0 / scale))) / 2.0;
            
            CGContextTranslateCTM(context, translate, 0.0);
            CGContextScaleCTM(context, 1.0 / scale, 1.0);
            
        }
        
        CGContextDrawImage(context, CGRectMake(0.0,0.0,viewSize.width,viewSize.height), imageRep);
        
        // グラフィクス・コンテキストをもとに戻す。
        CGContextRestoreGState(context);
    }
    else {
        // do nothing;
    }
}

#pragma mark Accessor Method
- (void)setImage:(NSBitmapImageRep *)bitmapImage
{
    image = bitmapImage;
    [self setNeedsDisplay:YES];
}


@end

DIProofImageView.h
#import <Cocoa/Cocoa.h>

@interface DIProofImageView : NSView {

    NSPopUpButton *outputSpace;
    NSPopUpButton *intent;
    
    NSBitmapImageRep *image;
}

#pragma mark Accessor Method
@property (assign) IBOutlet NSPopUpButton *outputSpace;
@property (assign) IBOutlet NSPopUpButton *intent;
- (void)setImage:(NSBitmapImageRep *)bitmapImage;

#pragma mark Action Method
- (IBAction)selectedColorSpace:(id)sender;
- (IBAction)selectedRenderingIntent:(id)sender;
@end

DIProofImageView.m
#import "DIProofImageView.h"

#pragma mark Profile Iteration Callback Function
OSErr profileIterateProcPtr (CMProfileIterateData *iterateData, void *refCon)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSMutableArray *colorSpaces = refCon;

    // プロファイルのクラスを取得
    OSType profileClass = (*iterateData).header.profileClass;
    
    // クラスがアウトプット、ディスプレイに該当したら追加する。
    if (profileClass == cmOutputClass || profileClass == cmDisplayClass){

        // プロファイルからカラースペースを作成する。
        CMProfileRef prof;
        CMProfileLocation location = iterateData->location;
        
        // プロファイルを開く
        CMOpenProfile(&prof, &location);
        
        // プロファイルからカラースペースを作成
        NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithColorSyncProfile:prof];
        [colorSpaces addObject:colorSpace];
        
        // もういらない。開いたプロファイルは閉じる。
        [colorSpace release];
        CMCloseProfile(prof);
        
    }
    
    [pool drain];
    return noErr;
}
#pragma mark -
@implementation DIProofImageView

#pragma mark init & dealloc
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

#pragma mark awakeFromNib
- (void)awakeFromNib
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    // インストールされているプロファイルからカラースペースを作成して、Arrayに追加する。
    NSMutableArray *colorSpaces = [NSMutableArray array];
    CMIterateColorSyncFolder(profileIterateProcPtr, NULL, NULL, colorSpaces);
    
    // メニューを作成してポップアップボタンに設定する。
    NSMenu *outputMenu = [[NSMenu alloc] init];
    
    for (NSColorSpace *colorSpace in colorSpaces) {
        
        NSMenuItem *item = [[NSMenuItem alloc] init];
        
        // メニューアイテムにタイトルとカラースペースを設定、メニューに追加
        [item setTitle:[colorSpace localizedName]];
        [item setRepresentedObject:colorSpace];
        [outputMenu addItem:item];
        
        [item release];
    }
    
    [outputSpace setMenu:outputMenu];
    
    [outputMenu release];
    [colorSpaces removeAllObjects];

    
    // 描画意図(Rendering intent)をポップアップボタンに設定する。
    NSMutableDictionary *intentDic = [NSMutableDictionary dictionary];
    
    [intentDic setValue:[NSNumber numberWithInteger:1] forKey:@"Absolute Colorimetric"];
    [intentDic setValue:[NSNumber numberWithInteger:2] forKey:@"Relative Colorimetric"];
    [intentDic setValue:[NSNumber numberWithInteger:3] forKey:@"Perceptual"];
    [intentDic setValue:[NSNumber numberWithInteger:4] forKey:@"Saturation"];
    
    NSMenu *intentMenu = [[NSMenu alloc] init];
    
    for (NSString *key in [intentDic allKeys]) {
        
        NSMenuItem *item = [[NSMenuItem alloc] init];
        
        // メニューアイテムにタイトルとカラースペースを設定、メニューに追加
        [item setTitle:key];
        [item setRepresentedObject:[intentDic valueForKey:key]];
        [intentMenu addItem:item];
        
        [item release];
    }
    
    [intent setMenu:intentMenu];
    
    [intentMenu release];
    [intentDic removeAllObjects];
    
    [intent selectItemWithTitle:@"Perceptual"];
    
    [pool drain];
}

#pragma mark Drawing Method
- (void)drawRect:(NSRect)dirtyRect {
    
    if (image) {
        
        // 選択されているカラースペースと描画意図からビットマップを変換する
        NSColorSpace *colorSpace;
        NSColorRenderingIntent renderingIntent;
        NSBitmapImageRep *outputImage;
        
        colorSpace = [[outputSpace selectedItem] representedObject];
        renderingIntent = [[[intent selectedItem] representedObject] integerValue];
        outputImage = [image bitmapImageRepByConvertingToColorSpace:colorSpace
                                                    renderingIntent:renderingIntent];
        
        // ビューのサイズを取得
        CGSize viewSize = CGSizeMake([self bounds].size.width,
                                     [self bounds].size.height);
        
        // NSBitmapImageRep から CGImageRef を取得
        CGImageRef imageRep = [image CGImage];
        
        // 現在のグラフィクス・コンテキストを取得、変更するのでグラフィック状態を保存
        CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
        CGContextSaveGState(context);
        
        //描画意図を Absolute Colorimetric(絶対的な色域)に設定
        CGContextSetRenderingIntent(context, kCGRenderingIntentAbsoluteColorimetric);
        
        // 体裁を整えてイメージを描画
        CGFloat imageWidth = CGImageGetWidth(imageRep);
        CGFloat imageHeight = CGImageGetHeight(imageRep);
        
        CGFloat scale = imageHeight / imageWidth;
        
        BOOL isWide = (imageWidth >= imageHeight) ? YES : NO;
        
        if (isWide == YES) {
            
            CGFloat translate;
            translate = (viewSize.height - (viewSize.width * scale)) / 2.0;
            
            CGContextTranslateCTM(context, 0.0, translate);
            CGContextScaleCTM(context, 1.0, scale);
        }
        else {
            CGFloat translate;
            translate = (viewSize.width - (viewSize.height * (1.0 / scale))) / 2.0;
            
            CGContextTranslateCTM(context, translate, 0.0);
            CGContextScaleCTM(context, 1.0 / scale, 1.0);
            
        }
        
        [outputImage drawInRect:[self bounds]];
        
        // グラフィクス・コンテキストをもとに戻す。
        CGContextRestoreGState(context);
    }
    else {
        // do nothing;
    }
}

#pragma mark Accessor Method
@synthesize outputSpace, intent;

- (void)setImage:(NSBitmapImageRep *)bitmapImage
{
    image = bitmapImage;
    [self setNeedsDisplay:YES];
}

#pragma mark Action Method
- (IBAction)selectedColorSpace:(id)sender
{
    [self setNeedsDisplay:YES];
}
- (IBAction)selectedRenderingIntent:(id)sender
{
    [self setNeedsDisplay:YES];
}
@end

Interface Builder での作業



その他
指定したカラースペースに変換したビットマップを生成するのに ColorSync Manager API だけでやろうとするとけっこうめんどうです。NCWConcatColorWorld か CWConcatColorWorld を使って CMWorldRef を作成してやります。それを CWMatchBitmap にかけてやってビットマップを作成すれば良いと思います。また、この間に使用するいくつかの構造体を定義してやらなければなりません。

2011年4月30日土曜日

イメージを描画する(2)

あしびきの山鳥の尾のしだり尾の長々しメソッドをひとりかもせむ、写真家にもそんな気分のときってあると思います。というわけで前回ご紹介した長々しメソッドを使ってみたいと思います。

If a coverage (alpha) plane exists, a bitmap’s color components are premultiplied with it. If you modify the contents of the bitmap, you are therefore responsible for premultiplying the data. For this reason, though, if you want to manipulate the actual data, an NSBitmapImageRep object is not recommended for storage. If you need to work with data that is not premultiplied, you should use Quartz, specifically CGImageCreate with kCGImageAlphaLast.
NSBitmapImageRep Class Reference "Alpha PreMultiplication"

アルファ・プレーンがある場合、ビットマップの色成分は事前にかけ合わされます。なのでビットマップの色成分を変更する場合はデータを手前で事前にかけ合わせろと。だから NSBitmapImageRep はビットマップ・データの操作を前提としたストレージにはむいていない(手間だもんね)。そうゆう場合には Quartz をつかってね。ということでしょうか、Quartz、Cでばりばり。そのうちやりましょう。



プレーン(plane)って?
分からなかったのでメモしときます。ビットマップ画像を構成成分の重ね合わせとしてみることができます。このとき重ね合わされている成分それぞれををプレーンと呼びます。構成成分を色成分でみたとき、カラー・プレーンと呼ばれます。正確な表現ではありませんが、イメージとしては Photoshop の RGB(CMYK)の各チャンネルで表現されているもの、みたいな感じです。その他にビットの深さを基準にみたビット・プレーンもあります。ビット・プレーンを利用した電子透かしの原理など、知りませんでしたので目から鱗が落ちました。


では NSBitmapImageRep はどんなときに使うのでしょうか。リファレンスのメソッド群を眺めますとプログラムからビットマップ・データを作成するというのが一番の使い方のような気がしますが。だれか教えてくださいませ。


下記の説明は画像工学が専門でない僕の理解に基づいているので間違っている可能性があります。その場合ご指摘いただけたら幸いに存じます。?と思ったら NSBitmapImageRep Class Reference を直接ご参照ください。

initWithBitmapDataPlanes:(unsigned char **)planes
                              pixelsWide:(NSInteger)width
                               pixelsHigh:(NSInteger)height
                        bitsPerSample:(NSInteger)bps
                     samplesPerPixel:(NSInteger)spp
                                  hasAlpha:(BOOL)alpha
                                     isPlanar:(BOOl)isPlanar
                    colorSpaceName:(NSString *)colorSpaceName
                         bitmapFormat:(NSBitmapFormat)bitmapFormat
                            bytesPerRow:(NSInteger)rowBytes
                               bitsPerPixel:(NSInteger)pixelBits
(unsigned char **)planes
  • イメージ・データのバッファを指定。プレーン構成(plane configuration)の場合それぞれのバッファに1つのプレーンがあり、ひとつのプレーンには1つの成分があり、
  •  
  • R1R2R3R4R5...G1G2G3G4G5...B1B2B3B4B5...(A1A2A3A4A5...)
  •  
  • の順番になります。
  • アルファ・プレーンが存在する場合は例によって、事前にかけ合わせろと。isPlanar が NO のときはメッシュ構成(meshed configuration)
  •  
  • R1G1B1(A1)R2G2B2(A2)R3G3B3(A3)...
  •  
  • で、先頭のバッファのみが読込まれます。
  •  
  • NULL(または NULL の配列)を設定するとオブジェクトが適当なメモリを確保してくれます。確保されたメモリはオブジェクトに所有され、オブジェクトを解放すれば同時に解放されます。NULL でない場合はオブジェクトはただこのバッファを参照するだけで、変更不可なものとみなします。オブジェクトを解放してもこのバッファは解放されません。この場合は別に自分で解放する必要があります。
(NSInteger)width
  • 作成したい画像の横幅ピクセル
(NSInteger)height
  • 作成したい画像の高さピクセル
(NSInteger)bps
  • 1ピクセル中の1つの成分が何ビットで構成されているかの指定です。サンプルあたりのビット数。すべての成分でサンプルごとに同じビットを持っていると仮定されます。例えば256階調ならば8。
  •  
  • 指定できるビット:1, 2, 4, 8, 12, 16 のどれか。
(NSInteger) spp
  • データの成分の数またはピクセルあたりのサンプル数。意味を持つのは1〜5まで。アルファ値をもったCMYKならば5。RGBならば3。
(BOOL)alpha
  • YES:アルファ値をもっている
  • NO:もっていない
(BOOl)isPlanar
  • YES:プレーン構成
  • NO:メッシュ構成
(NSString *)colorSpaceName
  • 次のうちどれか。bps で 12 を指定している場合はモノクロのカラースペースは指定できません。
  •  
  • NSCalibrateWhiteColorSpace
  • NSCalibrateBlackColorSpace
  • NSCalibrateRGBColorSpace
  • NSDeviceWhiteColorSpace
  • NSDeviceBlackColorSpace
  • NSDeviceRGBColorSpace
  • NSDeviceCMYKColorSpace
  • NSNamedColorSpace
  • NSCustomColorSpace
(NSBitmapFormat)bitmapFormat
  • 次の3つ。Cのビット演算でORして使ってね、とのこと。
  •  
  • NSAlphaFirstBitmapFormat = 1 << 0
  • NSNonPremultipliedBitmapFormat = 1 << 1
  • NSFloatingPointSamplesBitmapFormat = 1 << 2

  • NSAlphaFirstBitmapFormat が0ならば R1G1B1A1... とアルファ値が最後に置かれます。
  • NSNonPremultipliedBitmapFormat が0ならばアルファ値は事前にかけ合わされています。
  • NSFloatingPointSamplesBitmapFormat が0ならば整数です。
(NSInteger)rowBytes
  • width が実際に何バイトで構成されているかの指定。width を使って正確に計算した値でもよいですが、コンピュータはワードごと(16ビットとか32ビットなど CPU に依存します。)の方が読み出しが速いので、画像にしない部分に余分なデータをもたせることがあります。バイト数をあらかじめオブジェクトに教えてやるのはこのためです。0を指定してやりますと、パフォーマンスが最適になるような値を生成してくれます。
(NSInteger)pixelBits
  • 1ピクセルが実際に何ビットで構成されているかの指定。通常はプレーン構成ならば bps 、メッシュ構成ならば "bps * spp" に等しくなります。これもまたパフォーマンス上の理由でそうしない場合があります。例えば RGB で8bps、3pps のメッシュ構成でビット列が構成されていると仮定しますと通常、
  •  
  • R1G1B1R2G2B2R3G3B3...
  •  
  • となっていますが、32ビットごとの読み出しが速いこと(システムに依存します)を考慮して、
  •  
  • R1G1B1Empty1R2G2B2Empty2R3G3B3Empty3...
  •  
  • とする場合があります。この場合は24ビットではなく32ビットになります。
  • 0を指定してやると余分なデータなしで bps と spp の値でピクセルあたりのビット数を解釈します。


前置きがものすごくながくなりましたが、ここからが本番です。
手順は、
  1. ビットマップ・イメージ(99 * 99, 8ビット RGB )を作成する。
  2. 設定した通りにできているかビットマップ・データに何か色を設定する。
  3. ちゃんと設定されたたか View に描画してみてる。
  4. TIFF で書き出してみる。
です。ウィンドウにカスタム・ビューを貼付けてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

- (id)initWithFrame:(NSRect)frame
{

    self = [super initWithFrame:frame];

    if (self) {

        bitmapImageRep = nil;

    }

    return self;
}
- (void) dealloc
{
 [bitmapImageRep release];
 [super dealloc];
}

- (void)drawImage
{
 if (bitmapImageRep)
  return;  
 
 // 中途半端にして bytesPerRow の最適化をみてみる。
 NSInteger pixelsWide = 99;
 NSInteger pixelsHigh = 99;
 
 // 手順1
 bitmapImageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                pixelsWide:pixelsWide
                pixelsHigh:pixelsHigh
                bitsPerSample:8
              samplesPerPixel:3
                  hasAlpha:NO
                  isPlanar:NO
               colorSpaceName:NSCalibratedRGBColorSpace
                 bitmapFormat:0
               bytesPerRow:0
                 bitsPerPixel:0];
 
 // 手順2
 unsigned char* bitmapData = [bitmapImageRep bitmapData];
 NSInteger spp = [bitmapImageRep samplesPerPixel];
 NSInteger bpr = [bitmapImageRep bytesPerRow];
 
 NSInteger width, height;
 for (width = 0; width < pixelsWide; ++width) {
  
  for (height = 0; height < pixelsHigh; ++height) {
   
   // ビットマップ・データに値を設定。緑と青縞模様。
   if ((height & 1)) {
    *(bitmapData + (width * spp) + (height * bpr) + 1) = 255;    
   }
   else {
    *(bitmapData + (width * spp) + (height * bpr) + 2) = 255;    
   }
  }
 }
}

- (void)drawRect:(NSRect)dirtyRect
{

 [self drawImage];
 
 CGFloat pixelsWide, pixelsHigh;
 pixelsWide = (CGFloat)[bitmapImageRep pixelsWide];
 pixelsHigh = (CGFloat)[bitmapImageRep pixelsHigh];
 
 // 手順3
 [bitmapImageRep drawInRect:NSMakeRect(0.0, 0.0, pixelsWide, pixelsHigh)];

}

-(void)saveImage
{

 // 手順4
 NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Desktop/test.tif"];

 NSData *imageData = [bitmapImageRep TIFFRepresentation];

 [imageData writeToFile:filePath atomically:YES];

}
@end

手順1で作成した ビットマッピ・イメージは僕の環境では bytePerRow が 297バイトではなく 320バイトになっていました。bitsPerPixel で0を渡しているので、サンプルごとのパディングはなく、各行の最後 23バイトは余分なデータだということが分かります。
 
手順2でビットマップデータに直接値を設定しています。

手順3、手順4はそのままなので、特に説明はありません。


initWithBitimapDataPlanes:〜に渡す値は意外に NULL とか 0とか定数なのでらくちんでした。これでがしがしビットマップ・イメージが作れます。

2011年4月28日木曜日

イメージを描画する(1)

たまにはイメージをバイト列でごりごり扱いたい、写真家にもそんな気分のときってあると思います。

The NSBitmapImageRep class renders an image from bitmap data. Bitmap data formats supported include GIF, JPEG, TIFF, PNG, and various permutations of raw bitmap data.

というわけで、そんな僕のために Apple は NSBitmapImageRep を用意してくれていました。生のビットマップ・データをあつかって1ピクセルずつ描画しようというわけです。ふつうはこんな方法でイメージを描画いたしません(描画にものすごく時間がかかります)のであしからず。なのでお急ぎの方はご遠慮くださいませ。

手順は
  1. NSBitmapImageRep からビットマップ・データを抽出。
  2. イメージ・データが持っている情報をもとに1ピクセルのカラーを作成。
  3. 作成したカラーを設定して View へ1ピクセル描画する。
  4. 2と3を繰り返す。
ウィンドウにカスタム・ビューを設定してあります。AppDelegate からオープンパネルを開き、指定したファイルから NSData のインスタンスを作ます。それを setBitmapImageRepWithData: でカスタムビューに送り、それが NSBitmapImageRep で扱うことができる場合に NSBitmapImageRep のインスタンスを生成するようにしてあります。

#import "CustomView.h"

NSBitmapImageRep *bitmapImageRep;

@implementation CustomView

- (id)initWithFrame:(NSRect)frame {
 
    self = [super initWithFrame:frame];
    if (self) {
        bitmapImageRep = nil;
    }
 
    return self;
}

- (void) dealloc
{ 
 [bitmapImageRep release];
 [super dealloc];
}

- (void)drawRect:(NSRect)dirtyRect
{
    [self drawImage];
}

- (void)drawImage
{
 
 if (!bitmapImageRep)
  return;
 
 //手順1
 unsigned char *bitmapData = [bitmapImageRep bitmapData];
// 手順2
 NSInteger bpr = [bitmapImageRep bytesPerRow];
 NSInteger bppPbps = [bitmapImageRep bitsPerPixel] / [bitmapImageRep bitsPerSample];
 
 NSUInteger pixelsWide, pixelsHigh, indexW, indexH, indexC;
 pixelsWide = [bitmapImageRep pixelsWide];
 pixelsHigh = [bitmapImageRep pixelsHigh];

 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
 
 [self lockFocus];
 
 for (indexW = 0; indexW < pixelsWide; indexW++ ) {
  
  for (indexH = 0; indexH < pixelsHigh; indexH++) {
   
   CGFloat *components = calloc(bppPbps, sizeof(CGFloat));
   
   for (indexC = 0; indexC < bppPbps; indexC++) {
    
    *(components + indexC) = (*(bitmapData + (indexW * bppPbps) + (indexH * bpr) + indexC)) / 255.0;
    
   }   
   [[NSColor colorWithColorSpace:[bitmapImageRep colorSpace]
          components:components
         count:bppPbps] set];
   // 手順3
   NSRectFill(NSMakeRect(indexW, (pixelsHigh - indexH), 1, 1));
   
   free(components);
  }
  
 }
 
 [self unlockFocus];
 [pool release];
 
}

- (BOOL)setBitmapImageRepWithData:(NSData *)imageData
{
 if (bitmapImageRep) {
  [bitmapImageRep release];
  bitmapImageRep = nil;
 }
 
 NSString *className = [[NSImageRep imageRepClassForData:imageData] className];
 
 if ([className isEqualToString:@"NSBitmapImageRep"] == YES) {
  
  bitmapImageRep = [[NSBitmapImageRep imageRepWithData:imageData] retain];
  
  [self drawImage];
  return YES;
  
 }
 
 return NO;
 
}
@end


手順1でビットマップ・データのポインタを取得しています。手順2のループのコードから分かるように、データが1次元でのみ構成されていると仮定してます。データが(例えば)カラー・プレーンで構成されている場合は isPlanar の戻り値をチェックして、それに応じて読み込む必要があります。ここはとりあえず決め打ちします。

手順2で bppPbps を計算するのはイメージ・データによっては適当にパディングされ、必ずしも

bitsPerPixel = bitsPerSample * samplesPerPixel

にならないためです。
ループ中の

*(components + indexC) = (*(bitmapData + (indexW * bppPbps) + (indexH * bpr) + indexC)) / 255.0;

は bitmapData からカラー・コンポーネントを取得しています。コンポーネントのフォーマットが整数で8ビットであることを仮定しています。ここも bitmapFormat の戻り値をチェックすると整数か小数か確認できますが決め打っています。NSColor が0〜1でカラー情報をもつので 255.0(8ビットを仮定しない場合はpow(2,[bitmapImageRep bitsPerSample]) - 1)で割ります。colorWithColorSpace:〜でbitmapImageRep と同じカラースペースを指定してやります。

手順3は設定した色を1ピクセルで View に塗りつぶします。 View の座標系が左下が原点となっているので(pixelHeight - indexH)。単に indexH としたい場合は -(BOOL)isFlipped を実装して YES を返すようにすれば左上が原点になります。


NSBitmapImageRep を使って View に普通に描画するには、View にフォーカスをロックしてdrawInRect:(NSRect)rect を使い

NSRect rect = NSMakeRect( 0.0, 0.0, pixelsWide, pixelsHigh);
[bitmapImageRep drawInRect:rect];

としますと、さらっといきます。

余談ですがこの NSBitmapImageRep は Cocoa で最も長いイニシャライザをもってます。

initWithBitmapDataPlanes:(unsigned char **)planes
                              pixelsWide:(NSInteger)width
                               pixelsHigh:(NSInteger)height
                        bitsPerSample:(NSInteger)bps
                     samplesPerPixel:(NSInteger)spp
                                  hasAlpha:(BOOL)alpha
                                     isPlanar:(BOOl)isPlanar
                    colorSpaceName:(NSString *)colorSpaceName
                         bitmapFormat:(NSBitmapFormat)bitmapFormat
                            bytesPerRow:(NSInteger)rowBytes
                               bitsPerPixel:(NSInteger)pixelBits

148文字です。ツイッターでつぶやかれる方ご注意ください。そんな NSBitmapImageRep ラヴ。