Pages 2

iWork '06の Pages 2 が面白い。

Pages 2 は、日本語環境に対応していない...という批判(?)はよく聞くけど、なんの。横書きしかできなくてもルビが使えなくても、数式を埋め込むのが面倒でも、それでも個人の環境で便利に使える場面というのは多々ある。

ちょっと Web 見ただけでもなんとたくさんのテンプレートが無料で配布されていることか。CanonEpson のサイトにいってみると横書きでもいいんだ、ということがよく分かる。なにかを紙に印刷するのが目的であれば、工夫次第で様々なことができる。Pages 2 があれば、もう無料のテンプレートいらないや。はがきだって(横書きだけど)できるんだぞぅ。

と、少々ワケの分からないことを宣いましたが。Pages 2 から AppleScript に対応しているんですね。まだまだできないことも多いけど。

ということで Pages を AppleScript で操ってカレンダーなんかを作ってみました。いろんな部分で手を抜いていますが、Pages を持っているならぜひ、お試しください。面白いですから(なんか変なことが起きても責任は持てませんが)。

Script Editor で開く

using terms from application "Pages"
    -- for A4 size
    property paperWidth : 21.0
    property paperHeight : 29.7
    property cellWidth : 2.8
    property cellHeight : 2.4

    property topMargin : 1.0
    property bottomMargin : 1.5
    property leftMargin : 0.5
    property rightMargin : 0.5

    -- calendar cells props
    property strokeWidth : 0.15
    property strokeColor : {26214, 26214, 26214}
    property textFont : "Georgia"
    property textSize : 14.0
    property textAlignment : right

    -- weekday cells props
    property wCellWidth : cellWidth
    property wCellHeight : 1.5
    property wTextFont : "Optima-Bold"
    property wTextSize : 18.0
    property wTextAlignment : center

    -- month cells props
    property mCellWidth : cellWidth * 7
    property mCellHeight : 2.0
    property mTextFont : "Optima-ExtraBlack"
    property mTextSize : 40.0
    property mTextAlignment : center
    property mTextColor : {46003, 46003, 46003}
    property mTextTracking : 3.0

    -- other props
    property weekdayList : {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
end using terms from

set theDate to "2006 4 1" -- for debug
set theDate to date theDate -- for debug
set theDate to current date

tell application "Pages"
    set ruler units to centimeters

    set theDocument to make new document
    tell theDocument
        set top margin to topMargin
        set bottom margin to bottomMargin
        set left margin to leftMargin
        set right margin to rightMargin

        -- 印刷可能領域の算出
        set contentsWidth to paperWidth - (right margin + left margin)
        set contentsHeight to paperHeight - (top margin + bottom margin)

        set tmp to my getCalendar(theDate)
        set dateList to {}
        repeat with thisItem in tmp
            set thisItem to contents of thisItem
            if thisItem is not {} then set end of dateList to thisItem
        end repeat

        -- カレンダーの出来上がりサイズ
        set calendarWidth to cellWidth * 7
        set calendarHeight to cellHeight * (count dateList)
        --印刷可能領域よりカレンダーサイズが大きい場合は処理しない
        if calendarWidth is greater than contentsWidth then return

        -- 余白の算出
        set blank to contentsWidth - calendarWidth
        -- 最初のセルの開始位置
        set x to left margin + (blank / 2)
        set y to (contentsHeight - calendarHeight) + top margin

        -- カレンダーの作成
        set num to 0
        repeat with i from 1 to (count dateList)
            set currentWeek to item i of dateList
            set dayCount to count currentWeek
            repeat with j from 1 to 7
                set num to num + 1
                if j is greater than dayCount then
                    set thisItem to ""
                else
                    set thisItem to (item j of currentWeek) as Unicode text
                end if
                set theProp to {width:cellWidth, height:cellHeight, horizontal position:x + (cellWidth * (j - 1)), vertical position:y, name:(num as Unicode text), object text:thisItem}
                set newShape to make new shape with properties theProp
                if (num mod 7) is 0 then
                    set thisColor to {0, 0, 65535}
                else if (num mod 7) is 1 then
                    set thisColor to {65535, 0, 0}
                else
                    set thisColor to {0, 0, 0}
                end if
                tell object text of newShape
                    set color of it to thisColor
                end tell
            end repeat
            set y to y + cellHeight
        end repeat

        -- カレンダーセルの見た目の変更
        tell shapes
            set stroke width to strokeWidth
            set stroke color to strokeColor
            set wrap to none
            set fill type to none
            tell object text
                set alignment to textAlignment
                set font name to textFont
                set font size to textSize
            end tell
        end tell

        -- 曜日セルの作成
        set weekdayPosition to (vertical position of first shape) - wCellHeight
        repeat with i from 1 to 7
            set newShape to make new shape
            tell newShape
                set horizontal position to x + (wCellWidth * (i - 1))
                set vertical position to weekdayPosition
                set width to wCellWidth
                set height to wCellHeight
                set wrap to none
                set stroke type to none
                set fill type to none
                set object text to item i of weekdayList
                set alignment of object text to wTextAlignment
                set font name of object text to wTextFont
                set font size of object text to wTextSize
            end tell
        end repeat

        -- 月のセルを作成
        set monthPosition to (vertical position of last shape) - mCellHeight
        make new text box with properties {vertical position:monthPosition, height:mCellHeight, width:mCellWidth, horizontal position:x, name:"month"}

        tell text box "month"
            set thisYear to year of (theDate) as Unicode text
            set thisMonth to month of (theDate) as Unicode text
            set object text to thisMonth & space & thisYear
            set alignment of object text to mTextAlignment
            set font name of object text to mTextFont
            set font size of object text to mTextSize
            set tracking of object text to mTextTracking
            set color of object text to mTextColor
        end tell
        say "Complete."
    end tell
end tell

on getCalendar(selectedDate)
    set theList to {{}, {}, {}, {}, {}, {}}
    set numDaysInMonth to {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

    set currentYear to year of selectedDate
    set currentMonth to month of selectedDate as number

    set day of selectedDate to 1
    set time of selectedDate to 0

    copy selectedDate to firstOfMonth
    set startOffset to weekday of firstOfMonth as number

    set daysInMonth to item currentMonth of numDaysInMonth
    if ((currentMonth is 2) and (isLeap(currentYear))) then set daysInMonth to daysInMonth + 1

    set dayLabel to 1
    set num to 0
    repeat with i from 1 to 6
        set currentWeek to item i of theList
        repeat with j from 1 to 7
            set num to num + 1
            if (num is less than startOffset) then
                set end of currentWeek to ""
            else if (num is greater than or equal to (daysInMonth + startOffset)) then
                exit repeat
            else
                set end of currentWeek to dayLabel
                set dayLabel to dayLabel + 1
            end if
        end repeat
    end repeat
    return theList
end getCalendar

on isLeap(theYear)
    return (((theYear mod 4) is 0 and ((theYear mod 100) is not 0)) or (theYear mod 400) is 0)
end isLeap

文字、化ける?

気がつけば、5 月。ゴールデンウィーク。みなさん、せっかくの連休。しっかり、AppleScript に打ち込みましょう!

しないって。せっかくの連休にそんなこと。

さて、RSS の配信を始めてから 1 年経ちました。案外、続きました。

文字が化けるんです。

なにが?

RSS ではありません。紛らわしいよ、この書き方。

QuickTime Player 7 です。テキストトラックです。TextEdit で作ったテキストファイルでテキストトラックを作ったら日本語のチャプターを作れるんです(保存した文字コードは 「日本語(Mac OS)」)。これはこれでいいのですが、わたくしといたしましては、AppleScript でテキストトラックを作りたい。で、しこしこしこしこと AppleScript を書いて実行...。駄目なんですね。日本語を含んでいると文字が化けてしまう。

Script Editor で開く

tell application "QuickTime Player"
    make new movie
    tell movie 1
        make new track with data "おはよう"
    end tell
end tell

文字コード SJIS のファイルを読み込んでテキストトラックを作る分には大丈夫なんですね。AppleScript から渡す文字コードが問題なのかな...と試してみるんですが、解決しない。

QuickTime が元々こういうものなのか、それとも QuickTime 7 になってからのことなのか。その辺も判然としない。テキストトラックで日本語がおかしくなってしまうということは、チャプタートラックも作れないんですよね。どうしたものか...。

PDF に変換する

味も素っ気もない記事名ですね。

/Library/Scripts/printing Scripts の中に Finder で選択している項目を PDF に変換するスクリプトがあります。これが動かない。

なんでかな、と試行錯誤してやっと分かりました。変換対象のファイルへのパスに日本語が含まれていると変換に失敗するんですね。もちろん、ファイル名に日本語を使っていてもだめ。ファイルの中身が日本語かどうかやエンコーディングが何かといったことは関係ないっぽい。

パスに日本語が入っているとだめということなら、それなりの対処をすればいいだけでなんとか使えるようになりました。

しかし、この変換を行っているコマンドって RTF かテキストファイル(他にもいくつかの画像タイプに対応しているようですが、書類関連ではということです)しか変換できないのですね。画像が入っている RTFD ファイルなんかは変換できないし、Safari で保存した Web アーカイブなんかも変換できない。せめて、RTFD だけでも PDF に変換してほしいものです。

property typeList : {"TEXT"}
property extensionList : {"rtf", "txt"}
property tmpFolder : path to temporary items folder from user domain

(*
Usage: /System/Library/Printers/Libraries/convert
    [-f <input filename>]
    [-o <output filename>]
    [-i <input mimetype>]
    [-j <output mimetype>]
    [-P <PPD filename>]
    [-u]
    [-a <attribute string>]
    [-U <username>]
    [-J <jobname>]
    [-c <copies>]
    [-D]
*)

tell application "Finder"
    set curSelection to selection

    set processFiles to {}
    repeat with thisItem in curSelection
        if not (class of thisItem is folder) then
            if (name extension of thisItem is in extensionList or file type of thisItem is in typeList) then
                set end of processFiles to thisItem as alias
            end if
        end if
    end repeat
    if processFiles is {} then return

    repeat with thisItem in processFiles
        set inFile to my duplicateFile(thisItem)
        set outFile to (tmpFolder as Unicode text) & "out.pdf"
        my convertPDF(POSIX path of inFile, POSIX path of outFile)
        set fileName to (my trimExtension(thisItem) & ".pdf") as Unicode text
        set name of (outFile as alias) to fileName
        set parentFolder to my dirName(thisItem)
        move (((tmpFolder as Unicode text) & fileName) as alias) to parentFolder replacing yes
    end repeat
end tell

on duplicateFile(theFile)
    set tmpFile to "process_file"

    tell application "Finder"
        if (file tmpFile of tmpFolder exists) then
            move file tmpFile of tmpFolder to trash
        end if
        set copiedFile to duplicate theFile to tmpFolder replacing yes
        set name of copiedFile to tmpFile
        return (file tmpFile of tmpFolder) as alias
    end tell
end duplicateFile

on convertPDF(inFile, outFile)
    set converter to "/System/Library/Printers/Libraries/convert "
    set mime to "'application/pdf'"

    set theCommand to converter & "-f " & (quoted form of inFile) & " -o " & (quoted form of outFile) & " -j " & mime
    try
        do shell script theCommand
    on error eMessage number enumber
        return {enumber, eMessage}
    end try
end convertPDF

on trimExtension(theFile)
    set {name:fileName, name extension:theExtension} to info for theFile
    if theExtension is missing value then set theExtension to ""
    set fileCharNum to count fileName
    set exCharNum to count theExtension
    if exCharNum is not 0 then
        return text 1 thru (fileCharNum - exCharNum - 1) of fileName
    end if
    return fileName
end trimExtension

on dirName(theFile)
    tell application "Finder" to ((container of file theFile) as alias) as Unicode text
end dirName

データ構造

よく読むプログラミングの書籍の一つに『プログラミングの宝箱 アルゴリズムとデータ構造 (C magazine)』という書籍があります。

内容は C 言語なのですが、平易に書かれていてコンパクト(横になって読むときに腕が疲れない)なので個人的に気に入っています。で、できるのかな...という感じでリンクリストを作ってみました。AppleScript で。

 
on run
    set {firstCell, lastCell} to {missing value, missing value}

    repeat with i from 20 to 40 by 2
        set newCell to makeCell(i, missing value)

        if lastCell is not missing value then
            set nextCell of lastCell to newCell
            set lastCell to newCell
        else
            -- 最初のセルの場合
            set {firstCell, lastCell} to {newCell, newCell}
        end if
    end repeat

    set thisCell to firstCell
    repeat
        display dialog (counter of thisCell as text)
        set thisCell to nextCell of thisCell
        if thisCell is missing value then exit repeat
    end repeat
end run

on makeCell(int, cellObje)
    script Cell
        property counter : int
        property nextCell : cellObje
    end script
end makeCell

だからどうだといわれればそれまでなのですが、AppleScript でもリンクリストを作れますね。で、調子に乗って 2 分木も作ってみる。...特に問題もなく動きますね。処理速度的にどうかって問題はあるのですが(詳しく検証していないので分かりません)。

しかし、使いどころがない...。AppleScript の場合、データはアプリケーションが持っていて、だいたい次のようにしてデータを取得します。

tell application "iTunes"
    set trackList to tracks of library playlist 1
end tell

こうやって全体、または一部のデータをリストで取得し、そのリストに対して処理を行う、という感じ。なので、わざわざデータ構造を作る必要がなく、いかにリスト処理を高速に行えるか、ってことが問題になるのですが。

2 分木やマップが使えると、連想配列を作ることができます。AppleScript でこういうデータ構造を作って調べた人っているのでしょうか?

AppleScript はアプリケーションや言語(do shell script や call method を通してシェルや Perl や Objective-C 等)をつなげるためのスクリプト言語なのだから AppleScript 単体でデータ構造を作る必要もない、と言われればそうなのですが、どっちかというと『AppleScript で何かを作る』より、『AppleScript の限界は?AppleScript でどこまでできる』というどうしようもなく退廃的かつ非生産的な事柄に興味が向かってしまうんですね。

ところで。

property countList : {}

on run
    repeat 100 times
        set end of countList to random number
    end repeat
end run

このようなスクリプトをアプリケーションで保存し、暇をみては起動します。すると、サイズ(Finder の情報でみるサイズです)がどんどん増えていきます。counterList にどんどん実数を追加していくのだから当たり前です。では、この増えたサイズを元に戻すにはどうしたらいいのでしょうか?

set couterList to {}

このようにするといいのでしょうか?

AppleScript でよくわからないのはこういったメモリに関して。変数は使われなくなったらその変数に使われているメモリは解放される、と以前何かで目にした記憶があるのですが。では、次のような場合、この変数に使われているメモリは解放されているのでしょうか?

set thisValue to "abcdef"
-- 何らかの処理
set thisValue to missing value

または次のようなスクリプト。

set thisValue to "abcdef"
-- 何らかの処理
set thisValue to null

null ってなんですか?予約語になっているのですが。

thisValue をみてみるといずれも missing value と null が返ってくる。これで使ったメモリを解放できるのでしょうか? C 言語の free() みたいなことってできるのでしょうか?

ハンドラの中で使った変数は、ハンドラが終了したらメモリが解放されるというのも何かでみた記憶があります。だから、メモリ効率のために主要な処理は run ハンドラではなく、ハンドラ内で行う、と。

偉そうに AppleScript のことを書いていても、こういう基本的なことがいまいちよく分かっていません...。

Safari のメール送信

HAPPY Macintosh Developing TIME! さんを見ていたら、3 月 2 日の記事に Safari 2.0 のメール送信機能のことが書かれていた。Safari の 「ファイル」メニューにある「このページの内容をメールで送信」というメニューのことです。

Safari で表示しているページをそのままでメールで送ることができる機能...なんですが、これはどうやら AppleEvent でアプリケーション間通信を行っているらしいのです。

それはともかく、そういえばと思い Safari の用語説明を見てみると...ありました。

tell application "Safari"
    if not (front document exists) then return

    email contents of front document
end tell

email contents という命令が追加されているんでした。なんだろうなと思っていたのですが、これだったんですね。

実行すると Mail で新しいメールを作成します。ただ、デフォルトのメーラーが対応していないと実行できない模様。デフォルトのメーラーが Mail だとこのスクリプトが利用できます。

いわゆる、ひとつの答え

風邪です。風邪。もう、しんどーい。

さて、以前に掲載した問題ですが、掲示板の方でお答えをいただきました。どうも、ありがとうございました。返答が長くなるのでこちらで答えさせていただきます。

『エディタごとに Wrapper となるスクリプトオブジェクトを用意する』(注、意訳。原典は、掲示板の方を)、というのが頂いた答えでした。書いてくださった方も常套手段と書かれているように、おそらく基本的な解答なのだと思います。

では、問題を作ったときに私が作ったスクリプトを掲載しておきます。

script QuoEdit
    on selectedText()
        tell application "QuoEdit"
            text of selection
        end tell
    end selectedText
end script

script CotEditor
    on selectedText()
        tell application "CotEditor"
            contents of selection
        end tell
    end selectedText
end script

script KEdit
    on selectedText()
        tell application "KEdit"
            selected text of front document
        end tell
    end selectedText
end script

script EnhanceEditor

    on currentEditor()
        tell application "System Events"
            return ((name of processes whose frontmost is true) as Unicode text)
        end tell
    end currentEditor

    on selectedText()
        set apps to currentEditor()
        if apps is "QuoEdit" then
            selectedText() of QuoEdit
        else if apps is "CotEditor" then
            selectedText() of CotEditor
        else if apps is "KEdit" then
            selectedText() of KEdit
        else
            return ""
        end if
    end selectedText
end script

display dialog (EnhanceEditor's selectedText())

ラッパーとなるスクリプトオブジェクトを用意して...というのはこういう感じでしょうか?

おそらく、こういう形が基本的かと思います。ただ、もっと簡潔に簡要に簡便に書けないものなのかと思うのです。例えば、このスクリプトの EnhanceEditor オブジェクトの selectedText() は、if 文が続きます。対象とするエディタが増えるとそれだけ if 文が増えます。どう考えても、不細工です。問題として提起したのは、この if 文が引っかかったからなのでした。もっとスマートな書き方があるはずだ、と。

引き続き、この問題に対する答えは受け付けています。iTMS カードは、やっぱりないですが(笑

「仕方ないな...このサイトの管理人はこんなことも分からないのか。手間がかかるが教えてやろう」という気骨のある方、どうかよろしく。

問題

まずは、前振り。うちの環境には CotEditorQuoEditKEdit と、3 種類のテキストエディタが入っています。もちろん、どのエディタも利用していますし、Mac OS X に標準で入っている TextEdit も(ホンットに、時々)利用しています。

全部で 4 種類ですね。どれも AppleScript に対応しています。当然、機能の拡張は AppleScript で行っています。

どれも AppleScript に対応している...のですが、それぞれ個性があります。例えば、選択文字列の取得。まずは、QuoEdit。

tell application "QuoEdit"
    set curSelection to selection of front document
    -- every text of document 1 of application "QuoEdit"
    text of curSelection
    -- "選択文字列"
end tell

次に kEdit。

tell application "KEdit"
    selected text of front document
    -- "選択文字列"
end tell

そして、CotEditor。

tell application "CotEditor"
    contents of selection
    -- "選択文字列"
end tell

いずれも、選択している文字列(参照ではない)を取得するスクリプトですが、微妙に異なっています。QuoEdit の selection は、application、document クラスなどで利用できます。なので、たんに selection とすると状況により結果が異なる場合があります。kEdit は、選択している文字列は書類のもの、ということで document クラスの属性になっています。CotEditor は、application クラスの属性として用意されています。application クラスだから、例えば、検索パネルを表示して検索文字を入力してから selection とすると、その入力した検索文字が返ってくるかと思ったのですが、そうではないようです(意地悪で言っているのではありません)。

どのクラスでどの属性が定義されているか、この辺りに作者それぞれの考えが反映されていてなかなか面白いものがあります。が、ここではその考えを追うことはしません。

ここからが問題です。

現在作業中のエディタをスクリプトで判断し、それぞれのエディタに応じた方法で選択した文字列を取得するにはどうすればいいか?

例えば、CotEditor で作業をしているなら contents of selection とし、その結果を返せばいいのです。他も同様。もちろん、上記のそれぞれのスクリプトを用意し、各アプリケーションで使い分ければいいだけの話です。しかし、そうではなく、selectedText() というような名前のハンドラ(インターフェース)を 1 つ用意し、どのエディタであってもこのハンドラ経由で選択文字列を取得するにはどうしたらいいのか?という問題です。

いろんなエディタが AppleScript に対応しているのは確かに便利です。が、エディタを変更すると、それまで利用していたエディタ用のスクリプトは使えません(エディタを作業によってころころと変える人ってそんなにいないと思いますが...)。これを解決するために 1 つハンドラを用意し、全ての操作はそのハンドラ経由で。selectedText() ハンドラの内部でいかに小難しいことをしているかは知らなくても、selectedText() を呼び出せば CotEditor でも QuoEdit でも kEdit でも(あるいは、その他のエディタでも)選択している文字列が取得できる。

一人で考えていても、分からない。で、問題としてここに掲載。解答を募集します。優秀な解答には、iTMS カード(5,000 円)をお一人様に。努力賞として iTMS カード(2,500 円)をお二人様に。ふるってご応募ください。

...というようなことをすると AppleScript のサイトとして盛り上がるかなぁ。。。

追記。iTMS カードは、冗談です。