💨

Mac Automatorで動画から静止画像を切り出す

2025/01/29に公開

モチベーション

動画からお気に入りの瞬間を画像として保存したい。

QuickTimeでコピーしてPreviewでクリップボードから新規画像を作成する方法は、手順が多く面倒。

写真(Photo.app)もフレームの書き出し機能があるが、カスタマイズが不可能。.tiffファイルで書き出されるのは後から変換すればいいのだが、なぜか画像の解像度がランダムに変わる。

VLCなどでは静止画をキャプチャする機能があるが、icloud管理されている動画ファイルを読み込ませるためには、一度コピーする手間が生じる。

要件

  • スクリーンショットを用いるアプローチとする。ディスプレイ依存が発生するが、見えている通りの画像になるメリットもあるため。
  • Photo.appあるいはiMovieで動画を全画面表示にする。お気に入りの瞬間にてキーボードショートカット一発で保存されるようにする。
  • 動画のアスペクトレシオは16:9 (portrait)を仮定する。

課題

  • 全画面表示すると画面上部に高さ不明の黒いギャップが発生するため、Ctrl-Shift-3の撮り方だと困る。
  • Ctrl-Shift-4で特定ウィンドウを撮れば(全画面表示でも撮影対象ウィンドウにできる)、上記問題は発生しないが、ウィンドウを選択するキー操作が発生する。

ソリューション

最前面のウィンドウのスクリーンショットを撮るAppleScriptを作成し、Automatorを介してクイックアクション(サービス)として登録する。システム環境設定にて、そのサービスに好きなショートカットキー(自分はF6)を付ける。

AppleScript

-- アクティブなアプリケーション情報を取得
on getActiveAppInfo()
	tell application "System Events"
		set frontProcess to 1st process whose frontmost is true
		set bundleID to bundle identifier of frontProcess
	end tell
	
	tell application id bundleID
		try
			set appName to name
			set windowID to id of front window
		on error
			set windowID to my getWindowID(appName)
		end try
	end tell
	
	return windowID
end getActiveAppInfo

-- スクリーンショットを撮影
on captureScreenshot(windowID)
	do shell script "
    filename=~/Desktop/sc-$(date +%Y%m%d-%H%M...%S).jpg && 
    screencapture -t png -l " & windowID & " \"$filename\" && 
    dimensions=$(sips -g pixelWidth -g pixelHeight \"$filename\" | grep -E 'pixelWidth|pixelHeight' | awk '{print $2}') && 
    width=$(echo \"$dimensions\" | sed -n '1p') && 
    height=$(echo \"$dimensions\" | sed -n '2p') && 
    target_height=$height && 
    target_width=$((height / 16 * 9)) && 
    sips -c \"$target_height\" \"$target_width\" \"$filename\" --out \"$filename\" && 
    sips -s format jpeg -s formatOptions 100 \"$filename\" --out \"$filename\""
end captureScreenshot

-- JavaScriptを使ってWindow IDを取得
on getWindowID(appName)
	set JS to "ObjC.import('CoreGraphics');
    Ref.prototype.$ = function() {
        return ObjC.deepUnwrap(ObjC.castRefToObject(this));
    }
    Application.prototype.getWindowList = function() {
        let pids = Application('com.apple.systemevents')
                  .processes.whose({ 'bundleIdentifier':
                        this.id() }).unixId();

        return  $.CGWindowListCopyWindowInfo(
                $.kCGWindowListExcludeDesktopElements,
                $.kCGNullWindowID).$()
                 .filter(x => pids.indexOf(x.kCGWindowOwnerPID) + 1
                           && x.kCGWindowLayer     == 0
                           && x.kCGWindowStoreType == 1
                           && x.kCGWindowAlpha     == 1
                ).map(x => [x.kCGWindowNumber]);           
    }
    Application('" & appName & "').getWindowList();"
	return (word 1 of (do shell script "osascript -l JavaScript -e " & quoted form of JS)) as integer
end getWindowID

-- メイン処理
set windowID to my getActiveAppInfo()
my captureScreenshot(windowID)

スクリーンショットは一度png形式で保存し、sipsにてアスペクト比を仮定した切り出しとjpgへの変換を行なっている。

ワークフローの完成形

反応がわかりにくいので完了後に通知を行うフローを設けている。

実行時の注意点

初回起動するタイミングで、画面へのアクセスの付与を促されるため、指示に従って追加をする。その後一旦アプリを閉じる必要がある。

参考文献

Window IDを取得する方法
https://www.macscripter.net/t/get-window-id/72891/13

Discussion