ブログの整理

ブログの見た目を変えてみたのだけど、色々とチェックしている際に気になったのが記事内にある(主に AppleScript の)ソースコード。

見にくいですよね。

もう少し見やすくならないものかと色々と探してみました。

コードの整形やカラーリングを行うには SyntaxHighlighter なるものが主流のようですね。

なんだけど、いまいちピンとこない。

他にないかなと思いつつ探していたら、Gist のコードをそのまま貼付けることができるらしい。試しに貼ってみる。

見やすいと思うのですが、どうでしょう?

導入も簡単だし、AppleScript Editor から直接 Gist に投稿、ソースコードのリンクを作成し取得するという一連の流れがすべて AppleScript で制御できる。

問題はそれなりのスクリプトならともかく、断片のようなスクリプトをどうするかって事だけど。

とりあえずは Gist との連携でいってみようか...。

画面共有を開始する一番簡単な方法

近くて遠い隣の部屋。そこに Mac はあるのに...。

そういう時の画面共有。便利ですね。

しかし、画面共有.app って /System/Library/CoreServices にあってアクセスするのが面倒だったりする。Dock にでも登録しておけばいいんでしょうけど、もう少し手軽にこいつを利用する方法はないものか、と。

実は AppleScript に対応していたりするんですね。画面共有.app って。

なら、話は簡単です。

まず、共有したい Mac で画面共有を有効にします。

「システム環境設定」の「共有」にある「画面共有」にチェックを入れ、共有を開始します。

このとき、コンピューターの名前を覚えてきます。これで画面共有が利用できるようになりました。

Screen_Sharing_Preferences

次に、呼び出し側の Mac で次のような Automator サービスを作成します。

Automator_Service

スクリプトは次のようなものです。

Script Editor で開く

tell application id "com.apple.ScreenSharing"
    activate
    -- 先ほど「共有」で覚えておいた相手側 Mac の名前を指定する
    GetURL "vnc://MacBook-late-2007.local" -- 適宜、書き換えてね
end tell

実行すると画面共有.app が起動し、無事に隣の部屋にある MacBook を操作することができるようになります。

最後に Automator のサービスを保存し、「システム環境設定」の「キーボード」にある「ショートカット」でサービスにショートカットキーを割り当てます。

Keyboard_Shortcut_Preferences

ここでは F5 のショートカットキーを割り当てています。

これでいつでも簡単に画面共有を起動させることができますね。

AppleScript で JSON を利用する

AppleScript なら JSON を簡単に利用できます。

そう、Marvericks ならね。

...冗談はさておき。最近では JSON でデータの交換ってことが多くなってきて、時代から置いてきぼりな感がある AppleScript です。JSON を AppleScript で利用できるようにする方法は既に幾つかあります。

前者は OSAX として提供されており、後者は AppleScript で解決するスクリプトです。

他方、OS X と iOS には NSJSONSerialization という JSON を扱うためのクラスが少し前から追加されています。このクラスを利用すれば AppleScript のリストやレコードを JSON に、逆に JSON を AppleScript のリストやレコードに変換することができます。

まず、NSJSONSerialization を利用するライブラリを作ります。

Script Editor で開く

property NSString : class "NSString"
property NSData : class "NSData"
property NSJSONSerialization : class "NSJSONSerialization"
property NSArray : class "NSArray"
property NSDictionary : class "NSDictionary"

on serialize(theText)
    -- AppleScript 文字列を NSString に変換
    set theText to NSString's stringWithString:theText
    -- NSString を NSData に変換
    set jsonData to theText's dataUsingEncoding:(current application's NSUnicodeStringEncoding)

    -- NSData に変換した JSON を パース
    set theResult to NSJSONSerialization's JSONObjectWithData:jsonData options:(current application's NSJSONReadingAllowFragments) |error|:(missing value)

    -- NSArray か NSDictionary で結果が返ってくる
    if (theResult's isKindOfClass:(NSDictionary's |class|)) as boolean then
        -- NSDictionary なら record に型変換
        return theResult as record
    else
        return theResult as list
    end if
end serialize

on deserialize(theObject)
    -- 渡されたデータが list か record か判別
    if (class of theObject) is list then
        -- list から NSArray に変換
        set theObject to NSArray's arrayWithArray:theObject
    else
        set theObject to NSDictionary's dictionaryWithDictionary:theObject
    end if

    if (NSJSONSerialization's isValidJSONObject:theObject) then
        -- JSON に変換できるなら
        set json to NSJSONSerialization's dataWithJSONObject:theObject options:(current application's NSJSONWritingPrettyPrinted) |error|:(missing value)
        -- 変換された NSData から NSString に変換
        set theText to NSString's alloc()'s initWithData:json encoding:(current application's NSUTF8StringEncoding)

        -- NSString を text に変換
        return theText as text
    else
        return missing value
    end if
end deserialize

このスクリプトをライブラリとして保存します(ライブラリとして保存する方法はこちらを参照)。

使い方はいたって簡単。

Script Editor で開く

-- record、list まじりの list
set theList to {"a", "b", {10, 20}, {age:20}}
tell script "ASJSON" to set json to deserialize(theList)
--> 結果
"[
  \"a\",
  \"b\",
  [
    10,
    20
  ],
  {
    \"age\" : 20
  }
]"

-- record 
set theRecord to {a:100, b:200, c:{10, 20}}
tell script "ASJSON" to set json to deserialize(theRecord)
--> 結果  
"{
  \"a\" : 100,
  \"b\" : 200,
  \"c\" : [
    10,
    20
 ]
}"

--> JSON を変換してみる
"{
  \"a\" : 100,
  \"b\" : 200,
  \"c\" : [
    10,
    20
  ],
  \"d\" : null,
  \"e\" : false,  
  \"f\" : 0.13
}"

set json to result
tell script "ASJSON" to set json to serialize(json)
--> 実数の値が...
--> {d:missing value, b:200, e:false, c:{10, 20}, a:100, f:0.129999995232}

実数の取り扱いに難があるものの、きれいに変換できます。実際の利用となると、エラーの処理(パースできなかったとき等)が必要になるものの、これで Web API を利用した AppleScript がより身近になりますね。

Welcome, Otto's Remote!!

Otto って Automator のあのロボット君の名前ですよね?

Automator アイコン

iOS デバイスから Mac のスクリプトを実行させる...。こういうアプリケーションがなかったため、一時期自分で作ってやろうかと本気で考えておりました。まあ、三日であきらめましたが。

そういうアプリケーションが存在するということを AS Hole さんの記事で知りました。

Otto's RemoteOtto's Antenna の導入や使い方、現状での問題点などはこの記事を参照していただくと一目瞭然。ここではこの記事で触れられていないことを取り上げます。

Otto's Remotex-callback-url に対応しているんですよね。これは地味にうれしい。現状では Automator とシェルには引数を(文字列として)渡すことができます。が、AppleScript には引数を渡すことができません。

しかし、x-callback-url を使うと AppleScript にも引数を渡すことができます。

以下のようなスクリプトを Mac 上に用意し、Otto's Antenna が利用するフォルダ(~/Library/Application Scripts/com.amolloy.ottosantenna/)に保存します。

Script Editor で開く

on run {arg}
    tell me
        activate
        display dialog arg
    end tell
end run

iOS デバイス上から以下の URL を開きます。

otto://x-callback-url/action?computer=[Mac の名前]&action=[スクリプト名]&argument1=[引数1]

computer と action、argument1 に渡す値は適宜変更してください。この URL を開くと Otto's Remote が起動し、指定したスクリプトに指定した引数を渡して実行されます。引数は複数でも大丈夫です。複数の場合は、

otto://x-callback-url/action?computer=[Mac の名前]&action=[スクリプト名]&argument1=[引数1]&argument2=[引数2]...

となります。引数は URL エンコードしておく必要があります。

AppleScript といっても現在ではシェルスクリプトとしても利用できるので、このようなことができます。x-callback-url を利用しなければいけないという制限はあるものの、逆に言えば利用範囲は広がります。

x-callback-url というのは iOS デバイス上でのアプリ間連携に関するプロトコルですので、なんらかのアプリで利用したデータを Otto's Remoteに渡し、Mac 上の AppleScript で処理ということができるようになります。

ちょっとした例として、現在地を Mac 上のファイルに追記して行くスクリプトを。

iOS 上で簡単に現在地を取得できるようなアプリがあればいいのですが、ここでは Pythonista というアプリを利用します。

このアプリは iOS 上で Python が実行できるアプリで、特筆すべきは iOS 上のいろんな機能(アドレス帳、通知センター、GPS、クリップボード、写真等々...)にアクセスすることができることです。

URL スキームも持っているので作成したスクリプトをいろんなところから実行することができます。ちょっと準備が手間ですが、やってみましょう。

まず、Mac 上に次のようなスクリプトを用意します。

Script Editor で開く

property filename : "GPS_LOG.txt"
property directory : path to desktop folder as text

on run {lat, long}
    set theDate to (current date) as text

    set theText to theDate & "," & (lat as text) & "," & (long as text)

    try
        set fh to open for access file (directory & filename) with write permission
        if (get eof fh) is 0 then
            write theText to fh as «class utf8»
        else
            set eof fh to (get eof fh) + 1
            set theText to (string id 10) & theText
            write theText to fh as «class utf8» starting at eof
        end if
        close access fh
    on error
        close access fh
    end try

    display notification "GPS を追記しました" with title "GPS"
    delay 1
end run

これを GPS という名前で保存しておきます。

次に iOS デバイス上の Pythonista で次のようなスクリプトを作成します。

import location
import time
import webbrowser

url = 'otto://x-callback-url/action?computer=[YOUR MAC NAME]&action=GPS&argument1='

location.start_updates()
time.sleep(2)
current = location.get_location()
location.stop_updates()

url = url + str(current['latitude']) + '&argument2=' + str(current['longitude'])

webbrowser.open(url)

URL の Mac の名前は適宜変更してください。

最後に Launch Center Pro のようなランチャーアプリで URL スキームを登録。

pythonista://GPS?action=run

実行すると Pythonista が起動し、スクリプトが実行され、Otto's Remote が起動し、現在地を Mac 上のスクリプトに送信し、ファイルに経度と緯度を追記します。

単純に位置情報が欲しいだけなら他にも方法があるでしょうけど...。

AppleScript の実行結果を iOS デバイス上で受け取ることはできませんが、これはどうとでもなる問題でしょう。結果をメモ.app を使って iCloud で同期とか、Dropbox を利用する、メールや SMS でもいいでしょう。

このように iOS デバイス上のアプリを組み合わせて Mac 上の AppleScript を動かす...。なんだか、妄想が広がりますね。

Mavericks の Finder タグをスクリプトから操作

Mavericks の新機能の一つに『タグ』があります。

ファイルやフォルダに付加することができ、OS 全体で利用できます。タグの使い方は「[K]【マニュアル】ファイル管理が超捗る!Appleの新OS『OS X Mavericks』のFinderタグ機能の使い方 - Knowledge Colors」が分かりやすかったです。

Finder にあったラベルが拡張されたようですが、AppleScript からは取得も設定もできないようです。

Finder のラベルは AppleScript で操作できるんですが...。

ちょっと調べたところ、NSURL の NSURLTagNamesKey で設定されているタグは取得できるんですね。なら、ASOC ライブラリで作ってしまいましょう。

新しく AppleScript/Objective-C ライブラリスクリプトを作成し、以下のように記述。

Script Editor で開く

property NSURL : class "NSURL"

on tags(thisFile)
    (*
        タグをリストで取得する
    *)
    set fileURL to NSURL's fileURLWithPath:thisFile
    set dict to fileURL's resourceValuesForKeys:{current application's NSURLTagNamesKey} |error|:(missing value)
    set tagList to dict's objectForKey:"NSURLTagNamesKey"
    if tagList is missing value then return missing value
    return (tagList as list)
end tags

次のようにして呼び出します。

Script Editor で開く

set theFile to choose file

tell script "Tags"
    tags(POSIX path of theFile)
end tell

タグがない場合は missing value が返ります。

では、設定はどうでしょうか。

タグの設定は NSURL の setResourceValue:forKey:error: メソッドで NSURLTagNamesKey を指定して設定できます。

Script Editor で開く

on setTags(tagList, thisFile)
    (*
        現在付けられているタグを上書き
    *)
    set fileURL to NSURL's fileURLWithPath:thisFile
    fileURL's setResourceValue:tagList forKey:(current application's NSURLTagNamesKey) |error|:(missing value)
end setTags

使ってみます。

Script Editor で開く

set theFile to choose file

tell script "Tags"
    setTags({"オレンジ", "レッド"}, POSIX path of theFile)
end tell

これもできました...が、既に設定しているタグを上書きするようです。設定しているタグをそのままに、新しく追加するには次のようにします。

Script Editor で開く

on addTags(tagList, thisFile)
    (*
        現在付けられているタグに追加
    *)
    set currentTags to tags(thisFile)
    if currentTags is missing value then set currentTags to {}
    repeat with i from 1 to count tagList
        set tag to item i of tagList
        if currentTags does not contain tag then
            set currentTags to currentTags & tag
        end if
    end repeat

    setTags(currentTags, thisFile)
end addTags

現在のタグに含まれないタグだけを新しいタグのリストから追加しています。これで追加も可能になりました。

ASOC を使ったライブラリですのでちょっと手間がかかりますが、これでタグを自由自在。ま、そのうち AppleScript のみで利用できるようになるのでしょうが、それまでの代替です。

AppleScript + 顔認識

Core Image のフィルタを利用すれば、画像に最近流行のトイカメラのようなエフェクトを簡単に施せるようになります。簡単な利用方法は「AppleScript + Core Image」で触れました。今度は Core Image の顔検出を利用してみましょう。

「AppleScript + Core Image」で利用した CoreImage.scptd ライブラリを拡張します。

このファイルを ~/Library/Script Libraries に保存しておいてください。

顔の検出は CIDetector で行います。 CoreImage.scptd の property に CIDetector を追加します。

property CIDetector : class "CIDetector"

顔の検出の手順は

  1. 検出器を作成/設定
  2. 検出器に画像を渡す

検出器は検出器の種類といくつかのオプションを指定して作成します。検出器の種類は現在のところ「人の顔」だけです。オプションには検出器の精度があります。精度は CIDetectorAccuracyLow と CIDetectorAccuracyHigh がありますが、前者は速度重視、後者は速度を犠牲にして正確さ重視になっています。

iOS ではどうか分からないのですが、どちらでも違いがあまり分かりませんでした...。

では作ってみましょう。

Script Editor で開く

on faceDetect(imageRef)
    -- 検出器のオプションを NSDictonary で作成
    set opts to {CIDetectorAccuracy:(current application's CIDetectorAccuracyHigh)}
    -- 検出器をオプションとタイプを指定して作成
    set detector to CIDetector's detectorOfType:(current application's CIDetectorTypeFace) context:(missing value) options:opts

    -- 顔の検出を行う際のオプションを NSDictonary で作成
    set opts to {CIDetectorImageOrientation:(imageRef's |properties|()'s valueForKey:"Orientation")}

    -- 顔検出を実行
    set faceList to detector's featuresInImage:imageRef options:opts

    -- 検出された顔の位置とサイズをログに出力
    repeat with i from 1 to count faceList
        set face to item i of faceList
        log ((i as text) & ": 検出")
        log (face's |bounds|())
    end repeat

    return faceList
end faceDetect

検出器を作成し、そのまま顔検出を行っています。検出は CIDetector の featuresInImage:options: メソッドで行います。このときに指定するオプションは画像の向き、まばたきの検出、笑顔の検出が指定できます。大事なのは画像の向きでこれが正しくないと検出が正確に行えません。

まばたきと笑顔は OS X Mavericks 以降でしか利用できません。

AppleScript としての注意点ですが、本来ならオプションは NSDictionary で作成します。が、ここでは AppleScript の record で代用しています。互換性がある...というわけではないのですが、代替として利用できます。

これは NSArray と AppleScript の list でも同様で、上記でも NSArray として返ってくる検出の結果を AppleScript 的に繰り返しで処理しています。

ここまでで顔検出が行えますのでいったんテストしてみましょう。テストで利用するのは以下の画像です(photo by pakutaso.com )。

元の画像

ちょっと時期外れのサンタさんです。

Script Editor で開く

on run
    set inputFile to choose file of type {"public.jpeg"}

    tell script "CoreImage"
        set imageRef to openImageFile(POSIX path of inputFile)
        set faceList to faceDetect(imageRef)
    end tell
end run

顔が写っている画像を選択して実行してみてください。顔が検出されれば、AppleScript Editor のログに結果が表示されます。

検出結果

うまく検出できているでしょうか?

顔の範囲が分かったら顔だけを切り抜いたり、様々なフィルタをかけることができます。

試しに Core Image Programming Guide にある Anonymous Faces Filter Recipe を AppleScript に移植してみましょう。このサンプルは検出された顔に対してモザイクをかけるものです。引き続き、先ほどのサンタさんの画像を使います。

やることは以下の通り。

  1. オリジナルの画像をモザイクにする
  2. 検出した顔の部分以外を黒く塗りつぶしたマスク画像を作る
  3. オリジナルの画像、モザイクをかけた画像、マスク画像を重ね合わせる

まず、新しく利用するクラスを追加します。

property CIColor : class "CIColor"
property CIVector : class "CIVector"

そして、以下のハンドラを追加。

Script Editor で開く

on max(a, b)
    -- 大きい方の値を返す
    if a > b then return a
    return b
end max

on min(a, b)
    -- 小さい方の値を返す
    if a < b then return a
    return b
end min

on faceMask(imageRef)
    -- 顔の部分のマスクを作成
    set faceList to faceDetect(imageRef)
    if (count faceList) is 0 then return missing value

    -- 顔の範囲を緑の円で、それ以外を黒で塗りつぶす   
    set maskImage to missing value
    repeat with face in faceList
        set centerX to (face's |bounds|()'s origin's x) + ((face's |bounds|()'s |size|'s width) / 2.0)
        set centerY to (face's |bounds|()'s origin's y) + ((face's |bounds|()'s |size|'s height) / 2.0)
        set radius to (min(face's |bounds|()'s |size|'s width, face's |bounds|()'s |size|'s height)) / 1.5

        set faceCenter to current application's NSMakePoint(centerX, centerY)

        set radialGradient to filterWithName("CIRadialGradient")
        (radialGradient's setValue:radius forKey:"inputRadius0")
        (radialGradient's setValue:(radius + 1.0) forKey:"inputRadius1")
        (radialGradient's setValue:(CIColor's colorWithRed:0.0 green:1.0 blue:0.0 alpha:1.0) forKey:"inputColor0")
        (radialGradient's setValue:(CIColor's colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0) forKey:"inputColor1")
        (radialGradient's setValue:(CIVector's vectorWithCGPoint:faceCenter) forKey:"inputCenter")

        set circleImage to (radialGradient's valueForKey:"outputImage")

        if (missing value is maskImage) then
            copy circleImage to maskImage
        else
            -- 複数の顔が検出されたら作成したマスク画像を重ね合わせる
            set filter to filterWithName("CILightenBlendMode")
            (filter's setValue:circleImage forKey:"inputImage")
            (filter's setValue:maskImage forKey:"inputBackgroundImage")
            set maskImage to (filter's valueForKey:"outputImage")
        end if
    end repeat

    return maskImage
end faceMask

on anonymousFacesFilter(imageFile)
    (* 顔にモザイクをかけた画像を作成する *)
    set imageRef to my openImageFile(imageFile)

    set imageRect to imageRef's extent()
    set {w, h} to {imageRect's |size|'s width, imageRect's |size|'s height}

    -- モザイク画像を作る
    set pixellate to filterWithName("CIPixellate")
    pixellate's setValue:imageRef forKey:"inputImage"
    pixellate's setValue:((max(w, h)) / 60) forKey:"inputScale"

    -- 顔だけを除外するマスク画像を作成する
    set maskImage to faceMask(imageRef)

    -- CIBlendWithMask フィルタでモザイクとオリジナル、マスクを重ねる
    set blend to filterWithName("CIBlendWithMask")
    blend's setValue:(pixellate's valueForKey:"outputImage") forKey:"inputImage"
    blend's setValue:imageRef forKey:"inputBackgroundImage"
    blend's setValue:maskImage forKey:"inputMaskImage"

    return blend's valueForKey:"outputImage"
end anonymousFacesFilter

ライブラリを保存したら、次のように利用します。

Script Editor で開く

on run
    set inputFile to choose file of type {"public.jpeg"}

    tell script "CoreImage"
        set imageRef to anonymousFacesFilter(POSIX path of inputFile)
        set outputFile to POSIX path of (choose file name default name "Untitled.jpg")
        saveAsJPEG(imageRef, outputFile)
    end tell
end run

結果は以下のように顔の部分だけモザイクになっています。

スクリプト実行後の画像

CIFilter には他にも色々なフィルタが用意されています。また、CIImage には property メソッドがあり、画像のメタデータにアクセスすることも可能です。GPS を読み取って Map アプリでその場所を開くなんてこともできます。こういったことは Image Events 単体ではできないことなので重宝するのではないかと。

AppleScript + Core Image

Cocoaフレームワークを簡単に AppleScript で利用できるようになった...のは確かなんですが、実際のコードがなきゃもう一つ理解が進みませんし、利用もされません。

AppleScript に新機能が追加されるのはいいのですが、いつもこのあたりの情報が少なすぎていまいち浸透しない。

ってなわけで色々と公開してみます。実際の利用例ってやつを、ね。

簡単に効果が分かるってことで Core Image を使ってみます。Mac OS X 10.4 の頃に鳴り物入りで導入されましたね。GPU を使った高速な画像処理がウリです。AppleScript で Core Image を使うことで Image Events 以上の画像処理が可能になります。

Core Image の分かりやすい解説は Apple のものだったりします。英語なんですがね。

では、使ってみましょう。

iOS 7 に新しく追加された カメラフィルタがあります。これらは Core Image のフィルタとして提供されています。

iPhone カメラフィルタ一覧

もちろん、これらのフィルタは iOS だけではなく、OS X でも利用できます。手順としては次のようになります。

  1. 画像ファイルを開く
  2. フィルタを生成
  3. 画像にフィルタを適用
  4. 適用された画像を保存

それぞれ見ていきます。

画像ファイルを開く

扱う画像ファイルは JPEG のみとします。

とりあえず、新規スクリプトファイルを作ります。作成したら以下のように記述し、スクリプトバンドル形式で ~/Library/Script Libraries に CoreImage.scptd という名前で保存します。

Script Editor で開く

use framework "QuartzCore"

property CIImage : class "CIImage"

property NSString : class "NSString"
property NSURL : class "NSURL"

保存します。

スクリプト保存ダイアログ

保存したら「バンドルの内容」を表示し、「AppleScript/Objective-C ライブラリ」にチェックを入れます。

ライブラリとして利用するため「AppleScript/Objective-C ライブラリ」をチェック

再度、保存します。これでライブラリとして活用できるようになりました。では、説明。

まず、Core Image を使うので Core Image を含むフレームワークの QuartzCore.framework を取り込んでいます。Objective-C では

import <QuartzCore/CoreImage.h>

で CoreImage だけを取り込めますが、どうもそういうことができないようで QuartzCore.framework 全てを取り込んでいます。

property として CIImage、NSString、NSURL を定義していますが、これは単純に

  • タイプを短くするため
  • スクリプトを少しでも見やすくするため

です。別名を付けてもいいのですが、分かりやすい方がいいでしょう。

では、AppleScript から渡した画像ファイルを CIImage として開きます。

Script Editor で開く

on openImageFile(imageFile)
    -- imageFile: POSIX path 形式のファイルパス

    -- AppleScript text を NSString に変換
    set imageFile to NSString's stringWithString:(imageFile)

    -- ファイルパスを NSURL(file://)に変換
    set fileURL to NSURL's fileURLWithPath:imageFile

    -- CIImage を生成
    return CIImage's alloc()'s initWithContentsOfURL:fileURL
end openImageFile

なんでいちいち NSString に変換しているのか?

変換しなくてもいきなり NSURL に AppleScript の text クラスを渡しても大丈夫なようなのですが、念のためです。基本的には AppleScriptObjC には Cocoa - AppleScript での型の自動変換といった機能はありません。そのため両者では明示的に型を変換しておいた方が変なエラーで悩まなくてすみます。

CIImage を生成するメソッドはいくつかありますが、ここでは initWithContentsOfURL: を使っています。ここで気になるのは『init で作ったオブジェクトの解放は誰が担うんだ』ということですが、多分、気にしなくてもいいと思います。

ちょっと調べきれていないのですが、ASOC は ARC を採用しています。いつの間にか GC じゃなくなっていました。確か、登場当初は GC だったと思うのですが、ARC が導入されたあたりで ASOC も ARC になったんだと思います。

なので、参照されなくなった時点で解放されると思います。ほんまかいな?

AppleScriptObjC でのメモリ管理に関する情報が少なすぎてなんともいえないのですが、少なくともやってはいけないことは分かっています。それは、明示的にオブジェクトを release することです。一発で落ちるということはありませんが、そのうち AppleScript Editor が落ちます。

ARC については以下のサイトを。

ARC が採用されているからといって ARC が AppleScript で利用できるというわけではありません。あくまで AppleScript から利用している Cocoa のオブジェクトが ARC で管理されているということです。

AppleScriptObjC のメモリ管理に関しては以下を。

これぐらいしか情報がない...。OS X Mavericks 以前は、また異なるんですけどね、状況が...。

画像ファイルから CIImage が生成できたので次は ファイルタを作成します。

フィルタを生成

Core Image ではフィルタの入力に画像(または動画)を渡し、出力でフィルタが適用された画像を取り出します。基本的には、

  1. フィルタに画像を渡す
  2. フィルタの設定値を変更する
  3. フィルタから画像を取り出す
  4. 取り出した画像を次の画像に渡す(1 に戻る)...

これらの繰り返しで処理が行われます。

フィルタは CIFilter を利用します。ライブラリスクリプトの先頭の property に CIFilter を追加します。

(* 前略 *)
....
property CIImage : class "CIImage"
property CIFilter : class "CIFilter"
...
(* 後略 *)

CIFilter は「フィルタの名前を指定して生成」と「フィルタの名前と入力値を同時に指定して生成」する 2 種類の方法があります。後者はスクリプトが読みにくくなるので前者を採用します。

Script Editor で開く

on filterWithName(filterName)
    -- CIFilter をフィルタの名前で生成
    set filter to CIFilter's filterWithName:filterName
    -- フィルタの設定を初期値に
    filter's setDefaults()

    return filter
end filterWithName

このハンドラをライブラリスクリプトに追加します。

フィルタを生成した後、フィルタの設定値をデフォルトにしています。iOS では必要ないのですが、OS X では必須です。

フィルタにどんな種類があるかは以下を参照。

iOS より OS X の方が利用できるフィルタは多いです。これでフィルタも生成できるようになったので画像に適用してみましょう。

画像にフィルタを適用

iOS 7 のカメラで利用できるフィルタは以下の通りです。

  • モノ(CIPhotoEffectMono)
  • 色調(CIPhotoEffectTonal)
  • ノアール(CIPhotoEffectNoir)
  • フェード(CIPhotoEffectFade)
  • クローム(CIPhotoEffectChrome)
  • プロセス(CIPhotoEffectProcess)
  • トランスファー(CIPhotoEffectTransfer)
  • インスタント(CIPhotoEffectInstant)

括弧内は CIFiltet を生成するときに必要なフィルタの名前です。

ライブラリスクリプトにフィルタに画像を渡し、フィルタを適用し、画像を出力するハンドラを追加します。

Script Editor で開く

on applyEffect(imageFile, photoFilterName)
    -- CIImage を生成
    set imageRef to my openImageFile(imageFile)

    -- CIFiter を生成
    set filter to my filterWithName(photoFilterName)

    -- フィルタの設定値を入力
    filter's setValue:imageRef forKey:"inputImage"

    -- フィルタが適用された画像を取得
    return filter's valueForKey:"outputImage"
end applyEffect

CIFilter には色々な入力値があります。これらの値を変更することで画像に色々な処理を施すことができます。今回利用するカメラ用のフィルタには画像以外の入力値がありません。

フィルタが適用された画像は CIFilter の outputImage メソッドで取り出すことができます。

最後にこの画像をファイルに保存します。

適用された画像を保存

スクリプトライブラリでは画面への描画が行えません。Xcode で ASOC アプリケーションを作れば話は別ですが、ウィンドウやビューがない AppleScript Editor で作る ASOC アプリケーションや AppleScript のライブラリでは直接ディスプレイ上に画像を描画できません(方法はないわけではないですが、それなら Xcode で ASOC アプリケーションを作った方が手っ取り早い)。

NSBitmapImageRep は CIImage から生成するメソッドがあるのでこれを利用し、ファイルに保存します。

Script Editor で開く

on saveAsJPEG(imageRef, theFile)
    -- NSBitmapImageRep を CIImage から生成
    set rep to NSBitmapImageRep's alloc()'s initWithCIImage:imageRef

    -- NSBitmapImageRep から JPEG データを取得
    set jpegData to rep's representationUsingType:(current application's NSJPEGFileType) |properties|:(missing value)

    -- ファイルに保存
    jpegData's writeToFile:theFile atomically:true
end saveAsJPEG

このハンドラをライブラリスクリプトに追加します。また property に NSBitmapImageRep も追加します。

(* 前略 *)
....
property NSString : class "NSString"
property NSURL : class "NSURL"
property NSBitmapImageRep : class "NSBitmapImageRep"
...
(* 後略 *)

JPEG 決めうちですが、NSBitmapImageRep は他の画像形式も扱えます。また、このハンドラではもとの画像が持っている情報(Exif や位置情報)などは失われます。元の画像に上書きするのではないので問題はないと思いますが。

これで必要なハンドラが揃いました。最後にこれを利用するスクリプトを作ります。

クライアントスクリプト

Script Editor で開く

on run
    set inputFile to choose file of type {"public.jpeg"} with prompt "フィルタを適用する画像を選んでください"
    set outputFile to choose file name with prompt "保存先:" default name "Output.jpg"

    tell script "CoreImage"
        set imageRef to applyEffect(POSIX path of inputFile, "CIPhotoEffectFade")
        saveAsJPEG(imageRef, POSIX path of outputFile)
    end tell
end run

選択した画像にフェードフィルタを適用しています。実行すると以下のようになります。

フィルタ適用前/適用後

ちょっと画像が小さくて分かりにくいですが...。

スクリプトを書くのが嫌だというナマケモノには、これらの機能を簡単に試せるようなスクリプトをご用意しました。

バンドル形式になっています。バンドルの中にライブラリを入れています。スクリプトメニューに登録、もしくはそのまま開いて AppleScript Editor で実行してみてください。