スクリーンショットに好みの影を付けるコマンド作り

このようなブログを続けていると、スクリーンショットを非常によく撮る。というか、ほぼ毎回撮影する。RetinaMac環境に移行して、先日ようやくブログに載せる画像もRetina環境に対応した。

ところで、以前からスクリーンショットで撮影したウィンドウ画像は、影の領域が無駄に広いと感じていた。でも、影なしにしてしまうと輪郭線もなく、メリハリのない画像になってしまう...。だから、いつも影ありの画像で撮影して、それを使っていた。そんなとき、コメントで耳寄りな情報を頂いた。MiniShadowというアプリケーションを使うと、スクリーンショットの影のサイズを縮小できるのだ!(ありがとう、Ryoさん)

百聞は一見に如かず、まずはそのスクリーンショット

元画像が影あり 元画像が影なし ファイルサイズ
(影あり, 影なし)
None 75KB, 75KB
Nano 78KB, 77KB
Mini 81KB, 80KB
元画像 112KB, 75KB
  • 影あり・なしに関係なく、影の領域を調整できるところがMiniShadowの素晴らしいところ。
  • 影のある元画像は、画像の周囲に輪郭線が残されるところも興味深い仕様。(Noneの画像を比較するとよく分かる)
  • 一番下の元画像がスクリーンショット撮って出しのそのままの状態である。
  • 上の3枚に比べて、画像の周囲の影の領域がいかに大きすぎるか確認できると思う。
  • 一方、MiniShadowで処理した画像は、影の領域がほど良い感じに縮小されている!
  • これだけ影の領域に差があると、画像ファイルのサイズにもかなり影響してくる。
  • 影ありの元画像が112KB、一方MiniShadowで処理した影ありの画像なら最大でも81KB。
  • 影の領域を調整するだけで、3割近いファイルサイズを圧縮していることになる。

影の見た目だけでなく、これはRetina対応で大きくなりがちな画像サイズを、少しでも快適に表示する手法の一つにもなり得るのだ!

コマンドから利用したい

  • ところでMiniShadowの使い方は、表示されるMiniShadowウィンドウに画像をドラッグ&ドロップする仕組みだ。


  • ドラッグ&ドロップは、GUIを象徴する素晴らしく分かり易い操作方法かもしれないが、
  • 何度も繰り返していると、実は非常に手間のかかる操作だということに、いずれ気付く...。
  • そして残念なことに、MiniShadowにはドラッグ&ドロップの操作しか用意されていないのだ。
  • せめて、ファイル >> 開く のメニュー操作さえ用意されていれば、AppleScriptから簡単に利用できるのだが、
  • さすがにドラッグ&ドロップだけでは、他のアプリと連携させて作業を自動化できる希望ががほとんどない。

というわけで、できないなら自分で作ってみるか!という、いつもの調子で作り始めてみた。

  • 作るのはGUIのアプリケーションではなく、コマンド。(AppleScriptから利用することが目的なので)
  • 画像ファイルのパスを引数にとり、影付き画像をファイルに出力したい。

dropshadowコマンドプロジェクトの開始

  • まずはXcodeを起動して、コマンドラインの雛形 Command Line Tools から始めようかと思ったが、
  • どうやらそれではCocoa環境を自由にアクセスできる環境になっていない...。
  • 何らかの設定をすれば良いのかもしれないが、その方法がまったく分からない。
  • ならば、Cocoa Application の雛形で始めて、main関数の部分にコマンドの処理を書いてしまえば良いのではないかと。
  • アプリケーションを起動させる1行はコメントアウトして、さっそく作業開始。
  • 調べてみると、Cocoa環境においては影付けは非常に簡単である。
  • NSShadowに影のでき方を設定しておけば、それ以降に描画された画像はすべて影付きになるのだ!
  • 通常はGUIアプリケーションではNSViewの中で目に見える画像として描画されるが、今回はコマンドなので画像の表示は不要である。
  • そのような場合は描画するNSImageを指定して、それに対して画像を描けば、透明でない部分の影が投影されるのである。
  • NSImageに描画した内容はそのままでは目に見えない画像データだが、それをファイルに保存すればプレビュー.appなどで閲覧できる画像ファイルとなる。
  • つまり、NSShadowの設定をして、元画像を自分が用意したNSImageに描画して、それをファイルに保存できれば、影付きの画像ができるはず。
  • まずは必要最小限のコードだけで、とりあえず動くコマンドを作ってみた。


//
// main.m
// dropshadow
//
// Created by zarigani on 2012/11/13.
// Copyright (c) 2012 bebe工房. All rights reserved.
//

#import <Cocoa/Cocoa.h>

//影付きイメージを描画して返す
NSImage* dropshadowImage(NSImage *image)
{
//スケールを取得する
CGFloat scale = [ [NSScreen mainScreen] backingScaleFactor];
//Retina環境に応じたポイントサイズを取得する
NSBitmapImageRep* imageRep = [NSBitmapImageRep imageRepWithData:[image TIFFRepresentation] ];
NSSize pointSize = NSMakeSize([imageRep pixelsWide]/scale, [imageRep pixelsHigh]/scale);
//描画する場所を準備
NSRect newRect = NSZeroRect;
newRect.size.width = pointSize.width + 20;
newRect.size.height = pointSize.height + 20;
NSImage *newImage = [ [NSImage alloc] initWithSize:newRect.size];
//描画する場所=newImageに狙いを定める、描画環境を保存しておく
[newImage lockFocus];
[NSGraphicsContext saveGraphicsState];
//拡大・縮小した時の補間品質の指定
[ [NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
//影の設定
NSShadow *shadow = [ [NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(0.0, -2.0)];
[shadow setShadowBlurRadius:8];
[shadow setShadowColor:[ [NSColor blackColor] colorWithAlphaComponent:0.5] ];
[shadow set];
//描画する
NSRect drawRect;
drawRect.origin = NSMakePoint(10, 10);
drawRect.size = pointSize;
[image drawInRect:drawRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//描画環境を元に戻す、描画する場所=newImageから狙いを外す
[NSGraphicsContext restoreGraphicsState];
[newImage unlockFocus];
//影付きのイメージを返す
return newImage;
}

//PNGファイルとして保存する
void saveImageByPNG(NSImage *image, NSString* fileName)
{
NSData *data = [image TIFFRepresentation];
NSBitmapImageRep* bitmapImageRep = [NSBitmapImageRep imageRepWithData:data];
NSDictionary* properties = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey:NSImageInterlaced];
data = [bitmapImageRep representationUsingType:NSPNGFileType properties:properties];
[data writeToFile:fileName atomically:YES];
}




int main(int argc, char *argv[])
{
// return NSApplicationMain(argc, (const char **)argv);

//影付きイメージを生成する
NSImage *image = [ [NSImage alloc] initWithContentsOfFile:[NSString stringWithUTF8String:argv[1] ] ];
NSImage *shadowImage = dropshadowImage(image);
saveImageByPNG(shadowImage, @"output.png");

//画像情報を出力する
NSRect align = [shadowImage alignmentRect];
NSLog(@"(%f, %f, %f, %f)", align.origin.x, align.origin.y, align.size.width, align.size.height);
return 0;
}

  • さっそくビルドして、コマンドを実行してみる。
  • ちなみに、ビルドして出来上がるのはCocoaアプリケーションなのだが、今回はアプリケーションの本体は作らずに、コマンドとしてmain.mだけ利用している。
  • よって、コマンドを実行する時は、出来上がったdropshadow.appのパッケージを開いて、その中の contents/MacOS/dropshadow を実行するのだ。
$ cd ~/Desktop
$ ~/Library/Developer/Xcode/DerivedData/dropshadow-hftdjsrekvguksehmjnnhkpqfbcg/Build/Products/Debug/dropshadow.app/Contents/MacOS/dropshadow ファイル名
  • すると、こうなった。(影なしの画像をdropshadowコマンドで処理したもの)

やった!MiniShadowのMiniみたいな感じ。

  • 悩んだ部分は、画像の解像度が72dpiと144dpiの2つが混在しているとき、
  • Retina環境では72dpiの画像が2倍サイズの画像になってしまうこと。
  • ちなみに、画像解像度はsipsコマンドで確認できる。
$ sips -g all スクリーンショット 2012-11-13 04.17.25.png
  pixelWidth: 530
  pixelHeight: 534
  typeIdentifier: public.png
  format: png
  formatOptions: default
  dpiWidth: 143.990
  dpiHeight: 143.990
  samplesPerPixel: 4
  bitsPerSample: 8
  hasAlpha: yes
  space: RGB
  profile: Color LCD
  • dpiWidth、dpiHeightが143.990 ≒ 144 になっていれば、高解像度な画像。
  • NSImageはピクセル数でなく、解像度のポイント数しか返さないので、
  • 現実のピクセル数は、NSBitmapImageRepから取得する必要があった。
  • これにディスプレイのスケール値で割り算して、どちらの画像も高解像度な画像として表示するようにしてみた。

ファイル名を加工する

  • 影付き画像を常にoutput.pngとして出力するだけではあまりにも不便なので、
  • 画像ファイルと同じフォルダで、ファイル名に「-shadow」を付加して出力するようにしてみた。


int main(int argc, char *argv[])
{
// return NSApplicationMain(argc, (const char **)argv);

//ファイルパスをパースしておく
// NSString *aPath = @"~/a/b/c.d.e";
NSString *aPath = [NSString stringWithUTF8String:argv[1] ];
NSString *fPath = [aPath stringByStandardizingPath]; // /Users/HOME/a/b/c.d.e
NSString *fDir = [fPath stringByDeletingLastPathComponent]; // /Users/HOME/a/b
NSString *fNameExt = [fPath lastPathComponent]; // c.d.e
NSString *fExt = [fPath pathExtension]; // e
NSString *fDirName = [fPath stringByDeletingPathExtension]; // /Users/HOME/a/b/c.d
// NSLog(@"name.ext=%@ ext=%@ dir=%@ dir/name=%@", fNameExt, fExt, fDir, fDirName);
NSString *shadowPath = [ [fDirName stringByAppendingString:@"-shadow."] stringByAppendingString:fExt];

//影付きイメージを生成する
NSImage *image = [ [NSImage alloc] initWithContentsOfFile:fPath];
NSImage *shadowImage = dropshadowImage(image);
saveImageByPNG(shadowImage, shadowPath);

//画像情報を出力する
NSRect align = [shadowImage alignmentRect];
NSLog(@"%@ (%f, %f, %f, %f)", shadowPath, align.origin.x, align.origin.y, align.size.width, align.size.height);
return 0;
}

コマンドとして仕上げる

  • さらに、現状は引数に一つのファイルパスしかとれない。複数画像を一括して処理できないなんて、あまりにも不便である。
  • また、一つの決まった影の投影しかできないが、オプション指定によって影の投影条件を多少なりとも変更できた方が良いかもしれない。


//
// main.m
// dropshadow
//
// Created by zarigani on 2012/11/13.
// Copyright (c) 2012 bebe工房. All rights reserved.
//

#import <Cocoa/Cocoa.h>

//影付きイメージを描画して返す
NSImage* dropshadowImage(NSImage *image, float blurRadius, float alphaValue)
{
float margin = blurRadius * 1.25;
//スケールを取得する
CGFloat scale = [ [NSScreen mainScreen] backingScaleFactor];
//Retina環境に応じたポイントサイズを取得する
NSBitmapImageRep* imageRep = [NSBitmapImageRep imageRepWithData:[image TIFFRepresentation] ];
NSSize dotSize = NSMakeSize([imageRep pixelsWide]/scale, [imageRep pixelsHigh]/scale);
//描画する場所を準備
NSRect newRect = NSZeroRect;
newRect.size.width = dotSize.width + margin*2;
newRect.size.height = dotSize.height + margin*2;
NSImage *newImage = [ [NSImage alloc] initWithSize:newRect.size];
//描画する場所=newImageに狙いを定める、描画環境を保存しておく
[newImage lockFocus];
[NSGraphicsContext saveGraphicsState];
//拡大・縮小した時の補間品質の指定
[ [NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
//影の設定
NSShadow *shadow = [ [NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(0.0, -blurRadius * 0.25)];
[shadow setShadowBlurRadius:blurRadius];
[shadow setShadowColor:[ [NSColor blackColor] colorWithAlphaComponent:alphaValue] ];
[shadow set];
//描画する
NSRect drawRect;
drawRect.origin = NSMakePoint(margin, margin);
drawRect.size = dotSize;
[image drawInRect:drawRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//描画環境を元に戻す、描画する場所=newImageから狙いを外す
[NSGraphicsContext restoreGraphicsState];
[newImage unlockFocus];

return newImage;
}

//PNGファイルとして保存する
void saveImageByPNG(NSImage *image, NSString* fileName)
{
NSData *data = [image TIFFRepresentation];
NSBitmapImageRep* bitmapImageRep = [NSBitmapImageRep imageRepWithData:data];
NSDictionary* properties = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey:NSImageInterlaced];
data = [bitmapImageRep representationUsingType:NSPNGFileType properties:properties];
[data writeToFile:fileName atomically:YES];
}




int main(int argc, char *argv[])
{
// return NSApplicationMain(argc, (const char **)argv);

int opt, i;
float blurRadius = 8.0;
float alphaValue = 0.5;

//opterr = 0;/* エラーメッセージを非表示にする */

while((opt = getopt(argc, argv, "a:b:")) != -1){
switch(opt){
case 'a':
sscanf(optarg, "%f", &alphaValue);
printf(" Option -%c = %f\n", opt, alphaValue);
break;

case 'b':
sscanf(optarg, "%f", &blurRadius);
printf(" Option -%c = %f\n", opt, blurRadius);
break;

// 解析できないオプションが見つかった場合は「?」を返す
// オプション引数が不足している場合も「?」を返す
case '?':
printf("Unknown or required argument option -%c\n", optopt);
printf("Usage: COMMAND [-m | -e] [-s suffix] name ...\n");
return 1;
}
}

for(i = optind; i < argc; i++){
//ファイルパスをパースしておく
// NSString *aPath = @"~/a/b/c.d.e";
NSString *aPath = [NSString stringWithUTF8String:argv[i] ];
NSString *fPath = [aPath stringByStandardizingPath]; // /Users/HOME/a/b/c.d.e
NSString *fDir = [fPath stringByDeletingLastPathComponent]; // /Users/HOME/a/b
NSString *fNameExt = [fPath lastPathComponent]; // c.d.e
NSString *fExt = [fPath pathExtension]; // e
NSString *fDirName = [fPath stringByDeletingPathExtension]; // /Users/HOME/a/b/c.d
// NSLog(@"name.ext=%@ ext=%@ dir=%@ dir/name=%@", fNameExt, fExt, fDir, fDirName);
NSString *shadowPath = [ [fDirName stringByAppendingString:@"-shadow."] stringByAppendingString:fExt];

//影付きイメージを生成する
NSImage *image = [ [NSImage alloc] initWithContentsOfFile:fPath];
NSImage *shadowImage = dropshadowImage(image, blurRadius, alphaValue);
saveImageByPNG(shadowImage, shadowPath);

//画像情報を出力する
NSRect align = [shadowImage alignmentRect];
NSLog(@"%@ (%f, %f, %f, %f)", shadowPath, align.origin.x, align.origin.y, align.size.width, align.size.height);
}
return 0;
}

やった!影の調整もできるようになった!

デフォルト= dropshadow -a 0.5 -b 8 dropshadow -a 0.8 -b 50 dropshadow -b 2

ダウンロード&インストール&使い方

インストール
  • 上記URLからダウンロードしたファイルを解凍して、Xcodeで開いてビルドする。
  • Xcodeのprodcutsにdropshadow.appがあるので、二本指クリックしてFinderで表示する(Show in Finder)
  • dropshadow.appのパッケージの内容を表示して、contents/MacOS/dropshadowが目指すコマンド本体。
  • 好みのフォルダへ移動してインストール完了。
使い方
$ dropshadow file.png ...(複数ファイルを指定可能)
# dropshadow file.png ... = dropshadow -a 0.5 -b 8 file.png ...

$ dropshadow -a 0.8 file.png ...(複数ファイルを指定可能)
# 影の透明度(=alphaValue)を0.8にする(0 =< alphaValue =< 1)

$ dropshadow -b 50 file.png ...(複数ファイルを指定可能)
# 影のぼかし半径(=blurRadius)を50にする(0 =< blurRadius)