Archive for 10月, 2017

2017/10/31 Keynote書類内の画像サイズを縮小

指定フォルダ内に入っているKeynote書類を順次オープンして、書類内の画像サイズを縮小(最適化)するAppleScriptです。

GUI Scriptingを用いているため、システム環境設定.app>セキュリティとプライバシー>プライバシー>アクセシビリティでScript Editorに対してGUI Scripting(アプリケーションにコンピュータの制御を許可)をオンにしておく必要があります。

Keynoteに搭載されている機能のうち、AppleScript用語辞書を通じてAppleScript側に機能が解放されていることが望ましい機能の筆頭に、「ファイル」メニューに存在している「ファイルサイズを減らす」コマンドがあります。

Keynote書類にペーストされた画像素材を適正サイズに縮小することで、大幅に書類のファイルサイズを小さくすることができ、Keynote書類の数が増えてくるとSSDの容量節約にかなり効果的です。

keynote1_resized.png

ただ、AppleScript用語辞書に記載されていないので、非正規のルート(GUI Scripting)でメニューなどを操作して呼び出すことになります。

GUI Scriptingは強制的なメニュー(GUI部品)操作なので、コマンド実行の結果を受け取ることもできませんし、その結果として何かアプリケーション側がメッセージを返してきたとしても、受信できません。

keynote2_resized.png

そして、アプリケーションがSheetで結果を返すと、通常だとそこでアプリケーションの実行が止まるので、強引にシート上のボタンをクリックして(リターンキーを押して)表示を解除しています。

実行直後、メニュー項目を走査するので少々待たされます。オブジェクトへの参照をテキスト化してInfo.plist内部にでも登録しておくとよいでしょう。

AppleScript名:Keynote書類内の画像サイズを縮小
– Created 2017-10-01 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4945

property menuItemRef1 : missing value

–Select target folder which contains Keynote documents
set aFol to choose folder with prompt “Keynote書類の入っているフォルダを選択してください”

tell application “Finder”
  tell folder (aFol as string)
    –File Kind is localized. UTI is better
    
set kList to (every file whose kind is equal to “Keynoteプレゼンテーション”) as alias list
  end tell
end tell

if menuItemRef1 = missing value then
  –Application Name, Root File Menu Item, target menu item (Localized)
  
set menuItemRef1 to returnMenuItemRef(“Keynote”, “ファイル”, “ファイルサイズを減らす”) of me
  
–We can get these texts from .strings file
end if

repeat with i in kList
  set j to contents of i
  
  
tell application “Keynote”
    open j
  end tell
  
  
–「ファイル」>「ファイルサイズを減らす」コマンドが使えるかどうか確認
  
set aRes to checkReduceFileSizeCommandEnabled() of me
  
  
if aRes = true then
    –ファイルサイズを減らす
    
clickReduceFileSizeMenuViaGUIScripting() of me
    
    
–「この書類のファイルサイズは減らせません」シートが表示されているかどうかチェック
    
set aRes to chkSheetDialogAndDismissIt() of me
    
    
delay 1
    
    
–保存&クローズ
    
tell application “Keynote”
      close every document with saving
    end tell
  end if
end repeat

–「この書類のファイルサイズは減らせません」シートが表示されているかどうかチェック
on chkSheetDialogAndDismissIt()
  activate application “Keynote”
  
tell application “System Events”
    tell process “Keynote”
      tell window 1
        set sEXT to exists of sheet 1
      end tell
    end tell
  end tell
  
  
activate application “Keynote”
  
tell application “System Events”
    tell process “Keynote”
      if sEXT = true then
        –「この書類のファイルサイズは減らせません」シートが表示されていた場合
        
tell button 1 of sheet 1 of window 1
          keystroke return –なぜかクリックできない。ふしぎ
        end tell
      end if
    end tell
  end tell
  
  
return (not sEXT)
end chkSheetDialogAndDismissIt

–メニューから「ファイル」>「ファイルサイズを減らす」を実行
on clickReduceFileSizeMenuViaGUIScripting()
  activate application “Keynote”
  
tell application “System Events”
    tell process “Keynote”
      click menuItemRef1
    end tell
  end tell
end clickReduceFileSizeMenuViaGUIScripting

–「ファイル」>「ファイルサイズを減らす」が実行可能か確認
on checkReduceFileSizeCommandEnabled()
  activate application “Keynote”
  
tell application “System Events”
    tell process “Keynote”
      return (enabled of menuItemRef1)
    end tell
  end tell
end checkReduceFileSizeCommandEnabled

on returnMenuItemRef(appName, menubarMenuItem, lastMenuItemName)
  tell application “System Events”
    tell process appName
      set menuList to name of every menu of menu bar 1
      
set aMenuInd to offsetInList(menubarMenuItem, menuList) of me
    end tell
    
    
set aList to entire contents of (menu bar item aMenuInd of menu bar 1 of process appName)
    
    
repeat with i in aList
      set j to contents of i
      
try
        set aNameVal to value of attribute “AXTitle” of j
        
if (aNameVal = lastMenuItemName) then
          return j
        end if
      end try
    end repeat
  end tell
  
  
return false
end returnMenuItemRef

on offsetInList(aStr, aList)
  set anArray to current application’s NSArray’s arrayWithArray:aList
  
set aInd to (anArray’s indexOfObject:aStr)
  
if aInd = current application’s NSNotFound or (aInd as number) > 9.99999999E+8 then
    error “Invalid Character Error”
  else
    return (aInd as integer) + 1 –0 to 1 based index conversion
  end if
end offsetInList

★Click Here to Open This Script 

2017/10/30 Keynoteでオープン中の書類のページをPDFに書き出してオープン

Keynoteでオープン中の書類の現在表示中のページをPDFに書き出して、オープンするAppleScriptです。

Keynoteで仕様書などのおおきめの(ページ数の多い)書類を(修正や追記をしながら)開いていて、同時に複数のページを見ておく必要があるときに、現在のページのみPDFに書き出して表示させてみたものです。

指定のページのみPDFに書き出す機能はKeynoteに存在していないので、とりあえず書類をまるごとPDFに書き出して、現在表示中のページ以外を削除するようにしてみました。

AppleScript名:Keynoteでオープン中の書類のページをPDFに書き出してオープン
– Created 2017-10-30 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “Quartz”
–http://piyocast.com/as/archives/4942

property |NSURL| : a reference to current application’s |NSURL|
property PDFDocument : a reference to current application’s PDFDocument

tell application “Keynote”
  tell front document
    tell current slide
      set curPage to slide number
    end tell
    
    
set dtPath to ((path to desktop) as string) & (do shell script “uuidgen”) & “_” & (curPage as string) & “.pdf”
    
    
export to file dtPath as PDF
  end tell
end tell

set targAlias to (dtPath as alias)
set aRes to stripSpecificPageInPDF(targAlias, curPage) of me

tell application “Finder” to open targAlias

–指定PDFから指定ページを残して他を削除
on stripSpecificPageInPDF(inFileAlias, targPageNum)
  set inNSURL to |NSURL|’s fileURLWithPath:(POSIX path of inFileAlias)
  
set theDoc to PDFDocument’s alloc()’s initWithURL:inNSURL
  
  
set pRes to theDoc’s pageCount()
  
  
repeat with i from pRes to 1 by -1
    if i is not equal to targPageNum then
      (theDoc’s removePageAtIndex:(i - 1))
    end if
  end repeat
  
  
set aRes to (theDoc’s writeToURL:inNSURL) as boolean
  
  
return aRes
end stripSpecificPageInPDF

★Click Here to Open This Script 

2017/10/29 指定フォルダ以下の指定形式の書類のファイル名重複チェック

指定フォルダ以下の指定形式の書類をすべてもとめ、拡張子をはずしたファイル名に重複がないかチェックするAppleScriptです。

実行にはShane StanleyのAppleScript Libraries「Metadata Lib」(Spotlight検索)を必要とします。

filenames.png

指定フォルダ以下のすべてのファイルを、1つの出力フォルダに同一形式で書き出したような場合に、

 ・もともとは別フォルダに存在していた
 ・ファイル名は同じだが拡張子が違うために重複がわからなかった

ことが理由で、書き出し時にファイル名の衝突が発生するケースが(よく)あります。

その衝突チェックを事前に行う目的で作ったものです。以前にPure AppleScriptで(Cocoa呼び出しがサポートされていない時代に)作ってみたことがありますが、かなりおおがかりになっていました。

AppleScript名:指定フォルダ以下の指定形式の書類をすべてもとめて拡張子をはずしたファイル名に重複がないかチェック v2
– Created 2017-10-28 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use mdLib : script “Metadata Lib” version “1.0.0″
–http://piyocast.com/as/archives/4940

set docUTIList to {“net.daringfireball.markdown”, “com.apple.iwork.pages.sffpages”}
set origFol to (choose folder)
set dRes to detectDocNameDuplicateWithoutExt(origFol, docUTIList) of me
–> true / false

–origFolはaliasでもPOSIX pathでも可
on detectDocNameDuplicateWithoutExt(origFol, docTypeList as list)
  script spdMD
    property allResList : {}
  end script
  
  
set (allResList of spdMD) to {}
  
  
repeat with i in docTypeList
    set j to contents of i
    
set aResList to (mdLib’s searchFolders:{origFol} searchString:(“kMDItemContentTypeTree CONTAINS %@”) searchArgs:{j})
    
if aResList = missing value or aResList = {} then
      –Hitしなかった
    else
      set (allResList of spdMD) to (allResList of spdMD) & aResList
    end if
  end repeat
  
  
set aLen to length of contents of (allResList of spdMD)
  
if aLen = 0 then error “No match”
  
  
set anArray to current application’s NSArray’s arrayWithArray:(allResList of spdMD)
  
set aRes to anArray’s valueForKeyPath:“lastPathComponent.stringByDeletingPathExtension”
  
set b1Res to uniquify1DList(aRes as list, true) of me
  
set b1Len to length of b1Res
  
  
if aLen = b1Len then
    return true – No Duplicates
  else
    return false –Some duplicates
  end if
end detectDocNameDuplicateWithoutExt

–1D/2D Listをユニーク化
on uniquify1DList(theList as list, aBool as boolean)
  set aArray to current application’s NSArray’s arrayWithArray:theList
  
set bArray to aArray’s valueForKeyPath:“@distinctUnionOfObjects.self”
  
return bArray as list
end uniquify1DList

★Click Here to Open This Script 

2017/10/28 指定アプリの指定メニュー内の指定GUI部品を検索

指定アプリケーションの指定メニュー内の指定GUI部品を検索するAppleScriptです。

GUI Scriptingを使ってアプリケーションのメニュー操作を行うと、ちょっとアプリケーション開発側がメニューの文言や項目を増やしたぐらいで影響を受けて、アプリケーションのバージョンアップやOSのバージョンアップで使えなくなることが多々あります。

GUI Scriptingとは原理上そういうものなので、必要に応じて修正を行ってメンテナンスする必要があります。自分でもXcode上でアプリケーションを作っていると、画面構成やメニュー項目を変更する必要があれば行いますし、実に手軽に行えるので「変更するな」とはとても言えません。

menuitem1_resized.png
▲returnMenuItemRef(アプリケーション名, rootメニュー項目, 検索対象メニュー項目タイトル) of meでPDF書き出しメニュー項目への参照を取得できる

OSやアプリケーション自体のバージョンアップを気にしないでメニュー項目をGUI Scriptingから継続的に呼び出すようにするのは、不可能ではありません。条件を制限すればある程度対応できます。

アプリケーションの名前、メニューの名前、メニュー項目の名前が変わっていなければ、途中のオブジェクト階層などが変更になっても対処は可能です(項目名が変わってしまったら対応はできませんけれども)。

本Scriptは動的にアプリケーションのメニュー項目を全取得し、指定のアプリケーションの指定メニュー内の指定メニュー項目をサーチします。サーチ対象を少なくしてスピードをかせぐために、指定メニュー項目以下のオブジェクトのみを取得しています。

オブジェクトの取得自体はそれほど時間はかからないものの、取得したオブジェクトから目的のものをサーチする(属性値を取得して比較する)のに(オブジェクト数に応じた)時間がかかってしまうため、こうした処理対象のしぼりこみは必須です。

当初、予想よりもパフォーマンスが出なかった(6秒ぐらいかかっていた)のですが、原因がトンでもないところにありました。私の環境では、「最近使った項目」の記憶最大数を拡張してあったのですが、そこに大量に項目が記憶されており、それがパフォーマンスを低下させていました。

recentmenu_resized.png

「最近使った項目」については、メニューから(ファイル履歴を)消去すればパフォーマンスを向上できました(0.7秒程度)。

Scriptの冒頭で必要なメニュー項目への参照をまとめて取得してpropertyにでも登録しておき、Script内で実際にメニュー操作を行う箇所でpropertyに入れておいたメニュー項目を実行すると効果的でしょう(毎回メニュー項目を検索するのは無駄なので)。

menu-item2_resized.png

メニューにはタイトルが設定されていますが、その他のGUI部品にも一意に指し示せるようなユニークなIDとかURLとかが取得できるとよいのですが、、、、(ーー;;

もともと、この手の動的なGUI部品オブジェクト検索はWebブラウザ上の部品検索などで行っており、Webコンテンツ操作で重宝していました。

AppleScript名:指定アプリの指定メニュー内の指定GUI部品を検索
– Created 2017-10-01 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4936

set menuItemRef1 to returnMenuItemRef(“MacDown”, “ファイル”, “PDF…”) of me
–>  menu item “PDF…” of menu “エクスポート” of menu item “エクスポート” of menu “ファイル” of menu bar item “ファイル” of menu bar 1 of application process “MacDown” of application “System Events”

on returnMenuItemRef(appName, menubarMenuItem, lastMenuItemName)
  tell application “System Events”
    tell process appName
      set menuList to name of every menu of menu bar 1
      
set aMenuInd to offsetInList(menubarMenuItem, menuList) of me
    end tell
    
    
set aList to entire contents of (menu bar item aMenuInd of menu bar 1 of process appName)
    
    
repeat with i in aList
      set j to contents of i
      
try
        set aNameVal to value of attribute “AXTitle” of j
        
if (aNameVal = lastMenuItemName) then
          return j
        end if
      end try
    end repeat
  end tell
  
  
return false
end returnMenuItemRef

on offsetInList(aStr, aList)
  set anArray to current application’s NSArray’s arrayWithArray:aList
  
set aInd to (anArray’s indexOfObject:aStr)
  
if aInd = current application’s NSNotFound or (aInd as number) > 9.99999999E+8 then
    error “Invalid Character Error”
  else
    return (aInd as integer) + 1 –0 to 1 based index conversion
  end if
end offsetInList

★Click Here to Open This Script 

2017/10/27 OTMXAttributeでxattrを削除する

オープンソースのプログラム「OTMXAttribute」(By Ryan Coffman)をフレームワーク化したXAttribute.frameworkを呼び出してファイルの拡張属性(xattr)を削除するAppleScriptです。

実際に本Scriptを試すためには、「XAttribute.framework」を~/Library/Frameworksに自己責任でインストールしてください。

–> Download Framework Binary

なにもObjective-Cのプログラムを呼び出さなくても、shell scriptのxattrコマンドを呼び出すだけでもよかったわけですが、あくまで実験です。

用途は、YouTubeからダウンロードした映像ファイルから音声部分のみを抽出する際にXattr属性がついていると、(ランタイム環境によっては)実行が阻害されるようだったので、処理途中でXattr属性を削除することを試してみた、というものです。

さまざまな部品を組み込むことで、Script Menuから呼び出したAppleScriptで、Safariで表示中のYouTubeのムービーをダウンロードして音声部分のみを抽出する処理を呼び出せているので、よいのではないかと。

scriptmenu.png
▲SafariのScript Menuに登録してあるYouTube系Script

AppleScript名:OTMXAttributeでxattrを削除する
– Created 2017-10-27 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “XAttribute” –https://github.com/rylio/OTMXAttribute
–http://piyocast.com/as/archives/4933

set aFile to POSIX path of (choose file)
set xRes to removeXAttrFromFile(aFile, “com.apple.quarantine”)

on removeXAttrFromFile(aFile, anXattr)
  –Get Xattr String
  
set anAttribute to (current application’s OTMXAttribute’s stringAttributeAtPath:aFile |name|:anXattr |error|:(missing value))
  
if anAttribute = missing value then return true –There is no use to remove xattr
  
  
–Remove Xattr
  
set xRes to (current application’s OTMXAttribute’s removeAttributeAtPath:aFile |name|:anXattr |error|:(missing value))
  
if xRes = missing value then return false
  
return (xRes as boolean)
end removeXAttrFromFile

★Click Here to Open This Script 

2017/10/26 Numbersで選択中の範囲を横方向に比較しカラム間の差分検出してセルの色変更 v2

Numbersの書類上で選択中の範囲(横に2列)を横方向に比較し、変更のあったセルの色を変更するAppleScriptです。

2017-10-25-23_44_59.gif

実行にはShane StanleyのAppleScript Library「BridgePlus」のインストールが必要です。Numbersの選択範囲から返ってくる1D Arrayを2D Arrayに変換するために使用しています。

numb1_resized.png
▲実行前。Numbers書類上のアクティブなシートの最初のテーブルが処理対象

numb2_resized.png
▲実行後。差分のあったセルを赤くマーク

Numbersの選択セル範囲を取得するのに文字列を計算するのではなく、Numbersの機能を使って手軽に行うように変更しました。最近、ちょっとCocoa風に処理するのに慣れてしまって、アプリケーションの内蔵機能を呼び出したほうがシンプルに書ける場合にはそうすべきでしょう。

処理速度については、対象データがそれほど大きくないことを前提として書いたので、データ量が多くなった場合には(数千行以上)急に遅くなるかもしれません。

ただ、まいどまいど思いますが、Numbersで選択中のTableの情報を取得できないのはダメすぎです。

それにしても、辞書.appの辞書名をチョロチョロ変えるのは勘弁してほしいところです(→スクリーンショット)。無意味な名称変更がまんべんなく、、、、、

AppleScript名:Numbersで選択中の範囲を横方向に比較しカラム間の差分検出してセルの色変更 v2
– Created 2016-10-26 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use bPlus : script “BridgePlus”
–http://piyocast.com/as/archives/4929

load framework

tell application “Numbers”
  tell front document
    tell active sheet
      set tCount to count every table
      
if tCount is not equal to 1 then
        display notification “Wrong Table number (not one) in current sheet.”
        
return
      end if
      
      
tell table 1
        set aSel to properties of selection range
        
set selName to name of aSel
        
set {s1, s2} to parseByDelim(selName, “:”) of me
        
        
–始点の情報を取得する
        
set s1Row to (address of row of range s1) as integer
        
set s1Column to (address of column of range s1) as integer
        
        
–終点の情報を取得する
        
set s2Row to (address of row of range s2) as integer
        
set s2Column to (address of column of range s2) as integer
        
        
–選択範囲の情報を取得する
        
set aHeight to s2Row - s1Row + 1 –高さ(Height of selection range)
        
set aWidth to s2Column - s1Column + 1 –幅(Width of selection range)
        
        
set dList to value of every cell of selection range –Every Data from Selection (1D Array)
        
set diffList to getDiffAddressList(dList, aWidth) of me
        
        
repeat with i in diffList
          copy i to {adrX, adrY}
          
          
set adrX to adrX + s1Column
          
set adrY to adrY + s1Row - 1
          
          
tell row adrY
            tell cell adrX
              –Async Mode Execution (x2 Speed)
              
ignoring application responses
                set text color to {65535, 0, 65535}
              end ignoring
            end tell
          end tell
          
        end repeat
        
      end tell
    end tell
  end tell
end tell

on getDiffAddressList(dList, aWidth)
  set d2List to (current application’s SMSForder’s subarraysFrom:(dList) groupedBy:aWidth |error|:(missing value)) as list
  
set diffList to {}
  
set diffAdrList to {}
  
  
set yOffsetCount to 1
  
  
repeat with i in d2List
    set i1 to first item of i
    
set i2 to rest of i
    
    
set xOffsetCount to 1
    
    
repeat with ii in i2
      set jj to contents of ii
      
if i1 is not equal to jj then
        set the end of diffList to {i1, jj}
        
set the end of diffAdrList to {xOffsetCount, yOffsetCount}
        
exit repeat
      end if
      
      
set xOffsetCount to xOffsetCount + 1
      
    end repeat
    
    
set yOffsetCount to yOffsetCount + 1
  end repeat
  
  
return diffAdrList
end getDiffAddressList

on parseByDelim(aData, aDelim)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to aDelim
  
set dList to text items of aData
  
set AppleScript’s text item delimiters to curDelim
  
return dList
end parseByDelim

★Click Here to Open This Script 

2017/10/26 オープン中のムービーのファイルにラベルを付ける

QuickTime Playerでオープン中のファイルにラベルを付けるAppleScriptです。

QuickTimeムービーを取捨選別しているような場合に、オープン中のムービーだけにラベルをつけることで、作業を効率化しようというものです。

QuickTime Playerだけではなく、他のアプリケーションでも同様の処理は有用であるため、試してみるといいんじゃないでしょうか。

qt1.png
▲QuickTime Playerでオープン中のムービー

qt2.png
▲Script実行前

qt3.png
▲Script実行後

AppleScript名:オープン中のムービーのファイルにラベルを付ける
– Created 2017-10-26 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4925
tell application “QuickTime Player”
  set aDocList to every document
end tell

repeat with i in aDocList
  
  
tell application “QuickTime Player”
    tell i
      set aProp to properties
      
set aFile to (file of aProp) as alias
    end tell
  end tell
  
  
tell application “Finder”
    set label index of aFile to 5
  end tell
  
end repeat

★Click Here to Open This Script 

2017/10/26 NumbersのRangeのnameの範囲から幅と高さを求める

Numbersのselection range(のname)の範囲(”A1:B12″とか)の幅と高さを求めるAppleScriptです。

本ScriptはNumbersの機能を使わないで書いてありますが、Numbersを利用できる環境および用途の場合にはNumbersを用いたほうが手軽でよいと思います。

AppleScript名:NumbersのRangeのnameの範囲から幅と高さを求める
– Created 2017-10-25 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4924

property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSRegularExpressionSearch : a reference to current application’s NSRegularExpressionSearch

set aData to “A2:B29″
set {aWidth, aHeight} to calcWidthAndHeightOfNumbersRange(aData) of me
–> {2, 28}

on calcWidthAndHeightOfNumbersRange(aData)
  set aList to parseByDelim(aData, “:”) of me
  
if length of aList is not equal to 2 then error “Invalid Parameter Error”
  
set calcList to {}
  
  
repeat with i in aList
    set j to contents of i
    
set aRes to returnAlphabetOnly(j) of me
    
set bRes to returnNumberOnly(j) of me
    
set a2Res to numbersAddrToDecimal(aRes) of me
    
set the end of calcList to {a2Res as integer, bRes as integer}
  end repeat
  
  
copy calcList to {{x1, y1}, {x2, y2}}
  
  
set xWidth to (x2 - x1) + 1
  
set yHeight to (y2 - y1) + 1
  
  
return {xWidth, yHeight}
end calcWidthAndHeightOfNumbersRange

on parseByDelim(aData, aDelim)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to aDelim
  
set dList to text items of aData
  
set AppleScript’s text item delimiters to curDelim
  
return dList
end parseByDelim

on returnNumberOnly(aStr)
  set anNSString to NSString’s stringWithString:aStr
  
set anNSString to anNSString’s stringByReplacingOccurrencesOfString:“[^0-9]” withString:“” options:(NSRegularExpressionSearch) range:{0, anNSString’s |length|()}
  
return anNSString as text
end returnNumberOnly

on returnAlphabetOnly(aStr)
  set anNSString to NSString’s stringWithString:aStr
  
set anNSString to anNSString’s stringByReplacingOccurrencesOfString:“[^A-Za-z]” withString:“” options:(NSRegularExpressionSearch) range:{0, anNSString’s |length|()}
  
return anNSString as text
end returnAlphabetOnly

–Numbersの横方向アドレス(A〜Zの26進数)文字列を10進数に変換
on numbersAddrToDecimal(origStr)
  return aNthToDecimal(origStr, {“A”, “B”, “C”, “D”, “E”, “F”, “G”, “H”, “I”, “J”, “K”, “L”, “M”, “N”, “O”, “P”, “Q”, “R”, “S”, “T”, “U”, “V”, “W”, “X”, “Y”, “Z”}) of me
end numbersAddrToDecimal

–n進数文字列を10進数に変換する
on aNthToDecimal(origStr, nTh)
  set resNumber to 0
  
set sList to reverse of (characters of origStr)
  
set aLen to length of nTh
  
set digitCount to 0
  
  
repeat with i in sList
    set j to contents of i
    
set aRes to offsetInList(j, nTh) of me
    
    
if digitCount = 0 then
      set digitNum to 1
    else
      set digitNum to digitCount * aLen
    end if
    
    
set resNumber to resNumber + (aRes * digitNum)
    
set digitCount to digitCount + 1
  end repeat
  
  
return resNumber
end aNthToDecimal

on offsetInList(aChar, aList)
  set anArray to NSArray’s arrayWithArray:aList
  
set aInd to (anArray’s indexOfObject:aChar)
  
if aInd = current application’s NSNotFound or (aInd as number) > 9.99999999E+8 then
    return false
  else
    return (aInd as integer) + 1
  end if
end offsetInList

★Click Here to Open This Script 

2017/10/26 Numbersの横セルのアドレス文字列(26進数)を10進数リストに変換

Numbersのセルのアドレス文字列を10進数のリストに変換するAppleScriptです。

Numbersのセル間のデータの比較を行おうとしたら、selection range(のname)が”A2:B12″といった形式で返ってきて、rangeの幅とか高さは自分で計算する必要があったので、それぞれのアドレス文字列を数値に変換する必要があると思われました(このあたり、何回も同じ処理を組んでいるような気がするのは、気のせい?)。

→ 6年前に組んだAppleScriptですでにもっと手軽に求める方法を実装してありました

そこで、”A2″とか”B12″といったアドレスの文字列を10進数のリストに変換してみることに。

アルファベットだけ、数字だけを抽出してそれぞれ処理しています。いつものとおり、ありあわせのルーチンを組み合わせただけで、あらたに作ったのはごく一部。

最終的に、Numbersの選択範囲の大きさを計算して、選択範囲から取得した1D List(ExcelとちがってNumbersは選択範囲のデータを取得すると連続した1D Listになるため)を選択範囲のデータ幅に合わせて2D Listに変換し、各行で差分がないかどうかチェックしました。

NSNotFoundの値が9223372036854775807になっているOS側のバグ(macOS 10.12.5〜10.12.6あたり?)に対応してあります。ちなみに、macOS 10.13.1の最新Betaではここだけは直っているものの、Web上で検索するとNSNotFoundの値の設定ミスはAppleがしょっちゅうカマしているようなので、対策し続けておいた方がよさそうです。

AppleScript名:Numbersの横セルのアドレス文字列(26進数)を10進数リストに変換
– Created 2017-10-25 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4923

property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSRegularExpressionSearch : a reference to current application’s NSRegularExpressionSearch

set {xColumn, yRow} to calcNumbersRangeNameToDecimalList(“B29″) of me
–> {2, 29}

set {xColumn, yRow} to calcNumbersRangeNameToDecimalList(“AZ12″) of me
–> {52, 12}

on calcNumbersRangeNameToDecimalList(aData)
  set aRes to returnAlphabetOnly(aData) of me
  
set bRes to returnNumberOnly(aData) of me
  
set a2Res to numbersAddrToDecimal(aRes) of me
  
return {a2Res as integer, bRes as integer}
end calcNumbersRangeNameToDecimalList

–文字列から数字だけを抽出して返す
on returnNumberOnly(aStr)
  set anNSString to NSString’s stringWithString:aStr
  
set anNSString to anNSString’s stringByReplacingOccurrencesOfString:“[^0-9]” withString:“” options:(NSRegularExpressionSearch) range:{0, anNSString’s |length|()}
  
return anNSString as text
end returnNumberOnly

–文字列からアルファベットだけを抽出して返す
on returnAlphabetOnly(aStr)
  set anNSString to NSString’s stringWithString:aStr
  
set anNSString to anNSString’s stringByReplacingOccurrencesOfString:“[^A-Za-z]” withString:“” options:(NSRegularExpressionSearch) range:{0, anNSString’s |length|()}
  
return anNSString as text
end returnAlphabetOnly

–Numbersの横方向アドレス(A〜Zの26進数)文字列を10進数に変換
on numbersAddrToDecimal(origStr)
  return aNthToDecimal(origStr, {“A”, “B”, “C”, “D”, “E”, “F”, “G”, “H”, “I”, “J”, “K”, “L”, “M”, “N”, “O”, “P”, “Q”, “R”, “S”, “T”, “U”, “V”, “W”, “X”, “Y”, “Z”}) of me
end numbersAddrToDecimal

–n進数文字列を10進数に変換する
on aNthToDecimal(origStr, nTh)
  set resNumber to 0
  
set sList to reverse of (characters of origStr)
  
set aLen to length of nTh
  
set digitCount to 0
  
  
repeat with i in sList
    set j to contents of i
    
set aRes to offsetInList(j, nTh) of me
    
    
if digitCount = 0 then
      set digitNum to 1
    else
      set digitNum to digitCount * aLen
    end if
    
    
set resNumber to resNumber + (aRes * digitNum)
    
set digitCount to digitCount + 1
  end repeat
  
  
return resNumber
end aNthToDecimal

on offsetInList(aChar, aList)
  set anArray to NSArray’s arrayWithArray:aList
  
set aInd to (anArray’s indexOfObject:aChar)
  
if aInd = current application’s NSNotFound or (aInd as number) > 9.99999999E+8 then
    error “Invalid Character Error”
  else
    return (aInd as integer) + 1 –0 to 1 based index conversion
  end if
end offsetInList

★Click Here to Open This Script 

2017/10/25 MagicKitで指定ファイルのUTI情報を取得

オープンソースのFramework「MagicKit」(By Aidan Steele)を呼び出して、指定ファイルのUTI(Uniform Type Identifiers)の情報を取得するAppleScriptです。

本Scriptの実行にはGithub上のプロジェクトをダウンロードして各自でXcode上でFrameworkをビルドし、MagicKit.frameworkを~/Library/Frameworksフォルダ以下にインストールする必要があります。

ちょうどUTIについてこういうツールがないかと思っていたところでした(BridgePlusがなくても調べられることがのぞましい)。指定ファイルのUTIやMIME Typeを調べられるのは便利です。

とくに、NSDataから直接UTIを得られる「magicForData:」 メソッドがとても便利そうで、コレが使いたかったのでテストしてみた次第です(まだそこまで試してないですけれど)。

本当のところは、UTIの文字列(例:”public.image”)を与えると、その下位に所属するUTIの一覧をArrayとかlistとかで返してくれるAPIがあるとよいのですが、、、うまく探しきれていないだけで「存在しないのはおかしい」感じではあります。

AppleScriptでも、choose file of typeコマンドでUTIの世界を垣間見ることはありますが、その階層構造を調べられないのは少々ストレスが溜まるところです。

AppleScript名:MagicKitで指定ファイルのUTI情報を取得
– Created 2017-10-25 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “MagicKit”
–https://github.com/aidansteele/magickit
–http://piyocast.com/as/archives/4922

set aFile to POSIX path of (choose file)
set aRes to current application’s GEMagicKit’s magicForFileAtPath:aFile
–>  (GEMagicResult) ISO Media, Apple QuickTime movie

set aMime to (aRes’s mimeType()) as string
–>  ”video/quicktime; charset=binary”

set utiList to (aRes’s uniformTypeHierarchy()) as list
–>  {”com.apple.quicktime-movie”, “public.movie”, “public.audiovisual-content”, “public.data”, “public.content”, “public.item”}

set aDesc to (aRes’s |description|()) as string
–>  ”ISO Media, Apple QuickTime movie”

★Click Here to Open This Script 

2017/10/22 CotEditorのコンソールにログ出力

CotEditor内蔵のコンソールウィンドウに任意の文字列を出力するAppleScriptです。

CotEditorの「ウィンドウ」メニューに「パネル」という項目があり、

cot21.png

ここで「コンソールパネル」が選択でき、フローティングパレットが表示されます(CotEditor v3.2.2にて確認)。

cot1.png

このパレットが用意された意図や経緯はわかりませんが(syslog経由でConsole.appに出せるのに)、おそらく内蔵Script Menuから実行する各種scriptのデバッグ出力のログ表示用と思われます(10:88ってなんだ???)。

CotEditorのAppleScript用語辞書に「write to console」というコマンドが用意されており、指定の文字列をこのコンソールパネルに出力できます。

cot3.png

このコンソール出力のためのAppleScriptを書いてためしてみました。テキストを出力することはできますが、リスト(配列)やレコード(dictionary)を出力することはできません。テキスト以外の形式のデータはテキストに変換して出力することになります。

なお、「コンソールパネル」はフローティングパレットUIなので、CotEditorが最前面にある時にしか表示されません。

辞書にあっても実際に機能するか、期待どおりに機能するかはやはり実際に試してみないとわかりません。

AppleScript名:CotEditorのコンソールにログ出力
–http://piyocast.com/as/archives/4918
tell application “CotEditor”
  activate
  
write to console “ぴよまるさんだよ”
end tell

★Click Here to Open This Script 

2017/10/21 macOS 10.13でPDFViewのcurrentPageにバグ

macOS 10.13上のPDFViewのcurrentPageメソッドにバグを見つけたものの、Appleに報告しても直しもしなさそうなので困っているという話です。

PDF表示部品であるPDFViewというクラスがあり、非常に便利に使っています。

pdfv1.png

PDFViewには、

pdfv2.png

currentPageというメソッドがあり、これを通じて「現在表示中のPDFの現在位置のページのオブジェクト(PDFPage)」を返してくれるようになっている

pdfv3.png

pdfv5.png

はずなんですが!!!(^ー^;;;

macOS 10.13でAppleScriptでcurrentPageを取得すると、PDFPageオブジェクトではなく謎の数値が返ってきます(ーー;;;

■macOS 10.12.6
2017-10-21 16:58:20.255660+0900 PDFViewTest[45351:95940914] <pdfpage : 0×600000414120>

■macOS 10.13.1(Build 17B35a)
2017-10-21 16:57:00.664314+0900 PDFViewTest[6109:764273] 105827998483136

前からそうですが、macOS 10.13あたりから露骨に、Appleにバグを直す気がないというか品質管理に関心がないという雰囲気なので、レポートしたところで直る見込みがあるんだかないんだか不明な今日このごろです。

このバグがAppleScriptから呼び出すとバグを発生するのか、Objective-CやSwiftでも同様なのか、問題の切り分けをCocoa勉強会で呼びかけて話をしていますが、どんなもんなんでしょうか。
→ Swiftで確認していただきました。Swiftでは問題ないとのこと。ScriptingBridgeなのか、、、、macOS 10.13つくづく問題多いな、、、(ーー;;;

とりあえず、currentPageで現在表示中のページを取得して管理する方式ではなく、アプリケーション側で現在表示中のページを管理する方式に変更すれば、このバグをAppleが放置しても回避できるだろうか、といったところです。

–> Download test project archive

スクリプト名:pdfviewtest.html

– AppDelegate.applescript
– PDFViewTest

– Created by 長野谷 隆昌 on 2017/10/20.
– Copyright © 2017年 Piyomaru Software. All rights reserved.

script AppDelegate
  
property parent : class “NSObject”
  
  
– IBOutlets
  
property theWindow : missing value
  
property tf1 : missing value–NSTextField
  
property leftPDF : missing value–PDFView
  
  
  
on applicationWillFinishLaunching:aNotification
    

  
end applicationWillFinishLaunching:
  
  
  
on applicationShouldTerminate_(sender)
    
return current application’s NSTerminateNow
  
end applicationShouldTerminate:
  
  
  
– click event handler
  
on clicked:aSender
    
set aTag to (tag of aSender) as integer
    
if aTag = 100 then
      
set aPOSIX to POSIX path of (choose file)
      
      
set aURL to current application’s |NSURL|’s fileURLWithPath:aPOSIX
      
      
set newPDFdoc to current application’s PDFDocument’s alloc()’s initWithURL:aURL
      
log newPDFdoc
      
–> –10.12.6
      
      
set leftTotalPage to newPDFdoc’s pageCount()
      
      leftPDF’s setDocument:newPDFdoc
      leftPDF’s setAutoScales:
true
      leftPDF’s setDisplaysPageBreaks:
true
      leftPDF’s goToFirstPage:(
missing value)
      
      
set curPage to (leftPDF’s currentPage())—–This method “currentPage”
      
log curPage
      
–> –10.12.6
      
      
set firstPage to (newPDFdoc’s pageAtIndex:0)’s label as string
      
log firstPage
      
    
end if
    
  
end clicked:
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2017/10/18 macOS 10.12から10.13のマシンを呼び出してAppleScriptをリモート実行

リモートAppleEventsを使ってmacOS 10.12上で編集中のScriptを10.13側で実行して結果を表示するScriptです。

リモートAppleEventsは、OSのバージョンが違っていても複数のMacを連携させたシステムを組めるので、知っている人は少ないとしても有用な機構です。ちょっと前のOSでないと動かない周辺機器(FAXとかドキュメントスキャナとか)を最新のOS環境から利用できたりもします(直接ドライブするのではなく、結果だけもらってきたり、入力があったことを通知したり)。

同一のLAN内で、固定のIPアドレスを振って運用しています。

figs.png

macOS 10.13側

macOS 10.13のMac側の「システム環境設定」の「共有」で、

share_resized.png

リモートAppleEventsをオンにします。AppleScriptをリモートで受信して実行するアプレット「RemoteAgent」を作成して起動しっぱなしの設定にして(アプレットとして保存するときに「ハンドラの実行後に終了しない」のオプションをオンに設定)保存し、起動したままにしておきます。アプレットの名称にはとくに意味とか必然性はありません。たまたま、そういう名前で書いたのでそうなってるだけです。

AppleScript名:RemoteAgent
– Created 2017-10-18 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “OSAKit”
use framework “AppKit”
–http://piyocast.com/as/archives/4906

on run
  
end run

on testMe(aParam)
  return aParam
end testMe

on getVers()
  set vers to (do shell script “defaults read /System/Library/CoreServices/SystemVersion ProductUserVisibleVersion”)
  
set build to (do shell script “defaults read /System/Library/CoreServices/SystemVersion ProductBuildVersion”)
  
return {vers, build}
end getVers

on execASstring(aString)
  
  
set aRect to current application’s NSMakeRect(0, 0, 500, 200)
  
  
–Make AppleScript Controller & Script Editor View
  
set osaCon to current application’s OSAScriptController’s alloc()’s init()
  
set osaView to current application’s OSAScriptView’s alloc()’s initWithFrame:aRect
  
  
–Make Result View
  
set resView to current application’s NSTextView’s alloc()’s initWithFrame:aRect
  
resView’s setRichText:true
  
resView’s useAllLigatures:true
  
  
–Connect OSAScriptController to Editor View & Result View
  
osaCon’s setScriptView:osaView
  
osaCon’s setResultView:resView
  
  
–Set AppleScript Source to Editor View & Execute it
  
set aSource to current application’s NSString’s stringWithString:aString
  
osaView’s setString:aSource
  
osaCon’s runScript:(missing value)
  
  
–Get AppleScript’s Result as string
  
set aRes to resView’s |string|() as string
  
  
return aRes
end execASstring

–指定AppleScriptファイルのソースコードを取得する(実行専用Scriptからは取得できない)
– Original Created 2014-02-23 Shane Stanley
on getASsourceFor(anAlias as {alias, string})
  set anHFSpath to anAlias as string
  
set aURL to current application’s |NSURL|’s fileURLWithPath:(POSIX path of anHFSpath)
  
set theScript to current application’s OSAScript’s alloc()’s initWithContentsOfURL:aURL |error|:(missing value)
  
return theScript’s source() as text
end getASsourceFor

★Click Here to Open This Script 

macOS 10.12側

macOS 10.12側で呼び出し用のScriptを書いてScript Menuに登録しておきます。Script中のUser名とパスワードはmacOS 10.13側のものを書いておきます。ここではmacOS 10.13側のIPアドレスを直接記述していますが「MBP11.local」のようなBonjour名称でももちろんOKです。

ここにユーザー名とパスワードを書かないと毎回ダイアログが出てユーザー名、パスワード、キーチェーンに保存するかなど聞かれますが、結局保存されなくて毎回聞かれるので、直接コード中に書いておくか、別途キーチェーンに保存して実行時に読み出してもいいでしょう。

AppleScript名:macOS 10.13でリモート実行
– Created 2017-10-18 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4906
tell application “Script Editor”
  tell front document
    set aSource to contents
  end tell
end tell

tell application “RemoteAgent” of machine “eppc://user:password@192.168.0.7″
  set {vRes, bRes} to getVers()
  
set aRes to execASString(aSource)
end tell

set vText to “macOS “ & vRes & ” (Build:” & bRes & “) Result:”
display dialog vText default answer aRes

★Click Here to Open This Script 

macos10126_1.png

こんなAppleScriptをScript Editor上でオープンした状態で(複数Window表示時には最前面であること)、Scriptを実行。

macos10126_2.png

LANごしにリモートAppleEventを実行し、macOS 10.13上のアプレットで指定のAppleScriptを動的にコンパイルして実行。結果をmacOS 10.12のマシン側に返してきます。

macos10126_3.png

こんな感じで動作確認を行なっています。ただリモート実行するだけなら、もうちょっとシンプルに書けたような気もしますが、Cocoa系の機能を使ってリモートScript実行を行なってみたときの部品をそのまま流用しています。

LAN経由、とくにWiFi経由でのリモートAppleEventsは通信にコストがかかる(時間がかかる)ため、パラメータだけ与えてまとめて結果をもらうようにするべきです。VPN経由で遠隔地のMacの操作を行うような場合にも同様です。

2017/10/18 QuartzComposerでグラフ表示てすと v4(10.13対応)

QuartzComposerのCompositionを任意のパラメータでレンダリングして表示するAppleScriptのmacOS 10.13.x対応版です。

composition.png

macOS 10.13で変更になったのはQuartzComposerまわりではなく、NSDictionary/NSMutableDictionaryまわりで、キーと値を列挙して、

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

set aDict to (current application’s NSMutableDictionary’s dictionaryWithObjectsAndKeys_(1, “aKey”, 2, “bKey”, missing value)) as record
–>  {bKey:2, aKey:1}

★Click Here to Open This Script 

などとNSDictionary/NSMutableDictionaryを作成するタイプのメソッド(末尾にmissing valueがつくもの)です。macOS 10.13上で実行すると、

1013error.jpg

このように(↑)エラーになるので、

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

set aDict to (current application’s NSDictionary’s dictionaryWithObjects:{1, 2} forKeys:{“aKey”, “bKey”}) as record
–> {bKey:2, aKey:1}

★Click Here to Open This Script 

などと書き換える必要があります(Thanks Shane!)。

前者の書き方については、書くのが面倒に感じていたので本Blog中でもかぞえるほどしか登場していませんでした。10.13向けの書き換え例としてこれが向いていると判断して掲載してみた次第です。QuartzComposer自体にはとくに思い入れもありません。

なお、表示対象のQuartzCompositionは、本Script Bundle中に入っていることを前提としていますが、バンドル外のものを表示するように書き換えるのも簡単なので実験してみるとよいでしょう。

comp_loc.png

本Script自体はmacOS 10.12上で作成して実行確認しています(当然、10.13.1beta上でも動作確認していますが)。10.13以降で追加になった機能を前提としていません。

–> Download script bundle including Composition

AppleScript名:QuartzComoserでグラフ表示てすと v4(10.13対応)
– Created 2015-11-03 by Takaaki Naganoya
– Modified 2017-10-18 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “Quartz”
use framework “AppKit”
use framework “Carbon” – AEInteractWithUser() is in Carbon
–http://piyocast.com/as/archives/4900

property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSWindowCloseButton : a reference to current application’s NSWindowCloseButton
property NSScreen : a reference to current application’s NSScreen
property NSPredicate : a reference to current application’s NSPredicate
property NSDictionary : a reference to current application’s NSDictionary
property NSBackingStoreBuffered : a reference to current application’s NSBackingStoreBuffered
property NSMutableArray : a reference to current application’s NSMutableArray
property NSTitledWindowMask : a reference to current application’s NSTitledWindowMask
property NSString : a reference to current application’s NSString
property NSWindow : a reference to current application’s NSWindow
property NSNumber : a reference to current application’s NSNumber
property NSNormalWindowLevel : a reference to current application’s NSNormalWindowLevel
property QCView : a reference to current application’s QCView
property NSColor : a reference to current application’s NSColor

if current application’s AEInteractWithUser(-1, missing value, missing value) is not equal to 0 then return

set chartData to NSMutableArray’s new()

–chartData’s addObject:(NSMutableDictionary’s dictionaryWithObjectsAndKeys_(”練馬区”, “label”, 3, “value”, missing value))–older way (Obsolete in 10.13)
chartData’s addObject:(my recWithLabels:{“label”, “value”} andValues:{“練馬区”, 3})
chartData’s addObject:(my recWithLabels:{“label”, “value”} andValues:{“青梅市”, 1})
chartData’s addObject:(my recWithLabels:{“label”, “value”} andValues:{“中野区”, 2})

–上記データの最大値を求める
set aMaxRec to chartData’s filteredArrayUsingPredicate:(NSPredicate’s predicateWithFormat_(“SELF.value == %@.@max.value”, chartData))
set aMax to value of aMaxRec
set aMaxVal to (first item of aMax) as integer

–Scalingの最大値を求める
if aMaxVal 10 then
  set aScaleMax to (10 div aMaxVal)
  
set aScaleMin to aScaleMax div 10
else
  set aScaleMax to (10 / aMaxVal)
  
set aScaleMin to 1
end if

try
  set aPath to path to resource “Chart.qtz”
on error
  return
end try

set qtPath to NSString’s stringWithString:(POSIX path of aPath)

set aView to QCView’s alloc()’s init()
set qtRes to (aView’s loadCompositionFromFile:qtPath)

aView’s setValue:chartData forInputKey:“Data”
aView’s setValue:(NSNumber’s numberWithFloat:(0.5)) forInputKey:“Scale”
aView’s setValue:(NSNumber’s numberWithFloat:(0.2)) forInputKey:“Spacing”
aView’s setAutostartsRendering:true

set maXFrameRate to aView’s maxRenderingFrameRate()

set aWin to (my makeWinWithView(aView, 800, 600, “AppleScript Composition Test”))

(aView’s setValue:(NSNumber’s numberWithFloat:aScaleMax / 10) forInputKey:“Scale”)
delay 5

my closeWin:aWin
aView’s stopRendering() –レンダリング停止

–make Window for Display
on makeWinWithView(aView, aWinWidth, aWinHeight, aTitle)
  set aScreen to NSScreen’s mainScreen()
  
set aFrame to {{0, 0}, {aWinWidth, aWinHeight}}
  
  
set aBacking to NSTitledWindowMask
  
  
set aDefer to NSBackingStoreBuffered
  
  
– Window
  
set aWin to NSWindow’s alloc()
  (
aWin’s initWithContentRect:aFrame styleMask:aBacking backing:aDefer defer:false screen:aScreen)
  
aWin’s setBackgroundColor:(NSColor’s whiteColor())
  
  
aWin’s setTitle:aTitle
  
aWin’s setDelegate:me
  
aWin’s setDisplaysWhenScreenProfileChanges:true
  
aWin’s setHasShadow:true
  
aWin’s setIgnoresMouseEvents:false
  
aWin’s setLevel:(NSNormalWindowLevel)
  
aWin’s setOpaque:false
  
aWin’s setReleasedWhenClosed:true
  
aWin’s |center|()
  
aWin’s makeKeyAndOrderFront:(me)
  
–aWin’s movableByWindowBackground:true
  
  
– Set Custom View
  
aWin’s setContentView:aView
  
  
–Set Close Button  
  
set closeButton to NSWindow’s standardWindowButton:(NSWindowCloseButton) forStyleMask:(NSTitledWindowMask)
  
  
return aWin
end makeWinWithView

–close win
on closeWin:aWindow
  repeat with n from 10 to 1 by -1
    (aWindow’s setAlphaValue:n / 10)
    
delay 0.02
  end repeat
  
aWindow’s |close|()
end closeWin:

on recWithLabels:theKeys andValues:theValues
  return (NSDictionary’s dictionaryWithObjects:theValues forKeys:theKeys) as record
end recWithLabels:andValues:

★Click Here to Open This Script 

2017/10/17 Double PDFアップデート作業中

Mac App Store上で発売中の「Double PDF」が、AppleがmacOS 10.13上で作成してくれやがった非常識なバグの影響を受けてmacOS 10.13のGM以降で正常に動作していないことが判明(初期のmacOS 10.13betaで動作確認していた時は動いていたのに ーー;)。

AppleがOSに作成したバグに対処するために、小規模なアップデートを行います。macOS 10.13.1のbetaが出ていますが、この段階で直っていないので、10.13.2か10.13.3以降まで直らないはず(つまり、macOS 10.13.xのこのあたりの問題は今年中は直らない=今年中はメイン環境の移行は行わない)。

さっさとAppleがバグを修正してくれれば、こちらで作業をする必要は何もないわけですが、、、

いちいち余計な処理を行うと処理速度が低下するわけで、実に腹立たしいところです。

NSRect関連:
NSMakeRect、NSZeroRect、NSRectをパラメータにするAPIの呼び出し箇所。

PDFKit関連:
10.13で変更が加わった箇所の動作確認が必要(ーー;;

2017/10/16 並列ダウンロードのじっけん v2

AppleScriptによる狂気のthreadごっこ。並列ダウンロードの実験AppleScriptです。

昨日のScriptを実際に何度も実行してみて「Threadを生成しても順次実行されているようにしか見えないのはなぜだろう?」などと思いつつ、スレッドのオブジェクトを配列に突っ込んでおいて、一括で実行命令(start:)を行うように改良してみました。

また、スレッド内で実行する内容に単なるカウントという(安全ではあるものの)無意味な処理ではなく、ファイルのダウンロード実行を行なっています。

自分的にはいろいろとブレークスルーな内容ではあるものの、ダウンロードにしてもThread処理するよりも逐次実行したほうが速そうな感じであるのと、shellのcurlコマンドで並列でダウンロードしたほうが安全で速そうとか、かなり「不可能にチャレンジした以外にあまりほめるべき点がない」という雰囲気になっています。

ダウンロード先のURLが数個程度だと何も問題はないものの、数十個指定すると帰ってこなくなるなど、何か問題が起きているようなので、あくまで「実験」レベルの実装です。

AppleScript名:並列ダウンロードのじっけん v2
– Created 2015-08-20 by Takaaki Naganoya
– Created 2017-10-16 by Takaaki Naganoya
– 2015 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4898

property aProp : {}
property aMax : {}

set aList to {“http://piyocast.com/as/wp-content/uploads/2016/08/xbook1_ver2.png.pagespeed.ic.1UE9W7-aVC.png”, “http://piyocast.com/as/wp-content/uploads/2016/08/xbook2_v2.png.pagespeed.ic.uWjZsXaLOP.png”, “http://piyocast.com/as/wp-content/uploads/2016/09/xscript_assistant.jpg.pagespeed.ic.fk8YHumFYV.jpg”}

set aProp to {}
set thList to current application’s NSMutableArray’s new()
set aMax to length of aList
set aCount to 1

repeat with i in aList
  set j to contents of i
  
set aURL to (current application’s |NSURL|’s URLWithString:j)
  
set aThread to (current application’s NSThread’s alloc()’s initWithTarget:me selector:“_threadLoop:” object:aURL)
  (
aThread’s setName:(“thread “ & (aCount as string)))
  (
thList’s addObject:aThread)
  
  
set noter1 to current application’s NSNotificationCenter’s defaultCenter()
  (
noter1’s addObserver:me selector:“_threadWillExit:” |name|:(current application’s NSThreadWillExitNotification) object:(thList’s lastObject()))
  
  
set aThread to “” –purge object from memory
  
set aCount to aCount + 1
end repeat

thList’s makeObjectsPerformSelector:“start” withObject:0

repeat 100000 times
  if length of aProp aMax then exit repeat
  
delay “0.0001″ as real
end repeat

–各Threadが実行するハンドラ
on _threadLoop:aInfo
  checkURLResourceExistence(aInfo, 10) of me
end _threadLoop:

–Threadが終了する際に呼ばれるハンドラ
on _threadWillExit:aNotification
  set tmpRes to aNotification’s object’s |name|()
  
log tmpRes as string
end _threadWillExit:

– 指定URLにファイル(画像など)が存在するかチェック
–> {存在確認結果(boolean), レスポンスヘッダー(NSDictionary), データ(NSData)}
on checkURLResourceExistence(aURL, timeOutSec as real)
  set aRequest to (current application’s NSURLRequest’s requestWithURL:aURL cachePolicy:(current application’s NSURLRequestUseProtocolCachePolicy) timeoutInterval:timeOutSec)
  
set aRes to (current application’s NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value))
  
set dRes to (first item of (aRes as list))
  
set bRes to (second item of (aRes as list))
  
if bRes is not equal to missing value then
    set hRes to (bRes’s allHeaderFields())
    
set aResCode to (bRes’s statusCode()) as integer
  else
    set hRes to {}
    
set aResCode to -1 –error
  end if
  
set the end of aProp to {(aResCode = 200), hRes, dRes}
end checkURLResourceExistence

★Click Here to Open This Script 

2017/10/15 スレッド処理

AppleScriptでスレッドを生成して実行するサンプルです。

実際にスレッドを生成して、実行して、各スレッドの状態を調査することができます。

REST APIなど、実行に時間のかかるネットワーク系の処理で使えないかと思って試作を行なっていたものです。

書いてから2年以上も放置していたのは、「スレッドセーフなルーチンを頑張って書くよりも、個別にアプレットに書き出して同時実行したほうが安全だしお手軽」という理由によります。スレッド内で実行してクラッシュしないように書くのはとても大変です(ーー;

AppleScript名:スレッド処理
– Created 2015-08-20 by Takaaki Naganoya
– 2015 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4897

set aThread to current application’s NSThread’s alloc()’s initWithTarget:me selector:“_threadLoop:” object:(current application’s NSString’s stringWithString:“Apple”)

set bThread to current application’s NSThread’s alloc()’s initWithTarget:me selector:“_threadLoop:” object:(current application’s NSString’s stringWithString:“Orange”)

set aRes to current application’s NSThread’s isMultiThreaded()
–>  true

set bRes to current application’s NSThread’s currentThread()
–>  (NSThread) {number = 1, name = main}

set a1Res to current application’s NSThread’s currentThread()’s threadDictionary()
–>  (NSDictionary) {NSAppleEventManagerHandlingStack:{}, NSDocumentsContinuingFileAccess:(NSSet) {}, OSADefaultComponentInstanceKey:(OSAComponentInstance) , OSADefaultLanguageKey:(OSALanguage) , OSAAvailableLanguagesKey:{(OSALanguage) , (OSALanguage) , (OSALanguage) }}

set cRes to current application’s NSThread’s callStackReturnAddresses()
–>  (NSArray) {140735481653180, 140735481652754, 140735559055105, 140735559057361, 4473123264, 4473263701, 4473142396, 4473141941, 4472935365, 4472916653, 140735705914913, 140735481264735, 4400422881, 4400227287, 140735611717816, 140735611717253, 140735611714364, 140735611713043, 140735453174803, 140735453224127, 140735482160121, 140735481878159, 140735481875416, 140735475504495, 140735475503850, 140735475503403, 140735666104491, 140735666101848, 140735666060019, 140735665521220, 140735553762761, 1}

set dRes to current application’s NSThread’s callStackSymbols()
–>  (NSArray) {”0 CoreFoundation 0×00007fff886427bc __invoking___ + 140″, ” …… (omit)

aThread’s setName:“Apple”
bThread’s setName:“Orange”

aThread’s |threadPriority|()
–> 0.5 –(0.0〜1.0)

set c1Res to aThread’s |name|()
–>  (NSString) “Apple”
set c2Res to bThread’s |name|()
–>  (NSString) “Orange”

aThread’s threadDictionary()
–>  (NSDictionary) {}

aThread’s stackSize()
–>  524288 –this depends on each machine’s memory configuration?

aThread’s isExecuting()
–>  false

aThread’s isFinished()
–>  false

aThread’s isCancelled()
–>  false

set noter1 to current application’s NSNotificationCenter’s defaultCenter()
noter1’s addObserver:me selector:“_threadWillExit:” |name|:(current application’s NSThreadWillExitNotification) object:aThread
noter1’s addObserver:me selector:“_threadWillExit:” |name|:(current application’s NSThreadWillExitNotification) object:bThread

aThread’s start()
bThread’s start()

–各Threadが実行するハンドラ
on _threadLoop:aInfo
  repeat 3 times
    set aText to aInfo as text
    
do shell script “logger -s “ & quoted form of aText & ” &”
    
set aNum to random number from 1 to 3
    
delay aNum
  end repeat
end _threadLoop:

–Threadが終了する際に呼ばれるハンドラ
on _threadWillExit:aNotification
  
  
set tmpRes to aNotification’s object’s |name|()
  
say ((tmpRes as text) & ” finished.”) using “Alex”
  
end _threadWillExit:

★Click Here to Open This Script 

2017/10/14 指定EnumがどのFrameworkに所属しているか検索 v2

文字列として与えたCocoaのEnumがどのFrameworkに所属しているかを検索するAppleScriptです。

macOS 10.12.6+Xcode 9.0、macOS 10.13.1beta+Xcode 9.0でためしてみました。指定クラスがどのFrameworkに所属しているかを調べるAppleScriptとは異なり、Enumの定義がどのFrameworkに所属している(らしい)かを調べるものです。Header Fileを調べまくって指定の文字列が入っているFramework名をリストアップします。

このために、「Class名として評価できない」ことを確認してから処理しています。

Xcodeのバンドル内のSDKの奥深くに存在するヘッダーファイル群に対してSpotlightで検索したらヒットしなかったので、(こんな時のために整備しておいた)NSFileManager経由でのファイル取得ルーチンによりHeader Fileを検索しています。

何かの正解というわけではなく、だいたいこのあたり・・・という「あたり」をつけるためのものです。開発環境で実行すると3.5sec程度かかります。

AppleScript名:指定EnumがどのFrameworkに所属しているか検索 v2
– Created 2017-10-13 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
–http://piyocast.com/as/archives/4896

property NSFileManager : a reference to current application’s NSFileManager
property NSString : a reference to current application’s NSString
property NSPredicate : a reference to current application’s NSPredicate
property NSMutableArray : a reference to current application’s NSMutableArray
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding

set a1Res to searchEnumFromHeaderFiles(“NSUTF8StringEncoding”) of me
–>  {”DiscRecording.framework”, “Foundation.framework”, “SpriteKit.framework”}

set a2Res to searchEnumFromHeaderFiles(“NSNumberFormatterRoundUp”) of me
–>  {”Foundation.framework”}

set a3Res to searchEnumFromHeaderFiles(“NSParagraphStyleAttributeName”) of me
–>  {”AppKit.framework”}

on searchEnumFromHeaderFiles(targString)
  set aClass to current application’s NSClassFromString(targString)
  
if aClass is not equal to missing value then return false
  
  
set dPath to POSIX path of (path to application id “com.apple.dt.Xcode”)
  
set aFol to dPath & “Contents/Developer/Platforms/MacOSX.platform/” & “Developer/SDKs/MacOSX.sdk/System/Library/Frameworks”
  
  
set bList to retFullPathWithinAFolderWithRecursiveFilterByExt(aFol, “h”) of me
  
set matchedList to {}
  
  
repeat with i in bList
    set j to contents of i
    
    
set aStr to (NSString’s stringWithContentsOfFile:j encoding:NSUTF8StringEncoding |error|:(missing value))
    
if aStrmissing value then
      set aRange to (aStr’s rangeOfString:targString)
      
      
if aRange’s location() ≠ current application’s NSNotFound and (aRange’s location()) < 9.99999999E+8 then
        set tmpStr to (current application’s NSString’s stringWithString:j)
        
set pathList to tmpStr’s pathComponents()
        
set thePred to (current application’s NSPredicate’s predicateWithFormat:“pathExtension == ’framework’”)
        
set aRes to (pathList’s filteredArrayUsingPredicate:thePred)’s firstObject() as text
        
set the end of matchedList to aRes
      end if
      
    end if
  end repeat
  
  
set aArray to current application’s NSArray’s arrayWithArray:matchedList
  
set bArray to aArray’s valueForKeyPath:“@distinctUnionOfObjects.self”
  
return bArray as list
end searchEnumFromHeaderFiles

–指定フォルダ以下のすべてのファイルを再帰で取得(拡張子で絞り込み)
on retFullPathWithinAFolderWithRecursiveFilterByExt(aFol, aExt)
  set anArray to NSMutableArray’s array()
  
set aPath to NSString’s stringWithString:aFol
  
set dirEnum to NSFileManager’s defaultManager()’s enumeratorAtPath:aPath
  
  
repeat
    set aName to (dirEnum’s nextObject())
    
if aName = missing value then exit repeat
    
set aFullPath to aPath’s stringByAppendingPathComponent:aName
    
anArray’s addObject:aFullPath
  end repeat
  
  
set thePred to NSPredicate’s predicateWithFormat:“pathExtension == [c]%@” argumentArray:{aExt}
  
set bArray to anArray’s filteredArrayUsingPredicate:thePred
  
  
return bArray as list
end retFullPathWithinAFolderWithRecursiveFilterByExt

★Click Here to Open This Script 

2017/10/14 指定クラスがどのFrameworkに所属しているか検索 v3

文字列として与えたCocoaのClassがどのFrameworkに所属しているかを検索するAppleScriptのShane Stanleyによる改修版に微修正を加えたものです。

Classの所属するバンドルを求めるNSBundle’s bundleForClass:なんてものがあるとは知りませんでした。しかも、野蛮にbridgesupportファイルをオープンして検索して回るわけでもないので、1回あたりの実行時間が0.001秒以下。これはすごい(^ー^;

ただ、そうはいっても万能ではないので、参考程度といったところでしょうか(それでもオリジナル版より高機能)。

AppleScript名:指定クラスがどのFrameworkに所属しているか検索 v3
– Created 2017-10-14 by Shane Stanley
– Modified 2017-10-14 by Takaaki Naganoya
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4895

set fRes1 to searchClassInFrameworks(“JSContext”) of me
–>  ”JavaScriptCore.framework”

set fRes2 to searchClassInFrameworks(“NSApplication”) of me
–>  ”AppKit.framework”

set fRes3 to searchClassInFrameworks(“NSRect”) of me
–>  false

set fRes4 to searchClassInFrameworks(“PDFPage”) of me
–>  ”Quartz.framework”

set fRes5 to searchClassInFrameworks(“NSUTF8StringEncoding”) of me
–>  false

set fRes6 to searchClassInFrameworks(“CIColor”) of me
–>  ”CoreImage.framework”

on searchClassInFrameworks(aTarget)
  set aClass to current application’s NSClassFromString(aTarget)
  
if aClass = missing value then return false
  
set theComponenents to (current application’s NSBundle’s bundleForClass:aClass)’s bundleURL’s pathComponents()
  
set thePred to current application’s NSPredicate’s predicateWithFormat:“pathExtension == ’framework’”
  
set aRes to (theComponenents’s filteredArrayUsingPredicate:thePred)’s firstObject() as text
  
return aRes
end searchClassInFrameworks

★Click Here to Open This Script 

2017/10/13 指定クラスがどのFrameworkに所属しているか検索

文字列として与えたCocoaのClassがどのFrameworkに所属しているかを検索するAppleScriptです。Spotlight検索のためにShane Stanleyの「Metadata Lib」のインストールを必要とします。

割と切実に欲しかったAppleScriptです。よくShane Stanleyから「そのクラス呼ぶんだったら、このFrameworkをuseで宣言しとかないとダメだぞー」と小突かれているので、「自動でチェックしてえなぁ(涙)」と思っていたところでした。

use文によるFrameworkの使用宣言は、AppleScriptそれ自体で宣言していなくても、Script Editor側で使用宣言しているとそのまま動いてしまったりするパターンがあるため、割と見過ごしてしまう例がありました(Script Editor上でエラーメッセージ出ないし)。

わざわざAppleのWeb Referenceを検索したりと、本質的でない不毛な作業が必要。

そこで、Class名を文字列で与えるとどのFrameworkをuseしないといけないのかを調べる本AppleScriptを書いたわけです。

ながらく「書きたい」と思っていたわけですが、どこを手がかりにすればよいか長らくわかっていませんでした。

それが、macOS 10.13.0のScripting Bridgeのバグに直面して、Scripting Bridgeの「仕組み」そのものについて自分でも調べる機会がありました(正確に把握するため、どこがダメでどこでAppleがミスしたのか追調査)。

すると、macOS内の/System/Library/Frameworksフォルダ内にあるApple純正フレームワークの中に「(フレームワーク名).bridgesupport」という記述ファイルが存在しており、この中にScripting Bridge経由で機能を公開する内容が書いてあることが見て取れました。

macOS 10.13.0では、この.bridgesupportファイルの記述が間違っていた、というのがShane Stanleyの指摘です。

あれ??? この.bridgesupportファイルの中には当該フレームワークが他の言語に対して公開しているクラス名やメソッド名などの一覧が書かれているわけで、この中を検索すれば、目的のクラスを使うにはどのフレームワークをuseコマンドで参照するように宣言すればよいかわかる???? 

完全にもともとの用途とは違う使い道ですが、わかることはわかる(はず)。

冗談半分で書いてみたら、自分の目的を果たすことができました。1回の検索あたり0.7〜1.1秒程度です。結果をキャッシュするとか、検索するごとに毎回読むのをやめるとかすればもっと大幅に高速化はできるものと思われますが、そこまで気合いを入れる内容ではないのでこんな感じでしょうか。

ただし、例外で「CIColor」がみつかりません。ほかにも、この方法で見つけられないものがあるかもしれません。思いつきを形にしたぐらいの内容なので、ミスがあってもご容赦を。

AppleScript名:指定クラスがどのFrameworkに所属しているか検索
– Created 2017-10-13 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use mdLib : script "Metadata Lib" version "1.0.0"
–http://piyocast.com/as/archives/4894

property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding

set t1Res to searchInBridgeSupport("NSString") of me
–>  {"Foundation.framework"}

set t2Res to searchInBridgeSupport("CLLocation") of me
–>  {"MapKit.framework", "CoreLocation.framework"}

set t3Res to searchInBridgeSupport("NSRect") of me
–>  {"WebKit.framework", "Foundation.framework", "AppKit.framework", "ScreenSaver.framework", "Quartz.framework", "Quartz.framework", "Carbon.framework"}

on searchInBridgeSupport(aKeyword)
  set theFolder to "/System/Library/Frameworks/"
  
set theFiles to mdLib’s searchFolders:{theFolder} searchString:"kMDItemDisplayName ENDSWITH %@" searchArgs:{".bridgesupport"}
  
  
set keyList to {}
  
set the end of keyList to "<class name=’" & aKeyword & "’>"
  
set the end of keyList to aKeyword
  
  
repeat with ii in keyList
    set matchedList to {}
    
set targString to contents of ii
    
    
repeat with i in theFiles
      set j to contents of i
      
      
set aStr to (NSString’s stringWithContentsOfFile:j encoding:NSUTF8StringEncoding |error|:(missing value))
      
set aRange to (aStr’s rangeOfString:targString)
      
      
if aRange’s location() ≠ current application’s NSNotFound and (aRange’s location()) < 9.99999999E+8 then
        if j does not contain "PyObjC" then –ignore PyObjC’s bridge support
          set tmpStr to (current application’s NSString’s stringWithString:j)
          
set pathList to tmpStr’s pathComponents() as list
          
set pathRes to contents of item 5 of pathList
          
set the end of matchedList to pathRes
        end if
      end if
    end repeat
    
    
if length of matchedList > 0 then
      return matchedList
    end if
    
  end repeat
  
  
return {}
end searchInBridgeSupport

★Click Here to Open This Script 

2017/10/12 リストの連結(Cocoa版やや高速版)

リスト(配列)の連結を行うAppleScriptのCocoa版の高速版です。

自分でも実験してCocoa版の実行速度が遅すぎたので不思議に思っていましたが、Shane Stanleyからツッコミがあって、「こう書くと速いよ」と教えてもらいました。

自分の環境で実行してみたところ、0.5 Sec前後。Pure AppleScriptの限界チューニング版(0.069 Sec)よりは10倍ぐらい遅いですが、arrayByAddingObjectsFromArray:で連結したときよりは10倍高速。このぐらいだと(Cocoa呼び出しで余計にかかる処理時間も)納得できる感じでしょうか。

AppleScript名:リストの連結(addObjectsFromArray)
– Created 2017-10-12 by Shane Stanley
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4893

property NSMutableArray : a reference to current application’s NSMutableArray

set anArray to NSMutableArray’s new()

repeat 10000 times
  anArray’s addObjectsFromArray:{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
end repeat

return anArray

★Click Here to Open This Script 

2017/10/11 リストの連結

リスト(配列)の連結を行うAppleScriptのCocoa版です。

1Dリスト(1次元配列)同士の連結を行うのは、Pure AppleScriptだと、

set aList to {}
set bList to {1, 2, 3}
set cList to {4, 5, 6}

set the end of aList to bList
set the end of aList to cList
aList
–> {{1, 2, 3}, {4, 5, 6}}

★Click Here to Open This Script 

こういうやり方と、

set aList to {2, 3, 4, 5, 6}
set bList to {1, 2, 3, 4, 5}

aList & bList
–> {2, 3, 4, 5, 6, 1, 2, 3, 4, 5}

★Click Here to Open This Script 

というやり方があるわけですが、Cocoaの機能を用いたAppleScriptObjCだとリストの要素連結だと、

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

set anArray to current application’s NSMutableArray’s new()
anArray’s addObject:{1, 2, 3}
anArray’s addObject:{4, 5, 6}
anArray as list
–>  {{1, 2, 3}, {4, 5, 6}}

★Click Here to Open This Script 

これを使うことが多かったのですが、リストごと連結するパターンを試していなかったので、調べてみました。

AppleScript名:リストの連結(arrayByAddingObjectsFromArray)
– Created 2017-10-10 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4892

property NSMutableArray : a reference to current application’s NSMutableArray

set anArray to NSMutableArray’s new()

repeat 10000 times
  set anArray to anArray’s arrayByAddingObjectsFromArray:{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
end repeat

return (anArray as list)
–>  {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10…….}
–4.89sec

★Click Here to Open This Script 

ただし、小さいデータをこまかく連結する程度だとPure AppleScriptの方が速いので、巨大なデータを扱うとかXcode上でCocoa AppleScriptアプレットを作るためにサブルーチン間でCocoaオブジェクトをやりとりするような用途の場合に意識する程度でしょうか。

同じ要素数(10万アイテムの連結)でもPure AppleScriptで限界までチューニングして高速化すると0.069秒で処理終了します。

2017/10/10 なろう小説APIで各カテゴリごとの集計を実行

「小説家になろう」サイトのAPI「なろう小説API」を呼び出して、カテゴリごとの該当件数を集計するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。

本AppleScriptは、「小説家になろう」掲載作品を、大カテゴリと小カテゴリでコードを指定して、ループで存在件数の集計を行います。カテゴリごとに分布が偏っているようなので、該当件数が0件のカテゴリは結果出力しないようにしています。筆者の環境では集計に22〜25秒ぐらいかかっています(インターネット接続回線速度に依存)。

http headerにgzip転送リクエスト要求を書きつつ、実際のデータ自体もgzipで圧縮されているので、二重に圧縮している状態です。実測したところ、http headerでgzip指定を行なったほうがトータルで1秒程度速かったので「そんなもんかな」と思いつつ、そのままにしています。

Web APIからのデータ受信時のNSDataからのZip展開にオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用しています。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

ジャンルは数値で指定するようになっていますが、その数値が何を示しているかという情報はAPI側からの出力にはないので、Webサイト上から文字情報をコピペで取得し、AppleScript内に記載して(ハードコーディング)カテゴリコードリストと照合して出力しています。

実際に集計してみると、ノンカテゴリが53%ということで、カテゴリ分けの機能が有効に活用されていないことが見てとれます。

そのことについては運営側も重々承知しているようで、APIの検索オプションに「キーワードに異世界転生があるものを含む」といったものがあるなど、ジャンルよりもキーワード重視するようにしているようです。

そういいつつも、使われているキーワードについては若干の表記ゆらぎがあるようで、単純にこのオプションを指定しても「異世界転生もの」をすべて抽出できていないように見えます。キーワード自体にどの程度「表記揺れ」が存在しているのかを調べてみるとよいのかもしれません。

APIの仕様上、2,000件しか詳細データを取得できないように見えるので、そのあたりがちょっと気になります(どうも全数調査を行いにくい仕様)。

分析するまでもなく、異世界転生ものが多く、ノンジャンル作品でも異世界転生ものばっかりという印象です。掘り出しもので「ソ連の宇宙技術は最強過ぎたのだが、それを西側諸国が完全に理解したのはつい最近だった」という作品に行き当たり、これが強烈に面白いです。

AppleScript名:なろう小説APIで各カテゴリごとの集計を実行
– Created 2017-10-10 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4891
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSNumberFormatterRoundUp : a reference to current application’s NSNumberFormatterRoundUp
property NSNumberFormatterRoundDown : a reference to current application’s NSNumberFormatterRoundDown

set invList to {}

set bgList to {1, 2, 3, 4, 99, 98}
set bigGnereLabel to {“恋愛”, “ファンタジー”, “文芸”, “SF”, “その他”, “ノンジャンル”}

set gList to {101, 102, 201, 202, 301, 302, 303, 304, 305, 306, 307, 401, 402, 403, 404, 9901, 9902, 9903, 9904, 9999, 9801}
set smlGenreLabel to {“異世界〔恋愛〕”, “現実世界〔恋愛〕”, “ハイファンタジー〔ファンタジー〕”, “ローファンタジー〔ファンタジー〕”, “純文学〔文芸〕”, “ヒューマンドラマ〔文芸〕”, “歴史〔文芸〕”, “推理〔文芸〕”, “ホラー〔文芸〕”, “アクション〔文芸〕”, “コメディー〔文芸〕”, “VRゲーム〔SF〕”, “宇宙〔SF〕”, “空想科学〔SF〕”, “パニック〔SF〕”, “童話〔その他〕”, “詩〔その他〕”, “エッセイ〔その他〕”, “リプレイ〔その他〕”, “その他〔その他〕”, “ノンジャンル〔ノンジャンル〕”}

–全体の件数取得
set aRec to {gzip:“5″, out:“json”, lim:“1″}
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
set wholeCount to (allCount of first item of aRESTres)

–カテゴリごとの集計
repeat with i in bgList
  repeat with ii in gList
    set aRec to {gzip:“5″, biggenre:i as string, genre:ii as string, out:“json”, lim:“1″}
    
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
    
set aTotal to allCount of first item of aRESTres
    
    
if aTotal is not equal to 0 then
      set big to contents of i
      
set small to contents of ii
      
set bigLabel to getLabelFromNum(bgList, bigGnereLabel, big) of me
      
set smlLabel to getLabelFromNum(gList, smlGenreLabel, small) of me
      
set aPerCentatge to roundingDownNumStr(((aTotal / wholeCount) * 100), 1) of me
      
set the end of invList to {biggenre:bigLabel, genre:smlLabel, totalNum:aTotal, percentage:aPerCentatge}
    end if
  end repeat
end repeat

set bList to sortRecListByLabel(invList, “totalNum”, false) of me –降順ソート
–> {{totalNum:274072, biggenre:”ノンジャンル”, percentage:53.1, genre:”ノンジャンル〔ノンジャンル〕”}, {totalNum:47121, biggenre:”ファンタジー”, percentage:9.1, genre:”ハイファンタジー〔ファンタジー〕”}, {totalNum:28883, biggenre:”恋愛”, percentage:5.6, genre:”現実世界〔恋愛〕”}, {totalNum:23217, biggenre:”文芸”, percentage:4.5, genre:”ヒューマンドラマ〔文芸〕”}, {totalNum:21320, biggenre:”ファンタジー”, percentage:4.1, genre:”ローファンタジー〔ファンタジー〕”}, {totalNum:17079, biggenre:”恋愛”, percentage:3.3, genre:”異世界〔恋愛〕”}, {totalNum:16798, biggenre:”その他”, percentage:3.2, genre:”その他〔その他〕”}, {totalNum:13892, biggenre:”その他”, percentage:2.6, genre:”詩〔その他〕”}, {totalNum:13341, biggenre:”文芸”, percentage:2.5, genre:”コメディー〔文芸〕”}, {totalNum:10120, biggenre:”文芸”, percentage:1.9, genre:”ホラー〔文芸〕”}, {totalNum:9502, biggenre:”その他”, percentage:1.8, genre:”エッセイ〔その他〕”}, {totalNum:8486, biggenre:”文芸”, percentage:1.6, genre:”純文学〔文芸〕”}, {totalNum:7211, biggenre:”文芸”, percentage:1.3, genre:”アクション〔文芸〕”}, {totalNum:6199, biggenre:”SF”, percentage:1.2, genre:”空想科学〔SF〕”}, {totalNum:5780, biggenre:”その他”, percentage:1.1, genre:”童話〔その他〕”}, {totalNum:3295, biggenre:”文芸”, percentage:0.6, genre:”推理〔文芸〕”}, {totalNum:3217, biggenre:”文芸”, percentage:0.6, genre:”歴史〔文芸〕”}, {totalNum:2606, biggenre:”SF”, percentage:0.5, genre:”VRゲーム〔SF〕”}, {totalNum:1471, biggenre:”SF”, percentage:0.2, genre:”パニック〔SF〕”}, {totalNum:1454, biggenre:”SF”, percentage:0.2, genre:”宇宙〔SF〕”}, {totalNum:190, biggenre:”その他”, percentage:0.0, genre:”リプレイ〔その他〕”}}

on callNarouAPI(aRec, callFrom, callNum)
  set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
  
  
–set aRec to {gzip:”5″, |st|:callFrom as string, out:”json”, lim:callNum as string}
  
set aURL to retURLwithParams(reqURLStr, aRec) of me
  
set aRes to callRestGETAPIAndParseResults(aURL) of me
  
  
set aRESCode to (responseCode of aRes) as integer
  
if aRESCode is not equal to 200 then return false
  
  
set aRESHeader to responseHeader of aRes
  
set aRESTres to (json of aRes) as list
  
end callNarouAPI

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList as list, aLabelStr as string, ascendF as boolean)
  set aArray to NSArray’s arrayWithArray:aRecList
  
  
set sortDesc to NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
  
set bList to sortedArray as list
  
return bList
end sortRecListByLabel

on getLabelFromNum(aList, labelLIst, aNum)
  set aInd to offsetOf(aList, aNum) of me
  
set anItem to contents of item aInd of labelLIst
  
return anItem
end getLabelFromNum

on offsetOf(aList as list, aTarg)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set aIndex to aArray’s indexOfObjectIdenticalTo:aTarg
  
return (aIndex + 1)
end offsetOf

on roundingDownNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundDown)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingDownNumStr

on roundingUpNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundUp)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingUpNumStr

★Click Here to Open This Script 

2017/10/07 なろう小説APIを呼び出す v2(Zip展開つき)

「小説家になろう」サイトのAPI「なろう小説API」を呼び出してデータを取得するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。用意されているのはデータ取得のメソッドであり、結果の保持や集計、しぼりこみについてはローカル側で勝手に行う放任仕様です条件抽出のパラメータもあります。

ただし、本APIが用意しているZip圧縮を利用するのに少々骨が折れました。

http header中でContent-Encodingに「gzip」を指定する程度では対応できませんでした。Zip圧縮転送指定時(パラメータにgzipを指定)APIから返ってくるデータそのものがZip圧縮されており、APIから返ってきたデータそのもの(NSData)を展開する必要がありました。

そこで、NSDataをそのままZip展開できるオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用してみました。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

とりあえず呼び出して500件のデータを取得していますが、500件ずつ取得してループするように書き換えるとよいでしょう。

AppleScript名:なろう小説API v2(Zip展開つき)
– Created 2017-10-03 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4890
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection

set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
–set reqURLStr to “http://api.syosetu.com/novel18api/api/”–18禁API

set aRec to {gzip:“5″, out:“json”, lim:“500″}

set aURL to retURLwithParams(reqURLStr, aRec) of me

set aRes to callRestGETAPIAndParseResults(aURL) of me

set aRESCode to (responseCode of aRes) as integer
if aRESCode is not equal to 200 then return false

set aRESHeader to responseHeader of aRes
set aRESTres to (json of aRes) as list

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

★Click Here to Open This Script 

2017/10/07 Keynoteで現在表示中のスライド上にある表のカラム幅を自動調整 v2

Keynoteでオープン中の最前面のドキュメントの現在表示中のスライド上にある表のカラム幅を自動調整するAppleScriptのアップデート版です。

Keynote v7.3+macOS 10.12.6/macOS 10.13.1で動作検証を行なっています。

前バージョンでは「わかりやすさ」を強調したので、実行速度がいまひとつでした。

表のセル幅程度であれば「ちょっと動きが見える」程度のかわいらしい「遅さ」でしたが、これがInDesign上のTextFrame中の数百個の連続する文字だった場合には、属性情報を変更するだけでもうんざりするほど時間がかかります。

しかし、アホのようにループで順次処理しなくても、範囲を指定してまとめて命令を投げれば大幅に実行速度が向上するケースがあります(アプリケーション側の対応次第なので、実際にためしてみる必要はあります)。

Keynote 7.3で実際にScriptを動かしてみたところ、

 ループで列幅変更:0.146sec
 範囲指定して一括変更:0.06sec

と、ずいぶん処理時間が短縮されました。前バージョンは動作が目で見てわかる程度でしたが、本バージョンでは実行するとすぐに動作が終わって列幅が変更される感じです。

実際にためして有効かどうかチェックを行なっておく必要はありますが、オブジェクトへのアクセス方法次第で処理速度が大幅に変わることがあるので知っておいて損はないでしょう。

AppleScript名:Keynoteで現在表示中のスライド上にある表のカラム幅を自動調整 v2
– Created 2017-10-06 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4889
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

tell application “Keynote”
  tell front document
    tell current slide
      
      
set tCount to count every table
      
if tCount = 0 then return
      
      
tell table 1
        
        
set cCount to count every column
        
set cWidth to width of every column
        
set aWidth to width –table width
        
        
set aveWidth to (aWidth - (first item of cWidth)) / (cCount - 1)
        
        
tell columns 2 thru cCount
          set width to aveWidth
        end tell
        
      end tell
    end tell
  end tell
end tell

★Click Here to Open This Script 

2017/10/06 Keynoteで現在表示中のスライド上にある表のカラム幅を自動調整

Keynoteでオープン中の最前面のドキュメントの現在表示中のスライド上にある表のカラム幅を自動調整するAppleScriptです。

Keynote v7.3+macOS 10.12.6/macOS 10.13.1で動作検証を行なっています。

keynote_before.png
▲実行前(表のカラム幅が不均一)

keynote_after.png
▲実行後(表のカラム幅が均一)

最前面のドキュメント(front document)の、現在選択中のスライド(current slide)の上にある表(table)の一番左のカラムをヘッダーカラムとして実行前のままの状態を保持して、残りのカラムを均等幅になるよう幅(width)を計算して設定します。

KeynoteのScriptingについては、電子書籍「Keynote Control 廖Keynote Control◆廚脳楮戮望匆陲靴討い泙后6縮のある方はぜひお求めください。

AppleScript名:Keynoteで現在表示中のスライド上にある表のカラム幅を自動調整
– Created 2017-10-06 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4888
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

tell application “Keynote”
  tell front document
    tell current slide
      
      
set tCount to count every table
      
if tCount = 0 then return
      
      
tell table 1
        
        
set cCount to count every column
        
set cWidth to width of every column
        
set aWidth to width –table width
        
        
set aveWidth to (aWidth - (first item of cWidth)) / (cCount - 1)
        
        
repeat with i from 2 to cCount –Skip Header Column
          tell column i
            set width to aveWidth
          end tell
        end repeat
        
      end tell
    end tell
  end tell
end tell

★Click Here to Open This Script 

2017/10/05 クリップボード内のZero Width Spaceを削除する

クリップボードから、Cocoaの機能を使うと削除も置換もできないZero Width Spaceを削除するAppleScriptです。

zerowidthspace.png
ScriptableなUnicode文字情報チェックツール「UnicodeChecker」

特定のエディタ(ASObjCExplorer)が出力するものの、Cocoaの機能を使うと削除や置換ができないために、Objective-CやSwiftだけで何も対策しないで組んであると編集自体が行えないという恐怖のキャラクターZero Width Space。

各テキストエディタのZero Width Spaceへの対応度はまちまちで、

TextWranler(=BBEdit):表示および削除が可能
tetwrangler.png

CotEditor:ちょっと前まで認識・表示しなかった。最新版(v3.2.2)では表示・削除ができるようになった
coteditor.png

mi:表示できないが、Zero Width Spaceがある場所で不自然にカーソルが進まなくなるので、存在は確認できる。マウスで前後の文字ごとまとめて範囲選択して削除することは可能

といった状況です。実際にCocoaのAPIを用いて文字置換するとぜんぜんダメなので、Pure AppleScriptの機能を用いて組んでいます。

自分がMac App Storeで販売中のPDF差分検出アプリケーション「Double PDF」でもこのZero Width Space対策を行なっており、本文テキスト同士の比較時にはあらかじめ削除するようにしています。

scrit_menu_zero.png

本Scriptは利用頻度が妙に高いので、Script Menuに入れて呼び出すような運用を行っています。クリップボードにZero Width Space駆除対象文字列を入れて(コピーして)本Scriptを実行すると、クリップボード内からZero Width Spaceが駆除されます。

AppleScript名:クリップボード内のZero Width Spaceを削除する
– Created 2017-10-05 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4881
set aText to the clipboard
set bText to aText as string
set cText to repChar(bText, string id 8203, “”) of me
set the clipboard to (cText as string)

–文字置換
on repChar(origText as string, targChar as string, repChar as string)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to targChar
  
set tmpList to text items of origText
  
set AppleScript’s text item delimiters to repChar
  
set retText to tmpList as string
  
set AppleScript’s text item delimiters to curDelim
  
return retText
end repChar

★Click Here to Open This Script 

AppleScript名:Zero Width Spaceのプロパティを取得
–http://piyocast.com/as/archives/4881
tell application “UnicodeChecker”
  properties of code point (8203 + 1)
end tell
–> {bidi mirrored:false, containing plane:plane id 0 of application “UnicodeChecker”, id:8203, line break:”ZW”, assigned:true, canonical combining class description:”Not_Reordered”, unicode name:”ZERO WIDTH SPACE”, assigned to abstract character:true, code point type:Format, class:code point, bidi class description:”Boundary_Neutral”, script name:”Common”, general category description:”Format”, bidi class:”BN”, containing block:block “General Punctuation” of application “UnicodeChecker”, general category:”Cf”, name:”", canonical combining class:0}

★Click Here to Open This Script 

2017/10/03 横書きテキストを縦書きに変換 v6

指定のテキストを強制的に擬似的な縦書きテキストに変換するAppleScriptです。簡易的な禁則処理を実装してあります。

tatetweet.png

Twitter投稿時に縦書きで投稿する用途に向けて書いてみたものです。1行あたりの文字数を指定して擬似縦書きテキストに変換します。

vertivalorig.png

vertivalconv.png

テキスト中の任意の改行を反映させ、かんたんな禁則処理も実装してあります。任意改行を考慮しなければ、禁則処理の負担もそれほど大きくないですが、任意改行を生かすために処理データを分割処理しています。

ただ、禁則処理自体はそれほど真剣に作り込んでいないので、エラー検出が投げやりな箇所が何点かあったはずです(めんどくさくなると、データ処理範囲超過の検出をエラートラップで逃げるのは常套手段)。

禁則処理を適用してリフロー計算した結果、末尾行がすべて空白文字になるパターンが多々あったので、末尾の空白行(縦に)の削除処理を付加しています。無駄にゴテゴテと機能を追加したので、Blog掲載のお気楽プログラムのくせに長くなりすぎで、、、、

くりかえしになりますが、データが長くなったときの高速化処理はとくに行なっていません。掲載リストの実行程度の「ちいさいデータ」であれば、開発環境で0.009秒程度で実行できています。

あとは、テキストエディタ上で選択中のテキストを取得して本Scriptに選択テキストを渡して、テキストエディタ上に新規テキストとして作成するとか、文字列長をチェックしたうえでTweetしてみたりすると、GUIアプリケーション連携がキモのAppleScript「らしい」処理になってくることでしょう。

AppleScript名:横書きテキストを縦書きに変換 v6
– Created 2017-10-03 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4875

property NSArray : a reference to current application’s NSArray
property NSString : a reference to current application’s NSString
property NSStringTransformFullwidthToHalfwidth : a reference to current application’s NSStringTransformFullwidthToHalfwidth
property NSMutableArray : a reference to current application’s NSMutableArray

set lineMax to 9

set aText to “テキスト縦書きを行うAppleScriptの「禁則処理」および任意改行への対応バージョンです。”

set sRes to makeTategakiStr(lineMax, aText) of me

–縦行数を指定しつつ指定テキストを縦書き化
on makeTategakiStr(lineMax as integer, aText as string)
  set curMax to 0
  
set sList to paragraphs of aText –途中で強制改行が入っているケースに対処
  
  
set aList to {}
  
  
repeat with i in sList
    set outList to strToTategakiList(lineMax, i) of me
    
set the end of aList to outList
  end repeat
  
  
set aList to kinsokuList2(aList) of me
  
  
set curLen to length of aList
  
set curMax to getMaxItemCountFrom2DArray(aList) of me
  
  
set tmpList to {}
  
set twoDList to make2DBlankArray(curLen, curMax) of me
  
  
set curY to 1
  
repeat with x from 1 to curMax
    
    
set curX to 1
    
repeat with y from curLen to 1 by -1
      set aCon to getItemByXY(x, y, aList, “ ”) of me
      
set twoDList to setItemByXY(curX, curY, twoDList, aCon as string) of me
      
set curX to curX + 1
    end repeat
    
    
set curY to curY + 1
  end repeat
  
  

  
set twoDList2 to checkBlankVerticalLine(twoDList, “ ”)
  
  
  
set aRes to list2dToStringByUsingDelimiters(twoDList2, “ ”, return) of me
  
set zRes to hanToZen(aRes) of me
  
  
return zRes
end makeTategakiStr

–与えた文字列を縦書き2Dリストに変換
on strToTategakiList(lineMax as integer, aText as string)
  set zText to hanToZen(aText) of me
  
  
set outList to {}
  
set oneLine to {}
  
set aCount to 1
  
set curMax to 0
  
  
repeat with i from 1 to (length of aText)
    set aChar to character i of aText
    
    
set aChar to retTateChar(aChar) of me
    
    
set the end of oneLine to aChar
    
    
set aCount to aCount + 1
    
if aCount > lineMax then
      set aCount to 1
      
set the end of outList to oneLine
      
set oneLine to {}
    end if
  end repeat
  
  
if oneLine is not equal to {} then
    set the end of outList to oneLine
  end if
  
  
return outList
end strToTategakiList

–半角→全角変換
on hanToZen(aStr as string)
  set aString to NSString’s stringWithString:aStr
  
return (aString’s stringByApplyingTransform:(NSStringTransformFullwidthToHalfwidth) |reverse|:true) as string
end hanToZen

–2D Listに配列の添字的なアクセスを行なってデータを取得
on getItemByXY(aX as integer, aY as integer, aList as list, aBlankItem) –1 based index
  try
    set aContents to contents of (item aX of item aY of aList)
  on error
    set aContents to aBlankItem
  end try
  
return aContents
end getItemByXY

–2D Listに配列の添字的なアクセスを行なってデータを設定
on setItemByXY(aX as integer, aY as integer, tmpList as list, aContents) –1 based index
  set (item aX of item aY of tmpList) to aContents
  
return tmpList
end setItemByXY

–空白の2D Array を出力する
on make2DBlankArray(curLen as integer, curMax as integer)
  set outArray to {}
  
repeat curMax times
    set tmpList to {}
    
repeat curLen times
      set the end of tmpList to “”
    end repeat
    
set the end of outArray to tmpList
  end repeat
  
return outArray
end make2DBlankArray

–2D Listをアイテム間デリミタ、および行間デリミタを指定しつつテキスト化
on list2dToStringByUsingDelimiters(aList as list, itemDelimiter as string, lineDelimiter as string)
  set outList to {}
  
repeat with i in aList
    set aStr to listToStringUsingTextItemDelimiter(i, itemDelimiter) of me
    
set the end of outList to aStr
  end repeat
  
  
return listToStringUsingTextItemDelimiter(outList, lineDelimiter) of me
end list2dToStringByUsingDelimiters

–1D Listをアイテム間デリミタ、および行間デリミタを指定しつつテキスト化
on listToStringUsingTextItemDelimiter(sourceList as list, textItemDelimiter as string)
  set anArray to NSArray’s arrayWithArray:sourceList
  
return (anArray’s componentsJoinedByString:textItemDelimiter) as string
end listToStringUsingTextItemDelimiter

–2D Listの各要素のアイテム数のうち最多のものを返す
on getMaxItemCountFrom2DArray(aList as list)
  set anArray to NSArray’s arrayWithArray:aList
  
set bArray to (anArray’s valueForKeyPath:“@unionOfObjects.@count”)
  
return (bArray’s valueForKeyPath:“@max.self”) as integer
end getMaxItemCountFrom2DArray

–特殊文字の横書き用から縦書き用への置き換え
on retTateChar(aChar as string)
  if aChar = “<” then return “︿”
  
if aChar = “>” then return “﹀”
  

  
if aChar = “《” then return “︽”
  
if aChar = “》” then return “︾”
  

  
if aChar = “「” then return “﹁”
  
if aChar = “」” then return “﹂”
  

  
if aChar = “『” then return “﹃”
  
if aChar = “』” then return “﹄”
  

  
if aChar = “【” then return “︻”
  
if aChar = “】” then return “︼”
  

  
if aChar = “[” then return
  
if aChar = “]” then return
  

  
if aChar = “{” then return “︷”
  
if aChar = “}” then return “︸”
  

  
if aChar = “(” then return “︵”
  
if aChar = “)” then return “︶”
  

  
if aChar = “、” then return
  
if aChar = “。” then return
  
if aChar = “ー” then return “︱”
  
if aChar = “〜” then return
  
if aChar = “=” then return “‖”
  

  
if aChar = “1” then return “一”
  
if aChar = “2” then return “二”
  
if aChar = “3” then return “三”
  
if aChar = “4” then return “四”
  
if aChar = “5” then return “五”
  
if aChar = “6” then return “六”
  
if aChar = “7” then return “七”
  
if aChar = “8” then return “八”
  
if aChar = “9” then return “九”
  
if aChar = “0” then return “〇”
  
  
return aChar
end retTateChar

–とりあえずな禁則処理(任意改行を考慮し、2D Listでデータを受け取る)
on kinsokuList2(toDList as list)
  
  
set kinsokuCharList to {, , “﹁”, “﹂”, “﹃”, “﹄”, “︻”, “︼”, , , “︷”, “︸”, “︵”, “︶”, “︱”, , “ァ”, “ィ”, “ゥ”, “ェ”, “ォ”, “ョ”, “ぁ”, “ぃ”, “ぅ”, “ぇ”, “ぉ”, “ょ”}
  
set outList to {}
  
  
repeat with ii in toDList
    set aList to contents of ii
    
set aLen to length of aList
    
set startNum to 2
    
    
repeat
      set chgF to false
      
repeat with i from startNum to aLen
        try
          set aChar to contents of first item of item i of aList
        on error
          –ちょっと強引、、、
          
exit repeat
        end try
        
        
considering case and diacriticals –超重要!!
          if aChar is in kinsokuCharList then
            –行頭に禁則文字が入っていたら、前の行に追い出す
            
set the end of item (i - 1) of aList to aChar
            
set tmpList to contents of item i of aList
            
set item i of aList to removeItemInArray(tmpList, 1) of me
            
            
–リフロー処理
            
–repeat with refI from (i + ((i < aLen) as integer)) to aLen
            
repeat with refI from (i + 1) to aLen
              –現在行の先頭の文字を取得
              
set aaChar to first item of item refI of aList
              
–前行の末尾に追加
              
set the end of item (refI - 1) of aList to aaChar
              
–現在行の先頭の文字を削除
              
set tmpList to contents of item refI of aList
              
set item refI of aList to removeItemInArray(tmpList, 1) of me
            end repeat
            
            
if chgF = false then
              set chgF to true
              
copy (i + ((aLen > i) as integer)) to startNum
            end if
          end if
        end considering –超重要!!
        
      end repeat
      
if chgF = false then exit repeat
    end repeat
    
    
set outList to outList & aList
  end repeat
  
  
return outList
end kinsokuList2

on removeItemInArray(aList as list, anItemNo as integer)
  set anArray to NSMutableArray’s arrayWithArray:aList
  
anArray’s removeObjectAtIndex:(anItemNo - 1)
  
return anArray as list
end removeItemInArray

on offsetOf(aList as list, aTarg)
  set aArray to NSArray’s arrayWithArray:aList
  
set aIndex to aArray’s indexOfObjectIdenticalTo:aTarg
  
return ((aIndex as integer) + 1)
end offsetOf

–縦方向に空白行が存在していたら削除、末尾からスキャン
on checkBlankVerticalLine(aList as list, aBlankChar as string)
  set tLen to length of (item 1 of aList)
  
copy aList to bList
  
set hitList to makeRepeatinglList(length of bList, true) of me
  
  
set aCount to 1
  
repeat
    –縦方向に1行分、すべて空白文字かどうかチェック
    
set workList to {}
    
repeat with ii in bList
      set jj to contents of ii
      
set the end of workList to (item aCount of jj = aBlankChar)
    end repeat
    
    
–縦方向に1行空白だったら、1行分の削除を行う
    
if workList = hitList then
      repeat with ii from 1 to length of bList
        set jj to contents of item ii of bList
        
set item ii of bList to removeItemInArray(jj, 1) of me
      end repeat
      
set tLen to tLen - 1
    end if
    
set aCount to aCount + 1
    
if aCount > tLen then
      exit repeat
    end if
  end repeat
  
  
return bList
end checkBlankVerticalLine

–指定アイテムを指定個数連結したリストを作成
on makeRepeatinglList(hitNum as integer, hitItem)
  set outList to {}
  
repeat hitNum times
    set the end of outList to hitItem
  end repeat
  
return outList
end makeRepeatinglList

★Click Here to Open This Script 

2017/10/02 2Dリストを左に90度回転

2Dリスト(配列)を左方向に(反時計回転方向に)90度回転させるAppleScriptです。

rotatecounterclockwisepng.png

それほど大きくない2Dリストを回転させることを目的に作りました。処理速度も重視していません。数十アイテム程度をターゲットにしており、数千アイテムを超えると速度も落ちることでしょう。

2Dリストの各要素の要素数が異なる場合、最大の要素数のアイテムに合わせて足りない場所に詰め物(blankItem)をする仕様です。

AppleScript名:2Dリストを左(counterclockwise)に90度回転
– Created 2017-10-02 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
–http://piyocast.com/as/archives/4872

property NSArray : a reference to current application’s NSArray

set origList to {{1, 2, 3, 4, 5, 6, 7}, {11, 12, 13, 14, 15, 16}, {21, 22, 23, 24, 25, 26, 27, 28}}

set wList to rotateListCounterClockwise90(origList, 0) of me

–>  {{0, 0, 28}, {7, 0, 27}, {6, 16, 26}, {5, 15, 25}, {4, 14, 24}, {3, 13, 23}, {2, 12, 22}, {1, 11, 21}}

set sList to rotateListCounterClockwise90(wList, 0) of me
–>  {{28, 27, 26, 25, 24, 23, 22, 21}, {0, 0, 16, 15, 14, 13, 12, 11}, {0, 7, 6, 5, 4, 3, 2, 1}}

set eList to rotateListCounterClockwise90(sList, 0) of me
–>  {{21, 11, 1}, {22, 12, 2}, {23, 13, 3}, {24, 14, 4}, {25, 15, 5}, {26, 16, 6}, {27, 0, 7}, {28, 0, 0}}

set nList to rotateListCounterClockwise90(eList, 0) of me
–>  {{1, 2, 3, 4, 5, 6, 7, 0}, {11, 12, 13, 14, 15, 16, 0, 0}, {21, 22, 23, 24, 25, 26, 27, 28}}

on rotateListCounterClockwise90(aList, blankItem)
  set curMax to 0
  
  
set curLen to length of aList
  
set curMax to getMaxItemCountFrom2DArray(aList) of me
  
  
set tmpList to {}
  
  
set twoDList to make2DBlankArray(curLen, curMax) of me
  
  
set curY to 1
  
repeat with x from curMax to 1 by -1
    
    
set curX to 1
    
repeat with y from 1 to curLen by 1
      set aCon to getItemByXY(x, y, aList, blankItem) of me
      
set twoDList to setItemByXY(curX, curY, twoDList, aCon) of me
      
set curX to curX + 1
    end repeat
    
    
set curY to curY + 1
  end repeat
  
  
return twoDList
end rotateListCounterClockwise90

on getMaxItemCountFrom2DArray(curList)
  set anArray to NSArray’s arrayWithArray:curList
  
set eRes to (anArray’s valueForKeyPath:"@unionOfObjects.@count")’s valueForKeyPath:"@max.self"
  
return eRes as integer
end getMaxItemCountFrom2DArray

–2D Listに配列の添字的なアクセスを行なってデータを取得
on getItemByXY(aX, aY, aList, aBlankItem) –1 based index
  try
    set aContents to contents of (item aX of item aY of aList)
  on error
    set aContents to aBlankItem
  end try
  
return aContents
end getItemByXY

–2D Listに配列の添字的なアクセスを行なってデータを設定
on setItemByXY(aX, aY, tmpList, aContents) –1 based index
  set (item aX of item aY of tmpList) to aContents
  
return tmpList
end setItemByXY

–空白の2D Array を出力する
on make2DBlankArray(curLen, curMax)
  set outArray to {}
  
repeat curMax times
    set tmpList to {}
    
repeat curLen times
      set the end of tmpList to ""
    end repeat
    
set the end of outArray to tmpList
  end repeat
  
return outArray
end make2DBlankArray

★Click Here to Open This Script 

2017/10/02 2Dリストを右に90度回転

2Dリスト(配列)を右方向に(時計回転方向に)90度回転させるAppleScriptです。

rotateclockwise2png.png

それほど大きくない2Dリストを回転させることを目的に作りました。処理速度も重視していません(データが巨大になったときへの対策を行なっていないということです)。

それほど複雑な処理ではなく、ごくたまに登場する程度の種類の内容ですが、単独でサブルーチンとして作りためていなかったので、まとめておきました。

数十アイテム程度をターゲットにしており、数千アイテムを超えると速度も落ちることでしょう。

2Dリスト中の各要素の要素数が異なる場合、最大の要素数のアイテムに合わせて足りない場所に詰め物(blankItem)をする仕様です。

オリジナルのリスト(配列)から移動先のリスト(配列)にただ順々にデータをコピーしているだけなので、「回転」というほどおおげさな処理をおこなっているわけでもありません。掲載リストに記述しているサンプルデータの回転程度であれば、実測値で0.001秒以下の処理時間しかかかりません。

ただこれを、何も考えずに画像データの回転とか膨大なデータベースのデータの回転(Pivot)に使うのは無茶な話だという、それだけのことです。

AppleScript名:2Dリストを右(clockwise)に90度回転
– Created 2017-10-02 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4868

property NSArray : a reference to current application’s NSArray

set origList to {{1, 2, 3, 4, 5, 6, 7}, {1, 2, 3, 4, 5, 6}, {1, 2, 3, 4, 5, 6, 7, 8}}

set eList to rotateListClockwise90(origList, 0) of me

–>  {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}, {5, 5, 5}, {6, 6, 6}, {7, 0, 7}, {8, 0, 0}}

set sList to rotateListClockwise90(eList, 0) of me
–>   {{8, 7, 6, 5, 4, 3, 2, 1}, {0, 0, 6, 5, 4, 3, 2, 1}, {0, 7, 6, 5, 4, 3, 2, 1}}

set wList to rotateListClockwise90(sList, 0) of me
–>   {{0, 0, 8}, {7, 0, 7}, {6, 6, 6}, {5, 5, 5}, {4, 4, 4}, {3, 3, 3}, {2, 2, 2}, {1, 1, 1}}

set nList to rotateListClockwise90(wList, 0) of me
–>  {{1, 2, 3, 4, 5, 6, 7, 0}, {1, 2, 3, 4, 5, 6, 0, 0}, {1, 2, 3, 4, 5, 6, 7, 8}}

on rotateListClockwise90(aList, blankItem)
  set curMax to 0
  
  
set curLen to length of aList
  
set curMax to getMaxItemCountFrom2DArray(aList) of me
  
  
set tmpList to {}
  
  
set twoDList to make2DBlankArray(curLen, curMax) of me
  
  
set curY to 1
  
repeat with x from 1 to curMax
    
    
set curX to 1
    
repeat with y from curLen to 1 by -1
      set aCon to getItemByXY(x, y, aList, blankItem) of me
      
set twoDList to setItemByXY(curX, curY, twoDList, aCon) of me
      
set curX to curX + 1
    end repeat
    
    
set curY to curY + 1
  end repeat
  
  
return twoDList
end rotateListClockwise90

on getMaxItemCountFrom2DArray(curList)
  set anArray to NSArray’s arrayWithArray:curList
  
set eRes to (anArray’s valueForKeyPath:“@unionOfObjects.@count”)’s valueForKeyPath:“@max.self”
  
return eRes as integer
end getMaxItemCountFrom2DArray

–2D Listに配列の添字的なアクセスを行なってデータを取得
on getItemByXY(aX, aY, aList, aBlankItem) –1 based index
  try
    set aContents to contents of (item aX of item aY of aList)
  on error
    set aContents to aBlankItem
  end try
  
return aContents
end getItemByXY

–2D Listに配列の添字的なアクセスを行なってデータを設定
on setItemByXY(aX, aY, tmpList, aContents) –1 based index
  set (item aX of item aY of tmpList) to aContents
  
return tmpList
end setItemByXY

–空白の2D Array を出力する
on make2DBlankArray(curLen, curMax)
  set outArray to {}
  
repeat curMax times
    set tmpList to {}
    
repeat curLen times
      set the end of tmpList to “”
    end repeat
    
set the end of outArray to tmpList
  end repeat
  
return outArray
end make2DBlankArray

★Click Here to Open This Script