ApplescriptでKeynoteをハックしてみた


# 概要
友達からプレゼン用のkeynoteにテロップを自動で挿入するプログラムが欲しいと言われたのでAppleScriptで作ってみた。

# 目次
- AppleScriptとは
- どんな感じのプログラム
- 実装
- おわりに
- 参考URL

# AppleScriptとは
https://en.wikipedia.org/wiki/AppleScript
名前から想像できる通り、Appleが作ったMac OS用のスクリプト言語で、macOSに同梱されている基本ソフトです。マウスやキーボードを使わずに、Macや外部のアプリを操作するすることができます。
実装してみると、構文が簡単で学習コストが低いかなと思います。一般敵にはmacOSに標準でインストールされているスクリプトエディタで記述すると思いますが、今回はAppleScriptのプラグインがあったのでVSCodeを使用しました。

# どんな感じのプログラム
### 用意するもの
以下二点です。

- テロップを入れたいkeyファイル
- こんな感じのテキストファイル

```text:telop.text
テロップ１
テロップ2
テロップ3
```
一行一行がスライドの1枚目から順番に挿入されて行きます。

### 実装内容は以下になります。

- keynoteのスライドの指定した位置に予め用意したテキストファイルの文字列を貼り付ける
- 実行時に文字の色選択ができる
- スライドの枚数とテキストの行数が違う場合はバリデーションをかける
- しゃべらせる

# 実装
それでは早速実装して行きましょう。
基本的な文法は[こちら](https://qiita.com/MMRN/items/5e622eee5c736a42ee38)をご参照ください。

### VSCodeの場合
まずAppleScriptのプラグインを入れましょう。
https://marketplace.visualstudio.com/items?itemName=idleberg.applescript

### ファイル作成
applescriptの拡張子をつけてファイルを作成しましょう。
今回は`text_overlay.applescript`としました。

## パーツ作り

### テキストのプロパティー
[こちら](https://iworkautomation.com/keynote/text-item-overlay.html)を参考にテキストの色や大きさなどを変数に入れる。
今回はこんな感じ

```
-- OBJECT TEXT PROPERTIES
property textOverlayTypeface : "Arial Bold"
property textOverlayFontSize : 40
property textOverlayColorName : "WHITE"
property textOverlayRGBColorValues : {65535, 65535, 65535}

-- TEXT ITEM PROPERTIES
property textOverlayOpacityValue : 100
property textOverlayDefaultPosition : 3
property textItemOverlayOffset : 870
property textItemPointerOffset : 1012.5
```

### Keynoteを開く
次にKeynoteを開きましょう。

```
tell application "Keynote"
		activate
end tell
```

### Keyファイルを開く
次にテロップを入れたいkeyファイルを選択する。

```
tell application "Keynote"
	activate
	try
		set the chosenDocumentFile to ¬
			(choose file of type ¬
				{"com.apple.iwork.keynote.key", ¬
					"com.apple.iwork.keynote.kth", ¬
					"com.apple.iwork.keynote.sffkey", ¬
					"com.apple.iwork.keynote.key-tef", ¬
					"com.microsoft.powerpoint.ppt", ¬
					"org.openxmlformats.presentationml.presentation", ¬
					"org.openxmlformats.presentationml.presentation.macroenabled", ¬
					"com.microsoft.powerpoint.pps", ¬
					"org.openxmlformats.presentationml.slideshow", ¬
					"org.openxmlformats.presentationml.slideshow.macroenabled", ¬
					"com.microsoft.powerpoint.pot", ¬
					"org.openxmlformats.presentationml.template", ¬
					"org.openxmlformats.presentationml.template.macroenabled"} ¬
					default location (path to documents folder) ¬
				with prompt "keynoteのドキュメントを選択してください。")
		open the chosenDocumentFile
	on error errorMessage number errorNumber
		if errorNumber is not -128 then
			display alert errorNumber message errorMessage
		else
			error number -128
		end if
	end try
end tell
```

## テキストファイルから文字列を取得

```
	global text_file
	global text_lines

	set text_lines to {}
	set tmp_file to choose file of type {"Txt","Text"} with prompt "挿入する言葉が記載された.txtファイルを選んでください。"
	set text_file to POSIX path of (tmp_file)
	set tmp_data to do shell script "cat "  & quoted form of (POSIX path of tmp_file)

	repeat with nextLine in  paragraphs of tmp_data
		if length of nextLine is greater than 0 then
			copy nextLine to the end of text_lines
		end if
	end repeat
```
global変数を使ってる理由は[こちら](http://www.script-factory.net/history/2012/06/27_1832.xhtml)をご確認ください。

Tip:変数の中身をみたい時は`log`が便利です
例: `log text_lines`

## テロップ挿入

```
	set textOverlayDefaultPositionName to item

	set dialogText to "Overlay Text: " & POSIX path of tmp_file & return & ¬
		"Text Color: " & textOverlayColorName & return & ¬
		"Text Size: " & textOverlayFontSize & return & ¬
		"Typeface: " & textOverlayTypeface & return & ¬
		"Text Opacity: " & textOverlayOpacityValue & return & ¬
		"Text Position: " & textOverlayDefaultPositionName

	display dialog dialogText & return & return & "テキストの挿入を行いますか？" buttons ¬
		{"キャンセル", "実行"} default button 1
	set addDeleteOverlay to the button returned of the result
	tell the front document
		set documentWidth to its width
		set documentHeight to its height

		if addDeleteOverlay is not "キャンセル" then
			repeat with i from 1 to the count of slides
				set thisSlide to item i of slides
				set the presenter notes of thisSlide to item i of text_lines
				tell slide i
					set thisTextItem to ¬
						make new text item with properties {object text:item i of text_lines as text}
					tell thisTextItem
						tell object text
							set font to textOverlayTypeface
							set size to textOverlayFontSize
							set color of it to textOverlayRGBColorValues
						end tell
						set textItemWidth to its width
						set textItemHeight to its height
						set opacity to textOverlayOpacityValue
						if textOverlayDefaultPosition is 0 then
							-- center
							set x to (documentWidth - textItemWidth) div 2
							set y to (documentHeight - textItemHeight) div 2
						else if textOverlayDefaultPosition is 1 then
							-- top left
							set x to textItemOverlayOffset
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 2 then
							-- top right
							set x to documentWidth - textItemWidth - textItemOverlayOffset
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 3 then
							-- bottom ceter
							set x to (documentWidth - textItemWidth ) div 2
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 4 then
							-- bottom right
							set x to documentWidth - textItemWidth - textItemOverlayOffset
							set y to documentHeight - textItemHeight - textItemOverlayOffset
						end if
						set position to {x, y}
					end tell
				end tell
			end repeat
		else
			delete (every text item of every slide whose object text of it is text_lines)
		end if
	end tell
```

少しコードが長くなりましたが、行っていることは以下です。

- 実行ダイアログに各プロパティーの表示
- キャンセル/実行ボタンの設置
- 実行する場合各プロパティーに前に定義した値を入れて、テロップをスライドに挿入

## テキストカラー選択の追加

```
{"キャンセル", "実行", "色の変更"} default button 1
```
ボタンの項目に`色の変更`を追加

```
if addDeleteOverlay is "色の変更" then
  set textOverlayRGBColorValues to choose color default color textOverlayRGBColorValues
end if
```
`色の変更`がクリックされた時の処理を追加

## バリデーションの追加
```
	tell the front document
		if count of slides is not length of paragraphs of tmp_data then
			display dialog dialogText & return & return & "スライドの数とテキストの行数が違いますが、実行しますか？" buttons ¬
			{"キャンセル", "実行", "色の変更"} default button 1
		else
			display dialog dialogText & return & return & "テキストの挿入を行いますか？" buttons ¬
			{"キャンセル", "実行", "色の変更"} default button 1
		end if
	end tell
```

## しゃべらせる
```
say "処理が正常に終了しました。今日もお仕事頑張ってください。"
```
`say`を使えばなんでもしゃべってくれます。

## 最終コード
```
-- OBJECT TEXT PROPERTIES
property textOverlayTypeface : "Arial Bold"
property textOverlayFontSize : 40
property textOverlayColorName : "WHITE"
property textOverlayRGBColorValues : {65535, 65535, 65535}

-- TEXT ITEM PROPERTIES
property textOverlayOpacityValue : 100
property textOverlayDefaultPosition : 3
property textItemOverlayOffset : 870
property textItemPointerOffset : 1012.5

tell application "Keynote"
	activate
	try
		set the chosenDocumentFile to ¬
			(choose file of type ¬
				{"com.apple.iwork.keynote.key", ¬
					"com.apple.iwork.keynote.kth", ¬
					"com.apple.iwork.keynote.sffkey", ¬
					"com.apple.iwork.keynote.key-tef", ¬
					"com.microsoft.powerpoint.ppt", ¬
					"org.openxmlformats.presentationml.presentation", ¬
					"org.openxmlformats.presentationml.presentation.macroenabled", ¬
					"com.microsoft.powerpoint.pps", ¬
					"org.openxmlformats.presentationml.slideshow", ¬
					"org.openxmlformats.presentationml.slideshow.macroenabled", ¬
					"com.microsoft.powerpoint.pot", ¬
					"org.openxmlformats.presentationml.template", ¬
					"org.openxmlformats.presentationml.template.macroenabled"} ¬
					default location (path to documents folder) ¬
				with prompt "keynoteのドキュメントを選択してください。")
		open the chosenDocumentFile
	on error errorMessage number errorNumber
		if errorNumber is not -128 then
			display alert errorNumber message errorMessage
		else
			error number -128
		end if
	end try

	global text_file
	global text_lines

	set text_lines to {}
	set tmp_file to choose file of type {"Txt","Text"} with prompt "挿入する言葉が記載された.txtファイルを選んでください。"
	set text_file to POSIX path of (tmp_file)
	set tmp_data to do shell script "cat "  & quoted form of (POSIX path of tmp_file)

	repeat with nextLine in  paragraphs of tmp_data
		if length of nextLine is greater than 0 then
			copy nextLine to the end of text_lines
		end if
	end repeat

	set textOverlayDefaultPositionName to item

	set dialogText to "Overlay Text: " & POSIX path of tmp_file & return & ¬
		"Text Color: " & textOverlayColorName & return & ¬
		"Text Size: " & textOverlayFontSize & return & ¬
		"Typeface: " & textOverlayTypeface & return & ¬
		"Text Opacity: " & textOverlayOpacityValue & return & ¬
		"Text Position: " & textOverlayDefaultPositionName

	tell the front document
		if count of slides is not length of paragraphs of tmp_data then
			display dialog dialogText & return & return & "スライドの数とテキストの行数が違いますが、実行しますか？" buttons ¬
			{"キャンセル", "実行", "色の変更"} default button 1
		else
			display dialog dialogText & return & return & "テキストの挿入を行いますか？" buttons ¬
			{"キャンセル", "実行", "色の変更"} default button 1
		end if
	end tell
	set addDeleteOverlay to the button returned of the result
		tell the front document
		set documentWidth to its width
		set documentHeight to its height

		if addDeleteOverlay is "色の変更" then
			set textOverlayRGBColorValues to choose color default color textOverlayRGBColorValues
		end if


		if addDeleteOverlay is not "キャンセル" then
			repeat with i from 1 to the count of slides
				set thisSlide to item i of slides
				set the presenter notes of thisSlide to item i of text_lines
				tell slide i
					set thisTextItem to ¬
						make new text item with properties {object text:item i of text_lines as text}
					tell thisTextItem
						tell object text
							set font to textOverlayTypeface
							set size to textOverlayFontSize
							set color of it to textOverlayRGBColorValues
						end tell
						set textItemWidth to its width
						set textItemHeight to its height
						set opacity to textOverlayOpacityValue
						if textOverlayDefaultPosition is 0 then
							-- center
							set x to (documentWidth - textItemWidth) div 2
							set y to (documentHeight - textItemHeight) div 2
						else if textOverlayDefaultPosition is 1 then
							-- top left
							set x to textItemOverlayOffset
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 2 then
							-- top right
							set x to documentWidth - textItemWidth - textItemOverlayOffset
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 3 then
							-- bottom ceter
							set x to (documentWidth - textItemWidth ) div 2
							set y to textItemOverlayOffset
						else if textOverlayDefaultPosition is 4 then
							-- bottom right
							set x to documentWidth - textItemWidth - textItemOverlayOffset
							set y to documentHeight - textItemHeight - textItemOverlayOffset
						end if
						set position to {x, y}
					end tell
				end tell
			end repeat
		else
			delete (every text item of every slide whose object text of it is text_lines)
		end if
	end tell
	say "処理が正常に終了しました。今日もお仕事頑張ってください。"
end tell
```

#おわりに
普段は業務でRailsとVueを使っているのですが、全く違う言語を触るのもたまにはいいなと感じました。
Macユーザーなら割とAppleScript使っていろいろ面白そうなことできそうなので、また機会があれば触ってみたいです。

# 参考URL

keynoteを開く
https://iworkautomation.com/keynote/document-open.html

テキストを作成して記載する
https://iworkautomation.com/keynote/text-item-overlay.html

プレゼンテータノート
https://iworkautomation.com/keynote/slide-presenter-notes.html

テキストを読み込む(文字化け対応)
http://underwoodmac.cocolog-nifty.com/blog/2011/07/applescriptread.html

グローバル変数
http://www.script-factory.net/history/2012/06/27_1832.xhtml

基本文法
https://qiita.com/MMRN/items/5e622eee5c736a42ee38