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 で実行してみてください。

AppleScript の新機能 (7) - まとめ

Mavericks というか、AppleScript 2.3 の主な変更点をまとめてきました。

これら以外にも変更点はあります。それらについて、補足...のようなまとめ書き。

全く触れなかった変更点として、

  • iCloud のサポート
  • 通知センターが利用できるように
  • Speakable-Workflow

があります。

iCloud のサポート

これは、AppleScript の書類を複数の Mac 間で共有できるってことなんですが、まだ、使ったことがない...。使ったことがないので、不具合にも遭遇していない。あると便利かなと思うのですが、AppleScript Editor 起動時のファイルオープンダイアログの表示はやめてほしいな...。

通知センター

スクリプト内から処理の経過などを通知センターで表示できるようになりました。というか、そういう命令が新しく追加されました。世の中ではこれが一番話題になっているようですね。通知センターを切ってしまっている身にはそれほど関係がない...。

スクリプトのデバック時に役に立つかな。

使い方は専用の命令を呼び出すだけ。

Script Editor で開く

display notification "通知したい本文"

オプションがいくつかあってタイトルとサブタイトル、通知時の音を指定することができます。

Script Editor で開く

display notification "通知したい本文" with title "タイトル" subtitle "サブタイトル" sound name "Glass"

ここで指定する通知音は、システム環境設定の「サウンド」に表示される音声ファイルの名前です。

System_Prefs_Sounds.png

言い換えるなら /System/Library/Sounds 以下のファイル名です。もちろん、ユーザーが追加したものも利用できます。display notification で表示される通知の設定はシステム環境設定の「通知」で変更できます。

System_Prefs_Notifications.png

ここでは AppleScript Editor が表示されていますが、display notification を利用する(した)アプレットやドロプレットはここの「通知センターで表示する項目」に追加されるので、個別に通知に関する設定が行えます。

利用するときの注意点としては、通知をスクリプトの最後に出すときは『通知を出す前にスクリプトが終了してしまうので、スクリプトの終了前にウェイトをおいてください』ってことです。具体的には、

display notification "通知です"
delay 1 -- 通知を出す前にスクリプトが終了してしまうので、1 秒停止

このような感じです。アプレットやドロップレットとしてスクリプトを保存して利用するときに必要な処理です。通知を OS に登録する以前にアプレットが終了してしまうんですね。ちょっとかっこわるいぞ。

また通知で表示されるウィンドウ(?)はクリックすることで通知を出したアプリケーションを全面に表示させることができます。

これを利用し、

  1. なんらかの処理
  2. 通知を表示
  3. 通知をクリック
  4. 再度、処理を実行

と、連続してスクリプトを実行させることができます。同じ処理を何度も繰り返すスクリプトってどんな状況か思い浮かばないのですが、通知センターから起動できるってのはちょっと便利かも。

Speakable-Workflow

これは Automator の話なのですが、Automator で作ったワークフローで Speakable Item を作ることができるようです。いったいなんのことか分からない人の方が多いような気がしますが。自分で作ったスクリプトやワークフローを音声認識を使って起動させることができます。話しかけて Mac を操作できる...と。ただし、英語に限る。

もともと、Mac には音声認識で操作するための機能は入っていました。システム環境設定の「アクセシビリティ」にある「音声認識コマンド」がそれです。

System_Prefs_Accessibility_Speech_Recognition.png

「入」にすると小さいウィンドウが表示されます。

Speech_ Recognition.png

「esc」キーを押しながら話しかけることで処理が行われます。例えば、「Switch to iTunes」などと話しかけると iTunes が前面に表示されます。

話しかけるといってもなんでもいいわけではなく、内蔵されている「コマンド」を発話しなければいけません。このコマンドは「音声コマンドウィンドウ」を表示させることで調べることができます。

Show_Speech_Commands_Window.png

標準の状態でいくつかのコマンドが入っているわけですが、ここにはない処理を行いたいとき『Automator のワークフローでも作ることができますよ』というのが、今回の新機能になります(AppleScript のスクリプトでも可能。これはかなり昔からできるようになっていました)。

コマンドは ~/Library/Speech/Speakable Items に保管されています(音声認識コマンドを利用したことがなければ、このフォルダはありません)。コマンド自体はプロパティリスト形式のテキストファイルで作成することができます(いつのまにかそうなっていたんですね)。

Automator や AppleScript で作ったコマンドもここに保管しておけば音声認識コマンドで利用することができます。ファイル名は英語で...例えば、「Go to Home Folder.scpt」などと保存しておきます。このファイル名がコマンド名になりますので、話しかけるような英語にしておくと音声認識しやすくていいです。単語一つだけだとなかなか認識されにくかったりしますし、他の似たようなコマンドと間違われてしまいます。

使いこなせれば有用で便利には違いないのですが、

  • 英語しか認識できない
  • その英語もネイティブじゃないと認識されにくい
  • その姿は冷静に考えるとけっこう滑稽

といった欠点があるため、あまり陽の目をみない機能になっています。せっかく専用のキャラクターもいたのにね。

OS_9_Speech_Recognition.jpg

その他

あえて、触れなかった部分もいくつかあります。例えば、ライブラリの用語説明の作り方。これは時間がかかるのでまたいつか、暇があれば...。

Script Library のインストール場所は 4 カ所ありますが、これも触れていませんでした。

  1. ~/Library/Script Libraries
  2. /Library/Script Libraries
  3. アプレット内の Resouces/Script Libraries
  4. アプリケーション内の Resouces/Script Libraries

アプレットやアプリケーション内におさめておけば、別にインストールしてもらう必要がないので便利かもしれません。また、これらの中に入れておくとライブラリはそのアプレット(アプリケーション)実行時のみライブラリが有効になります(言い換えると、他のスクリプトなどからは利用できない)。

コード署名についてですが、触れなかったことの中に『コード署名されたアプレット(アプリケーション)の実行時にはインターネットの接続が必要』というものがあります。これは、アプレット(アプリケーション)が有効なものかどうかを Apple に問い合わせるためです。なんで、触れなかったかというと検証していないからです。

接続されていないと起動時にエラーになりますので、頭の片隅にでもおいときましょう。

今回のバージョンアップにより、AppleScript Language Guide もアップデートされています。主に Script Libraries 関連情報の追加です。

script クラスのリファレンスを見てもらうと分かるように、id、name、version 属性が追加されています。

AppleScript Langage Guide と合わせて AppleScript Release Notes: 10.9 も公開されています。

こんなところかな?

まぁ、色々な不具合については触れてませんがね。

AppleScript の新機能 (6) - GUI Scripting

もうそろそろ新機能の話題は飽きてきた...。これで最後にしたいものです。

AppleScript のいざという時の奥の手。GUI Scripting。ここにもセキュリティの魔の手がのびてきました...。

いや。悪いことではないんですけどね。

GUI Scripting の使用/不使用は、従来はシステム環境設定の「アクセシビリティ」で一括して管理されていました。

これが OS X Mavericks では「セキュリティとプライバシー」の「プライバシー」タブにある「アクセシビリティ」で管理されるようになりました。

このため Mavericks 以前では使用を許可すると全てのアプリケーションでの使用が可能でしたが、Mavericks 以後ではアプリケーションごとに個別に使用を許可するように変更されました。

...困ったものです。以前まではアプレットの起動時に GUI Scripting が許可されているかどうかを調べ、許可されていなかったらアプレットから許可を行うことができたのですが、今後はこの方法が使えないようになったのです。

具体的には次のような処理はできません。

Script Editor で開く

(* スクリプト初期化処理... *)

tell application "System Events"
    set bool to UI elements enabled
    if not bool then
        -- GUI Scripting が利用できないなら
        set UI elements enabled to true
    end if
end tell

(* 以降の処理 *)

System Events の System Events Suite の application クラスにあった UI Elements Enabled 属性は、Process Suite の application クラスの属性に変更されています。かといって、各アプリケーション毎の状況を調べられるわけではなく、読み出しのみの属性になっています。

現状、起動時に UI Elements enabled を設定するようなアプレットやアプリケーションはその処理の時点で、処理が終了します(UI Elements enabled 属性が読み出しのみになっているのでエラーになる)。

今後は、アプレットの起動時などにシステム環境設定を開いて、アプレットやアプリケーションごとに『手動』でアクセシビリティに追加してもらう必要があります。

AppleScript にかぎったことではなく、アクセシビリティにアクセスするようなアプリケーションは(許可しない限り)軒並み同じ目に遭っているので仕方はないのですが...。

また、アプリケーション単位での許可ですので、例えば、AppleScript Editor で作っていたスクリプトをスクリプトメニューから実行させると、実行できなかったりします。AppleScript Editor だけをアクセスビリティに追加してもだめなのです。

スクリプトを利用するアプリケーション、ここでは System UI Server も追加する必要があります。もし、Terminal から osascript でスクリプトを利用するなら、Terminal も追加する必要があります。これは、Automator で作ったアプリケーションでも同様です(もちろん、アクセシビリティに関連する操作をするものだけですが)。

で、問題は AppleScript で作られたアプレットやドロップレットです。これらは毎回利用の許可を求められることがあります。なぜかというと、ここに詳しいです(やっと、日本語訳されました)。

簡単にいうと『property の値が毎回変わるため、同一のアプリケーションとして認識されない』ためです。ここにはこの問題の解決法が書かれているのですが...、まぁ、書かれているようにセキュリティの脆弱性を招くため『自分で利用するもの』だけに適用するのがいいでしょうし、でも、なるべく使わない方がいいかもしれません。

最後に。アクセシビリティに関する Terminal コマンドに tccutil というものがあります。

これはアクセシビリティにアクセスするアプリケーションの一覧をリセットすることができます(要管理者権限)。例えば、以下のようにすると『連絡先』にアクセスするアプリケーションをリセットすることができます。

$ tccutil reset AddressBook

アクセスビリティなら、

$ tccutil reset Accessibility

となります。

現状ではリセットのみ利用できます。その他の操作はできないのですが、アップデートで機能が追加されたりするのかな?