Archive for the 'ファイル処理(file)' Category

2017/12/08 指定文字の花文字を取得してRTFで書き出す

指定文字の花文字を取得して、収録グリフ数が10,000以上のフォントから30をデスクトップにRTFで書き出すAppleScriptです。

注意:ただし、本ScriptはRetina Display未対応です

hanamoji1.png

前のバージョンからの改良点は、

 〇慊衒源が指定フォント中にグリフを持っているかどうかチェックしてから処理
 ▲ぅ鵐好函璽襪気譴討い襯侫ント数が少ない場合への対処

といったところです。

花文字作成部分の処理内容は、

 ”漸荵慊衒源を収録しているフォントの一覧を取得
 ↓,納萋世靴織侫ントのうち、グリフ数が10,000以上のものをピックアップ
 スタイル付きテキストを作成。仕上がり(描画)サイズを取得
 せ転紊りサイズでRaw画像を作成
 ズ鄒したRaw画像を塗りつぶす(White)
 ε匹蠅弔屬靴Raw画像にスタイル付きテキストを描画
 Raw画像の各座標から色情報をピックアップ、指定スレッショルド値以上であればドットが存在していると判定
 ┘疋奪箸存在している場合には描画文字列を、存在していない場合にはスペースを配列に追加
 作成した2次元配列をテキストに変換。フォント名をテキストに含める指定を行なっている場合にはフォントのPostScript名を文字列に出力

というところです。

これまでは(macOS 10.10より前)AppleScriptで画像処理を行おうとすると、Photoshopあたりで処理するのが定番でしたが、Cocoaの機能を利用することで、カラープロファイル処理が厳密に求められるような内容でなければPhotoshopなしでけっこうな処理が行えるようになってきました。

テキスト処理において(文字コード自動判別が可能になったため)テキストエディタが要らなくなってきたように、データベース処理で(小規模データであれば)FileMaker Proが要らなくなってきて、同様に画像処理でもPhotoshopなしで処理できるものが増えてきた今日このごろです。

AppleScript名:指定文字の花文字を取得してRTFで書き出す
– Created 2017-11-19 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/5012

property NSArray : a reference to current application’s NSArray
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSBezierPath : a reference to current application’s NSBezierPath
property NSMutableParagraphStyle : a reference to current application’s NSMutableParagraphStyle
property NSPNGFileType : a reference to current application’s NSPNGFileType
property NSFontAttributeName : a reference to current application’s NSFontAttributeName
property NSKernAttributeName : a reference to current application’s NSKernAttributeName
property NSLigatureAttributeName : a reference to current application’s NSLigatureAttributeName
property NSFont : a reference to current application’s NSFont
property NSUUID : a reference to current application’s NSUUID
property NSMutableAttributedString : a reference to current application’s NSMutableAttributedString
property NSUnderlineStyleAttributeName : a reference to current application’s NSUnderlineStyleAttributeName
property NSImage : a reference to current application’s NSImage
property NSParagraphStyleAttributeName : a reference to current application’s NSParagraphStyleAttributeName
property NSString : a reference to current application’s NSString
property NSForegroundColorAttributeName : a reference to current application’s NSForegroundColorAttributeName
property NSBitmapImageRep : a reference to current application’s NSBitmapImageRep
property NSColor : a reference to current application’s NSColor
property NSColorSpace : a reference to current application’s NSColorSpace
property NSFontManager : a reference to current application’s NSFontManager
property NSPredicate : a reference to current application’s NSPredicate
property NSDictionary : a reference to current application’s NSDictionary

set aString to “あ”
set hanaMax to 30
set hanaSize to 36
set targFontName to “Osaka-Mono” –”Courier New”–結果を出力するRTFのフォント名(PostScript名)

set fRes to getEveryFontPSNameANdGlyphsNum() of me
set theArray to NSArray’s arrayWithArray:fRes
set thePred to NSPredicate’s predicateWithFormat:“fontNum > 10000″
set bArray to (theArray’s filteredArrayUsingPredicate:thePred) as list

if hanaMax > (length of bArray) then
  set hanaMax to (length of bArray)
end if

set aCount to 1

repeat hanaMax * 2 times
  set aFontName to contents of item aCount of bArray
  
  
–花文字文字列を作成
  
set fRes to getHanamojiStr(hanaSize, fontName of aFontName, aString, 0.7, true) of me
  
if fResfalse then
    –StyledStringで結果出力(RTFとしてファイル保存)
    
set aStyledStr to makeRTFfromParameters(fRes, targFontName, 11, 0.0, 0.0) of me
    
set aRange to current application’s NSMakeRange(0, aStyledStr’s |length|())
    
set aVal1 to NSFont’s fontWithName:targFontName |size|:11
    
    
aStyledStr’s beginEditing()
    (
aStyledStr’s addAttribute:(NSFontAttributeName) value:aVal1 range:aRange)
    
aStyledStr’s endEditing()
    
    
set targFol to POSIX path of (path to desktop)
    
set aUUID to NSUUID’s UUID()’s UUIDString() as text
    
set bRes to my saveStyledTextAsRTF(aUUID, targFol, aStyledStr) –RTFで書き出す
    
    
set aCount to aCount + 1
    
if aCount > hanaMax then exit repeat
  end if
end repeat

–花文字文字列を計算して返す
on getHanamojiStr(aFontSize as real, aFontName as string, aString as string, aThread as real, incFontName as boolean)
  if length of aString is not equal to 1 then return false
  
  
–指定文字コードが指定フォント中に存在するかチェック
  
set fRes to retGlyphsInFont(aFontName, id of aString) of me
  
if fRes = false then return false
  
  
set aThreadShould to 768 * aThread
  
if (chkMultiByteChar(aString) of me) = false then
    set spaceChar to string id 12288 –全角スペース(UTF-16)
  else
    set spaceChar to string id 32 –半角スペース
  end if
  
  
set fillColor to NSColor’s whiteColor –塗り色
  
set bString to aString & ” “ –処理内容の帳尻合わせ(そのままだと右端が欠けるのでスペースを入れた)
  
set anAssrStr to makeRTFfromParameters(bString, aFontName, aFontSize, -2, (aFontSize * 1.2)) of me
  
set aSize to anAssrStr’s |size|()
  
  
if class of aSize = record then
    set attrStrWidth to width of aSize
    
set attrStrHeight to height of aSize
  else if class of aSize = list then –macOS 10.13.xのバグ回避
    copy aSize to {attrStrWidth, attrStrHeight}
  end if
  
  
set {xPos, yPos} to {0, 0}
  
  
set tmpImg1 to makeImageWithFilledColor(attrStrWidth, attrStrHeight, fillColor) of me
  
set tmpImg2 to drawAttributedStringsOnImage(tmpImg1, anAssrStr, xPos, yPos) of me
  
set aRawimg to NSBitmapImageRep’s imageRepWithData:(tmpImg2’s TIFFRepresentation())
  
  
–画像から順次指定座標の色データを拾って花文字listに反映
  
set strList to {}
  
repeat with y from 1 to attrStrHeight - 1
    
    
set strListX to {}
    
repeat with x from 0 to attrStrWidth - 1
      set tmpCol to getColorFromRawImage(aRawimg, x, y) of me
      
      
if tmpCol is not equal to false then
        if tmpCol is not equal to {255, 255, 255} then
          copy tmpCol to {tmpR, tmpG, tmpB}
          
if (tmpR + tmpG + tmpB) < aThreadShould then
            set the end of strListX to aString
          else
            set the end of strListX to spaceChar
          end if
        else
          set the end of strListX to spaceChar
        end if
      end if
      
    end repeat
    
set the end of strList to strListX
  end repeat
  
  
–2D List→Text
  
set aRes to list2dToStringByUsingDelimiters(strList, “”, return) of me
  
  
if incFontName = true then
    set fName to getDisplayedNameOfFont(aFontName) of me
    
set aRes to “■” & fName & return & return & aRes
  end if
  
  
return aRes
end getHanamojiStr

–指定Raw画像中の指定座標のピクセルの色をRGBで取り出す
on getColorFromRawImage(aRawimg, x as real, y as real)
  set origColor to (aRawimg’s colorAtX:x y:y)
  
set srgbColSpace to NSColorSpace’s deviceRGBColorSpace
  
if srgbColSpace = missing value then return false
  
  
set aColor to (origColor’s colorUsingColorSpace:srgbColSpace)
  
  
set aRed to (aColor’s redComponent()) * 255
  
set aGreen to (aColor’s greenComponent()) * 255
  
set aBlue to (aColor’s blueComponent()) * 255
  
  
return {aRed as integer, aGreen as integer, aBlue as integer}
end getColorFromRawImage

–画像のうえに指定のスタイル付きテキストを描画して画像を返す
on drawAttributedStringsOnImage(anImage, anAssrStr, xPos as real, yPos as real)
  anImage’s lockFocus()
  
anAssrStr’s drawAtPoint:(current application’s NSMakePoint(xPos, yPos))
  
anImage’s unlockFocus()
  
return anImage
end drawAttributedStringsOnImage

–書式つきテキストを組み立てる
on makeRTFfromParameters(aStr as string, fontName as string, aFontSize as real, aKerning as real, aLineSpacing as real)
  set aVal1 to NSFont’s fontWithName:fontName |size|:aFontSize
  
set aKey1 to (NSFontAttributeName)
  
  
set aVal2 to NSColor’s blackColor()
  
set aKey2 to (NSForegroundColorAttributeName)
  
  
set aVal3 to aKerning
  
set akey3 to (NSKernAttributeName)
  
  
set aVal4 to 0
  
set akey4 to (NSUnderlineStyleAttributeName)
  
  
set aVal5 to 2 –all ligature ON
  
set akey5 to (NSLigatureAttributeName)
  
  
set aParagraphStyle to NSMutableParagraphStyle’s alloc()’s init()
  
aParagraphStyle’s setMinimumLineHeight:(aLineSpacing)
  
aParagraphStyle’s setMaximumLineHeight:(aLineSpacing)
  
set akey7 to (NSParagraphStyleAttributeName)
  
  
set keyList to {aKey1, aKey2, akey3, akey4, akey5, akey7}
  
set valList to {aVal1, aVal2, aVal3, aVal4, aVal5, aParagraphStyle}
  
set attrsDictionary to NSMutableDictionary’s dictionaryWithObjects:valList forKeys:keyList
  
  
set attrStr to NSMutableAttributedString’s alloc()’s initWithString:aStr attributes:attrsDictionary
  
return attrStr
end makeRTFfromParameters

–指定サイズの画像を作成し、背景を指定色で塗る
on makeImageWithFilledColor(aWidth as real, aHeight as real, fillColor)
  set anImage to NSImage’s alloc()’s initWithSize:(current application’s NSMakeSize(aWidth, aHeight))
  
  
anImage’s lockFocus()
  
set theRect to {{x:0, y:0}, {width:aWidth, height:aHeight}}
  
set theNSBezierPath to NSBezierPath’s bezierPath
  
theNSBezierPath’s appendBezierPathWithRect:theRect
  
fillColor’s |set|()
  
theNSBezierPath’s fill()
  
anImage’s unlockFocus()
  
  
return anImage
end makeImageWithFilledColor

–2D Listをアイテム間および行間のデリミタを個別に指定してテキスト変換
on list2dToStringByUsingDelimiters(aList as list, itemDelimiter, lineDelimiter)
  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
  
  
set bStr to listToStringUsingTextItemDelimiter(outList, lineDelimiter) of me
  
return bStr
end list2dToStringByUsingDelimiters

on listToStringUsingTextItemDelimiter(sourceList as list, textItemDelimiter)
  set CocoaArray to NSArray’s arrayWithArray:sourceList
  
set CocoaString to CocoaArray’s componentsJoinedByString:textItemDelimiter
  
return (CocoaString as string)
end listToStringUsingTextItemDelimiter

–ユーザー環境にインストールされているすべてのフォントのPostScript名とグリフ数を返す
on getEveryFontPSNameANdGlyphsNum()
  set aFontList to NSFontManager’s sharedFontManager()’s availableFonts()
  
set thePred to NSPredicate’s predicateWithFormat:“NOT SELF BEGINSWITH ’.’”
  
set aFontList to (aFontList’s filteredArrayUsingPredicate:thePred) as list
  
  
set aList to {}
  
repeat with i in aFontList
    set aName to contents of i
    
set aNum to countNumberOfGlyphsInFont(aName) of me
    
set the end of aList to {fontName:aName, fontNum:aNum}
  end repeat
  
  
return aList
end getEveryFontPSNameANdGlyphsNum

–指定Postscript名称のフォントに定義されている文字数を数えて返す
on countNumberOfGlyphsInFont(fontName as string)
  set aFont to NSFont’s fontWithName:fontName |size|:9.0
  
if aFont = missing value then return false
  
set aProp to aFont’s numberOfGlyphs()
  
return aProp as number
end countNumberOfGlyphsInFont

–フォントのPostScript NameからDisplayed Nameを取得
on getDisplayedNameOfFont(aName as string)
  set aFont to NSFont’s fontWithName:aName |size|:9.0
  
set aDispName to (aFont’s displayName()) as string
  
return aDispName
end getDisplayedNameOfFont

–全角文字が存在するか
on chkMultiByteChar(checkString as string)
  set aStr to NSString’s stringWithString:checkString
  
set aRes to aStr’s canBeConvertedToEncoding:(current application’s NSASCIIStringEncoding)
  
return (aRes as boolean)
end chkMultiByteChar

–スタイル付きテキストを指定フォルダ(POSIX path)にRTFで書き出し
on saveStyledTextAsRTF(aFileName as string, targFol as string, aStyledString)
  set bstyledLength to aStyledString’s |string|()’s |length|()
  
set bDict to NSDictionary’s dictionaryWithObject:“NSRTFTextDocumentType” forKey:(current application’s NSDocumentTypeDocumentAttribute)
  
set bRTF to aStyledString’s RTFFromRange:(current application’s NSMakeRange(0, bstyledLength)) documentAttributes:bDict
  
  
set theName to NSString’s stringWithString:aFileName
  
set theName to theName’s stringByReplacingOccurrencesOfString:“/” withString:“_”
  
set theName to theName’s stringByReplacingOccurrencesOfString:“:” withString:“_”
  
set thePath to NSString’s stringWithString:targFol
  
set thePath to (thePath’s stringByAppendingPathComponent:theName)’s stringByAppendingPathExtension:“rtf”
  
  
return (bRTF’s writeToFile:thePath atomically:true) as boolean
end saveStyledTextAsRTF

–指定名称のフォントに指定の文字コードが含まれているかチェック
on retGlyphsInFont(fontName as string, strCode as integer)
  set aFont to NSFont’s fontWithName:fontName |size|:24.0
  
if aFont = missing value then return false
  
set aSet to aFont’s coveredCharacterSet()
  
set aRes to (aSet’s characterIsMember:strCode) as boolean
  
return aRes as list of string or string –as anything
end retGlyphsInFont

★Click Here to Open This Script 

2017/12/06 指定フォルダ以下のテキストファイルのファイル名のうち連番部分を抽出して、欠落や重複を検出する

指定フォルダ以下の指定形式のファイルのファイル名をSpotlightですべて取得し、重複や連番部分の欠落を抽出するAppleScriptです。

しょっちゅう同じようなScriptを作っているような気もしますが、それだけ個人的な必要度が高いものかもしれません。

連番部分を含む命名規則を持つファイル群、

  1AXXXXXX-101.txt
  1AXXXXXX-72.txt
  1AXXXXXX-9.txt

から(ファイル名はサンプル)、ファイル名の共通部分をスキャンして、ファイルごとに変更になる部分を抽出しています。一応、ねんのために拡張子を外したデータを処理しています。

ファイル名の共通部分をデータ同士から抽出する、というのが本Scriptの新機軸です。

データすべてから変更部分を取得するのではなく、データの文字数の最大、最小を取得し、それぞれの文字列に該当するデータを10件ずつピックアップし、その間で変更部分の検出を行なっています。

こうした「固定部分の自動検出」というのは、仕様がきちんと存在していない仕事において必要になってくると思われます。すべてのファイル名データから命名ルールを自動検出したり、例外に該当するものを除外したり、ということも考えられそうです。

なお、実行には、Shane StanleyのAppleScriptライブラリ「Metadata Lib」および「BridgePlus」のインストールを必要とします。

AppleScript名:指定フォルダ以下のテキストファイルのファイル名のうち連番部分を抽出して、欠落や重複を検出する
– Created 2017-12-05 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″
use bPlus : script “BridgePlus”
–http://piyocast.com/as/archives/5011

property NSCountedSet : a reference to current application’s NSCountedSet
property NSMutableArray : a reference to current application’s NSMutableArray
property SMSForder : a reference to current application’s SMSForder
property NSMutableSet : a reference to current application’s NSMutableSet
property NSIndexSet : a reference to current application’s NSIndexSet

set origFol to (POSIX path of (choose folder))
set txtFiles to mdLib’s searchFolders:{origFol} searchString:“kMDItemContentType == %@” searchArgs:{“public.plain-text”}

–ファイル名部分から拡張子を削除して収集
set anArray to ((current application’s NSMutableArray’s arrayWithArray:txtFiles)’s valueForKeyPath:“lastPathComponent.stringByDeletingPathExtension”)

–ファイル名文字列リストから、共通部分(変更のない部分)を抽出
set intStr to retInterSectionStr(anArray) of me

set cList to {}
repeat with i in (anArray as list)
  set aRes to repChar(i as string, intStr, “”) of me
  
set aNumF to chkNumeric(aRes) of me
  
if aNumF = true then
    set the end of cList to aRes as integer
  else
    log i
  end if
end repeat

set aRes to calcGaps(cList) of me
set bRes to returnDuplicatesOnly(cList) of me

return {gaps:aRes, dups:bRes}
–>  {gaps:{195, 284, 285}, dups:{}}

–与えられた文字列リストのうち、文字列共通部分を抽出
on retInterSectionStr(anArray)
  set aMin to (anArray’s valueForKeyPath:“@min.length”) as integer
  
set aMax to (anArray’s valueForKeyPath:“@max.length”) as integer
  
  
set bList to {}
  
repeat with i from aMin to aMax
    set aCount to 0
    
    
repeat with ii in (anArray as list)
      set aLen to length of ii
      
if aLen = i then
        set the end of bList to contents of ii
        
if aCount = 10 then
          exit repeat
        else
          set aCount to aCount + 1
        end if
      end if
    end repeat
    
  end repeat
  
  
set aStr to contents of first item of bList
  
set bList to rest of bList
  
  
repeat with i in bList
    set bStr to contents of i
    
set intStr to calcIntersection(aStr, bStr) of me
    
if intStr = false then
      exit repeat
    else
      copy intStr to aStr
    end if
  end repeat
  
  
return aStr
end retInterSectionStr

–2つの文字列から共通部分を(先頭から)抽出
on calcIntersection(aStr, bStr)
  set aList to characters of aStr
  
set bList to characters of bStr
  
  
set aLen to length of aList
  
set bLen to length of bList
  
  
if aLen bLen then
    set aMax to bLen
  else
    set aMax to aLen
  end if
  
  
set hitF to false
  
repeat with i from 1 to aMax
    set aChar to contents of item i of aList
    
set bChar to contents of item i of bList
    
if aChar is not equal to bChar then
      set hitF to true
      
exit repeat
    end if
  end repeat
  
  
if hitF = false then return false
  
  
return (text 1 thru (i - 1) of aStr)
end calcIntersection

–文字置換
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

–文字列が数字のみから構成されているかを調べて返す
on chkNumeric(checkString as string)
  set digitCharSet to current application’s NSCharacterSet’s characterSetWithCharactersInString:“0123456789″
  
set ret to my chkCompareString:checkString baseString:digitCharSet
  
return ret as boolean
end chkNumeric

on chkCompareString:checkString baseString:baseString
  set aScanner to current application’s NSScanner’s localizedScannerWithString:checkString
  
aScanner’s setCharactersToBeSkipped:(missing value)
  
aScanner’s scanCharactersFromSet:baseString intoString:(missing value)
  
return (aScanner’s isAtEnd()) as boolean
end chkCompareString:baseString:

–連番リストからGap検出
on calcGaps(aList as list)
  set nArray to (NSMutableArray’s arrayWithArray:aList)
  
set maxRes to (nArray’s valueForKeyPath:“@max.self”)’s intValue()
  
set minRes to (nArray’s valueForKeyPath:“@min.self”)’s intValue()
  
  
–最小値から最大値までの連番リスト作成
  
set theIndexSet to NSIndexSet’s indexSetWithIndexesInRange:{minRes, maxRes}
  
set theList to (SMSForder’s arrayWithIndexSet:theIndexSet) as list
  
  
–補集合
  
set aSet to NSMutableSet’s setWithArray:theList
  
set bSet to NSMutableSet’s setWithArray:nArray
  
aSet’s minusSet:bSet
  
  
return aSet’s allObjects() as list
end calcGaps

on returnDuplicatesOnly(aList as list)
  set aSet to NSCountedSet’s alloc()’s initWithArray:aList
  
set bList to (aSet’s allObjects()) as list
  
  
set dupList to {}
  
repeat with i in bList
    set aRes to (aSet’s countForObject:i)
    
if aRes > 1 then
      set the end of dupList to (contents of i)
    end if
  end repeat
  
  
return dupList
end returnDuplicatesOnly

★Click Here to Open This Script 

2017/11/22 Finder上で選択中の画像を横方向に連結

Finder上で選択中の画像ファイルを横方向に連結してデスクトップにPNG形式で書き出すAppleScriptです。

Finder上で選択中の画像ファイルに対して、

finder_selection.png

それらが画像ファイルかどうかを確認し、画像ファイルであれば横方向に(10pointの隙間を作って)連結してデスクトップ上にPNG形式で書き出します。

c524ba57-f107-4605-92c9-dfb95720cf31_resized.png

こんなふうに(↑)。

Retina解像度対策(x2)は行なっていないので、解像度の異なる画像同士を連結しようとすると問題が(解像度の不一致による極端なサイズの違い)出る可能性があります。

なお、指定パスからのUTIツリーの取得にオープンソースのフレームワーク「MagicKit」を使用しています。本Scriptの実行にはGithub上のプロジェクトをダウンロードして各自でXcode上でFrameworkをビルドし、MagicKit.frameworkを~/Library/Frameworksフォルダ以下にインストールする必要があります。

AppleScript名:Finder上で選択中の画像を横方向に連結
– Created 2017-11-21 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “QuartzCore”
use framework “AppKit”
use framework “MagicKit” –https://github.com/aidansteele/magickit
–http://piyocast.com/as/archives/4995

property NSMutableArray : a reference to current application’s NSMutableArray
property NSArray : a reference to current application’s NSArray
property NSString : a reference to current application’s NSString
property NSUUID : a reference to current application’s NSUUID
property NSPNGFileType : a reference to current application’s NSPNGFileType
property NSBitmapImageRep : a reference to current application’s NSBitmapImageRep
property NSImage : a reference to current application’s NSImage
property |NSURL| : a reference to current application’s |NSURL|
property GEMagicKit : a reference to current application’s GEMagicKit

property xGap : 10 –連結時の画像間のアキ(横方向)

tell application “Finder”
  set aSel to selection as alias list
  
if aSel = {} or aSel = “” then return
end tell

–選択した画像をArrayに入れる
set imgList to NSMutableArray’s new()
repeat with i in aSel
  set aPath to POSIX path of i
  
  
–指定ファイルのUTIを取得して、画像(public.image)があれば処理を行う
  
set aRes to (GEMagicKit’s magicForFileAtPath:aPath)
  
set utiList to (aRes’s uniformTypeHierarchy()) as list
  
if “public.image” is in utiList then
    set aNSImage to (NSImage’s alloc()’s initWithContentsOfFile:aPath)
    (
imgList’s addObject:aNSImage)
  end if
end repeat

–KVCで画像の各種情報をまとめて取得
set sizeList to (imgList’s valueForKeyPath:“size”) as list –NSSize to list of record conversion
set maxHeight to ((NSArray’s arrayWithArray:sizeList)’s valueForKeyPath:“@max.height”) as real
set totalWidth to ((NSArray’s arrayWithArray:sizeList)’s valueForKeyPath:“@sum.width”) as real
set totalCount to ((NSArray’s arrayWithArray:sizeList)’s valueForKeyPath:“@count”) as integer

–出力画像作成
set tSize to current application’s NSMakeSize((totalWidth + (xGap * totalCount)), maxHeight)
set newImage to NSImage’s alloc()’s initWithSize:tSize

–順次画像を新規画像に上書き
set xOrig to 0
repeat with i in (imgList as list)
  set j to contents of i
  
set curSize to j’s |size|()
  
set aRect to {xOrig, (maxHeight - (curSize’s height())), (curSize’s width()), (curSize’s height())}
  
set newImage to composeImage(newImage, j, aRect) of me
  
set xOrig to (curSize’s width()) + xGap
end repeat

–デスクトップにPNG形式でNSImageをファイル保存
set aDesktopPath to current application’s NSHomeDirectory()’s stringByAppendingString:“/Desktop/”
set savePath to aDesktopPath’s stringByAppendingString:((NSUUID’s UUID()’s UUIDString())’s stringByAppendingString:“.png”)
set fRes to saveNSImageAtPathAsPNG(newImage, savePath) of me

–2つのNSImageを重ね合わせ合成してNSImageで返す
on composeImage(backImage, composeImage, aTargerRect)
  set newImage to NSImage’s alloc()’s initWithSize:(backImage’s |size|())
  
  
copy aTargerRect to {x1, y1, x2, y2}
  
set bRect to current application’s NSMakeRect(x1, y1, x2, y2)
  
  
newImage’s lockFocus()
  
  
set newImageRect to current application’s CGRectZero
  
set newImageRect’s |size| to (newImage’s |size|)
  
  
backImage’s drawInRect:newImageRect
  
composeImage’s drawInRect:bRect
  
  
newImage’s unlockFocus()
  
return newImage
end composeImage

–NSImageを指定パスにPNG形式で保存
on saveNSImageAtPathAsPNG(anImage, outPath)
  set imageRep to anImage’s TIFFRepresentation()
  
set aRawimg to NSBitmapImageRep’s imageRepWithData:imageRep
  
set pathString to NSString’s stringWithString:outPath
  
set newPath to pathString’s stringByExpandingTildeInPath()
  
set myNewImageData to (aRawimg’s representationUsingType:(NSPNGFileType) |properties|:(missing value))
  
set aRes to (myNewImageData’s writeToFile:newPath atomically:true) as boolean
  
return aRes –成功ならtrue、失敗ならfalseが返る
end saveNSImageAtPathAsPNG

★Click Here to Open This Script 

2017/11/15 FileManager Lib 2.0

ここのところ、US AppleのMailing List「AppleScript Users ML」や直メールなどでmacOS 10.13のファイル処理について、macOS 10.13のバグだか仕様だかわかりにくい状況を回避しつつ、目的のファイル操作を行うベストな方法について情報交換が行われてきました。

候補にあがっている「道具」は、Finder、System Events、Cocoa(NSFileManager)、FileManager Libです(UNIX Shellもアリかも)。

Finder

macOS 10.13.xでは、フィルター参照によるオブジェクトの絞り込みが遅く(けっこう前からこんな状況)、さらに処理対象ファイルのリネーム後の存在確認を行ってみても、処理対象がaliasであるにもかかわらず存在が確認できなくなるなど、かなり信頼性が低くなっています。

こんにち、指定フォルダ以下のすべてのファイルを取得するのにSpotlightを用いずFinderで再帰処理するのが「ありえない」のと同様、64bit環境のmacOSにおいてファイル処理を行うのにFinderを用いるのは現実的でなくなりつつあります。

System Events

macOS標準搭載の、OS側の各種機能をAppleScriptに提供する専用のGUIなしアプリケーション群(○○ Scripting/○○ Eventsという名前)のひとつ。統廃合がすすみ、目下最大の機能を持つに至ったのがこのSystem Eventsです。

ファイル処理速度もFinderより上で、(4,000個ぐらいの)大量のファイルに対してフィルタ参照による絞り込みをおこなっても、Finderのように「処理が終わらない」といったことがありません(でもCocoaによる処理よりは遅い)。ただし、「disk item」(file/folderを一意に指し示せる汎用オブジェクト)という謎のオブジェクトを使うのと、それほど使い込んで処理した覚えがない(実績がいまいち)のと、ファイルのコピーを行うコマンドが存在しない点に要注意です。

Cocoa(NSFileManager)

Cocoaの機能を利用して高速にファイル処理できますし、Finderにバグがあっても関係ありません。Cocoa Scriptingを理解していれば問題はありませんが、これをマスターしていないと使えません(ASObjC Explorer 4とかScript Debugger 6がないと事実上無理ですし)。パスはPOSIX pathを用いる必要があります。

FileManager Lib

Shane Stanleyによる各種AppleScript Librariesのうちのひとつです。

Cocoa Scriptingをマスターしていなくても、Cocoaの速度をPure AppleScript程度の難易度で実現できるというメリットがあります(Script Debugger 6はあったほうがいいでしょう)。ただ、OSに標準インストールされていないので、自分でダウンロードしてインストールする必要があります。処理対象file/folderの指定にaliasが使える一方、返り値についてはaliasを使わない傾向があります。フィルタ参照はサポートしていません(Spotlightで行えという感じでしょうか)。

と、ひととおり手段をリストアップしたうえで、FileManager Libを用いた代表的なファイル処理の書き方を紹介しておきます。

フォルダ中のファイル一覧を取得

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFol to choose folder
set aList to contents of aFol
—> {”/Users/me/Desktop/0001.scpt”, “/Users/me/Desktop/1013error.jpg”….}

★Click Here to Open This Script 

フォルダ中のファイル一覧をフィルタ参照で取得

なし

ファイルの存在確認

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set tRes to exists item aFile
–> true

★Click Here to Open This Script 

ファイルの移動

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set aFol to choose folder

move item aFile to folder aFol with replacing

★Click Here to Open This Script 

ファイル/フォルダ削除(ゴミ箱に移動)

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set tRes to (trash item aFile)

★Click Here to Open This Script 

ファイル/フォルダ削除(即時削除)

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set tRes to (remove item aFile)

★Click Here to Open This Script 

リネーム

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set aInfo to info for aFile
set aExt to name extension of aInfo
set rRes to (rename item aFile to name (“New Name” & “.” & aExt))

★Click Here to Open This Script 

フォルダ作成

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose folder
set fRes to create folder at aFile use name “A NEW FOLDER”
–> true

★Click Here to Open This Script 

ファイルコピー

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use fLib : script “FileManagerLib”

set aFile to choose file
set aFol to choose folder
copy item aFile to folder aFol with replacing

★Click Here to Open This Script 

結論としては、macOS 10.13上ではFinderがいまいちで、System EventsやFileManager Libだと「必要とされる知識量」が少なくて済み、ひたすら処理速度を求めるならFileManager Lib(シンプル)かNSFileManager(やりたい処理をやりたいように)がおすすめといったところでしょうか。

2017/11/14 相対パスから絶対パスを計算して求める v2

相対パスから絶対パスを計算して求める( 峇霆爐箸覆訐簑丱僖后廚鮓気豊◆嵳燭┐蕕譴秦蠡丱僖后廚鬮「絶対パス」に変換する)AppleScriptです。

Markdown書類にリンクした画像のリンク切れチェックを行う必要があり、あまり考えずに作ってみました。

Markdown書類中にリンクした画像は相対パスでリンク先を記述しています。リンクについては指定画像を画像専用フォルダにコピーして相対パスを求め、リンクタグをクリップボードに入れるAppleScriptを書いて(リンクを)記述しています。

このとき、Markdown書類群のフォルダ階層構造を維持していればリンク切れは起こりませんが、一応チェックしておきたいのが人情。

そこで、チェック用のScriptを書いておくことに。だいたいはありあわせのサブルーチンを組み合わせることでSpotlight経由のMarkdown書類の検索や正規表現によるMarkdown中の画像リンク箇所の抽出など、たいした手間もかけずに記述できます。

ただ、Markdown書類のパス(絶対パス)をもとに、書類中に書かれている相対パスから実際にリンク画像が存在している絶対パスを求めるサブルーチンが存在していませんでした(OS側に用意されていてもよさそうなものですが)。

そこで、本ルーチンを書いて使ってみました。とくに問題もなく使えています。

AppleScript名:相対パスから絶対パスを計算して求める v2
– Created 2017-11-11 by Takaaki Naganoya
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4976

set absolutePath to “/Users/me/Documents/–Book 1「AppleScript最新リファレンス バージョン2」/5000 iOSデバイスとの連携/5100 iOSデバイスからMacに画面を出力するAirServer.md”
set relativePath to “../9999_images/img-1.jpeg”

set relativePath to calcAbsolutePath(absolutePath, relativePath) of me
–>  ”/Users/me/Documents/–Book 1「AppleScript最新リファレンス バージョン2」/9999_images/img-1.jpeg”

on calcAbsolutePath(aAbsolutePOSIXfile, bRelativePOSIXfile)
  set aStr to current application’s NSString’s stringWithString:aAbsolutePOSIXfile
  
set bStr to current application’s NSString’s stringWithString:bRelativePOSIXfile
  
  
set aList to aStr’s pathComponents() as list
  
set bList to bStr’s pathComponents() as list
  
  
set aLen to length of aList
  
  
set aCount to 1
  
repeat with i in bList
    set j to contents of i
    
if j is not equal to “..” then
      exit repeat
    end if
    
set aCount to aCount + 1
  end repeat
  
  
set tmp1List to items 1 thru (aLen - aCount) of aList
  
set tmp2List to items aCount thru -1 of bList
  
  
set allRes to current application’s NSString’s pathWithComponents:(tmp1List & tmp2List)
  
  
return allRes as text
end calcAbsolutePath

★Click Here to Open This Script 

2017/11/13 macOS 10.13ではファイル操作はFinderではなくSystem Eventsがおすすめ?

macOS 10.13のFinderをAppleScriptからコントロールすると、「指定のaliasをリネームした後に、aliasに対して存在確認を行うとfalseが返ってくる」という問題が見つかりました。

1フォルダ中に大量のファイルが存在していた場合の処理速度の問題もあるため、macOS 10.13ではもはや「Finderでファイル処理を行うと危ない」といえるレベル。

こっそり追加されつつも誰も本気で使っていなかった「System Eventsのファイル処理関連機能」の利用が検討すべき段階に来たように感じられます。

–http://piyocast.com/as/archives/4975
set aa to choose file

tell application “System Events”
  set a1 to (exists of aa) as string
  
set a2 to (class of aa) as string
  
set a3 to (name of aa) as string
  
  
display dialog a1 & “-” & a2 & “-” & a3
  
  
set name of aa to “changed_YYYYY”
  
  
set b1 to (exists of aa) as string
  
display dialog b1
end tell

★Click Here to Open This Script 

テストに使用したのはこのような(↑)簡単なScriptです。短いわりに破壊力は抜群で、macOS 10.13上で動かすと、リネーム後はファイルの存在(existence)がfalseで返ってきます。

macOS 10.13のFinderに問題があるのか、APFSがそもそもそのような仕様なのか、APFSとFinderの機能の整合性がとれていないのかは不明ですが、知らずに使うとハマるというか、怖い仕様です。

自分は最初からmacOS 10.13のFinderを信用せずにCocoaの機能でファイル処理を行うようにしていたので、この問題には直面していなかったのですが、誰にでもCocoa系の処理をおすすめするものではありません(手軽ではないので)。

そこで、System Eventsに実装されているファイル処理系の機能を真剣に検討してみました。

結論から言うと、Finder経由でファイル処理する場合とそれほど変わらない記述でファイル処理を記述できるため、手軽に処理できそうです。ただし、「ファイルのコピー(duplicate)」はどう書いても実行できなかったので、これ以外は大丈夫といったところでしょうか。

フォルダ中のファイル一覧を取得

定番中の定番処理ですが、ちょっとだけ書き方がFinderと違います。Finderだと結果をaliasのlistで取得するために「as alias list」といった書き方(これがない時代はけっこう大変でした)をしていましたが、これはSystem Eventsでは使えません。

また、every fileと書くとfileのlistが返ってきました(あとでaliasにcastする必要がある)。しかし、「every alias」という見たこともないような書き方をすることで、aliasのlistを取得できることが判明。びっくりです。

–http://piyocast.com/as/archives/4975
set a to choose folder

tell application “System Events”
  set aList to (every alias of a whose visible is true)
end tell

★Click Here to Open This Script 

フォルダ中のファイル一覧をフィルタ参照で取得

取得するオブジェクトを条件で抽出する「フィルタ参照」を用いて、条件に合うファイル(alias)のみを取得します。AppleScriptの定番中の定番処理です。

正直、Finderに対するフィルタ参照で、「すべての属性がフィルタ参照の対象として使えるかは(やってみないと)わからない」という状態だったので、System Eventsに対しても、ひととおり試してみる必要を感じます。

–http://piyocast.com/as/archives/4975
set a to choose folder

tell application “System Events”
  set aList to (every alias of a whose name extension is “pdf”)
end tell

★Click Here to Open This Script 

ファイルの存在確認

aliasに対してでもdisk itemに対してでも「exists」は取得できるようです。

–http://piyocast.com/as/archives/4975
set aFol to choose folder

tell application “System Events”
  tell folder (aFol as string)
    set eRes to exists of disk item “atext.txt”
    
–> true
  end tell
end tell

★Click Here to Open This Script 

ファイルの移動

指定ファイルを指定フォルダに移動させるコマンドです。ただし、Finderのmoveコマンドと異なり、移動先に同名のファイルが存在していても上書きするなどのオブションが用意されていない。
–http://piyocast.com/as/archives/4975
set aFile to choose file with prompt “Select a file to move”
set tFol to choose folder with prompt “Select target folder”

tell application “System Events”
  move (aFile as string) to (tFol as string)
end tell

★Click Here to Open This Script 

ファイル/フォルダ削除

Finderのdeleteコマンドは削除対象のファイル/フォルダをゴミ箱に移動させる命令だったが、System Eventsではいきなり削除する。
–http://piyocast.com/as/archives/4975
set a to choose file

tell application “System Events”
  set aa to (disk item (a as string))
  
delete aa
end tell

★Click Here to Open This Script 

リネーム

aliasに対してリネームを行う。System Eventsではファイルをdisk itemでアクセスする必要のあるコマンドやaliasでも処理できるコマンドが混在しているなど、いまひとつこなれていない印象がある。

–http://piyocast.com/as/archives/4975
set a to choose file

tell application “System Events”
  set aExt to name extension of a
  
set name of a to (“CCCC” & “.” & aExt)
  
return a
  
–> alias “Cherry:Users:me:Desktop:CCCC.png”
end tell

★Click Here to Open This Script 

フォルダ作成

Finderとあまり差がない。ただし、作成対象フォルダの指定はaliasを受け付けないのでas stringでパスを文字化してからfolderで指定する必要がある点に注意。

–http://piyocast.com/as/archives/4975
set a to choose folder

tell application “System Events”
  make new folder at folder (a as string) with properties {name:“new folder”}
end tell

★Click Here to Open This Script 

ひととおり、System EventsのAppleScript用語辞書を見て試してみただけのものであるため、今後もっといい書き方が発見される可能性もあります。あしからず。

2017/11/10 選択した画像を画像フォルダにコピーして、Markdown書式で画像パスタグをクリップボードに転送 v2

指定の画像を、書いている書籍の画像フォルダにコピーして、作成中のMarkdown書類からの相対パスを求めてMarkdown形式の画像リンクタグをクリップボードに転送するAppleScriptです。

md1.png

書籍の執筆作業は、「–」ではじまる名前の書籍ルートフォルダ以下に章ごとのフォルダを作成し、その中にMarkdown書類やPages書類を入れています。

画像については各原稿と同じフォルダ内ではなく、画像専用フォルダに配置(一般的なWebサイトの管理と同じ)。各Markdown書類には画像専用フォルダへの相対パスで画像リンクタグを入れています。

![md1.png](../9999_images/md1.png)

md2.png

本AppleScriptは、この書籍中の書籍中の画像専用フォルダに指定画像をコピーし、クリップボードに対して画像の相対パスを入れます。

書籍ルートフォルダ以下の画像専用フォルダを検索するためにSpotlight検索用の「MetaData Lib」を使用しています。

ファイルコピー時には画像専用フォルダ内のすべてのファイル名をチェックし、ファイル名の重複が発生していた場合にはファイル名末尾に連番をつけ、重複を回避しています。

前提としているMarkdownエディタは「MacDown」で、最前面の書類のパスを取得する部分でMacDownの機能を用いています。ただ、その程度の機能しか利用していないので、他のMarkdownエディタ用に転用するのも難しくないと思います(MarkdownエディタでAppleScriptに対応しているものが少数派なんですが)。

以前はMarkdown書類中にリンクする画像はDropboxにREST API経由でアップロードしてURLを用いていました。アップロード自体はそれなりですが、Markdown書類のオープン時や編集中、PDF出力時などは遅くて辟易しました。画像がローカルにあったほうが速度的なメリットが大きいと判断し、変更しました。

AppleScript名:選択した画像を画像フォルダにコピーして、Markdown書式で画像パスタグをクリップボードに転送 v2
– Created 2017-11-09 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/4968

property NSOrderedSame : a reference to current application’s NSOrderedSame
property NSFileManager : a reference to current application’s NSFileManager
property NSString : a reference to current application’s NSString

set imgPath to choose file of type {“public.image”}
set imgPOSIX to NSString’s stringWithString:(POSIX path of imgPath)
set imgFileName to (imgPOSIX’s lastPathComponent()) as string

–Get Front Document Path
tell application “MacDown”
  set dList to every document
  
if length of dList = 0 then
    display dialog “No Documents…” buttons {“OK”} default button 1
    
return
  end if
  
  
tell front document
    set aProp to properties
  end tell
  
  
set aPOSIXpath to POSIX path of ((file of aProp) as alias)
end tell

–Calculate Parent Book Folder Path
set aStr to NSString’s stringWithString:aPOSIXpath
set bStr to aStr’s stringByDeletingLastPathComponent()
set aList to bStr’s pathComponents() as list
–> {”/”, “Users”, “me”, “Documents”, “ぴよまるソフトウェア”, “書籍執筆関連、技術書典など”, “書籍原稿”, “–Book 1「AppleScript最新リファレンス」”, “5000 iOSデバイスとの連携”}

set bList to reverse of aList
–> {”5000 iOSデバイスとの連携”, “–Book 1「AppleScript最新リファレンス」”, “書籍原稿”, “書籍執筆関連、技術書典など”, “ぴよまるソフトウェア”, “Documents”, “me”, “Users”, “/”}

–Find Book Folder Root which name begins with “–”
set aCount to 1
set hitF to false
repeat with i in bList
  set j to contents of i
  
if j begins with “–” then –”–”ではじまる親フォルダを書籍のルートフォルダとみなす
    set hitF to true
    
exit repeat
  end if
  
set aCount to aCount + 1
end repeat

if hitF = false then
  display dialog “Folder structure error” buttons {“OK”} default button 1 with icon 1
  
return
end if

set cList to items 1 thru ((length of aList) - aCount + 1) of aList
set cStr to (NSString’s pathWithComponents:cList) as string

–Find Image folder in Book root folder
set theFolders to mdLib’s searchFolders:{cStr} searchString:“kMDItemContentType == [c]%@ && kMDItemFSName contains [c]%@” searchArgs:{“public.folder”, “_images”}

set theTargFol to contents of first item of theFolders
–> “/Users/me/Documents/ぴよまるソフトウェア/書籍執筆関連、技術書典など/書籍原稿/–Book 1「AppleScript最新リファレンス」/9999_images”

set newPath to ((NSString’s stringWithString:theTargFol)’s stringByAppendingPathComponent:imgFileName) as string
set newPath2 to chkExistPOSIXpathAndIncrementChildNumber(newPath) of me

–ファイルコピー
set aRes to my copyFileAt:imgPOSIX toFilePath:newPath2

–Markdown書類とコピーした画像ファイルの相対パスを計算する
set relativePath to calcRelativePath(aPOSIXpath, newPath2) of me

set allText to “![” & imgFileName & “](” & relativePath & “)”
–> “![img-2.php.jpeg](../9999_images/img-2.php.jpeg)”

set the clipboard to allText

–POSIX path stringを与えると、ファイル名の重複を検出して、ファイル名の名称回避を行って、ファイル名のみを返す
on chkExistPOSIXpathAndIncrementChildNumber(a)
  set aStr to NSString’s stringWithString:a
  
set bStr to aStr’s lastPathComponent()
  
set cStr to (bStr’s pathExtension()) as string
  
set dStr to (bStr’s stringByDeletingPathExtension()) as string
  
set eStr to (aStr’s stringByDeletingLastPathComponent()) as string
  
  
set aManager to NSFileManager’s defaultManager()
  
set aRes to (aManager’s fileExistsAtPath:aStr) as boolean
  
if aRes = false then return a
  
  
set hitF to false
  
repeat with i from 1 to 65535
    set tmpPath to (eStr & “/” & dStr & “_” & (i as string) & “.” & cStr)
    
set tmpStr to (NSString’s stringWithString:tmpPath)
    
set aRes to (aManager’s fileExistsAtPath:tmpStr) as boolean
    
set bRes to ((tmpStr’s caseInsensitiveCompare:eStr) is not equal to (NSOrderedSame)) as boolean
    
    
if {aRes, bRes} = {false, true} then
      set hitF to true
      
exit repeat
    end if
  end repeat
  
  
if hitF = false then return false
  
  
–ファイルパス(フルパス)からファイル名部分を取得
  
set returnFileName to tmpStr’s lastPathComponent()
  
return (returnFileName as string)
  
end chkExistPOSIXpathAndIncrementChildNumber

–ファイルコピー
on copyFileAt:origPOSIXPath toFilePath:newPOSIXPath
  set POSIXPath1 to NSString’s stringWithString:origPOSIXPath
  
set POSIXPath2 to NSString’s stringWithString:newPOSIXPath
  
set fileManager to NSFileManager’s defaultManager()
  
set theResult to fileManager’s copyItemAtPath:POSIXPath1 toPath:POSIXPath2 |error|:(missing value)
  
return (theResult as integer = 1)
end copyFileAt:toFilePath:

–2つのPOSIX pathの相対パスを計算する
on calcRelativePath(aPOSIXfile, bPOSIXfile)
  set aStr to NSString’s stringWithString:aPOSIXfile
  
set bStr to NSString’s stringWithString:bPOSIXfile
  
  
set aList to aStr’s pathComponents() as list
  
set bList to bStr’s pathComponents() as list
  
  
set aLen to length of aList
  
set bLen to length of bList
  
  
if aLen bLen then
    copy aLen to aMax
  else
    copy bLen to aMax
  end if
  
  
repeat with i from 1 to aMax
    set aTmp to contents of item i of aList
    
set bTmp to contents of item i of bList
    
    
if aTmp is not equal to bTmp then
      exit repeat
    end if
  end repeat
  
  
set bbList to items i thru -1 of bList
  
set aaItem to (length of aList) - i
  
  
set tmpStr to {}
  
repeat with ii from 1 to aaItem
    set the end of tmpStr to “..”
  end repeat
  
  
set allRes to NSString’s pathWithComponents:(tmpStr & bbList)
  
return allRes as text
end calcRelativePath

★Click Here to Open This Script 

2017/11/09 指定フォルダ内のOSAXのSDEFファイルを指定フォルダに書き出す

指定フォルダ内のOSAX(Scripting Additions)のAppleScript用語辞書(sdef)のファイルを指定フォルダに書き出すAppleScriptです。

大昔のClassic Mac OS時代の(68kのバイナリが入っている)OSAXのAppleScript用語辞書の内容を最新のmacOS環境でファイルとして書き出せる、というなかなかに「お前以外に誰が使うんだ?」という内容ですが、割とトンでもない破壊力のあるものです。

sdef1.png

Classic Mac OS時代のScripting Additionsを資料としてとってありますが、これらのAppleScript用語辞書はaeteリソース内に書かれていました(この画面↓はClassic Mac OSエミュレータの「SheepShaver」で起動した漢字Talk 8.6上のスクリプトエディタ。ResEditを探したものの仮想マシン内にみつかりませんでした)。

classic_osax_resized.png

これらの用語辞書は現行のmacOSのスクリプトエディタでもオープンして内容を確認できます。

sdef4_resized.png

さらに、用語辞書のウィンドウ上部のプロキシーアイコンをCommand-クリックすると、テンポラリディレクトリ内に用語辞書(sdef)が書き出されていることを確認できます。

sdef5_resized.png

ということは、この用語辞書(sdef)のパスを求めると、コピーして保管しておくことが可能だということがわかります。

実行してOSAXの入っているフォルダ、書き出しフォルダを指定すると、

sdef2.png

sdefを書き出してくれます。

sdef3_resized.png

ResEdit(死語)でオープンしなくても、sdefなら単なるテキストなので、内容を参照することもかんたんです。

1回実行したら2度目はなさそうな内容なので、ほとんど書き捨てな内容です。途中、スクリプト用語辞書を参照できないOSAXもあったりで、オープン時にエラーダイアログが表示されたりしますが、そこは自分でOKボタンをクリックして回避する必要があります。

AppleScript名:指定フォルダ内のOSAXのSDEFファイルを指定フォルダに書き出す
– Created 2017-11-09 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4961

set a to choose folder with prompt “Original Classic OSAX folder”
set aExportFol to choose folder with prompt “SDEF export folder”

tell application “Finder”
  set aList to (every file of a) as alias list
end tell

repeat with i in aList
  set aRes to saveOSAXsdef(contents of i, aExportFol) of me
  
if aRes = false then
    log i
  end if
end repeat

on saveOSAXsdef(a, aExportFol)
  set aInfo to info for a
  
set aKind to kind of aInfo
  
  
if aKind is in {“スクリプティング機能追加”, “Scripting addition”} then –Scripting Addirions (*Localized*)
    try
      tell application “Script Editor”
        try
          set aDoc to open a
        on error
          return
        end try
        
        
tell front document
          set aPath to path
        end tell
      end tell
    on error
      return false
    end try
    
    
set aExpPOS to (POSIX path of aExportFol)
    
    
–ファイルをコピー
    
set aRes to my copyFileAt:aPath toFolder:aExpPOS
  else
    return false
  end if
  
  
tell application “Script Editor”
    close every document without saving
  end tell
  
return true
end saveOSAXsdef

on copyFileAt:POSIXPath toFolder:folderPath
  set POSIXPath to current application’s NSString’s stringWithString:POSIXPath
  
set folderPOSIXPath to current application’s NSString’s stringWithString:folderPath
  
– build path for new file
  
set theName to POSIXPath’s lastPathComponent()
  
set newPath to folderPOSIXPath’s stringByAppendingPathComponent:theName
  
set fileManager to current application’s NSFileManager’s defaultManager()
  
set theResult to fileManager’s copyItemAtPath:POSIXPath toPath:newPath |error|:(missing value)
  
return (theResult as integer = 1)
end copyFileAt:toFolder:

★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/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 オープン中のムービーのファイルにラベルを付ける

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/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/09/27 macOS 10.13であらたにサポートされたHEIF画像をJPEGに変換

macOS 10.13であらたにサポートされたHEIF形式の画像をJPEGに変換するAppleScriptです。

HEIF(High Efficiency Image Format)は高品質の画像を少ないファイル容量で保持できる画像フォーマット。macOS 10.13およびiOS 11からサポートされるようになりました。拡張子は「heif」あるいは「heic」とのこと。

実際にHEIFのサンプル画像をJPEGに圧縮率を変更しつつ書き出してみたところ、60%ぐらいの圧縮率のJPEGファイルとHEIF画像が同程度のファイルサイズ(素材によるんでしょうけれど)。非圧縮のJPEG(1.6Mバイト)に対してHEIFは340Kバイト。見た目は非圧縮のJPEGと区別できませんでした。

heif_preview.png

HEIF形式の画像ファイルをFinder上でダブルクリックすると、Preview.appでなにごともなく表示されます。ただし、JPEGなどの画像をHEIFで書き出す機能はPreview.appにはついておらず、他の環境でサポートしていないHEIF画像をそのまま外に出してほしくない、といったところでしょうか。

heif_preview2.png

とりあえず、Cocoaを経由してHEIF画像をAppleScriptでハンドリングできないかと調べてみたところ、普通に(何も変更なく)NSImageに読み込んで、JPEGで書き出せました。

その逆に、JPEG画像をHEIF形式に変換する方法は、調べてもあまり素直な方法(Cocoaのオブジェクトを呼び出して書き出し)は見つかりません。無理やりBrew経由でコマンドラインツールをインストールして変換という状況。

それもそのはずで、HEIFは画像フォーマットというよりもコンテナであり、その中にはさまざまなデータ(画像、派生画像、イメージシーケンス、補助画像アイテム、メタデータ)を格納できるようになっているとのこと。

一応、AVFoundationに画像形式「AVFileTypeHEIF」の定義があるようです。

macOS 10.14〜10.15ぐらいでOS内にきちんとした仕組みを用意しようというところなんでしょうか。

AppleScript名:指定HEIF画像をJPG形式で保存
– Created 2017-09-27 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.7 –10.13 or later
use scripting additions
use framework “Foundation”
use framework “AppKit”
–http://piyocast.com/as/archives/4851

set aFile to POSIX path of (choose file of type {“public.heic”} with prompt “Select HEIF image file”)
set aImage to current application’s NSImage’s alloc()’s initWithContentsOfFile:aFile

set fRes to retUUIDfilePath(aFile, “jpg”) of me
set sRes to saveNSImageAtPathAsJPG(aImage, fRes, 0.6) of me

on retUUIDfilePath(aPath, aEXT)
  set aUUIDstr to (current application’s NSUUID’s UUID()’s UUIDString()) as string
  
set aPath to ((current application’s NSString’s stringWithString:aPath)’s stringByDeletingLastPathComponent()’s stringByAppendingPathComponent:aUUIDstr)’s stringByAppendingPathExtension:aEXT
  
return aPath
end retUUIDfilePath

–NSImageを指定パスにJPEG形式で保存
on saveNSImageAtPathAsJPG(anImage, outPath, qulityNum as real)
  set imageRep to anImage’s TIFFRepresentation()
  
set aRawimg to current application’s NSBitmapImageRep’s imageRepWithData:imageRep
  
set pathString to current application’s NSString’s stringWithString:outPath
  
set newPath to pathString’s stringByExpandingTildeInPath()
  
set myNewImageData to (aRawimg’s representationUsingType:(current application’s NSJPEGFileType) |properties|:{NSImageCompressionFactor:qulityNum})
  
set aRes to (myNewImageData’s writeToFile:newPath atomically:true) as boolean
  
return aRes –true/false
end saveNSImageAtPathAsJPG

★Click Here to Open This Script 

2017/09/25 書式つきテキストを組み立ててサイズを取得して画像書き出し v2

複数行にわたる書式付きテキストを組み立てて、描画サイズを取得して画像書き出しするAppleScriptです。

スタイル付きテキスト(NSAttributedString)を作成し、その描画エリアのサイズを取得。そんなもん、こんな手軽に求められるとは思ってもみませんでした。

描画エリアのサイズをもとに空白画像を作成して、そこにスタイル付きテキストを描画。結果の画像を指定パス名で書き出します。

test9999.png
▲本Scriptの実行結果(macOS 10.12)。出力用の文字列を長くしても、描画サイズを調べて画像化しているので、途中で途切れたりはしません

ただし、意外なところでOSバージョンによる挙動の差が出ました。書式付きテキストの描画サイズの取得ではなく、下地の塗りつぶし(白)についてです(下図参照)。

1013_strange_behavior.png
▲macOS 10.12(上)とmacOS 10.13GM(下)の本Scriptの実行結果。検証のために文字データを変更(途中の改行を減らしている)

macOS 10.13のRelease Build(Build 17A365)でも同様の結果になりました。macOS 10.10で実験したら10.12と同様の結果に。何か(トンでもない場所の)仕様が変わったのだろうか?

→ macOS 10.13のバグでした。対応版を掲載しています

AppleScript名:書式つきテキストを組み立ててサイズを取得して画像書き出し v2
– Created 2017-09-25 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/4843

property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSBezierPath : a reference to current application’s NSBezierPath
property NSMutableParagraphStyle : a reference to current application’s NSMutableParagraphStyle
property NSPNGFileType : a reference to current application’s NSPNGFileType
property NSFontAttributeName : a reference to current application’s NSFontAttributeName
property NSKernAttributeName : a reference to current application’s NSKernAttributeName
property NSLigatureAttributeName : a reference to current application’s NSLigatureAttributeName
property NSFont : a reference to current application’s NSFont
property NSMutableAttributedString : a reference to current application’s NSMutableAttributedString
property NSUnderlineStyleAttributeName : a reference to current application’s NSUnderlineStyleAttributeName
property NSImage : a reference to current application’s NSImage
property NSParagraphStyleAttributeName : a reference to current application’s NSParagraphStyleAttributeName
property NSString : a reference to current application’s NSString
property NSForegroundColorAttributeName : a reference to current application’s NSForegroundColorAttributeName
property NSBitmapImageRep : a reference to current application’s NSBitmapImageRep
property NSColor : a reference to current application’s NSColor

set outPath to “~/Desktop/test9999.png”
set fillColor to NSColor’s whiteColor –塗り色
set aFontSize to 48
set aString to “Takaaki” & return & “Naganoya” & return & “長野谷” & return & “隆昌”

set anAssrStr to makeRTFfromParameters(aString, “HiraMinProN-W3″, aFontSize, -2, (aFontSize * 1.5)) of me
set aSize to anAssrStr’s |size|()
–>  {width:408.0, height:187.2}

set attrStrWidth to width of aSize
set attrStrHeight to height of aSize

set {xPos, yPos} to {0, 0}

–下地の画像を作成
set tmpImg1 to makeImageWithFilledColor(attrStrWidth, attrStrHeight, fillColor) of me

–下地の画像の上にAttributed Stringを描画
set tmpImg2 to drawAttributedStringsOnImage(tmpImg1, anAssrStr, xPos, yPos) of me

–PNG形式でファイルに保存
set aRes to saveImageRepAtPathAsPNG(tmpImg2, outPath) of me

–画像のうえに指定のスタイル付きテキストを描画して画像を返す
on drawAttributedStringsOnImage(anImage, anAssrStr, xPos, yPos)
  anImage’s lockFocus()
  
anAssrStr’s drawAtPoint:(current application’s NSMakePoint(xPos, yPos))
  
anImage’s unlockFocus()
  
return anImage
end drawAttributedStringsOnImage

–書式つきテキストを組み立てる
on makeRTFfromParameters(aStr as string, fontName as string, aFontSize as real, aKerning as real, aLineSpacing as real)
  –Font
  
set aVal1 to NSFont’s fontWithName:fontName |size|:aFontSize
  
set aKey1 to (NSFontAttributeName)
  
  
–Color
  
set aVal2 to NSColor’s blackColor()
  
set aKey2 to (NSForegroundColorAttributeName)
  
  
–Kerning
  
set aVal3 to aKerning
  
set akey3 to (NSKernAttributeName)
  
  
–Underline
  
set aVal4 to 0
  
set akey4 to (NSUnderlineStyleAttributeName)
  
  
–Ligature
  
set aVal5 to 2 –all ligature ON
  
set akey5 to (NSLigatureAttributeName)
  
  
–Paragraph space
  
set aParagraphStyle to NSMutableParagraphStyle’s alloc()’s init()
  
aParagraphStyle’s setMinimumLineHeight:(aLineSpacing)
  
aParagraphStyle’s setMaximumLineHeight:(aLineSpacing)
  
set akey7 to (NSParagraphStyleAttributeName)
  
  
set keyList to {aKey1, aKey2, akey3, akey4, akey5, akey7}
  
set valList to {aVal1, aVal2, aVal3, aVal4, aVal5, aParagraphStyle}
  
set attrsDictionary to NSMutableDictionary’s dictionaryWithObjects:valList forKeys:keyList
  
  
set attrStr to NSMutableAttributedString’s alloc()’s initWithString:aStr attributes:attrsDictionary
  
return attrStr
end makeRTFfromParameters

–指定サイズの画像を作成し、背景を指定色で塗る
on makeImageWithFilledColor(aWidth, aHeight, fillColor)
  set anImage to NSImage’s alloc()’s initWithSize:(current application’s NSMakeSize(aWidth, aHeight))
  
  
anImage’s lockFocus()
  
set theRect to {{x:0, y:0}, {height:aHeight, width:aWidth}}
  
set theNSBezierPath to NSBezierPath’s bezierPath
  
theNSBezierPath’s appendBezierPathWithRect:theRect
  
fillColor’s |set|()
  
theNSBezierPath’s fill()
  
anImage’s unlockFocus()
  
  
return anImage
end makeImageWithFilledColor

–画像を指定パスにPNG形式で保存
on saveImageRepAtPathAsPNG(anImage, outPath)
  set imageRep to anImage’s TIFFRepresentation()
  
set aRawimg to NSBitmapImageRep’s imageRepWithData:imageRep
  
  
set pathString to NSString’s stringWithString:outPath
  
set newPath to pathString’s stringByExpandingTildeInPath()
  
  
set myNewImageData to (aRawimg’s representationUsingType:(NSPNGFileType) |properties|:(missing value))
  
set aRes to (myNewImageData’s writeToFile:newPath atomically:true) as boolean
  
  
return aRes
end saveImageRepAtPathAsPNG

★Click Here to Open This Script 

2017/09/24 迷路をRTFで作成して脱出経路を赤く着色する v3

2D迷路の作成プログラム「MazeFinder」(By Toby Jennings)をフレームワーク化したものを呼び出して、文字ベースの迷路データをRTFに変換して脱出経路情報を着色してデスクトップに書き出すAppleScriptのアップデート版です。

迷路データの作成はこの際どうでもよく、文字列の検索を行う部分で問題が起こったことへの対処を行いました。

NSStringの文字検索メソッドrangeOfString: で、検索文字列が見つからなかった場合にNSNotFound(=-1)が返ってくるはずですが、macOS 10.13 betaで動かしてみたところNSNotFoundではなく巨大な数が返ってくることに気づきました。9,223,372,036,854,775,807と巨大すぎてAppleScriptの処理系では数値の精度上限を超えてしまっています。

いろいろ情報収集してみたところ、10.13にかぎらずよく知られた動作のようで、-1ではなく巨大な数を返してしまうパターンがある、と認識しておく必要があるようです(いつものApple仕事)。

AppleScript名:迷路をRTFで作成して脱出経路を赤く着色する v3
– Created 2017-09-19 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
use framework “MazeFinder” –https://github.com/tcjennings/MazeFinder
–http://piyocast.com/as/archives/4842

property NSColor : a reference to current application’s NSColor
property Board2D : a reference to current application’s Board2D
property NSUUID : a reference to current application’s NSUUID
property NSDictionary : a reference to current application’s NSDictionary
property NSMutableArray : a reference to current application’s NSMutableArray
property NSMutableAttributedString : a reference to current application’s NSMutableAttributedString
property NSFontAttributeName : a reference to current application’s NSFontAttributeName
property NSFont : a reference to current application’s NSFont
property NSString : a reference to current application’s NSString
property NSDocumentTypeDocumentAttribute : a reference to current application’s NSDocumentTypeDocumentAttribute
property NSForegroundColorAttributeName : a reference to current application’s NSForegroundColorAttributeName
property Pathfinder : a reference to current application’s Pathfinder
property NSLiteralSearch : a reference to current application’s NSLiteralSearch
–NSNotFoundはプロパティに代入しても認識されなかった

set targFontName to “Courier-Bold” –PostScript Name
set targFontSize to 13 –Point

–迷路テキストデータ作成→Attributed Stringに
set aStr to create2DMazeAndSolveIt(30, 30, 1, 1, 28, 28) of me –Plain Text
set anAttr to changeAttrStrsFontAttribute(aStr, targFontName, targFontSize) of me –Attributed String

–迷路データに着色(参照呼び出し, Call by reference)
markCharOfAttributedString(anAttr, aStr, “!”, (NSColor’s redColor())) of me

–結果のRTFをデスクトップ上に書き出す。ファイル名はUUID.rtf
set targFol to current application’s NSHomeDirectory()’s stringByAppendingPathComponent:“Desktop”
set aRes to my saveStyledTextAsRTF(targFol, anAttr)

–指定のAttributed String内で指定文字列が含まれる箇所に指定の色をつける(結果はメイン側に参照渡し)
on markCharOfAttributedString(anAttr, origStr, aTargStr, aColor)
  set rList to searchWordWithRange(origStr, aTargStr) of me
  
repeat with ii in rList
    (anAttr’s addAttribute:(NSForegroundColorAttributeName) value:aColor range:ii)
  end repeat
end markCharOfAttributedString

–指定の文字列をAttributed Stringに変換して任意のフォントを一括指定
on changeAttrStrsFontAttribute(aStr, aFontPSName, aFontSize)
  set tmpAttr to NSMutableAttributedString’s alloc()’s initWithString:aStr
  
set aRange to current application’s NSMakeRange(0, tmpAttr’s |length|())
  
set aVal1 to NSFont’s fontWithName:aFontPSName |size|:aFontSize
  
tmpAttr’s beginEditing()
  
tmpAttr’s addAttribute:(NSFontAttributeName) value:aVal1 range:aRange
  
tmpAttr’s endEditing()
  
return tmpAttr
end changeAttrStrsFontAttribute

–指定テキストデータ(atargText)内に、指定文字列(aSearchStr)が含まれる範囲情報(NSRange)をすべて取得する
on searchWordWithRange(aTargText, aSearchStr)
  set aStr to NSString’s stringWithString:aTargText
  
set bStr to NSString’s stringWithString:aSearchStr
  
set hitArray to NSMutableArray’s alloc()’s init()
  
set cNum to (aStr’s |length|()) as integer
  
  
set aRange to current application’s NSMakeRange(0, cNum)
  
  
repeat
    set detectedRange to aStr’s rangeOfString:bStr options:NSLiteralSearch range:aRange
    
set aLoc to (detectedRange’s location)
    
–CAUTION !!!! Sometimes aLoc returns not NSNotFound (-1) but a Very large number
    
if (aLoc > 9.999999999E+9) or (aLoc = (current application’s NSNotFound)) then exit repeat
    
    
hitArray’s addObject:detectedRange
    
    
set aNum to aLoc as integer
    
set bNum to (detectedRange’s |length|) as integer
    
    
set aRange to current application’s NSMakeRange(aNum + bNum, cNum - (aNum + bNum))
  end repeat
  
  
return hitArray
end searchWordWithRange

–スタイル付きテキストを指定フォルダ(POSIX path)にRTFで書き出し
on saveStyledTextAsRTF(targFol, aStyledString)
  set bstyledLength to aStyledString’s |string|()’s |length|()
  
set bDict to NSDictionary’s dictionaryWithObject:“NSRTFTextDocumentType” forKey:(NSDocumentTypeDocumentAttribute)
  
set bRTF to aStyledString’s RTFFromRange:(current application’s NSMakeRange(0, bstyledLength)) documentAttributes:bDict
  
  
set theName to (NSUUID’s UUID()’s UUIDString())
  
set thePath to NSString’s stringWithString:targFol
  
set thePath to (thePath’s stringByAppendingPathComponent:theName)’s stringByAppendingPathExtension:“rtf”
  
return (bRTF’s writeToFile:thePath atomically:true) as boolean
end saveStyledTextAsRTF

–2D迷路を作成して脱出経路を検索して文字列で迷路データを出力する
–迷路サイズ x,y スタート座標 x, y ゴール座標 x,y
on create2DMazeAndSolveIt(xMax, yMax, xStart, yStart, xGoal, yGoal)
  set myBoard to Board2D’s alloc()’s init()
  
myBoard’s setupBoardWithRows:xMax WithColumns:yMax –60×60ぐらいが上限。正方形でないとダメ
  
myBoard’s createMazeFromBoard()
  
  
set myPathfinder to Pathfinder’s alloc()’s init()
  
set myPath to myPathfinder’s findPathThroughMaze:myBoard fromX:xStart fromY:yStart toX:xGoal toY:yGoal
  
  
set aCount to myPath’s |count|()
  
if aCount > 0 then
    set aRes to myBoard’s drawPathThroughMaze:myPath
    
return aRes as string
  else
    return false
  end if
end create2DMazeAndSolveIt

★Click Here to Open This Script 

2017/09/23 指定日時以降に作成されたファイルをSpotlight検索 v3

指定フォルダ(Desktop)以下のファイルで、指定日時以降に作成されたものをSpotlight検索するAppleScriptの修正版です。

Metadata Libの作者であるShane Stanleyから直接「こうしたほうがいいよ」と教えてもらいました。たしかに、dateをイチイチISO8601日付文字列に変換しないといけないというのは、さすがにおかしいと思っていました(汗)。

記述と処理がシンプルになっただけで、v2でもv3でも処理時間に大差はありません。同じ対象に対して同じ検索を行なって、0.01秒弱というところでした。

本ルーチンでは指定年月日でNSDateオブジェクトを生成し、それをもとにMetadata LibでSpotlight検索を行なっています。実行のさいには、Metadata Libを~/Library/Script Libraries/フォルダに入れておく必要があります。

従来、自分はAppleのMailing ListやWebフォーラムなどの情報、および海外で販売されていたAppleScript CD-ROMなどを購入するなどして、ほぼ一方的に英語ベースの情報を受け身で収集していたわけですが、このようなBlogによる情報発信やらGoogle翻訳の精度向上の影響で、割と些細な情報も相互に伝わるようになりつつある昨今です。

AppleScript名:指定日時以降に作成されたファイルをSpotlight検索 v3
– Created 2017-09-21 by Takaaki Naganoya
– Modified 2017-09-22 by Shane Stanley
– 2017 Piyomaru Software
use AppleScript version “2.5″
use scripting additions
use framework “Foundation”
use mdLib : script “Metadata Lib” version “1.0.0″
–http://piyocast.com/as/archives/4841

property NSTimeZone : a reference to current application’s NSTimeZone
property NSCalendar : a reference to current application’s NSCalendar

set aDate to getDateInternationalYMDhms(2017, 9, 21, 23, 2, 0) of me
–>  (NSDate) 2017-09-21 14:02:00 +0000

set thePath to POSIX path of (path to desktop)
–>  ”/Users/me/Desktop/”

set theFiles to mdLib’s searchFolders:{thePath} searchString:(“kMDItemFSCreationDate >= %@”) searchArgs:{aDate}
–> returns POSIX path list

on getDateInternationalYMDhms(aYear, aMonth, aDay, anHour, aMinute, aSecond)
  set theNSCalendar to NSCalendar’s currentCalendar()
  
set theDate to theNSCalendar’s dateWithEra:1 |year|:aYear |month|:aMonth |day|:aDay hour:anHour minute:aMinute |second|:aSecond nanosecond:0
  
return theDate
end getDateInternationalYMDhms

★Click Here to Open This Script 

2017/09/22 指定日時以降に作成されたファイルをSpotlight検索 v2

指定フォルダ(Desktop)以下のファイルで、指定日時以降に作成されたものをSpotlight検索するAppleScriptです。

本ルーチンの原型は、電子書籍を作成する際にMarkdown書類(MacDown)とPages書類をまとめてPDFに出力して1つのPDFに連結するAppleScriptで使っていたものです(こういう武器がないと、電子書籍を執筆からレイアウトから何から何までひとりで行うことなんてできません)。

bookfiles2.png
Pagesはともかく、MacDownからは指定フォルダにPDFを書き出すという機能は(MacDownのAppleScript用語辞書に機能が用意されていないため)AppleScriptからは利用できませんでした。そこで、GUI Scriptingで強引にデスクトップにPDF書き出しすることに。

デスクトップに書き出させたのは、保存ダイアログ上でCommand-Dのキーボードショートカットで一意に指定できる数少ないフォルダだからです(このあたり、生活の知恵)。

GUI Scriptingでファイル保存やファイル書き出しを行う場合、保存ダイアログがどこのフォルダを指し示すかはアプリケーション側まかせなので、保存/書き出し先フォルダをデスクトップに強引に指定する処理はたいへん重要です。GUI Scriptingはたいへんに弱点が多く不確実な手段ですが、不確実さを地道につぶしてあげることで、信頼性のある処理フローを構築できます。

PDF書き出し開始時のタイムスタンプを変数に記憶させておき、その時刻以降でデスクトップに生成されたPDFをすべて取得し、ファイル名でソート(ファイル名の先頭にソート用の仮想ノンブルをつけて管理)し、(末尾に生じていた意図しない空白ページ自動削除しつつ)1つのPDFに合成していました。

そのため、日時を指定して指定フォルダに存在するファイルをSpotlight検索する本ルーチンの重要度はとても高かったわけです。

当初は日付情報をJan 1 2001 00:00:00 GMTからの相対秒(Absolute Time)で指定していたのですが、OSのバージョンアップにともないこの書き方が使えなくなったため、仕方なく対処。

本ルーチンでは指定年月日でdateオブジェクトを生成したあと、ISO8601形式の日付文字列に変換し、それをもとにShane StanleyのMetadata LibでSpotlight検索を行なっています。実行のさいには、Metadata Libを~/Library/Script Libraries/フォルダに入れておく必要があります。

AppleScript名:指定日時以降に作成されたファイルをSpotlight検索 v2
– Created 2017-09-21 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.5″
use framework “Foundation”
use scripting additions
use mdLib : script “Metadata Lib” version “1.0.0″
–http://piyocast.com/as/archives/4838

property NSTimeZone : a reference to current application’s NSTimeZone
property NSCalendar : a reference to current application’s NSCalendar
property NSDateFormatter : a reference to current application’s NSDateFormatter

set aDate to getDateInternationalYMDhms(2017, 9, 21, 23, 2, 0) of me
–>  date “2017年9月21日木曜日 23:02:00″

set bStr to retISO8601DateTimeString(aDate) as string
–>  ”2017-09-21T14:02:00Z”

set thePath to POSIX path of (path to desktop)

set theFiles to mdLib’s searchFolders:{thePath} searchString:(“kMDItemFSCreationDate >= ’$time.iso(\”" & bStr & “\”)’”) searchArgs:{}
–> returns POSIX path list

–NSDate -> ISO8601 Date & Time String
on retISO8601DateTimeString(targDate)
  set theNSDateFormatter to NSDateFormatter’s alloc()’s init()
  
theNSDateFormatter’s setDateFormat:“yyyy-MM-dd’T’HH:mm:ss’Z’”
  
theNSDateFormatter’s setTimeZone:(NSTimeZone’s alloc()’s initWithName:“UTC”)
  
return (theNSDateFormatter’s stringFromDate:targDate) as text
end retISO8601DateTimeString

–現在のカレンダーで指定年月のdate objectを返す(年、月、日、時、分、秒)
on getDateInternationalYMDhms(aYear, aMonth, aDay, anHour, aMinute, aSecond)
  set theNSCalendar to NSCalendar’s currentCalendar()
  
set theDate to theNSCalendar’s dateWithEra:1 |year|:aYear |month|:aMonth |day|:aDay hour:anHour minute:aMinute |second|:aSecond nanosecond:0
  
return theDate as date
end getDateInternationalYMDhms

★Click Here to Open This Script 

2017/09/20 迷路をRTFで作成して脱出経路を赤く着色する

2D迷路の作成プログラム「MazeFinder」(By Toby Jennings)をフレームワーク化したものを呼び出して、文字ベースの迷路データをRTFに変換して脱出経路情報を着色してデスクトップに書き出すAppleScriptです。

maze1.png

本Scriptを試す場合には、MazeFinder.frameworkをダウンロードして~/Library/Frameworksフォルダに入れてください。

60×60の迷路を作成して着色してファイル書き出しを終了するまでに、開発環境で0.08秒程度かかりました。

–> Download Framework Binary

迷路作成の処理自体についてはとくに意味はありませんが、迷路プログラム自体にもいろいろあるようです。このMazeFinderについてはいろいろ制約条件がきつく(最大サイズ60×60程度、迷路が正方形でないとクラッシュ)、あまり迷路らしい迷路にもなっていません。

「テキストデータをRTFで出力して特定文字に着色する」というあたりの処理がミソでしょうか。

AppleScript名:迷路をRTFに作成して脱出経路を赤く着色する
– Created 2017-09-19 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
use framework “MazeFinder” –https://github.com/tcjennings/MazeFinder
–http://piyocast.com/as/archives/4831

property NSColor : a reference to current application’s NSColor
property Board2D : a reference to current application’s Board2D
property NSUUID : a reference to current application’s NSUUID
property NSDictionary : a reference to current application’s NSDictionary
property NSMutableArray : a reference to current application’s NSMutableArray
property NSMutableAttributedString : a reference to current application’s NSMutableAttributedString
property NSFont : a reference to current application’s NSFont
property NSString : a reference to current application’s NSString
property NSDocumentTypeDocumentAttribute : a reference to current application’s NSDocumentTypeDocumentAttribute
property NSForegroundColorAttributeName : a reference to current application’s NSForegroundColorAttributeName
property Pathfinder : a reference to current application’s Pathfinder
property NSLiteralSearch : a reference to current application’s NSLiteralSearch

set targFontName to “Courier-Bold” –”Courier New”/”Osaka-Mono”

–Create Maze Text
set aStr to create2DMazeAndSolveIt(30, 30, 1, 1, 28, 28) of me
set anAttr to NSMutableAttributedString’s alloc()’s initWithString:aStr

set bList to {“!”} –Mark Target List

–Set Fixed Width Font
set aRange to current application’s NSMakeRange(0, anAttr’s |length|())
set aVal1 to NSFont’s fontWithName:targFontName |size|:13
anAttr’s beginEditing()
anAttr’s addAttribute:(current application’s NSFontAttributeName) value:aVal1 range:aRange
anAttr’s endEditing()

–Change Attribute (red color)
repeat with i in bList
  set rList to searchWordWithRange(aStr, i as string) of me
  
repeat with ii in rList
    (anAttr’s addAttribute:(current application’s NSForegroundColorAttributeName) value:(NSColor’s redColor()) range:ii)
  end repeat
end repeat

–結果のRTFをデスクトップ上に書き出す
set targFol to current application’s NSHomeDirectory()’s stringByAppendingPathComponent:“Desktop”
set aFileName to (NSUUID’s UUID()’s UUIDString() as text)
set aRes to my saveStyledTextAsRTF(aFileName, targFol, anAttr)

on searchWordWithRange(aTargText, aSearchStr)
  set aStr to NSString’s stringWithString:aTargText
  
set bStr to NSString’s stringWithString:aSearchStr
  
set hitArray to NSMutableArray’s alloc()’s init()
  
set cNum to (aStr’s |length|()) as integer
  
  
set aRange to current application’s NSMakeRange(0, cNum)
  
  
set aCount to 1
  
repeat
    set detectedRange to aStr’s rangeOfString:bStr options:NSLiteralSearch range:aRange
    
if detectedRange’s location is equal to (current application’s NSNotFound) then exit repeat
    
    
hitArray’s addObject:detectedRange
    
    
set aNum to (detectedRange’s location) as integer
    
set bNum to (detectedRange’s |length|) as integer
    
    
set aRange to current application’s NSMakeRange(aNum + bNum, cNum - (aNum + bNum))
    
set aCount to aCount + 1
  end repeat
  
  
return hitArray
end searchWordWithRange

–スタイル付きテキストを指定フォルダ(POSIX path)にRTFで書き出し
on saveStyledTextAsRTF(aFileName, targFol, aStyledString)
  –Convert NSMutableStyledStrings to RTF
  
set bstyledLength to aStyledString’s |string|()’s |length|()
  
set bDict to NSDictionary’s dictionaryWithObject:“NSRTFTextDocumentType” forKey:(NSDocumentTypeDocumentAttribute)
  
set bRTF to aStyledString’s RTFFromRange:(current application’s NSMakeRange(0, bstyledLength)) documentAttributes:bDict
  
  
– Build Path
  
set theName to NSString’s stringWithString:aFileName
  
–set theName to theName’s stringByReplacingOccurrencesOfString:”/” withString:”_”
  
–set theName to theName’s stringByReplacingOccurrencesOfString:”:” withString:”_”
  
set thePath to NSString’s stringWithString:targFol
  
set thePath to (thePath’s stringByAppendingPathComponent:theName)’s stringByAppendingPathExtension:“rtf”
  
  
return (bRTF’s writeToFile:thePath atomically:true) as boolean
end saveStyledTextAsRTF

–2D迷路を作成して脱出経路を検索して文字列で迷路データを出力する
–迷路サイズ x,y スタート座標 x, y ゴール座標 x,y
on create2DMazeAndSolveIt(xMax, yMax, xStart, yStart, xGoal, yGoal)
  set myBoard to Board2D’s alloc()’s init()
  
myBoard’s setupBoardWithRows:xMax WithColumns:yMax –60×60ぐらいが上限。正方形でないとダメ
  
myBoard’s createMazeFromBoard()
  
  
set myPathfinder to Pathfinder’s alloc()’s init()
  
set myPath to myPathfinder’s findPathThroughMaze:myBoard fromX:xStart fromY:yStart toX:xGoal toY:yGoal
  
  
set aCount to myPath’s |count|()
  
if aCount > 0 then
    set aRes to myBoard’s drawPathThroughMaze:myPath
    
return aRes as string
  else
    return false
  end if
end create2DMazeAndSolveIt

★Click Here to Open This Script 

2017/09/19 DDFileReaderによる行単位でのテキストファイル読み込みテスト

オープンソースのテキスト行単位読み込みプログラム「DDFileReader」(By Dave DeLong)をCocoa Frameworkにした「DDFileReader.framework」を呼び出してテキストの行単位読み込みを行うAppleScriptです。

AppleScriptの標準命令readコマンドでも行単位読み込みができるので(掲載サンプル参照)、本フレームワークを使う必要はほとんどないのですが、とりあえず実験してみました。

本Scriptを試す場合には、DDFileReader.frameworkをダウンロードして~/Library/Frameworksフォルダに入れてください。オリジナルのままだとARC環境下でビルドできなかったので、releaseとかretainとかdeallocとかそのあたりのキーワードが含まれる行をコメントアウトして試していますが、その程度なので問題があったらぜひ知らせてください。自分でもコレを重要なプログラムで使うつもりはないのですが、問題があったら困ります。

–> Download Framework Binary

AppleScript名:DDFileReaderによる行単位でのテキストファイル読み込みテスト
– Created 2017-09-15 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “DDFileReader” –https://stackoverflow.com/questions/1044334/objective-c-reading-a-file-line-by-line
–http://piyocast.com/as/archives/4829

property DDFileReader : a reference to current application’s DDFileReader
property NSMutableArray : a reference to current application’s NSMutableArray

set tmpPath to POSIX path of (choose file)
set aReader to DDFileReader’s alloc()’s initWithFilePath:tmpPath

set tArray to NSMutableArray’s new()
repeat
  set aLine to aReader’s readLine()
  
if aLine = missing value then exit repeat
  
tArray’s addObject:aLine
end repeat

return length of (tArray as list)

★Click Here to Open This Script 

AppleScript名:Pure ASによるファイルの行単位読み込み
set aFile to choose file of type {“public.plain-text”}

set newList to {}

tell current application
  set fH to open for access aFile without write permission
  
try
    repeat
      set fRes to read fH as «class utf8» until return
      
log fRes
      
set the end of newList to fRes
    end repeat
  on error
    close access fH
  end try
  
end tell

return (length of newList)

★Click Here to Open This Script 

2017/09/15 64ビット化して処理能力が低下したCocoa Finderの代替ツールはSystem Events?

macOS 10.6〜10.7で64ビット化にともないCocoaで書き直されたFinderのパフォーマンス低下が(当時から)指摘されていましたが、日常的な用途ではあまり気にしていませんでした。

Cocoa Finderのはじめのうちは、OS起動直後に表示されるFinderのウィンドウ内のアイコン描画が間に合わないのか仮アイコンで表示され、徐々に正しいアイコンで表示されるような光景もよく目にしていました(さすがに最近はそういう光景は目にしなくなりましたけれども)。

そんな中、macOS 10.13beta+MacBook Air 2011で大量のファイルを処理したときに、Finder経由で処理するとトンでもなくスピードが(SSDなのに)遅いことに気づきました。SSDに合わせて再設計したAPFS+SSDの組み合わせなのに、とてつもなく遅い。速い速いというふれ込みだったのに、Finder経由だと驚くほど遅い。

最初は、AppleScriptとFinderの間のやりとりが遅いものだと思っていましたが、実はFinderの処理そのものが遅いことが判明。数百個のファイルを選択してFinder上で(メニューから「複製」コマンドを実行して)ファイルコピーさせたりすると、めまいがするほど低速です。

そして、Appleの「AppleScript Language Guide」で「list folder」コマンド(廃止予定)を調べていたら、同コマンドの代替としてSystem Events経由でファイル情報にアクセスするやり方が掲載されているのを見つけ、Finder経由のファイル処理がすでに推奨されていない状態なのではないか、と疑いを持つようになりました。

そこで、同じファイル処理を(macOS 10.13beta上で)System EventsとFinderに対して実行してみたところ、4,224ファイル存在しているフォルダから、全ファイル名を取得する処理は、

 System Events:1秒
 Finder:6秒

同フォルダに対して、ファイル名に「99」という文字を含むファイル名だけを抽出させてみた(フィルタ参照)ところ、

 System Events:14秒
 Finder:計測不能(timeout時間を3,600秒に設定して実行してみたものの、Finderがハングアップして処理が数十分返ってこない)

といったところ。速度、信頼性ともにFinderよりもSystem Eventsの方が高いという状況です。処理対象ファイル数が100以下ぐらいであればFinder経由でも気にならないのですが、それを超えると露骨にパフォーマンス低下が顕在化します。

ただし、最近はAppleScriptでもCocoaの機能(NSFileManager)を利用してファイルの処理を行なっていたので、System Events経由のファイル処理も「驚くほど速いわけでもない」という印象。指定フォルダ内のファイルの抽出もSpotlight経由で行なっていたので、それほど不自由は感じていませんでした。

サンプルを掲載する際にも、なるべくSystem EventsかNSFileManagerを経由してファイル処理を行うようにする考えです。

Finderのコントロールは、選択中のフォルダやファイルを取得するとか、処理結果のフォルダやファイルを新規Finderウィンドウで表示させるといった用途に限定することが望ましいのでしょう(あと、指定フォルダ内のファイル一覧をas alias listで取得するのも(フィルタ参照を併用しなければ)高速です)。

2017/09/06 ツイ4のページで新規連載マンガの画像を取得してPDF化(新規連載のPDF化)v3

Safariで表示中のWebマンガサイト「ツイ4」(更新情報をTwitterに投稿)のマンガを全エピソードダウンロードしてPDFにまとめるAppleScriptです。

実行にあたってはShane StanleyのAppleScript Libraries「BridgePlus」のインストールを必要とします(~/ibrary/Script Librariesフォルダに入れるだけ)。

実行開始時にはSafariでツイ4の特定のマンガのページをオープンしている必要があります。

tui4.png

Safariの最前面のウィンドウからURLやTitle、リンクされている画像の詳細情報を取得し、条件チェックなどを行なったのちに詳細なデータの抽出を行います。

次に、PDFの保存先を選択するダイアログを表示。このさい、デフォルトの保存先を「ピクチャ」フォルダ、ファイル名をマンガのタイトルに指定。

ページにリンクされていた画像(ツイではファイル名はシーケンシャル番号)から番号の情報だけを抽出して最大値、最小値を計算。この範囲で画像のダウンロード、PDFへの追記を行います。ただし、実運用してみたところ、Safariからすべての画像を取得できないようで(非同期表示しているようなので)、とりあえず1〜9999までの番号の画像を順次ダウンロードし、画像が存在しなければ処理を終了しています。

画像をダウンロードするたびにPDFに追記していますが、このあたりは途中でエラーが出て停止してもそれまでの処理内容が保存されることを意図してのことです。SSD搭載機では問題のない処理ですが、HDD搭載機では若干遅く感じるかもしれません(もはやHDD搭載機が身の回りにないので不明)。

これまでは、マンガの新規連載がはじまるとcurlコマンドで画像をダウンロードしてPDFに連結する作業を手で行なっていたのですが(誰も頼んでねえよ)、新規連載が増えたので自動化してみました。それでもありあわせの部品を組み合わせただけなので、それほど手間はかかっていません。

本Scriptとは別に更新された差分をPDFに連結するAppleScriptを作って日々実行し、大きな画面でブラウズするのに役立てています。割とこういう、ごくごく私的なScriptで野心的な処理を先行してテストしているものです。

AppleScript名:ツイ4のページで新規連載マンガの画像を取得してPDF化(新規連載のPDF化)v3
– Created 2016-09-05 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
use framework “QuartzCore”
use BridgePlus : script “BridgePlus”
–http://piyocast.com/as/archives/4808

property SMSForder : a reference to current application’s SMSForder
property |NSURL| : a reference to current application’s |NSURL|
property NSURLRequest : a reference to current application’s NSURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSArray : a reference to current application’s NSArray
property NSFileManager : a reference to current application’s NSFileManager
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSPredicate : a reference to current application’s NSPredicate
property PDFPage : a reference to current application’s PDFPage
property PDFDocument : a reference to current application’s PDFDocument
property NSURLRequestUseProtocolCachePolicy : a reference to current application’s NSURLRequestUseProtocolCachePolicy
property NSNumberFormatterPadBeforePrefix : a reference to current application’s NSNumberFormatterPadBeforePrefix
property NSImage : a reference to current application’s NSImage
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSRegularExpressionDotMatchesLineSeparators : a reference to current application’s NSRegularExpressionDotMatchesLineSeparators
property NSRegularExpressionAnchorsMatchLines : a reference to current application’s NSRegularExpressionAnchorsMatchLines
property NSRegularExpression : a reference to current application’s NSRegularExpression
property NSString : a reference to current application’s NSString

property theTargetSite : “http://sai-zen-sen.jp/”

tell application “Safari”
  if (count every document) = 0 then
    display notification “Safari does not open web page”
    
return
  end if
  
  
set docTitle to (do JavaScript “document.title” in front document) –Title
  
  
tell front document –URL
    set aURL to URL
  end tell
end tell

if aURL does not start with theTargetSite then
  display notification “This site is not the target”
  
return
end if

–Safariの最前面のウィンドウから画像リンクをすべて取得(Height, Width, URL)
set aList to getImageSizeAndURLOfFrontSafariDocument() of me

–取得した画像情報の2D Listをサイズで降順ソート
load framework –Force loading BridgePlus framework
set sortIndexes to {0, 1} –Key Item id: begin from 0
set sortOrders to {false, false}
set sortTypes to {“compare:”, “compare:”}
set resList to (current application’s SMSForder’s subarraysIn:(aList) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes |error|:(missing value))

–画像が取得できなかったら処理終了
if (resList as list) = {} then
  display notification “There is no images on this page”
  
return –No Result
end if

–最大サイズの画像情報を取得する(おそらくマンガ)
set {maxHeight, maxWidth, maxURL} to contents of first item of (resList as list)

set aNSURL to |NSURL|’s URLWithString:maxURL
set aNSURLfilename to (aNSURL’s lastPathComponent())
set aNSURLpure to aNSURL’s URLByDeletingLastPathComponent()
set aNSURLextension to aNSURLfilename’s pathExtension() as string
set aNSURLfilenameLen to (aNSURLfilename’s stringByDeletingPathExtension())’s |length|() as integer –画像ファイル名から拡張子を除去した部分の文字列長

–画像情報リストを画像サイズで抽出
set maxHeightStr to (maxHeight as integer) as string
set maxWidthStr to (maxWidth as integer) as string
set thePred to NSPredicate’s predicateWithFormat:(“(self[0] == “ & maxHeightStr & “) AND (self[1] == “ & maxWidthStr & “)”)
set bArray to (resList’s filteredArrayUsingPredicate:thePred) as list

–URLからファイル名の数値部分のみ抽出
set imageArray to current application’s NSMutableArray’s new()
repeat with i in bArray
  set j to contents of last item of i –(Image URL)
  
set aTmpURL to (|NSURL|’s URLWithString:j)
  
set aTmpfilename to (aTmpURL’s lastPathComponent()) as string
  
set numStr to first item of (my findPattern:(“^\\d{1,” & (aNSURLfilenameLen as string) & “}”) inString:aTmpfilename)
  
set jj2 to (SMSForder’s transformedFrom:numStr ICUTransform:“Fullwidth-Halfwidth” inverse:false) as integer
  (
imageArray’s addObject:jj2)
end repeat

–ファイル名から抽出した数値の最小値と最大値を求める。ただ、実運用したらWeb側から画像をすべて取得されない(非同期読み込みを行なっているらしい)ケースがあったため、ここの値は参考値程度にしか使えなかった
set maxRes to (imageArray’s valueForKeyPath:“@max.self”)’s intValue() –最大値
set minRes to (imageArray’s valueForKeyPath:“@min.self”)’s intValue() –最小値
log {minRes, maxRes}

–PDFのファイル名と場所をユーザーに確認
set pdfFile to (choose file name with prompt “Select PDF Name & Location” default location (path to pictures folder) default name (docTitle & “.pdf”))
set pdfFilePOSIX to POSIX path of pdfFile
set newFilePath to current application’s NSString’s stringWithString:pdfFilePOSIX

–Make Blank PDF
set aPDFdoc to PDFDocument’s alloc()’s init()

–Download each image and append to blank PDF
set insCount to 1 –画像ダウンロード用のページ数(Loop Counter)とPDF連結用のページ番号(insCount)を分離

–repeat with i from minRes as integer to maxRes as integer
repeat with i from 1 to 9999
  –URL部品の連結
  
set aFILENAME to numToZeroPaddingStr(i, aNSURLfilenameLen, “0″) of me
  
set aFULLURL to (aNSURLpure’s absoluteString() as string) & (aFILENAME as string) & “.” & (aNSURLextension as string)
  
set aURL to (|NSURL|’s URLWithString:aFULLURL)
  
  
–URL(画像)をダウンロード
  
set {uRes, headerRes, aData} to checkURLResourceExistence(aURL, 3) of me
  
  
if uRes = true then
    display notification “Episode “ & (i as string) & ” exists…”
    
set bImage to (NSImage’s alloc()’s initWithData:aData)
    (
aPDFdoc’s insertPage:(PDFPage’s alloc()’s initWithImage:bImage) atIndex:(insCount - 1))
    (
aPDFdoc’s writeToFile:newFilePath) –1Page更新するたびにファイル保存
    
set changedF to true –PDFにページが追記されたことを検出
  else
    display notification “No more new episode….”
    
exit repeat
  end if
  
  
set insCount to insCount + 1
end repeat

–FinderコメントにURLを記入
tell application “Finder”
  set comment of (pdfFile as alias) to (aNSURLpure’s absoluteString() as string)
end tell

–生成したPDFをオープン。ビューワー経由ではなくFinder経由でopen命令を送って表示
tell application “Finder”
  open (pdfFile as alias)
end tell
–ここで処理終了

—————

on getImageSizeAndURLOfFrontSafariDocument()
  set aList to {}
  
  
tell application “Safari”
    if its running then
      if (count every document) = 0 then return {}
      
set aRes to (do JavaScript “document.images.length” in front document)
      
      
repeat with i from 0 to (aRes - 1)
        set aHeight to do JavaScript ((“document.images[” & i as string) & “].height”) in front document
        
set aWidth to do JavaScript ((“document.images[” & i as string) & “].width”) in front document
        
set aSRC to do JavaScript ((“document.images[” & i as string) & “].src”) in front document
        
set the end of aList to {aHeight, aWidth, aSRC}
      end repeat
    end if
  end tell
  
  
return aList
end getImageSizeAndURLOfFrontSafariDocument

on findPattern:thePattern inString:theString
  set theOptions to ((NSRegularExpressionDotMatchesLineSeparators) as integer) + ((NSRegularExpressionAnchorsMatchLines) as integer)
  
set theRegEx to NSRegularExpression’s regularExpressionWithPattern:thePattern options:theOptions |error|:(missing value)
  
set theFinds to theRegEx’s matchesInString:theString options:0 range:{location:0, |length|:length of theString}
  
set theFinds to theFinds as list
  
set theResult to {}
  
set theNSString to NSString’s stringWithString:theString
  
  
repeat with i in theFinds
    set theRange to (contents of i)’s range()
    
set end of theResult to (theNSString’s substringWithRange:theRange) as string
  end repeat
  
return theResult
end findPattern:inString:

–1D List(文字)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DList:theList ascOrder:aBool
  set aDdesc to NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

–整数の値に指定桁数ゼロパディングして文字列で返す
on numToZeroPaddingStr(aNum as integer, aDigit as integer, paddingChar as text)
  set aNumForm to NSNumberFormatter’s alloc()’s init()
  
aNumForm’s setPaddingPosition:(NSNumberFormatterPadBeforePrefix)
  
aNumForm’s setPaddingCharacter:paddingChar
  
aNumForm’s setMinimumIntegerDigits:aDigit
  
  
set bNum to NSNumber’s numberWithInt:aNum
  
set aStr to aNumForm’s stringFromNumber:bNum
  
  
return aStr as text
end numToZeroPaddingStr

– 指定URLにファイル(画像など)が存在するかチェック
–> {存在確認結果(boolean), レスポンスヘッダー(NSDictionary), データ(NSData)}
on checkURLResourceExistence(aURL, timeOutSec as real)
  set aRequest to (NSURLRequest’s requestWithURL:aURL cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:timeOutSec)
  
set aRes to (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 404
  end if
  
return {(aResCode = 200), hRes, dRes}
end checkURLResourceExistence

–指定PDFのページ数をかぞえる(10.9対応。普通にPDFpageから取得)
–返り値:PDFファイルのページ数(整数値)
on pdfPageCount(aFile)
  set aFile to POSIX path of aFile
  
set theURL to |NSURL|’s fileURLWithPath:aFile
  
set aPDFdoc to PDFDocument’s alloc()’s initWithURL:theURL
  
set aRes to aPDFdoc’s pageCount()
  
return aRes as integer
end pdfPageCount

★Click Here to Open This Script 

2017/09/05 指定フォルダ以下のテキストファイルのファイル名冒頭についている数字から欠番を求める

ファイル名の先頭に1〜3桁の数字がついているテキストファイルに対して、指定フォルダ以下のものをすべて取得し、これらの数(連番)に抜けがないかをチェックするAppleScriptです。

filenames2.png

指定フォルダ以下に存在するテキストファイルをSpotlightで検索し、すべてのファイル名を取得してそこから先頭に存在する1〜3桁の数字を取得。全角文字が混入している場合にそなえて数字をすべて全角→半角変換。これらの数(おそらく連番)で途中に抜けがないかチェックします。

実行にはShane StanleyのAppleScript Libraries「BridgePlus」「MetaData Lib」のインストールが必要です。

連番リストの作成や全角→半角変換、Spotlight検索などをScript Librariesの機能に依存して書いています。また、集合(NSMutableSet)を扱えることで非常に高度な処理を完結に記述できています。処理速度も非常に高速です。

 127ファイルの連番チェック処理:0.113457024097 sec.
 449ファイルの連番チェック処理:0.381642997265 sec.

(結果はMacBook Pro Retina 2012 Core i7 2.66GHz+8GBでの所用時間)

AppleScript名:指定フォルダ以下のテキストファイルのファイル名冒頭についている数字から欠番を求める
– Created 2017-09-04 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions
use mdLib : script “Metadata Lib” version “1.0.0″
use bPlus : script “BridgePlus”
–http://piyocast.com/as/archives/4803

property SMSForder : a reference to current application’s SMSForder
property NSMutableArray : a reference to current application’s NSMutableArray
property NSMutableSet : a reference to current application’s NSMutableSet
property NSIndexSet : a reference to current application’s NSIndexSet
property NSRegularExpressionDotMatchesLineSeparators : a reference to current application’s NSRegularExpressionDotMatchesLineSeparators
property NSRegularExpressionAnchorsMatchLines : a reference to current application’s NSRegularExpressionAnchorsMatchLines
property NSRegularExpression : a reference to current application’s NSRegularExpression
property NSString : a reference to current application’s NSString

load framework –BridgePlus’s force framework loading command

–選択したフォルダ以下のPlain TextをすべてSpotlightで求める(POSIX path list)
set theFolder to (choose folder)
set aRes to retMissingNumberFromEachFiles(theFolder, {“public.plain-text”}) of me
–>  {}

on retMissingNumberFromEachFiles(theFolder, fileTypeList)
  set theFiles to mdLib’s searchFolders:{theFolder} searchString:“kMDItemContentType IN[c] %@” searchArgs:fileTypeList
  
if theFiles = {} then return
  
  
–取得したPOSIX Pathのリストからファイル名の部分のみ抽出
  
set anArray to NSMutableArray’s arrayWithArray:theFiles
  
set bArray to (anArray’s valueForKeyPath:“lastPathComponent”) as list
  
  
–各ファイルの名称の冒頭から1〜3桁 の数字を取り出して、全角–>半角変換を行いつつリストに追加
  
set nArray to NSMutableArray’s new()
  
repeat with i in bArray
    set j to contents of i
    
set aRes to (my findPattern:“^\\d{1,3}” inString:j)
    
if aRes is not equal to {} then
      set jj to (contents of first item of aRes) as string
      
set jj2 to (SMSForder’s transformedFrom:jj ICUTransform:“Fullwidth-Halfwidth” inverse:false) as integer
      (
nArray’s addObject:jj2)
    end if
  end repeat
  
  
–最大値、最小値をもとに連番リストを作成し、ファイル名から得られた配列データとの補集合を求める
  
set maxRes to (nArray’s valueForKeyPath:“@max.self”)’s intValue()
  
set minRes to (nArray’s valueForKeyPath:“@min.self”)’s intValue()
  
  
–最小値から最大値までの連番リスト作成
  
set theIndexSet to NSIndexSet’s indexSetWithIndexesInRange:{minRes, maxRes}
  
set theList to (SMSForder’s arrayWithIndexSet:theIndexSet) as list
  
  
set aSet to NSMutableSet’s setWithArray:theList
  
set bSet to NSMutableSet’s setWithArray:nArray
  
aSet’s minusSet:bSet –補集合
  
  
return (aSet’s allObjects() as list)
  
end retMissingNumberFromEachFiles

on findPattern:thePattern inString:theString
  set theOptions to ((NSRegularExpressionDotMatchesLineSeparators) as integer) + ((NSRegularExpressionAnchorsMatchLines) as integer)
  
set theRegEx to NSRegularExpression’s regularExpressionWithPattern:thePattern options:theOptions |error|:(missing value)
  
set theFinds to theRegEx’s matchesInString:theString options:0 range:{location:0, |length|:length of theString}
  
set theFinds to theFinds as list
  
set theResult to {}
  
set theNSString to NSString’s stringWithString:theString
  
  
repeat with i in theFinds
    set theRange to (contents of i)’s range()
    
set end of theResult to (theNSString’s substringWithRange:theRange) as string
  end repeat
  
return theResult
end findPattern:inString:

★Click Here to Open This Script 

2017/09/02 Finder上で選択中のファイルの中に書かれているタイトルに基づいてリネーム

Finder上で選択中のテキストファイルの中に書かれているタイトルを正規表現による検索で検出して、ファイル名に指定するAppleScriptです。

技術的にはぜんぜん見るべき点がありませんが、そこそこ役立つ見本です(ありものをつなぎ合わせた書き捨てレベル)。

「小説を読もう」サイトからKnight’s and Magicのテキストを(テキストエンコーディングにUTF-8を指定して)ダウンロードしてきたら、

text_download.png

ファイル名が、

filename_before.png

のようになっていたので、これを、

filename_after.png

のようにリネームしたいと考え、各テキストファイル中に書かれているタイトル部分を、

file_contents.png

正規表現で検索して指定してみました。ほとんどテキストの1行目にタイトルが書かれているので、1行目の内容をファイル名にしてもよかったのですが、

knight_magic4.png

このように(↑)たまにイレギュラーなファイルが存在しているので、正規表現でサーチしています。

実際にテストしてみたところ、全角文字の「#」と半角文字の「#」が登場していたので、全角文字で検索して存在しないようであれば半角文字で再検索するようになっています。

数百個ぐらいのファイルの処理だと、このようにFinderからselectionを拾ってきても十分なところですが、数千個以上になると別の方法を考えるべきでしょう。

AppleScript名:Finder上で選択中のファイルの中に書かれているタイトルに基づいてリネーム
– Created 2017-09-01 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4797

property NSRegularExpressionDotMatchesLineSeparators : a reference to current application’s NSRegularExpressionDotMatchesLineSeparators
property NSRegularExpressionAnchorsMatchLines : a reference to current application’s NSRegularExpressionAnchorsMatchLines
property NSRegularExpression : a reference to current application’s NSRegularExpression
property NSString : a reference to current application’s NSString

tell application “Finder”
  set aSel to selection as alias list
  
if aSel = {} or aSel = “” then return
end tell

repeat with i in aSel
  set aStr to (read i as «class utf8»)
  
  
–全角のタイトルマークでタイトルを検索
  
set aList to retHeaders(aStr, “#”) of me
  
if aList is not equal to {} then
    set newName to (contents of first item of aList)
    
tell application “Finder”
      set name of i to newName & “.txt”
    end tell
  else
    –半角のタイトルマークでタイトルを検索
    
set bList to retHeaders(aStr, “#”) of me
    
if bList is not equal to {} then
      set newName to (contents of first item of bList)
      
tell application “Finder”
        set name of i to newName & “.txt”
      end tell
    else
      log {i as string}
    end if
  end if
end repeat

on retHeaders(aCon, aHeaderMark)
  set tList to {}
  
set regStr to “^” & aHeaderMark & “{1,6}[^” & aHeaderMark & “]*?$”
  
  
set headerList to my findPattern:regStr inString:aCon
  
repeat with i in headerList
    set j to contents of i
    
set regStr2 to “^” & aHeaderMark & “{1,6}[^” & aHeaderMark & “]*?”
    
set headerLevel to length of first item of (my findPattern:regStr2 inString:j)
    
set the end of tList to (text (headerLevel + 1) thru -1 in j)
  end repeat
  
  
return tList
end retHeaders

on findPattern:thePattern inString:theString
  set theOptions to ((NSRegularExpressionDotMatchesLineSeparators) as integer) + ((NSRegularExpressionAnchorsMatchLines) as integer)
  
set theRegEx to NSRegularExpression’s regularExpressionWithPattern:thePattern options:theOptions |error|:(missing value)
  
set theFinds to theRegEx’s matchesInString:theString options:0 range:{location:0, |length|:length of theString}
  
set theFinds to theFinds as list – so we can loop through
  
set theResult to {} – we will add to this
  
set theNSString to NSString’s stringWithString:theString
  
repeat with i from 1 to count of items of theFinds
    set theRange to (item i of theFinds)’s range()
    
set end of theResult to (theNSString’s substringWithRange:theRange) as string
  end repeat
  
return theResult
end findPattern:inString:

★Click Here to Open This Script 

2017/08/31 指定フォルダ以下のすべてのファイルを再帰で取得 v2

指定フォルダ以下のすべてのファイルを再帰で取得するAppleScriptです。Finder経由ではなくNSFileManager経由で高速に処理します。

さすがにSpotlightの方が(実測値で70倍ぐらい)高速なので、Spotlightが効かない場所に対する非常用という位置づけです。

掲載リストのようにデスクトップフォルダ以下の、拡張子「.scpt」「.scptd」の書類のパスをPOSIX pathのlistで取得する処理を自分の環境で行なってみたところ、

  NSFileManager経由での再帰取得処理:1.5 sec.
  Spotlight経由でのファイル取得処理:0.02 sec.

という結果になりました(5回実行時の平均値)。いまどきFinderで再帰処理とかいうのは選択肢として「絶対にありえない」ので比較もしていませんが、同じ環境で同じ処理を行うとおそらく「数分」から「十数分」といったオーダーになるものと思います。

日常的にSpotlightによるファイル取得を行なっているので、本処理が活躍するケースはほとんどない見込みですが、ねんのため(Spotlightが効かない&メタデータが破損しているダメ環境でファイルを取得するための臨時処理)。

一緒に掲載しているSpotlight検索Scriptでは、Shane StanleyのMetadata Libを呼び出しています。実行のためには、Metadata Libを~/Libraries/Script Librariesフォルダにインストールしておく必要があります。

AppleScript名:指定フォルダ以下のすべてのファイルを再帰で取得 v2
– Created 2017-08-04 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/4796

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

–set aFol to POSIX path of (choose folder)
set aFol to POSIX path of (path to desktop folder)

–set aList to retFullPathWithinAFolderWithRecursive(aFol) of me
–set bList to retFullPathWithinAFolderWithRecursiveFilterByExt(aFol, “scpt”) of me
set cList to retFullPathWithinAFolderWithRecursiveFilterByExtList(aFol, {“scpt”, “scptd”}) of me
–set dList to retFullPathWithinAFolderWithRecursiveFilterByExtAndFileNameString(aFol, “png”, “スクリーン”) of me
–set eList to retFullPathWithinAFolderWithRecursiveFilterByExtAndFileNameString(aFol, {”scpt”, “scptd”}, “並列”) of me

–指定フォルダ以下のすべてのファイルを再帰で取得
on retFullPathWithinAFolderWithRecursive(aFol)
  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
  
  
return anArray as list
end retFullPathWithinAFolderWithRecursive

–指定フォルダ以下のすべてのファイルを再帰で取得(拡張子で絞り込み)
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

–指定フォルダ以下のすべてのファイルを再帰で取得(拡張子リストで絞り込み)
on retFullPathWithinAFolderWithRecursiveFilterByExtList(aFol, aExtList)
  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 IN [c]%@” argumentArray:{aExtList}
  
set bArray to anArray’s filteredArrayUsingPredicate:thePred
  
  
return bArray as list
end retFullPathWithinAFolderWithRecursiveFilterByExtList

–指定フォルダ以下のすべてのファイルを再帰で取得(文字列と拡張子で絞り込み)
on retFullPathWithinAFolderWithRecursiveFilterByExtListAndFileNameString(aFol, aExt, aNameString)
  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]%@ && lastPathComponent CONTAINS %@” argumentArray:{aExt, aNameString}
  
set bArray to anArray’s filteredArrayUsingPredicate:thePred
  
  
return bArray as list
end retFullPathWithinAFolderWithRecursiveFilterByExtListAndFileNameString

–指定フォルダ以下のすべてのファイルを再帰で取得(文字列と拡張子リストで絞り込み)
on retFullPathWithinAFolderWithRecursiveFilterByExtAndFileNameString(aFol, aExtList, aNameString)
  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 IN [c]%@ && lastPathComponent CONTAINS %@” argumentArray:{aExtList, aNameString}
  
set bArray to anArray’s filteredArrayUsingPredicate:thePred
  
  
return bArray as list
end retFullPathWithinAFolderWithRecursiveFilterByExtAndFileNameString

★Click Here to Open This Script 

AppleScript名:指定フォルダ以下のすべてのファイルを再帰で取得(spotlightで処理).scpt
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions
use mdLib : script “Metadata Lib” version “1.0.0″
–http://piyocast.com/as/archives/4796

–set theFolder to choose folder
set theFolder to (path to desktop folder)
set theFiles to mdLib’s searchFolders:{theFolder} searchString:“kMDItemContentType IN[c] %@” searchArgs:{“com.apple.applescript.script”, “com.apple.applescript.script-bundle”}

★Click Here to Open This Script 

2017/08/31 指定フォルダ内の指定文字列を名称に含むファイルを抽出する v3

指定フォルダ内に存在するファイルを名称の一部に含むキーワードで抽出するAppleScriptです。NSFileManager経由で高速に処理します。結果はPOSIX pathのlistで返します。

前バージョンのScriptに対してShane Stanleyからツッコミが入って、

(1)as «class furl»で結果を返す(fileのlistとして結果が返る)処理はmacOS 10.10.xでは結果が正しくBridgeされないので、もう少し丁寧に処理するか10.10を対象外に

(2)ループ処理について、AppleScriptで処理可能な常識的な範囲のアイテム数(高速化処理をしていなければ数千程度)の範囲であればrepeat with in のループ処理の方がobjectEnumeratorでループするより速い(数十万〜数百万アイテムの場合にはobjectEnumerator)

(3)Package File(実体はフォルダ)についても区別する/しない処理を行なったほうがいい

ということで修正してみました。指定フォルダから、指定文字列を名称に含むファイルを抽出する処理は、登場頻度もたいへん高いので、Finder経由ではなくこうしたCocoaの機能を利用するルーチンにさしかえると大幅な処理速度向上が見込まれます。とくに、非力なマシンでの速度向上が期待されます。

個人的には、macOS 10.10.xはScripting Bridge系のバグやFolder Actionのバグなど問題が多かったので、なるべく対象外にしたい気持ちでいっぱいです。

AppleScript名:指定フォルダ内の指定文字列を名称に含むファイルを抽出する v3
– Created 2017-08-28 by Takaaki Naganoya
– Modified 2017-08-29 by Shane Stanley
– Modified 2017-08-30 by Takaaki Naganoya
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4795

property NSURLIsDirectoryKey : a reference to current application’s NSURLIsDirectoryKey
property NSDirectoryEnumerationSkipsHiddenFiles : a reference to current application’s NSDirectoryEnumerationSkipsHiddenFiles
property NSPredicate : a reference to current application’s NSPredicate
property NSMutableArray : a reference to current application’s NSMutableArray
property NSDirectoryEnumerationSkipsPackageDescendants : a reference to current application’s NSDirectoryEnumerationSkipsPackageDescendants
property NSFileManager : a reference to current application’s NSFileManager
property |NSURL| : a reference to current application’s |NSURL|
property NSDirectoryEnumerationSkipsSubdirectoryDescendants : a reference to current application’s NSDirectoryEnumerationSkipsSubdirectoryDescendants

set sourceFolder to POSIX path of (choose folder)
set aKeyword to “スクリーン”

set file1aRes to my filterOutFilesByName:aKeyword fromDirectory:sourceFolder exceptPackages:false
set file1bRes to my filterOutFilesByNameFromDirectory(aKeyword, sourceFolder, false)
–>  {”/Users/me/Desktop/スクリーンショット 2017-04-15 13.18.03.png”, ….”/Users/me/Desktop/スクリーンテスト.scptd”}

set file2Res to my filterOutFilesByName:aKeyword fromDirectory:sourceFolder exceptPackages:true
set file2bRes to my filterOutFilesByNameFromDirectory(aKeyword, sourceFolder, true)
–>  {”/Users/me/Desktop/スクリーンショット 2017-04-15 13.18.03.png”, ….}

–Get files

–指定フォルダ内の指定文字列を含むファイル名のファイルをPOSIX pathのlistで抽出する
on filterOutFilesByName:(fileNameStr as string) fromDirectory:(sourceFolder) exceptPackages:(packageF as boolean)
  set fileManager to NSFileManager’s defaultManager()
  
set aURL to |NSURL|’s fileURLWithPath:sourceFolder
  
set theOptions to ((NSDirectoryEnumerationSkipsPackageDescendants) as integer) + ((NSDirectoryEnumerationSkipsHiddenFiles) as integer) + ((NSDirectoryEnumerationSkipsSubdirectoryDescendants) as integer)
  
set directoryContents to fileManager’s contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:{} options:theOptions |error|:(missing value)
  
set findPredicates to NSPredicate’s predicateWithFormat_(“lastPathComponent CONTAINS %@”, fileNameStr)
  
set foundItemList to directoryContents’s filteredArrayUsingPredicate:findPredicates
  
  
–Remove Folders From found URL Array
  
set anArray to NSMutableArray’s alloc()’s init()
  
repeat with i in foundItemList
    set j to contents of i
    
set {theResult, isDirectory} to (j’s getResourceValue:(reference) forKey:(NSURLIsDirectoryKey) |error|:(missing value))
    
    
–Collect files
    
if (isDirectory as boolean = false) then
      (anArray’s addObject:j)
      
    else if (packageF = false) then
      –Allow Package files?
      
set {theResult, isPackage} to (j’s getResourceValue:(reference) forKey:(current application’s NSURLIsPackageKey) |error|:(missing value))
      
if (isPackage as boolean) = true then
        (anArray’s addObject:j)
      end if
    end if
    
  end repeat
  
  
return (anArray’s valueForKey:“path”) as list
end filterOutFilesByName:fromDirectory:exceptPackages:

on filterOutFilesByNameFromDirectory(fileNameStr as string, fromDir, packageF as boolean)
  return my filterOutFilesByName:(fileNameStr as string) fromDirectory:fromDir exceptPackages:(packageF as boolean)
end filterOutFilesByNameFromDirectory

★Click Here to Open This Script 

2017/08/19 PDFの指定ページを削除 v4(複数ページ一括指定)

指定PDF書類のうちの指定ページをまとめて削除するAppleScriptです。

ページ指定にプラスの数値を指定すると絶対ページ数、マイナスの数値を指定するとページ末尾からの相対ページ数として解釈されます。ページ削除前に削除対象のページ数をすべて絶対ページに変換しつつ、重複分を削除し、削除対象ページを降順ソートします。

常識的な範囲内では、PDFからの指定ページ削除は行えるはずです。

ただ、この程度の実装だとすべてのPDFを対象にできないので困ります。Mac App Storeで販売中のアプリ「Double PDF」ではこのあたりの問題を解決したPDF処理ルーチンを仕込んであります。

AppleScript名:PDFの指定ページを削除 v4(複数ページ一括指定)
– Modified 2017-08-19 by Takaaki Naganoya
–Original By Shane Stanley
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “Quartz”
–http://piyocast.com/as/archives/4784

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

set inFile to (choose file of type {“pdf”} with prompt “Choose your PDF files:”)
set targPageList to {1, 3, 5, 7, -1, -2}

set pRes to removeSpecificPagesFromPDF(inFile, targPageList) of me

–指定PDF書類の複数ページの一括削除
on removeSpecificPagesFromPDF(inFileAlias, targPageNumList as list)
  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()
  
set t3List to relativeToAbsNumList(targPageNumList, pRes) of me
  
  
repeat with i in t3List
    copy i to targPageNum
    (
theDoc’s removePageAtIndex:(targPageNum - 1))
  end repeat
  
  
–Overwrite Exsiting PDF
  
set aRes to (theDoc’s writeToURL:inNSURL) as boolean
  
  
return aRes
end removeSpecificPagesFromPDF

–絶対ページと相対ページが混在した削除対象ページリストを絶対ページに変換して重複削除して降順ソート
on relativeToAbsNumList(aList, aMax)
  set newList to {}
  
  
repeat with i in aList
    set j to contents of i
    
if i < 0 then
      set j to aMax + j
    end if
    
    
if (j aMax) and (j is not equal to 0) then
      set the end of newList to j
    end if
  end repeat
  
  
set t1List to my uniquify1DList(newList, true)
  
set t2List to my sort1DNumList:t1List ascOrder:false
  
  
return t2List
end relativeToAbsNumList

on absNum(q)
  if q is less than 0 then set q to -q
  
return q
end absNum

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

–Sort 1-Dimension List(String Number List)
on sort1DNumList:theList ascOrder:aBool
  tell NSSet to set theSet to setWithArray_(theList)
  
tell NSSortDescriptor to set theDescriptor to sortDescriptorWithKey_ascending_(“floatValue”, aBool)
  
set sortedList to theSet’s sortedArrayUsingDescriptors:{theDescriptor}
  
return (sortedList) as list
end sort1DNumList:ascOrder:

★Click Here to Open This Script 

2017/08/19 PDFの指定ページを削除 v3(PDFDocument経由でアクセス)

指定PDF書類のうちの指定ページを削除するAppleScriptです。

以前、PDFKit中の機能のうち指定ページ(PDFPage)を削除するものがPDFPageに見つからなかったので、新規PDFDocumentを作成して元PDFの削除対象「以外の」ページをコピーして上書き保存することで擬似的にページ削除を実現していました

ページ削除機能がないのはおかしいと考え、しつこく調べていたところ….PDFPageではなくPDFDocumetにページ削除の機能が存在することに気づきました。そのため、PDFPage経由で指定のページを削除してみたものです。

skim1.png
▲実行前

skim2.png
▲実行後(7ページ目を削除した)

AppleScript名:PDFの指定ページを削除 v3(PDFDocument経由でアクセス)
– Modified 2017-08-19 by Takaaki Naganoya
–Original By Shane Stanley
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “Quartz”
–http://piyocast.com/as/archives/4781

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

set inFile to (choose file of type {“pdf”} with prompt “Choose your PDF files:”)
set targPage to 7

set pRes to removeSpecificPageInPDF(inFile, targPage) of me

on removeSpecificPageInPDF(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()
  
if absNum(targPageNum) of me > pRes or targPageNum = 0 then
    error “PDF Page Range error. This PDF document has “ & (pRes as string) & ” pages. But you pointed “ & (targPageNum as string) & ” page from your script. “ & return & ” (available abs range :1…” & (pRes as string) & “, relative range: -1…-” & (pRes as string) & “)”
  end if
  
  
–Allow Relative Page Num ( -1 = the last page)
  
if targPageNum 0 then
    set targPageNum to pRes + targPageNum + 1
  end if
  
theDoc’s removePageAtIndex:(targPageNum - 1)
  
  
–Overwrite Exsiting PDF
  
set aRes to (theDoc’s writeToURL:inNSURL) as boolean
  
  
return aRes
end removeSpecificPageInPDF

on absNum(q)
  if q is less than 0 then set q to -q
  
return q
end absNum

★Click Here to Open This Script 

2017/08/08 Metadata Lib 1.0

Spotlightの仕組みを用いてファイル検索を行うAppleScript Libraries「Metadata Lib」(By Shane Stanley)が公開されました。macOS 10.9以降の環境で利用できます。

Metadata Libのアーカイブをダウンロードしたのちに展開して、~/Library/Script Libraries/フォルダに「Metadata Lib.scptd」ファイルを入れればインストールは完了です。

とくにsdefでAppleScript用語辞書が定義されておらず、ソースもすべて読めるようになっています。ただし、各サブルーチンのハンドラの記法はAppleScriptネイティブの、

 someHander(aParamerer)

ではなく、Objective-C風(AppleScriptObjC風)の、

 someHander:aParameter

となっている点に注意が必要です。

内容に特殊なものはないので、「だいたいこんなかんじだよね〜」という軽いライブラリです。いきなり難解な内容にのたうちまわりながら苦しむといったことはまったくありません。今回のリリースはFirst Releaseですが、次あたりでAppleScript用語辞書をつけてきそうな雰囲気はあります。

mdls系コマンド

ファイルのメタデータを調べるにはシェルの「mdls」コマンドを使いますが、それと同じ内容を得られます。ただし、レコード中にオブジェクトを入れて返してくるので、mdfindよりも高速とのことです(Shane本人から念押し)。

use scripting additions
use mdLib : script “Metadata Lib” version “1.0.0″

set theFile to choose file
set theRecord to mdLib’s fetchMetadataFor:theFile

利用するだけれあれば、とくにAppleScriptObjCやCocoaの知識は必要とされません。

mdfind系コマンド

ファイルのメタデータ検索を行うにはシェルの「mdfind」コマンドを使いますが、だいたい同じです。本ライブラリで特徴的なのはNSPredicateで指定するPredicate文を記述して検索条件を指定する点にあります(自分はPredicate文は隠蔽するタチなので)。

また、mdfindよりも高速に検索が行える(Shane本人より念押し)ことに特徴があります。これは、mdfindがPOSIX Pathをテキストで返してくるのでAppleScript側で行単位にparseしつつPOSIX path経由でaliasに変換するような「データ変換」とshell呼び出しのオーバーヘッドの部分を指しているものと思われます(以前、mdfindとCocoa呼び出しで速度比較して、10%ぐらいCocoa呼び出しのほうが高速でした)。

そのため、本ライブラリでこの手のコマンドを利用するにはAppleScriptObjCやCocoaの知識が若干必要です。

もし、極力Cocoaの知識を使わずに本ライブラリによるメタデータ検索を行いたい場合には、あらかじめ検索パターンをFinder上で保存しておけば、searchStringsFromSavedSearch:ハンドラで保存しておいた検索パターンを指定して(Predicate文などを書かずに)、メタデータ検索を行うことも可能です(Shane本人より念押し)。

ひととおりライブラリの中身を見て回った感想は、Metadata検索系の機能だけでなく、ファイル検索系の他の機能も一緒にまとめてみては? というものでした。つまり、個人的にはぜんぜん使っていないFinder Tagとか、よく使っているFinder Labelによる検索とか、NSFileManagerによる再帰検索(Spotlightが効かない状態でも有効)など、バラバラに存在しているものを統合するといいんじゃないかと思えてきました。

どうなんでしょう?

2017/08/02 指定のアプリケーションのURL Schemeを取得する

指定のアプリケーションに設定してあるURL Schemeを取得するAppleScriptです。

指定のアプリケーションバンドル内のInfo.plistにあるCFBundleURLSchemesにアクセスして、カスタムURLプロトコルスキームを取得します。実行にはShane Stanleyの「Bridge Plus」AppleScriptライブラリを~/Library/Script Librariesフォルダにインストールしておくことが必要です。

たとえば、写真.app(Photos.app)であれば「photos://」というカスタムURLプロトコルが定義されており、本Scriptを実行して写真.app(Photos.app)を選択すると、

–> {appName:”Photos”, appBundleID:”com.apple.Photos”, urlScheme:{”photos”}}

という結果が返ってきます。

実際に調べてみると、OSのメジャーアップデート時に予想外のアプリケーションにカスタムURLスキームが新設されていることがあります。そういう調査のために作成したものです。

もちろん、1つのアプリケーションをいちいちchoose fileで選択して調べるとかそういうことではなく、指定フォルダ以下のアプリケーションをすべて取得して、それらのアプリケーションすべてのURL Schemeを調査するScriptのための部品です。手作業で1つ1つ調べるなんて、意味のないことです。

AppleScript名:指定のアプリケーションのURL Schemeを取得する
– Created 2017-07-23 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use BridgePlus : script “BridgePlus”
–http://piyocast.com/as/archives/4759

load framework

set aP to choose file
set aURLrec to getAppURLSchemes(aP) of me
–> {appName:”Photos”, appBundleID:”com.apple.Photos”, urlScheme:{”photos”}}

on getAppURLSchemes(aP)
  set aURL to current application’s |NSURL|’s fileURLWithPath:(POSIX path of aP)
  
set aBundle to current application’s NSBundle’s bundleWithURL:aURL
  
set aDict to aBundle’s infoDictionary()
  
  
set appNameDat to (aDict’s valueForKey:“CFBundleName”) as string
  
set bundleIDat to (aDict’s valueForKey:“CFBundleIdentifier”) as string
  
  
set urlSchemes to (aDict’s valueForKey:“CFBundleURLTypes”)
  
if urlSchemes is not equal to missing value then
    set urlList to urlSchemes’s valueForKey:“CFBundleURLSchemes”
    
set urlListFlat to (current application’s SMSForder’s arrayByFlattening:urlList) as list
  else
    set urlListFlat to {}
  end if
  
  
set aRec to {appName:appNameDat, appBundleID:bundleIDat, urlScheme:urlListFlat}
  
return aRec
end getAppURLSchemes

★Click Here to Open This Script 

2017/07/25 NSImageの垂直、水平反転

指定の画像を読み込んでNSImageにして、垂直反転、水平反転するAppleScriptです。

NSImageそのものを回転させるのはもんのすごく大変そうなので、反転だけしてみました。

piyowolf.png
▲オリジナル画像

flipimagevertically.png
▲垂直方向に反転した画像(flipImageVertically)

flipimagehorizontally.png
▲水平方向に反転した画像(flipImageHorizontally)

回転させるなら元画像よりひとまわり大きなNSImageViewを作ってNSImageを設定し、回転系のmethodでも呼び出して、透過部分を自動トリミングすればいいんじゃないか、などと思っています。

あるいは、NSAffineTransformで90度回転、270度回転の状態を作って、そのターゲットサイズの空白画像の上に重ね合わせるとか。とりあえず、参考にするObjective-CのプログラムをWeb上で探してはみるものの、NSImageそのものを回転させる記述例にはあまりお目にかかりません。

以上はあくまで「Cocoaの機能を直接利用した場合の話」であり、一般的な画像回転はOS標準装備のImageEventsを呼び出して手軽に行えます。

→ ImageEventsで画像回転(時計回りに90度)
→ ImageEventsで画像回転(反時計回りに90度)

AppleScript名:NSImageの垂直、水平反転
– Created 2017-07-25 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
–https://stackoverflow.com/questions/10936590/flip-nsimage-on-both-axes
–http://piyocast.com/as/archives/4748

set aFile to POSIX path of (choose file of type {“public.image”} with prompt “Select an Image”)

set currentImage to current application’s NSImage’s alloc()’s initWithContentsOfFile:aFile
set imgRes to flipImageHorizontally(currentImage) of me
set fRes to retUUIDfilePath(aFile, “png”) of me
set sRes to saveNSImageAtPathAsPNG(imgRes, fRes) of me

–水平方向の画像反転
on flipImageHorizontally(anNSImage)
  set transform to current application’s NSAffineTransform’s transform()
  
set dimList to anNSImage’s |size|()
  
set flipList to {-1.0, 0.0, 0.0, 1.0, dimList’s width, 0.0}
  
set tmpImage to current application’s NSImage’s alloc()’s initWithSize:(dimList)
  
tmpImage’s lockFocus()
  
transform’s setTransformStruct:flipList
  
transform’s concat()
  
anNSImage’s drawAtPoint:(current application’s NSMakePoint(0, 0)) fromRect:(current application’s NSMakeRect(0, 0, dimList’s width, dimList’s height)) operation:(current application’s NSCompositeCopy) fraction:1.0
  
tmpImage’s unlockFocus()
  
return tmpImage
end flipImageHorizontally

–垂直方向の画像反転
on flipImageVertically(anNSImage)
  set transform to current application’s NSAffineTransform’s transform()
  
set dimList to anNSImage’s |size|()
  
set flipList to {1.0, 0.0, 0.0, -1.0, 0.0, dimList’s height}
  
set tmpImage to current application’s NSImage’s alloc()’s initWithSize:(dimList)
  
tmpImage’s lockFocus()
  
transform’s setTransformStruct:flipList
  
transform’s concat()
  
anNSImage’s drawAtPoint:(current application’s NSMakePoint(0, 0)) fromRect:(current application’s NSMakeRect(0, 0, dimList’s width, dimList’s height)) operation:(current application’s NSCompositeCopy) fraction:1.0
  
tmpImage’s unlockFocus()
  
return tmpImage
end flipImageVertically

on retUUIDfilePath(aPath, aEXT)
  set aUUIDstr to (current application’s NSUUID’s UUID()’s UUIDString()) as string
  
set aPath to ((current application’s NSString’s stringWithString:aPath)’s stringByDeletingLastPathComponent()’s stringByAppendingPathComponent:aUUIDstr)’s stringByAppendingPathExtension:aEXT
  
return aPath
end retUUIDfilePath

–NSImageを指定パスにPNG形式で保存
on saveNSImageAtPathAsPNG(anImage, outPath)
  set imageRep to anImage’s TIFFRepresentation()
  
set aRawimg to current application’s NSBitmapImageRep’s imageRepWithData:imageRep
  
set pathString to current application’s NSString’s stringWithString:outPath
  
set newPath to pathString’s stringByExpandingTildeInPath()
  
set myNewImageData to (aRawimg’s representationUsingType:(current application’s NSPNGFileType) |properties|:(missing value))
  
set aRes to (myNewImageData’s writeToFile:newPath atomically:true) as boolean
  
return aRes –true/false
end saveNSImageAtPathAsPNG

★Click Here to Open This Script