CotEditorのメンテナーの@1024jp氏の談話によると、
https://x.com/1024jp/status/1896829943587508664
CotEditor v 5.1.0 → 5.1.1 でAppleScript経由のprint機能がうまく動作していなかったのを動作するように修正したとのこと。
ここのところ、macOS側の機能の不全により、AppleScript経由でのprint機能の実行がうまく動作していないケースが発生しており、それに対処したとのこと。
CotEditorのメンテナーの@1024jp氏の談話によると、
https://x.com/1024jp/status/1896829943587508664
CotEditor v 5.1.0 → 5.1.1 でAppleScript経由のprint機能がうまく動作していなかったのを動作するように修正したとのこと。
ここのところ、macOS側の機能の不全により、AppleScript経由でのprint機能の実行がうまく動作していないケースが発生しており、それに対処したとのこと。
CotEditorがバージョン5.1.0にアップデートしました(翌日、5.1.1にアップデートしましたが、AS的には差はありません)。
アップデート内容は多岐に渡っていますが、AppleScript的には、documentのcontentsにeditable(boolean)属性が追加されました。
実際に、AppleScriptからこの属性値を操作して、想定どおりの挙動を行うかどうかをチェックしてみました。こうしたテストは開発者側でも行なっているかどうか怪しいところなので(とくにAppleのノーチェックぶりが目に余る)、大事な作業です。
想定どおりの動作を行なっているので、確認OKです!
AppleScript名:v51.0 document editable test.scpt |
tell application "CotEditor" tell front document set editable of it to true set edF to editable –> true set contents of it to "ABC" set editable of it to false set edF to editable –> false set contents of it to "CDE" end tell end tell |
指定の画像をアスキーアート化して、ダイアログ表示するAppleScriptです。
–> Download Script archive with Library
面倒だったのでChatGPTに書かせてみましたが、Retina Display対応や細かい箇所のエラーを取りきれず、途中から自分で書き換えました。
画像のアスキーアート化Scriptは、できることはわかっていつつも、これほど役立たずに時間の無駄なプログラムも珍しいところ。こういうのはChatGPTに書かせるのがお似合いです。
それにしても、けっこうこまめにコメント入れるもんですね。正しい内容かどうかは定かではありませんが……>ChatGPT
AppleScript名:画像をAppleScriptでアスキーアート化.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/26 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" use framework "Foundation" use framework "AppKit" use scripting additions use radioLib : script "displayTextView" property retinaF : missing value — ファイル選択ダイアログで画像を選ぶ set imagePath to POSIX path of (choose file with prompt "画像を選択してください:") — ASCIIアートを生成 set asciiText to convertImageToASCII(imagePath) display text view asciiText main message "ASCII ART" sub message "Sub Message" with properties {font name:"Courier", size:12, width:620, height:620, color:{255, 255, 255}} on convertImageToASCII(imagePath) set asciiChars to "@%#*+=-:. " — 濃い→薄い順の文字列 — Retina用のスケールファクターを取得 set retinaF to (current application’s NSScreen’s mainScreen()’s backingScaleFactor()) as real — 画像を読み込む set imageURL to current application’s NSURL’s fileURLWithPath:imagePath set imageData to current application’s NSImage’s alloc()’s initWithContentsOfURL:imageURL — 論理サイズ(points単位)を取得("size"は予約語なのでエスケープ) set imageSize to imageData’s |size|() set {w_pt, h_pt} to {imageSize’s |width|, imageSize’s |height|} — リサイズ後の幅(文字数)を指定し、論理サイズに基づいてスケール計算 set newWidth to 80 set scaleFactor to newWidth / w_pt set newHeight to round (h_pt * scaleFactor * 0.55) — 文字の縦横比調整 — ピクセル単位のサイズに変換(Retina対応) set pixelWidth to newWidth * retinaF set pixelHeight to newHeight * retinaF set pixelSize to current application’s NSMakeSize(pixelWidth, pixelHeight) — 新しいピクセルサイズでNSImageを作成し、元画像を描画 set resizedImage to current application’s NSImage’s alloc()’s initWithSize:pixelSize resizedImage’s lockFocus() current application’s NSGraphicsContext’s currentContext()’s setImageInterpolation:(current application’s NSImageInterpolationHigh) imageData’s drawInRect:{{0, 0}, pixelSize} fromRect:{{0, 0}, imageSize} operation:(current application’s NSCompositingOperationCopy) fraction:1.0 resizedImage’s unlockFocus() — リサイズ後の画像からNSBitmapImageRepを取得 set resizedRep to current application’s NSBitmapImageRep’s imageRepWithData:(resizedImage’s TIFFRepresentation()) — 各ピクセルをASCII文字に変換(Retina対応のため、retinaFごとにサンプリング) set asciiArt to "" set stepX to retinaF as integer set stepY to retinaF as integer repeat with y from 0 to (pixelHeight – 1) by stepY repeat with x from 0 to (pixelWidth – 1) by stepX set pixelColor to (resizedRep’s colorAtX:(x * retinaF) y:(y * retinaF)) set r to pixelColor’s redComponent() set g to pixelColor’s greenComponent() set b to pixelColor’s blueComponent() set brightness to (r + g + b) / 3 set charIndex to round (brightness * ((count of asciiChars) – 1)) set asciiArt to asciiArt & (character (charIndex + 1) of asciiChars) end repeat set asciiArt to asciiArt & linefeed end repeat return asciiArt end convertImageToASCII |
XcodeでオープンしているWorkspace Document(.xcodeproj)のパスを取得してクローズし、再度オープンするAppleScriptです。macOS 15.3上のXcode 16.2で動作をテストしました。
Xcodeでオープン中の.applescriptファイルの構文確認を行うために、いろいろ試行錯誤してみたものの、Xcodeでオープン中にはファイルへの書き込み権限を取得できない雰囲気が濃厚。Xcodeのエディタで表示中のコンテンツ(.applescript)の本文に文字列を突っ込んでみても、Xcodeに蹴られます。
# AppleScriptからの制御が封じられているので、まっとうな手口が使えません
これに対策するために、Xcode Projectをいったんクローズして、構文確認を行うとよさそうだったので、試作品を作ってみたものです。
AppleScript名:XcodeのWorkspace Documentをクローズ後、再オープン.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/21 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions tell application "Xcode" set xcVer to version tell workspace document 1 set xWSprojPath to path close end tell end tell –delay 1 tell application "Xcode" open ((POSIX file xWSprojPath) as alias) end tell |
個人的な資料として、Shane Stanleyの電子書籍「AppleScript Explored」を日本語訳して使っておりました。部分的にmacOS最新版+Xcode最新版にスクリーンキャプチャを入れ替えて。
最初の版から、購入して読んでいましたが……実にいい本です。「もうちょっと画面キャプチャが多いとわかりやすいのに」とは思っていましたが、自分には書けないタイプの本です。
# そのフィードバックをもとに、画面キャプチャ主体の本「AppleScript+XcodeでつくるMacアプリ Xcode 14対応」を出したわけで
冗談半分でShaneに「日本語訳版を出してもいい?」と聞いてみたところ、快諾。えっ? いいの?!(^ー^;;;
いまXcodeで作っているAppleScriptのプロジェクトがあるのですが、こうした資料が充実していないと本当に辛いので、日本語版を出せることは個人的に素直に嬉しいです。
ただ、本書がOS X 10.8ぐらいの時代に書かれたという時代背景があり、そのあたりの状況認識を大幅に変更する必要はあると思います。当時はShaneがAppleScriptObjC Explorerを作っていたので、そうしたツールの存在を前提として書かれた部分もあるので、macOS 15時代+Xcode 16環境に合わせてアップデートする必要もあることでしょう。
Xcodeでオープン中のXcode Projectで表示中のファイルのフルパスを返すAppleScriptです。Xcodeで表示中のAppleScript書類(テキストベースの.applescript)をコンパイルして、省略表記した部分をすべて展開して元ファイルに書き戻すAppleScriptを書こうとして作ったものです。
Xcode.appのAppleScript用語辞書は、実用性がないというべきなのか、編集中のファイルに対する補助機能を作ろうとしても編集中のファイルのファイル名が取得できないとか、selectionの取得もできないとか、いまひとつ「使えない」という印象です(仕事をしているフリでもしてるんだろうか?>担当者)。
XcodeのAppleScript用語辞書には、日常的な作業を便利にするために必要とされる基礎的な機能が何もないので、「これでどうしろと?」と、途方に暮れてしまう出来です。
ただ、強引にファイル名の情報を取得しつつ(Windowのnameから取得)、Xcode Projectのルートフォルダからファイル検索することで、どうにかファイルのフルパスを取得してみました。取得したファイル名のファイルがProject内に複数存在する可能性もあるため、そのような場合にはどのファイルかを選択するように書いてみました(実にナンセンスな処理です)。
本Scriptでは、AppleScriptのプロジェクトに対して利用することを前提にしているため「.applescript」ファイルを取得する仕様になっていますが、別に「.m」でも「.swift」でも抽出できるように変更するのは容易です。
AppleScript名:Xcodeでオープン中のAppleScriptのフルパスを返す.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/02/14 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use scripting additions use framework "Foundation" set targString to " " & string id 8212 & " " –コピペやURL Events経由で受け渡すと壊れる tell application "Xcode" set xcVer to version —オープン中のファイル名(余計な文字列つき)を取得 tell window 1 set aStr to name end tell –Xcode Project書類のパスを取得 tell document 1 set aPrjPath to path of it end tell end tell set a1Str to parseFileNameFromXCodeWindowName(aStr, targString) of me if a1Str does not end with ".applescript" then return set pathString to current application’s NSString’s stringWithString:(aPrjPath) set parentFol to (pathString’s stringByDeletingLastPathComponent()) as string set aList to retFullPathUnderAFolderWithRecursiveFilterByFileName(parentFol, a1Str) of me if length of aList > 1 then set aRes to first item of (choose from list aList) else set aRes to first item of aList end if return aRes –> "/Users/me/Documents/testXC162/testXC162/AppDelegate.applescript" on parseFileNameFromXCodeWindowName(aStr, aDelim) set aCount to retFrequency(aStr, aDelim) of me set aRes to parseByDelim(aStr, aDelim) of me return item 2 of aRes end parseFileNameFromXCodeWindowName –指定文字列内の指定キーワードの出現回数を取得する on retFrequency(origText, aKeyText) set aRes to parseByDelim(origText, aKeyText) of me return ((count every item of aRes) – 1) end retFrequency on parseByDelim(aData, aDelim) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set dList to text items of aData set AppleScript’s text item delimiters to curDelim return dList end parseByDelim on retFullPathUnderAFolderWithRecursiveFilterByFileName(aFol, aFileName) set anArray to current application’s NSMutableArray’s array() set aPath to current application’s NSString’s stringWithString:aFol set dirEnum to current application’s 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 current application’s NSPredicate’s predicateWithFormat_("lastPathComponent == %@", aFileName) set bArray to anArray’s filteredArrayUsingPredicate:thePred return bArray as list end retFullPathUnderAFolderWithRecursiveFilterByFileName |
1D List(1次元配列)の重複項目検出を、複数の方式で速度計測するAppleScriptです。
List(Array)中の重複項目の抽出は、かなりよく出てくる処理です。自分が使っていたルーチンよりも他の人が使っているルーチンのほうが速かったので、ひととおり調べておくべきだと考え、10万項目の乱数データに対して処理してみることに。
テストScriptを書いて実行してみたら、本件では高速化ずみのVanilla ASのハンドラが一番速いという結果が出ました。意外です。
AppleScript名:複数の重複検出ルーチンを順次速度計測.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/05 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions script spd property aList : {} property bList : {} end script –Mesurement data gen. set (aList of spd) to {} repeat with i from 1 to 100000 set the end of (aList of spd) to (random number from 1 to 10000) end repeat –Benchmark Code set hList to {"returnDuplicatesOnly1:", "returnDuplicatesOnly2:", "returnDuplicatesOnly3:"} set resList to {} repeat with i in hList set nameOfTargetHandler to contents of i –Check Handler existence set existsHandler to (me’s respondsToSelector:nameOfTargetHandler) as boolean if existsHandler = true then –Call handler set a1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate() set aRes to (my performSelector:nameOfTargetHandler withObject:(aList of spd)) set b1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate() set c1Dat to b1Dat – a1Dat else error "Handler does not exists" end if set the end of resList to {nameOfTargetHandler, c1Dat} end repeat return resList –> {{"returnDuplicatesOnly1:", 0.065470099449}, {"returnDuplicatesOnly2:", 2.39611697197}, {"returnDuplicatesOnly3:", 0.038006067276}} –Cocoa Scripting最速? on returnDuplicatesOnly1:aList set arrayOne to current application’s NSArray’s arrayWithArray:aList set setOne to current application’s NSCountedSet’s alloc()’s initWithArray:(arrayOne) set arrayTwo to (arrayOne’s valueForKeyPath:"@distinctUnionOfObjects.self") set setTwo to current application’s NSCountedSet’s alloc()’s initWithArray:(arrayTwo) setOne’s minusSet:setTwo return setOne’s allObjects() as list –>{3, 6, 4} end returnDuplicatesOnly1: –1より遅い on returnDuplicatesOnly2:(aList as list) set aSet to current application’s 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 returnDuplicatesOnly2: –リスト中から重複項目をリストアップする(Vanilla AS高速化版) on returnDuplicatesOnly3:(aList) script spd property aList : {} property dList : {} end script copy aList to (aList of spd) set aCount to length of (aList of spd) set (dList of spd) to {} repeat aCount times set anItem to contents of (first item of (aList of spd)) set (aList of spd) to rest of (aList of spd) if {anItem} is in (aList of spd) then if {anItem} is not in (dList of spd) then –ここを追加した (v3) set the end of (dList of spd) to anItem end if end if end repeat return (dList of spd) end returnDuplicatesOnly3: |
BridgePlus AppleScript Libraryを使わずに、1D Listを2D Listに変換するAppleScriptの改修版です。
指定アイテム数で1D Listを2D化する処理において、対象listが指定アイテム数の倍数になっていなかった場合にギャップ項目を追加する処理を追加しました。10万項目の1D Listを2D化するのに0.5秒程度で処理できました(M2 MacBook Air)。実用レベルにはあると思われます。
動作OSバージョンには、とくに制限はありません。
AppleScript名:BridgePlusを使わずに1D–>2D list_v2.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/03 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions set dList to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} set aRes to my subarraysFrom:(dList) groupedBy:6 gapFilledBy:0 –> {{1, 2, 3, 4, 5, 6}, {7, 8, 9, 10, 11, 12}, {13, 14, 15, 16, 0, 0}} set bRes to my subarraysFrom:(dList) groupedBy:4 gapFilledBy:0 –> {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}} set cRes to my subarraysFrom:(dList) groupedBy:3 gapFilledBy:0 –> {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}, {13, 14, 15}, {16, 0, 0}} –1D Listの2D LIst化 on subarraysFrom:aList groupedBy:colCount gapFilledBy:gapItem script spd property aList : {} property bList : {} property cList : {} end script copy aList to (aList of spd) –deep copy (Important !!!) set (bList of spd) to {} set (cList of spd) to {} set aLen to length of (aList of spd) set aMod to aLen mod colCount –あらかじめ指定数(groupedBy)の倍数になっていない場合にはgap itemを末尾に足しておく if aMod is not equal to 0 then repeat (colCount – aMod) times set the end of (aList of spd) to gapItem end repeat set aLen to length of (aList of spd) end if –通常処理 repeat with i from 1 to aLen by colCount set (bList of spd) to items i thru (i + colCount – 1) of (aList of spd) set the end of (cList of spd) to (bList of spd) end repeat return (cList of spd) end subarraysFrom:groupedBy:gapFilledBy: |
BridgePlus AppleScript Libraryを使わずに、1D Listを2D Listに変換するAppleScriptです。
Script Debuggerの開発が終了したため、Frameworkを内蔵したBridgePlusの運用についても、今後は「Script Debuggerをダウンロードして動かしてほしい」といったお願いができなくなるかもしれません。
BridgePlus AppleScriptライブラリは有用ですが、Frameworkを含んでいるため実行できる環境が限られるかもしれません(ASObjC Explorerが復活しないかなー)。そのための「準備」です。
AppleScript名:BridgePlusを使わずに1D–>2D list.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/03 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions set dList to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} set aRes to my subarraysFrom:dList groupedBy:3 –> {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}, {13, 14, 15}} set aRes to my subarraysFrom:dList groupedBy:5 –> {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}} –1D Listの2D LIst化 on subarraysFrom:aList groupedBy:colCount script spd property aList : {} property bList : {} property cList : {} end script set (aList of spd) to aList set (bList of spd) to {} set (cList of spd) to {} set aLen to length of (aList of spd) set aMod to aLen mod colCount if aMod is not equal to 0 then error "Item number does not match paramaters" repeat with i from 1 to aLen by colCount set (bList of spd) to items i thru (i + colCount – 1) of (aList of spd) set the end of (cList of spd) to (bList of spd) end repeat return (cList of spd) end subarraysFrom:groupedBy: |
相次いで、AppleScriptに関する電子書籍を2冊刊行しました。
「AppleScript最新リファレンス OS X 10.11対応」から最新環境の情報を反映させ、さらにmacOS 15の情報を反映させた最新アップデート版です。最新のAppleScript v2.8環境を対象としています。
→ 販売ページ
macOS搭載のApple純正GUIアプリケーション操作用スクリプト言語「AppleScript」について、スクリプトの書き方、基本的な文法から高度なノウハウまで紹介する最新のリファレンス! macOS 12/13/14/15対応
90ページの記事を追加しました。すでに購入された方は、再ダウンロードにより無料で最新版を入手できます。
本書および「スクリプトエディタScripting Book with AppleScript」には、お待たせの(?)Piyomaru Script Assistant最新版を添付しています。
PDF 1,098ページ+付録Zipアーカイブ
macOS上のスクリプティング言語「AppleScript」によって、macOS標準装備のAppleScript記述用アプリ「スクリプトエディタ」を操作するノウハウについて基礎から応用までを詳細にまとめた電子書籍です。
→ 販売ページ
AppleScriptの中でも、超高レベルな内容であり、この内容が苦もなく理解できたら達人と言って問題ないでしょう。ただし、基礎から詳細に解説を行なっているため、難しい内容については読み飛ばしていただいてもけっこうです。それでも、日々のMac生活の中で役立つ超絶テクニックを感じることができるでしょう。 PDF 570ページ、Zipアーカイブ添付
これまであまり外部に出してこなかった、AppleScriptでAppleScriptを解析して処理する内容や、AppleScript用語辞書を解析して処理する内容、スクリプトアシスタントの書き方などの「秘伝のタレ」的な内容を多く含んでいます。
とくに、AppleScript構文色分け設定から実際のAppleScriptの各構文要素を特定して処理(変数のみの置換など)する内容は、Mac OS X 10.4の時代から続いてきた手法から最新の手法まで詳細にご紹介しています。
オープンソースのCocoa Frameworkをgithubなどからダウンロードしてビルドし、AppleScriptから実行することは日常的に行なっています。
ただし、これはmacOS 10.14で~/Library/Frameworksから読み込んで実行することが、スクリプトエディタ上では禁止、Script Debuggerでのみ可能という状況です。
macOS標準添付のFrameworkの多くは、/System/Library/Frameworks以下に(若干の例外はあれど)配置されていることが期待されます。
スクリプトエディタ上で実行するScriptについては、macOSデフォルトインストールされているAppleのFrameworkしか呼び出せない状況です。
そこで、/Library/Frameworksあたりに入れたFrameworkを強制的に呼び出せないかと実験してみたものが、これです。
choose fileで指定したFrameworkを強制的に読み込んで実行するテストを行なってみました。結果からいえば、Script Debugger上では実行できるのですが、スクリプトエディタではloadが行えず、エラーになりました。
結論:残念!
もともと、ホームディレクトリ以下のバイナリを呼び出すことをSIPで禁止したいというセキュリティ上のポリシーによって、macOS 10.14以降ではmacOS標準装備のスクリプトエディタが影響を受けました。
Script Debuggerでこの点は補われていたわけですが、AppleScriptから/Library/FrameworkにインストールしたFrameworkを呼び出すことを許可していただきたいところです。/Library/Frameworkなら、ホームディレクトリ以下ではないし、管理者権限がないとFrameworkのインストールもできないため、サードパーティのFrameworkのインストール先としては妥当です。何らかの悪意をもったソフトウェアがFrameworkを勝手にインストールする危険性も低いことでしょう。
AppleScript名:Framerokを任意のパスからloadして実行するじっけん.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/01/29 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions set fPath to POSIX path of (choose file) loadAndExecuteFramework(fPath, "SFPSDWriter") of me on loadAndExecuteFramework(frameworkPath, functionName) — Frameworkのパスをチェック (* if frameworkPath does not start with "/Library/" then display dialog "Error: Framework path must be inside /Library/" return end if *) — Frameworkをロード set frameworkNSURL to current application’s NSURL’s fileURLWithPath:frameworkPath set bundle to current application’s NSBundle’s bundleWithURL:frameworkNSURL if bundle is missing value then display dialog "Error: Failed to load framework at " & frameworkPath return end if — Frameworkのクラスをロード set isLoaded to bundle’s load() if isLoaded is false then display dialog "Error: Failed to load framework" return end if — クラスを取得 set className to current application’s NSClassFromString(functionName) if className is missing value then display dialog "Error: Function ’" & functionName & "’ not found" return end if log className — メソッドを実行(クラスメソッドとして想定) try className’s performSelector:"execute" display dialog "Function ’" & functionName & "’ executed successfully." on error errMsg display dialog "Error executing function: " & errMsg end try end loadAndExecuteFramework |
Finderの最前面のウィンドウで選択しておいた画像をオープンして自動画質補正を行いつつ、指定の番号からの連番をつけたJPEG画像に変換して指定フォルダに書き込むAppleScriptです。
さっそく、昨日書いた画質自動補正のプログラムを使い回しています。他のものもほぼ、過去に書いたものを使い回しているだけで、新規に書いた部分はほとんどありません。必要以上に長くなっており、おそらく呼び出していないルーチンなども含まれているはずです。
この、連番のJPEG画像はプロジェクターのスライドショー機能を用いて写真を表示するための仕様です。USBメモリなどにJPEG画像を入れておくと、ファイル名順に再生を行ってくれます。
当初はMacをプロジェクターにつないで写真.app(Photos.app)のBGMつきスライドショーを試してみたのですが、BGMに合わせた画像切り替えを行ってくれる一方で、強制的に1写真あたりの表示時間を指定することができず、「これでは使えない」として、プロジェクターの内蔵スライドショー機能を使うことにしたので、このようなScriptを作ったものです。
AppleScript名:Finder上で選択中のファイルをJPEG形式で指定フォルダに書き出し(自動補正つき).scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/01/23 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use scripting additions use framework "Foundation" use framework "AppKit" use framework "CoreImage" use framework "UniformtypeIdentifiers" property CIFilter : a reference to current application’s CIFilter property NSArray : a reference to current application’s NSArray property CIImage : a reference to current application’s CIImage property NSUUID : a reference to current application’s NSUUID property |NSURL| : a reference to current application’s |NSURL| property NSString : a reference to current application’s NSString property NSImage : a reference to current application’s NSImage property NSPredicate : a reference to current application’s NSPredicate property NSPNGFileType : a reference to current application’s NSPNGFileType property NSBitmapImageRep : a reference to current application’s NSBitmapImageRep tell application "Finder" set aSel to selection as alias list end tell set posList to {} set aCount to 1 set bFol to POSIX path of (choose folder with prompt "出力先フォルダを選択") repeat with i in aSel set j to POSIX path of i set aUTI to getUTIFromFile(j) of me if aUTI is not equal to missing value then –ファイルから求めたUTIが指定のUTIに含まれるかをチェック set bRes to filterUTIList({aUTI}, "public.image") if bRes is not equal to {} then –自動画質調整 set aNSImage to makeNSImageFromPOSIXpath(j) of me set bImgRes to autoFiltersForNSImage(aNSImage) of me if bImgRes = false then return –NSImageをJPEGで書き出す set aStr to makeFN(aCount, 5) of me set outPath to bFol & aStr & ".jpg" set sRes to saveNSImageAtPathAsJPG(bImgRes, outPath, 0.9) of JPGkit set aCount to aCount + 1 else log j end if log j end if end repeat on autoFiltersForNSImage(aNSImage) set aCIImage to convNSImageToCIimage(aNSImage) of me set filterList to aCIImage’s autoAdjustmentFilters if filterList = missing value then return false repeat with i in filterList set aFilter to contents of i (aFilter’s setValue:(aCIImage) forKey:"inputImage") set aOutImage to (aFilter’s valueForKey:"outputImage") copy aOutImage to aCIImage end repeat set outNSImage to convCIimageToNSImage(aOutImage) of me return outNSImage end autoFiltersForNSImage on convCIimageToNSImage(aCIImage) set aRep to NSBitmapImageRep’s alloc()’s initWithCIImage:aCIImage set tmpSize to aRep’s |size|() set newImg to NSImage’s alloc()’s initWithSize:tmpSize newImg’s addRepresentation:aRep return newImg end convCIimageToNSImage on convNSImageToCIimage(aNSImage) set tiffDat to aNSImage’s TIFFRepresentation() set aRep to NSBitmapImageRep’s imageRepWithData:tiffDat set newImg to CIImage’s alloc()’s initWithBitmapImageRep:aRep return newImg end convNSImageToCIimage on makeNSImageFromAlias(anAlias) set imgPath to (POSIX path of anAlias) set aURL to (|NSURL|’s fileURLWithPath:(imgPath)) return (NSImage’s alloc()’s initWithContentsOfURL:aURL) end makeNSImageFromAlias on makeNSImageFromPOSIXpath(aPOSIX) set aURL to (|NSURL|’s fileURLWithPath:(aPOSIX)) return (NSImage’s alloc()’s initWithContentsOfURL:aURL) end makeNSImageFromPOSIXpath on getUTIFromFile(aPath) set aWS to current application’s NSWorkspace’s sharedWorkspace() set pRes to (aWS’s isFilePackageAtPath:aPath) as boolean if pRes = false then set superType to (current application’s UTTypeData) else set superType to (current application’s UTTypePackage) end if set pathString to current application’s NSString’s stringWithString:aPath set aExt to (pathString’s pathExtension()) as string set aUTType to current application’s UTType’s typeWithFilenameExtension:aExt conformingToType:(superType) if aUTType = missing value then return missing value set aUTIstr to aUTType’s identifier() as string return aUTIstr end getUTIFromFile on filterUTIList(aUTIList, aUTIstr) set anArray to NSArray’s arrayWithArray:aUTIList set aPred to NSPredicate’s predicateWithFormat_("SELF UTI-CONFORMS-TO %@", aUTIstr) set bRes to (anArray’s filteredArrayUsingPredicate:aPred) as list return bRes end filterUTIList on makeFN(aNum, aDigit) set aText to "00000000000" & (aNum as text) set aLen to length of aText set aRes to text (aLen – aDigit + 1) thru -1 of aText return aRes end makeFN script JPGkit use scripting additions use framework "Foundation" use framework "AppKit" property parent : AppleScript on saveAImageASJPG(aFile, aNewFile) set aPOSIX to (POSIX path of aFile) set aImage to current application’s NSImage’s alloc()’s initWithContentsOfFile:(aPOSIX) set newPath to current application’s NSString’s stringWithString:(POSIX path of aNewFile) set newExt to (newPath’s pathExtension()) as string if newExt is not equal to "jpg" then set newPath to repFilePathExtension(newPath, ".jpg") of me end if set sRes to saveNSImageAtPathAsJPG(aImage, newPath, 1.0) of me end saveAImageASJPG on repFilePathExtension(origPath, newExt) set aName to current application’s NSString’s stringWithString:origPath set theExtension to aName’s pathExtension() if (theExtension as string) is not equal to "" then set thePathNoExt to aName’s stringByDeletingPathExtension() set newName to (thePathNoExt’s stringByAppendingString:newExt) else set newName to (aName’s stringByAppendingString:newExt) end if return newName as string end repFilePathExtension –NSImageを指定パスにJPEG形式で保存、qulityNumは0.0〜1.0。1.0は無圧縮 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 end script |
Core ImageのCIFilterから、autoAdjustmentFiltersを呼び出して、いい感じにKeynote上とかで自動画像調整を行うのと同様に、画像に対して自動調整を行うAppleScriptです。
KeynoteやPages上の画像の自動補正機能は、大当たりすることもなければ大はずしすることもない、なかなかお得な機能です。
ちょっと、Finder上で選択した画像にひととおり自動補正をかけてお茶をにごしたい。
そんな機能が存在しないかと調べていたのですが、autoAdjustmentFiltersの存在は知っていたものの、いろいろ試しては、
current application’s CIImage’s autoAdjustmentFilters()
とか、
current application’s CIFilter’s autoAdjustmentFilters()
などと間違った記述を行なってmissing valueが返ってきては、首をひねりまくっていました。
Googleの検索エンジンで探してもなかなかサンプルに行き当たらないあたり、みんな苦労しているんじゃないかと疑っていますが、これは、
CIImage画像に対してautoAdjustmentFilters()を実行すると、自動調整用のCIFilterにパラメータが設定された状態でarrayに入って返ってくるのでした(これは、わからないぞ)。
半信半疑で実行してみたら、なんとなくそれっぽく自動調整された画像がデスクトップに出力されます。
AppleScript名:CoreImageで指定画像をautoAdjustmentFilters.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/01/21 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.8" use framework "Foundation" use framework "AppKit" use framework "CoreImage" use scripting additions property CIFilter : a reference to current application’s CIFilter property NSUUID : a reference to current application’s NSUUID property |NSURL| : a reference to current application’s |NSURL| property CIImage : a reference to current application’s CIImage property NSString : a reference to current application’s NSString property NSImage : a reference to current application’s NSImage property NSPNGFileType : a reference to current application’s NSPNGFileType property NSBitmapImageRep : a reference to current application’s NSBitmapImageRep set imgPath to POSIX path of (choose file) set aNSImage to makeNSImageFromPOSIXpath(imgPath) of me set bImgRes to autoFiltersForNSImage(aNSImage) of me if bImgRes = false then return set outPath to retUUIDfilePath(POSIX path of (path to desktop), "png") of me set sRes to saveNSImageAtPathAsPNG(bImgRes, outPath) of me on autoFiltersForNSImage(aNSImage) set aCIImage to convNSImageToCIimage(aNSImage) of me set filterList to aCIImage’s autoAdjustmentFilters if filterList = missing value then return false repeat with i in filterList set aFilter to contents of i (aFilter’s setValue:(aCIImage) forKey:"inputImage") set aOutImage to (aFilter’s valueForKey:"outputImage") copy aOutImage to aCIImage end repeat set outNSImage to convCIimageToNSImage(aOutImage) of me return outNSImage end autoFiltersForNSImage on retUUIDfilePath(aPath, aEXT) set aUUIDstr to (NSUUID’s UUID()’s UUIDString()) as string set aPath to ((NSString’s stringWithString:aPath)’s stringByAppendingPathComponent:aUUIDstr)’s stringByAppendingPathExtension:aEXT return aPath end retUUIDfilePath on convCIimageToNSImage(aCIImage) set aRep to NSBitmapImageRep’s alloc()’s initWithCIImage:aCIImage set tmpSize to aRep’s |size|() set newImg to NSImage’s alloc()’s initWithSize:tmpSize newImg’s addRepresentation:aRep return newImg end convCIimageToNSImage on convNSImageToCIimage(aNSImage) set tiffDat to aNSImage’s TIFFRepresentation() set aRep to NSBitmapImageRep’s imageRepWithData:tiffDat set newImg to CIImage’s alloc()’s initWithBitmapImageRep:aRep return newImg end convNSImageToCIimage on makeNSImageFromAlias(anAlias) set imgPath to (POSIX path of anAlias) set aURL to (|NSURL|’s fileURLWithPath:(imgPath)) return (NSImage’s alloc()’s initWithContentsOfURL:aURL) end makeNSImageFromAlias on makeNSImageFromPOSIXpath(aPOSIX) set aURL to (|NSURL|’s fileURLWithPath:(aPOSIX)) return (NSImage’s alloc()’s initWithContentsOfURL:aURL) end makeNSImageFromPOSIXpath –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 |
Numbersの表の選択範囲のデータを指定の数(10,000とか、100万とか)で切り捨てるAppleScriptです。Numbers v14.3で検証してありますが、凝った機能は何も使っていないので、古いバージョンのNumbersでも問題なく動作することでしょう。
もともとは、Keynoteで作った資料でデータが細かくて見にくかったので、指定桁で切り捨て(100万円で切り捨て、など)するために作ったものです。
本来はiWorkアプリ上でこうした操作を行うには、「データフォーマット」で「カスタムフォーマット」を定義するのがiWorkの流儀です。
ただ、このカスタムフォーマットが割と遠回りでわかりにくかったので(+AppleScriptから操作できないので)、即物的にデータを編集するプログラムを作ってしまった次第です。割と、使い捨てレベルの素朴なScriptでもあります。
AppleScript名:選択範囲のデータを100万円で切り捨て.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/01/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — set calcNum to 10000 tell application "Numbers" tell front document tell active sheet tell table 1 set aSel to cell of selection range set newList to {} repeat with i in aSel set j to contents of i if j is not equal to missing value then set tmpV1 to (value of j) if tmpV1 is not equal to missing value then set tmpV2 to tmpV1 div calcNum ignoring application responses set value of j to tmpV2 end ignoring end if end if end repeat end tell end tell end tell end tell |
Pagesで作成した書類で、プログラムリストの上に配置したタイトルのテキストを取得するAppleScriptです。
割といきあたりばったりで作ってしまったツールです。
Pagesで電子書籍を作成し、レイアウトしたAppleScriptのプログラムリストのファイル名を求めるために、プログラムリストの上に配置した白い文字で記述したテキストフレーム(Pages上ではShapeオブジェクト)を特定します。
フィルタ参照で相対座標値を表現できるといいのですが、そういうのはできないので、地道に距離計算しています。
「上」「下」という相対的な位置関係を表現するのに、結局数値比較しかできないので、どうしたものかと考えていたのですが、結局この「上」という表現は用いずじまいでした。「一番距離が近いテキストフレーム、文字色は白っぽい」だけで割と正確に特定できたので、いいだろうかというところです。相対位置関係を表記するライブラリなども作っておくといいかもしれません。
予想外の要素が、白いとだけ思っていた文字色が、RGB値では若干ゆらいでいたので、そのあたりの辻褄合わせを地味にやっています。カラードメイン(色名をラフに計算する)系のライブラリを使えば「white」などと雑な表現で指定できたかもしれません。
実際に使っているものは、本Scriptにくわえて選択中のテキストフレームの内容をAppleScriptとしてメモリ上で構文確認とコンパイルを行なって、ここで取得したファイル名でAppleScriptとして保存する処理を行なっています。
AppleScript名:Pagesで選択中のテキストボックスの一番近くにある白い文字のボックスの名前を取得.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/01/16 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use scripting additions use framework "Foundation" use framework "OSAKit" property |NSURL| : a reference to current application’s |NSURL| property OSANull : a reference to current application’s OSANull property NSString : a reference to current application’s NSString property OSAScript : a reference to current application’s OSAScript property OSALanguage : a reference to current application’s OSALanguage property NSFontAttributeName : a reference to current application’s NSFontAttributeName property OSALanguageInstance : a reference to current application’s OSALanguageInstance property NSURLTypeIdentifierKey : a reference to current application’s NSURLTypeIdentifierKey tell application "Pages" tell front document set aSel to selection if aSel = {} then return set aaSel to first item of aSel set targPos to position of aaSel set targCon to object text of aaSel –選択中のtext frame(Pages上ではShape)の本文 tell current page –文字が入っているiWork objectのみが対象 set tList to every shape whose object text is not equal to targCon and object text of it is not equal to "" –オブジェクト set pList to position of every shape whose object text is not equal to targCon and object text of it is not equal to "" –座標 set cList to color of object text of every shape whose object text is not equal to targCon and object text of it is not equal to "" –文字色 end tell –クレヨンピッカーから指定しても、白色に若干の「ゆらぎ」があるようなので、数値比較で抽出 set aRes to findItemNums({65500, 65500, 65500}, cList) of me set aLen to length of aRes if aLen = 0 then display dialog "No Hit(error)" else if aLen = 1 then set tmpTarg to first item of aRes set tmpTargTextFrame to item tmpTarg of tList set oRes to object text of tmpTargTextFrame else set p2List to {} repeat with i in aRes set j to contents of i set the end of p2List to contents of item j of pList end repeat set L2ItemNums to retNearestItemByPosition({targPos}, p2List) of me set oRes to object text of (item (first item of L2ItemNums) of tList) end if end tell end tell return oRes –RGBの値がaNumにlistで入ってくる{r, g, b} –リスト中に入っている指定要素をサーチして、各チャネルの値よりも大きい場合に合致したとみなし、出現アイテム番号を返す(複数対応) on findItemNums(aNum, aList) if aList = {missing value} then return {} if aNum = {missing value} then return {} set iCount to 1 set hitF to false set hitList to {} copy aNum to {aNum1, aNum2, aNum3} repeat with i in aList set j to contents of i if j is not equal to missing value then copy j to {tmpR, tmpG, tmpB} if (tmpR > aNum1) and (tmpG > aNum2) and (tmpB > aNum3) then set the end of hitList to iCount end if end if set iCount to iCount + 1 end repeat return hitList end findItemNums on retNearestItemByPosition(L1Pos, L2Pos) set resItemNum to {} repeat with i in L1Pos set j to contents of i set iCount to 1 set tDList to {} repeat with ii in L2Pos set jj to contents of ii copy jj to {tmpX1, tmpY1} copy j to {tmpX2, tmpY2} if tmpX1 ≥ tmpX2 then set xDist to tmpX1 – tmpX2 else set xDist to tmpX2 – tmpX1 end if if tmpY1 ≥ tmpY2 then set yDist to tmpY1 – tmpY2 else set yDist to tmpY2 – tmpY1 end if set tArea to xDist * yDist set t2Area to absNum(tArea) of me set the end of tDList to {area:t2Area, itemNum:iCount} set iCount to iCount + 1 end repeat set resList to sortRecListByLabel(tDList, "area", true) of me –> {{itemNum:2, area:100}, {itemNum:3, area:1739}, {itemNum:4, area:3780}, {itemNum:1, area:4554}} set tItem to itemNum of first item of resList set the end of resItemNum to tItem end repeat return resItemNum end retNearestItemByPosition on arrangeTargItemByItemNumList(L2Pos, L2ItemNums) set L3Pos to {} repeat with i in L2ItemNums set j to contents of i set the end of L3Pos to item j of L2Pos end repeat return L3Pos end arrangeTargItemByItemNumList on absNum(q) if q is less than 0 then set q to –q return q end absNum –リストに入れたレコードを、指定の属性ラベルの値でソート on sortRecListByLabel(aRecList as list, aLabelStr as string, ascendF as boolean) set aArray to current application’s NSArray’s arrayWithArray:aRecList set sortDesc to current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF set sortedArray to aArray’s sortedArrayUsingDescriptors:{sortDesc} set bList to (sortedArray) as anything return bList end sortRecListByLabel |
Numbersで縦方向に行単位で任意の並べ替え(Ascending、Descending)を行うのは問題ないのですが、横方向に並べ替えをする機能がなかったので、作ってみました。
▲初期状態。左側のデータが新しく、右端が一番古いデータ。並べ替えを行う範囲を選択
▲実行後。横方向に逆順に並べ替えを行った。左端が一番古く、右端が一番新しいデータ
標準機能で欲しいところです。
データ取得は速いものの、スプレッドシートへのデータの書き込みに時間のかかるNumbers。本ScriptもApple Silicon Macで実行すればそこそこの速度で実行してくれますが、おそらくExcelに対して同様の処理を実行したら1,000倍ぐらい高速です。
それほど大量のデータを処理すると、非同期処理が途中でおかしくなるので、数千セルぐらいを上限としておいたほうがよいでしょう。
AppleScript名:Numbersで選択中の範囲を横方向に逆順に並べて、選択範囲に再設定.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/01/14 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" use scripting additions use framework "Foundation" script spd property aList : {} property bList : {} property cList : {} end script set (aList of spd) to get2DListFromNumbersSelection() of me if (aList of spd) = "" then return "No Selection" set (bList of spd) to {} repeat with i in (aList of spd) set tmpList to contents of i set tmpList to reverse of tmpList set the end of (bList of spd) to tmpList end repeat set (bList of spd) to FlattenList((bList of spd)) of me tell application "Numbers" tell front document tell active sheet tell table 1 set (cList of spd) to cell of selection range set iCount to 1 repeat with i in (cList of spd) set newVal to contents of item iCount of (bList of spd) if (newVal as string) is equal to "missing value" then set newVal to "" ignoring application responses set value of i to newVal end ignoring set iCount to iCount + 1 end repeat end tell end tell end tell end tell on get2DListFromNumbersSelection() –Numbersで選択範囲を縦に区切ったリストを返す tell application "Numbers" tell front document tell active sheet try set theTable to first table whose class of selection range is range on error return "" –何も選択されてなかった場合 end try tell theTable set selList to value of every cell of selection range –選択範囲のデータを取得 set selName to name of selection range –選択範囲のrange情報を取得 set {s1, s2} to parseByDelim(selName, ":") of me –始点の情報を取得する set s1Row to (address of row of range s1) as integer set s1Column to (address of column of range s1) as integer –終点の情報を取得する set s2Row to (address of row of range s2) as integer set s2Column to (address of column of range s2) as integer –選択範囲の情報を取得する set selHeight to s2Row – s1Row + 1 –高さ(Height of selection range) set selWidth to s2Column – s1Column + 1 –幅(Width of selection range) end tell end tell end tell end tell set aLen to length of selList set aaLen to selHeight set bList to {} repeat with i from 1 to aaLen set aHoriList to {} repeat with ii from 1 to selWidth set j1 to ii + (i – 1) * selWidth set tmpCon to contents of item j1 of selList set aClass to class of tmpCon if aClass = number or aClass = integer or aClass = real then set tmpCon to tmpCon as integer end if set the end of aHoriList to tmpCon end repeat set the end of bList to aHoriList end repeat return bList end get2DListFromNumbersSelection –テキストを指定デリミタでリスト化 on parseByDelim(aData, aDelim) set aText to current application’s NSString’s stringWithString:aData set aList to aText’s componentsSeparatedByString:aDelim return aList as list end parseByDelim –By Paul Berkowitz –2009年1月27日 2:24:08:JST –Re: Flattening Nested Lists on FlattenList(aList) set oldDelims to AppleScript’s text item delimiters set AppleScript’s text item delimiters to {"????"} set aString to aList as text set aList to text items of aString set AppleScript’s text item delimiters to oldDelims return aList end FlattenList |
Numbers上で選択範囲のデータに対して一括で数値を加算するAppleScriptです。
Numbers上で台割(PDFに対して付加するTOC用データ)を管理しているところに、実際の電子書籍の記事が増えて、各記事のノンブル(ページ番号)を変更する必要があるわけですが……これに、当初は「+18で」みたいなScriptを運用していたのですが、暗算で差分を計算していたものの、この仕様だと使いにくかったのです。
そこで、新たなページ数を入力すると、AppleSript側で差分を計算して後の選択範囲に対して加算するようにしてみました。
▲PDFを参照して新版の「おくづけ」は552ページになっていることを確認 ここで、本Scriptを実行
▲差分(増分)を暗算で計算して入力するのではなく、最初のページの変更後のページ数が何か、を入力。Script側で差分を自動計算
巨大なScriptも役立ちますが、こういう「ちょっとした」サイズのScriptもまた役に立ちます。
AppleScript名:選択範囲の値を取得して、冒頭の値の新しい数値を指定すると以下自動加算.scpt |
set sNum to text returned of (display dialog "Input New First Num" default answer "") tell application "Numbers" tell front document tell active sheet tell table 1 set mySelectedRanges to value of every cell of selection range set cellList to cell of selection range set addNum to sNum – (first item of mySelectedRanges) set iCount to 0 set newList to {} repeat with i in cellList set tmpVal to value of i set the end of newList to tmpVal + addNum end repeat repeat with i from 1 to (length of cellList) tell item i of cellList set value to item i of newList end tell end repeat end tell end tell end tell end tell |
スタイルつきテキストの中をスタイルつきテキストで検索するAppleScriptです。フォント、サイズ、色の属性を考慮して文字とこれらの属性値が合っている場合に合致しているものとみなします。
最初のバージョンはChatGPTに書かせたもので、実際に動かしてみたら激遅だったので、2パスで検索することで高速化してみました。オリジナルより100倍以上速くなっているはずです。
Step1: 検索対象文字列、検索文字列の両方をテキスト化して、テキストベースで出現情報を計算
Step2: 出現情報をもとにスタイルつきテキスト(NSAttributedString)の書式情報の照合を行い、合致するものを出力
という処理に変更しました。ChatGPTが出力してきたAppleScriptは、1文字ずつチェック範囲を変更しながら書式情報とテキスト情報の照合を行うという「脳筋処理」だったので、少なく見積もっても100倍。データ量が増えれば増えるほど高速化の度合いが上昇します。
AppleScript名:find Styled Text in Styled Text_v1.1.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/01/12 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use framework "Foundation" use scripting additions property NSString : a reference to current application’s NSString property NSLiteralSearch : a reference to current application’s NSLiteralSearch property NSMutableArray : a reference to current application’s NSMutableArray — サンプルデータ set targetText to "これはサンプルテキストです。サンプルは重要です。サンプルだよー。サンプルサンプル" set searchText to "サンプル" — 属性付き文字列の作成 set targetAttributedString to current application’s NSMutableAttributedString’s alloc()’s initWithString:targetText set searchAttributedString to current application’s NSMutableAttributedString’s alloc()’s initWithString:searchText — 属性を設定 (例: フォントと色) set targetFont to current application’s NSFont’s fontWithName:"HiraginoSans-W2" |size|:13 set targetColor to current application’s NSColor’s redColor() set searchFont to current application’s NSFont’s fontWithName:"HiraginoSans-W2" |size|:13 set searchColor to current application’s NSColor’s redColor() — 属性を追加 targetAttributedString’s addAttribute:(current application’s NSFontAttributeName) value:targetFont range:{0, targetAttributedString’s |length|()} targetAttributedString’s addAttribute:(current application’s NSForegroundColorAttributeName) value:targetColor range:{0, targetAttributedString’s |length|()} searchAttributedString’s addAttribute:(current application’s NSFontAttributeName) value:searchFont range:{0, searchAttributedString’s |length|()} searchAttributedString’s addAttribute:(current application’s NSForegroundColorAttributeName) value:searchColor range:{0, searchAttributedString’s |length|()} — 属性付き検索を実行 set matches to searchAttributedStringWithAttributes(targetAttributedString, searchAttributedString) of me –> {{location:3, |length|:4}, {location:14, |length|:4}, {location:24, |length|:4}, {location:32, |length|:4}, {location:36, |length|:4}} — NSAttributedStringの属性付き検索を行うハンドラー(複数一致対応) on searchAttributedStringWithAttributes(targetAttributedString, searchAttributedString) set targText to targetAttributedString’s |string|() as string set searchAttributes to searchAttributedString’s attributesAtIndex:0 effectiveRange:(missing value) set searchString to searchAttributedString’s |string|() as string –テキストベースで検索 set strRangeList to searchWordRanges(targText, searchString) of me –あとは、検索箇所の文字書式をループで照合する set matList to {} — 結果を格納するリスト — ループで対象の文字列が含まれる位置の書式情報を検索 repeat with i in strRangeList set j to contents of i set tmpLoc to location of j set tmpLen to |length| of j — 対象範囲の属性を取得 set targetAttributes to (targetAttributedString’s attributesAtIndex:(tmpLoc) effectiveRange:(current application’s NSMakeRange(tmpLoc, tmpLen))) if (targetAttributes = searchAttributes) then set the end of matList to {location:(tmpLoc as integer), |length|:(tmpLen as integer)} — 属性が一致した場合 end if end repeat return matList — 一致したすべての範囲を返す end searchAttributedStringWithAttributes –プレーンテキストベースで検索文字列の出現情報を返す(複数対応) on searchWordRanges(aTargText as string, aSearchStr as string) 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 aLen to (detectedRange’s |length|) as number if aLen = 0 then exit repeat hitArray’s addObject:detectedRange set aNum to (detectedRange’s location) as number set bNum to (detectedRange’s |length|) as number set aRange to current application’s NSMakeRange(aNum + bNum, cNum – (aNum + bNum)) end repeat return hitArray end searchWordRanges |
スタイルつきテキストの中をスタイルつきテキストで検索するAppleScriptです。フォント、サイズ、色の属性を考慮して文字とこれらの属性値が合っている場合に合致しているものとみなします。
たかだか3Kバイトごときの文字量のスタイルつきテキストから検索を行うのに、M2 Airで2・3秒ぐらいかかります。
属性値をDictionary in Arrayとして解釈して検索を行うよりも、大幅に処理に時間がかかるようです。
1文字ずつ開始位置をズラして文字&書式の再照合を行っているので、アホみたいに処理時間がかかります。Cocoaの機能を使って処理しているのに、Pagesに問い合わせるよりも時間がかかっている雰囲気です。
本ScriptはChatGPTに書かせたもので、おそらく処理に時間がかかって「無駄」になるものと見込んでいました。
スタイルつきテキストに対してスタイルつきテキストによる検索を行うのは、予想どおりものすごく負荷が大きくなるようで……本Scriptは残念ながら作り捨てになりそうです。
もう少し頭を使って、文字だけで出現位置チェックを行い、出現位置情報をもとに書式の照合を行うようにすれば、10倍ぐらい高速に処理できそうです。
AppleScript名:find Styled Text in Styled Text.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/01/12 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use framework "Foundation" use scripting additions — 使用例 set targetText to "これはサンプルテキストです。サンプルは重要です。サンプルだよー。" set searchText to "サンプル" — 属性付き文字列の作成 set targetAttributedString to current application’s NSMutableAttributedString’s alloc()’s initWithString:targetText set searchAttributedString to current application’s NSMutableAttributedString’s alloc()’s initWithString:searchText — 属性を設定 (例: フォントと色) set targetFont to current application’s NSFont’s fontWithName:"HiraginoSans-W2" |size|:13 set targetColor to current application’s NSColor’s redColor() set searchFont to current application’s NSFont’s fontWithName:"HiraginoSans-W2" |size|:13 set searchColor to current application’s NSColor’s redColor() — 属性を追加 targetAttributedString’s addAttribute:(current application’s NSFontAttributeName) value:targetFont range:{0, targetAttributedString’s |length|()} targetAttributedString’s addAttribute:(current application’s NSForegroundColorAttributeName) value:targetColor range:{0, targetAttributedString’s |length|()} searchAttributedString’s addAttribute:(current application’s NSFontAttributeName) value:searchFont range:{0, searchAttributedString’s |length|()} searchAttributedString’s addAttribute:(current application’s NSForegroundColorAttributeName) value:searchColor range:{0, searchAttributedString’s |length|()} — 属性付き検索を実行 set matches to searchAttributedStringWithAttributes(targetAttributedString, searchAttributedString) of me –> {{location:3, |length|:4}, {location:14, |length|:4}, {location:24, |length|:4}} — NSAttributedStringの属性付き検索を行うハンドラー(複数一致対応、変数名変更) on searchAttributedStringWithAttributes(targetAttributedString, searchAttributedString) — 検索対象の全長を取得 set targetLength to targetAttributedString’s |length|() set searchLength to searchAttributedString’s |length|() — 検索する属性辞書を取得 set searchAttributes to searchAttributedString’s attributesAtIndex:0 effectiveRange:(missing value) set searchString to searchAttributedString’s |string|() as string — 結果を格納するリスト set matches to {} — ループで対象の文字列を検索 set currentIndex to 1 repeat while (currentIndex + searchLength ≤ targetLength) — 対象範囲の属性辞書を取得 set targetAttributes to targetAttributedString’s attributesAtIndex:currentIndex effectiveRange:(missing value) — 対象範囲の文字列を取得 set targetRange to (current application’s NSMakeRange(currentIndex, searchLength)) set targetSubstring to (targetAttributedString’s attributedSubstringFromRange:(targetRange))’s |string|() — 属性と文字列の一致を確認 if (targetSubstring as string = searchString) and (targetAttributes = searchAttributes) then set end of matches to {location:currentIndex, |length|:searchLength} end if — 次の位置に進む set currentIndex to currentIndex + 1 end repeat return matches — 一致したすべての範囲を返す end searchAttributedStringWithAttributes |