ホームページ>開発ツール>Xojo / Real Studio Trial and Error・Cocoa/CarbonのDeclareでアニメGIFキャプチャーする
Xojo / Real Studio Trial and Error
目次
Cocoa/CarbonのDeclareでアニメGIFキャプチャーする
はじめに
以下は、Xojo Cocoaビルドについての話題です。
動きのあるキャプチャーをアニメーションGIFで実現できないか、調べてみました。
なお検証には、Xojo 2017 Release 1.1を用いています。(Mac mini 2018 + macOS 10.14.6 Mojave)
方針
以前、こちらの記事でMP4動画による画面キャプチャーを試みましたが、他にアニメーションGIFを使う方法も考えられます。
アニメーションGIFは256インデックスカラーという制約はあるものの、ポピュラーで実績もあります。
ただし動画とは異なり、キャプチャーのステップとアニメーションGIF作成ステップが別々になる点を考慮する必要があります。
まずはキャプチャーですが、APIは以前の記事で試行したものを使います。
連続して実行するためにタイマーを使いますが、使い勝手を考えてコードで実装します。
終了は、ここでは終了時間(回数)を指定して自動停止するようにしますが、別途停止メソッドを実装してもいいでしょう。
次に作成の方ですが、一般的な一枚絵のGIF(以下、静止画GIF)であればCocoaのAPIを使って簡単にできます。
なので、アニメーションGIFも同様の手法で書き出せるかと思ったのですが、そうはすんなりいきませんでした。// BitmapImageRepからGIF形式のData生成(dictはGIF用オプション。BitmapImageRepはNSImageから変換) Declare Function representationUsingType Lib "Cocoa" Selector "representationUsingType:properties:" (receiver As Ptr, type As Integer, prop As Ptr) As Ptr Dim gifData As Ptr = representationUsingType(bitmapImageRep, 2, dict) // 2 = NSGIFFileType
検索して見つかったのは、Core Graphics、即ちCarbon系のメソッド群を使うものでした。
参考サイト(1):複数画像からGIFをつくる · GitHub
参考サイト(2):objective c - GIF Image generated on iOS10 no longer loops forever on browser - Stack Overflow
参考サイト(3):cocoa - Saving CGImageRef to a png file? - Stack Overflow
参考サイト(4):Swift:CGImage ←→ NSImage ←→ CIImage【変換Extension】 - Qiita
Core Graphicsはデータがオブジェクトではなく構造体、とのことなのでキャスト、それも__bridgeとか、自分にとってあまり目にしないものが使われています。
ですが、結論から言うと、キャストについては何もしなくてよさそうでした。
また、これは実装して分かったことですが、キャプチャー時のフルカラーから256カラーに減色した時、静止画GIFではスムースに見えるものが、アニメーション化すると色のバランスが崩れる場合があります。(注:常に、という訳ではありません。)
いくつか試した結果、kCGImagePropertyGIFHasGlobalColorMapをfalseにすることで、回避できました。
他に、注意点としては、
元になった静止画GIF(の一枚) 出来上がったアニメーションGIF
以上を踏まえ、(残りの)仕様は以下の通りとしました。
- Dictionaryが、値にDictionaryを持つという変則?構成である(構造体に合わせるため?)
- 参考サイト(2)にある通り、「CGImageDestinationSetProperties before CGImageDestinationAddImage」とする
- CFBridgingReleaseは、なぜか64bitビルドではエラーになる。(ただし、使わなくても(自分で変数をReleaseすれば)問題はなさそう。)
- キャプチャー範囲は、ウィンドウ全体に固定(ウィンドウサイズにはタイトルバー/ツールバーは含まれないので、高さを、現物合わせで加算)
- キャプチャーした静止画GIFは、テンポラリーフォルダに作成し、終了時に削除する
- アニメーションGIFのオプション設定は、ファイルはループ回数のみ、フレームは表示時間とGlobalカラーマップとする(全フレーム共通)
- タイマーのインターバル時間と、アニメーションGIFのkCGImagePropertyGIFDelayTimeは、一致させる
- NSDataを経由せずに直接ファイル出力できるよう、CGImageDestinationCreateWithURLを用いる
- 他のアプリケーションに組み込み易いよう、必要なものはモジュールに纏める
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、動きのあるキャプチャーをアニメーションGIFで実現できることを確認しました。
- Xojoで新規プロジェクトを作成
- Window1に、Label(Name:Label1)、PushButton(Name:PushButton1)、Timer(Name:Timer1)を置く
(注:LabelとTimerは。アニメーションの効果をわかり易くするためのもの。)- 以下をPushButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action // テスト用 Timer1.Mode=2 // 引数は、対象ウィンドウ Capture(self) End Sub
- 以下をTimer1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action Label1.Text=Str(Val(Label1.Text)+1) End Sub
- 新規モジュールを作成(名前は、ここでは「GIFCapture」とした。)
- 以下をGIFCaptureにペースト
Public Sub Capture(win As Window) // 初期化 pWin=win // 対象ウィンドウ pMax=5 // キャプチャー回数 pTbarH=22 // タイトル/ツールバーを含みたい場合の加算分 // テンポラリフォルダ内に一時フォルダを作成 Dim dt As New Date Dim fname As String fname=Format(dt.Year,"0000")+Format(dt.Month,"00")+Format(dt.Day,"00") _ +Format(dt.Hour,"00")+Format(dt.Minute,"00")+Format(dt.Second,"00") // 現在日時の取得(フォルダ名を固有化するために用いる) pTempFI=SpecialFolder.Temporary.Child(fname) // テンポラリフォルダ内にフォルダを取得 pTempFI.CreateAsFolder // フォルダの生成 // タイマーを初期化して、キャプチャー開始 InitTimer(500) // 500 = 0.5秒 End Sub
- 以下をGIFCaptureにペースト
注)Xojo言語リファレンスのサンプルを使わせて頂きました。(FolderItem.Removeの項)Private Function DeleteEntireFolder(theFolder as FolderItem, continueIfErrors as Boolean = false) as Integer // Returns an error code if it fails, or zero if the folder was deleted successfully dim returnCode, lastErr, itemCount as integer dim files(), dirs() as FolderItem if theFolder = nil or not theFolder.Exists() then return 0 end if // Collect the folder‘s contents first. // This is faster than collecting them in reverse order and deleting them right away! itemCount = theFolder.Count for i as integer = 1 to itemCount dim f as FolderItem f = theFolder.TrueItem( i ) if f <> nil then if f.Directory then dirs.Append f else files.Append f end if end if next // Now delete the files for each f as FolderItem in files f.Delete lastErr = f.LastErrorCode // Check if an error occurred if lastErr <> 0 then if continueIfErrors then if returnCode = 0 then returnCode = lastErr else // Return the error code if any. This will cancel the deletion. return lastErr end if end if next redim files(-1) // free the memory used by the files array before we enter recursion // Now delete the directories for each f as FolderItem in dirs lastErr = DeleteEntireFolder( f, continueIfErrors ) if lastErr <> 0 then if continueIfErrors then if returnCode = 0 then returnCode = lastErr else // Return the error code if any. This will cancel the deletion. return lastErr end if end if next if returnCode = 0 then // We‘re done without error, so the folder should be empty and we can delete it. theFolder.Delete returnCode = theFolder.LastErrorCode end if return returnCode End Function
- 以下をGIFCaptureにペースト
注)Xojo言語リファレンスのサンプルを使わせて頂きました。(AddHandlerの項)Private Sub InitTimer(ival As Integer) // タイマー生成 pTimer = New Timer pTimer.Period = ival pTimer.Mode = Timer.ModeMultiple // この時点で動作開始 AddHandler pTimer.Action, AddressOf TimerRun End Sub
- 以下をGIFCaptureにペースト
Private Sub MakeCapture() // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // サイズのセット Dim rect As CGRect rect = CGRectMake(pWin.Left, pWin.Top-pTbarH, pWin.Width, pWin.Height+pTbarH) // pTbarH = タイトル/ツールバーを含みたい場合の加算分 // ウィンドウナンバーの取得 Declare Function windowNumber Lib "Cocoa" Selector "windowNumber" (receiver As Integer) As Integer Dim window_id As Integer = windowNumber(pWin.Handle) // ウィンドウのキャプチャー(Bitwise.ShiftLeft(1,3) = kCGWindowListOptionIncludingWindow (1 << 3)、0 = kCGWindowImageDefault) Declare Function CGWindowListCreateImage Lib "Carbon" (rect As CGRect, winopt As Integer, winid As Integer, imgopt As Integer) As Ptr Dim cgimage As Ptr = CGWindowListCreateImage(rect, Bitwise.ShiftLeft(1,3), window_id, 0) // NSBitmapImageRep初期化 Dim bitmapImageRep As Ptr = NSClassFromString("NSBitmapImageRep") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr bitmapImageRep = alloc(bitmapImageRep) Declare Function initWithCGImage Lib "Cocoa" Selector "initWithCGImage:" (receiver As Ptr, img As Ptr) As Ptr bitmapImageRep = initWithCGImage(bitmapImageRep, cgimage) // falseをNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") Declare Function numberWithBool Lib "Cocoa" Selector "numberWithBool:" (receiver As Ptr, path As Boolean) As Ptr numb = numberWithBool(numb, false) // GIF用オプションのセット Dim dict As Ptr = NSClassFromString("NSDictionary") // クラスメソッドなので、まずNSDictionaryクラスを取得 Declare Function dictionaryWithObject Lib "Cocoa" Selector "dictionaryWithObject:forKey:" (receiver As Ptr, objt As Ptr, key As CFStringRef) As Ptr dict = dictionaryWithObject(dict, numb, "NSImageDitherTransparency") // GIF出力(NSGIFFileType = 2) Declare Function representationUsingType Lib "Cocoa" Selector "representationUsingType:properties:" (receiver As Ptr, type As Integer, prop As Ptr) As Ptr Dim gifData As Ptr = representationUsingType(bitmapImageRep, 2, dict) // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(bitmapImageRep) // ファイル出力 Declare Function writeToFile Lib "Cocoa" Selector "writeToFile:atomically:" (receiver As Ptr, path As CFStringRef, atm As Boolean) As Boolean Dim ret As Boolean = writeToFile(gifData, pTempFI.Child(Str(pCnt)+".gif").NativePath, true) // clean up Declare Sub CFRelease lib "Carbon" (obj as Ptr) CFRelease(cgimage) End Sub
- 以下をGIFCaptureにペースト
Private Sub PutAnimeGIF() // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // 出力ファイルを指定してImageDestinationを初期化 Dim fo As FolderItem = SpecialFolder.Desktop.Child("test.gif") // デスクトップに作成 Dim url As Ptr = NSClassFromString("NSURL") // クラスメソッドなので、まずNSURLクラスを取得 Declare Function fileURLWithPath Lib "Cocoa" Selector "fileURLWithPath:" (receiver As Ptr, path As CFStringRef) As Ptr url = fileURLWithPath(url, fo.NativePath) // 出力ファイルパス Declare Function CGImageDestinationCreateWithURL lib "Carbon" (url as Ptr, typ As CFStringRef, cnt As Integer, optn As Ptr) as Ptr Dim dest As Ptr = CGImageDestinationCreateWithURL(url, "com.compuserve.gif", pMax, nil) // ファイルプロパティを生成(ループカウントのみ設定) Dim num1 As Ptr = NSClassFromString("NSNumber") // クラスメソッドなので、まずNSNumberクラスを取得 Declare Function numberWithInteger Lib "Cocoa" Selector "numberWithInteger:" (receiver As Ptr, val As Integer) As Ptr num1 = numberWithInteger(num1, 0) // 0 = 無限ループ Dim dict1 As Ptr = NSClassFromString("NSDictionary") // クラスメソッドなので、まずNSDictionaryクラスを取得 Declare Function dictionaryWithObject Lib "Cocoa" Selector "dictionaryWithObject:forKey:" (receiver As Ptr, obj As Ptr, key As CFStringRef) As Ptr dict1 = dictionaryWithObject(dict1, num1, "LoopCount") Dim gifProp As Ptr = NSClassFromString("NSDictionary") // クラスメソッドなので、まずNSDictionaryクラスを取得 gifProp = dictionaryWithObject(gifProp, dict1, "{GIF}") // ファイルプロパティをImageDestinationにセット Declare Sub CGImageDestinationSetProperties lib "Carbon" (obj as Ptr, prop As Ptr) CGImageDestinationSetProperties(dest, gifProp) Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr Declare Function initWithContentsOfFile Lib "Cocoa" Selector "initWithContentsOfFile:" (receiver As Ptr, path As CFStringRef) As Ptr Declare Function TIFFRepresentation Lib "Cocoa" Selector "TIFFRepresentation" (receiver As Ptr) As Ptr Declare Function CGImageSourceCreateWithData lib "Carbon" (data as Ptr, optn As Ptr) as Ptr Declare Function CGImageSourceCreateImageAtIndex lib "Carbon" (data as Ptr, cnt As Integer, optn As Ptr) as Ptr Declare Function dictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr Declare Function numberWithFloat Lib "Cocoa" Selector "numberWithFloat:" (receiver As Ptr, val As Single) As Ptr Declare Function numberWithBool Lib "Cocoa" Selector "numberWithBool:" (receiver As Ptr, path As Boolean) As Ptr Declare Sub setObject Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As Ptr, key As CFStringRef) Declare Sub CGImageDestinationAddImage lib "Carbon" (obj as Ptr, img As Ptr, prop As Ptr) Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) // 画像数(=フレーム数)のループ Dim dict2, num2, num3, frameP, data, imgSrc, cgImage As Ptr for i As Integer = 1 to pMax // ファイルからNSImage形式で画像を取得 Dim image As Ptr = NSClassFromString("NSImage") image = alloc(image) image = initWithContentsOfFile(image, pTempFI.Child(Str(i)+".gif").NativePath) // NSImage形式からCGImage形式に変換 data = TIFFRepresentation(image) // return NSData* imgSrc = CGImageSourceCreateWithData(data, nil) cgImage = CGImageSourceCreateImageAtIndex(imgSrc, 0, nil) // フレームプロパティを生成 dict2 = NSClassFromString("NSMutableDictionary") // 複数個設定するので、まず空のNSMutableDictionary生成 dict2 = dictionary(dict2) num2 = NSClassFromString("NSNumber") // クラスメソッドなので、まずNSNumberクラスを取得 num2 = numberWithFloat(num2, 0.5) // 0.5 = 0.5秒 setObject(dict2, num2, "DelayTime") // 表示時間 num3 = NSClassFromString("NSNumber") // クラスメソッドなので、まずNSNumberクラスを取得 num3 = numberWithBool(num3, false) setObject(dict2, num3, "HasGlobalColorMap") // Globalカラーマップを使わない(Localを使う) frameP = NSClassFromString("NSDictionary") // クラスメソッドなので、まずNSDictionaryクラスを取得 frameP = dictionaryWithObject(frameP, dict2, "{GIF}") // フレーム(画像とプロパティ)をImageDestinationに追加 CGImageDestinationAddImage(dest, cgImage, frameP) // clean up release(image) next // ImageDestination終了処理 Declare Sub CGImageDestinationFinalize lib "Carbon" (obj as Ptr) CGImageDestinationFinalize(dest) // clean up Declare Sub CFRelease lib "Carbon" (obj as Ptr) CFRelease(dest) End Sub
- 以下をGIFCaptureにペースト
Private Sub TimerRun(sender As Timer) // キャプチャー MakeCapture() // 回数カウント pCnt=pCnt+1 // 回数の上限を超えたら if pCnt>pMax then pTimer.Mode=0 // タイマーを止める PutAnimeGIF() // 合体とファイル出力 Dim ret As Integer = DeleteEntireFolder(pTempFI) // 一時フォルダを削除 end if End Sub
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pCnt、データ型:Integer、標準値:1、を追加)
Private Property pCnt as Integer = 1
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pMax、データ型:Integer、を追加)
Private Property pMax as Integer
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pTbarH、データ型:Integer、を追加)
Private Property pTbarH as Integer
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pTempFI、データ型:FolderItem、を追加)
Private Property pTempFI as FolderItem
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pTimer、データ型:Timer、を追加)
Private Property pTimer as Timer
- 以下をGIFCaptureにペースト(できなければプロパティに、名前:pWin、データ型:Window、を追加)
Private Property pWin as Window
- 他に、CGRectMake(メソッド)、CGRect(構造体)が必要ですが、macoslibからコピーさせて頂きました。(上記BurstCaptureまたは別途モジュールを用意してコピーする。)
注)macoslibではCGRect(のメンバーの型であるCGPoint、CGSize)のメンバーの型にSingleが割り当てられているが、64bitにも対応したい場合は、CGFloatに書き換える。
おわりに
アプリケーションに組み込む場合は、ボタンではなくメニュー項目に仕込む等した方が、使い勝手がいいと思われます。
当初は、アニメーションGIFの方がMP4動画よりファイルサイズが小さくなるのかと思っていたのですが、そうでもないようです。
静止画レベルでは圧縮していますが、アニメーションレベルでの圧縮等も考えた方がいいかもしれません。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):複数画像からGIFをつくる · GitHub
参考サイト(2):objective c - GIF Image generated on iOS10 no longer loops forever on browser - Stack Overflow
参考サイト(3):cocoa - Saving CGImageRef to a png file? - Stack Overflow
参考サイト(4):Swift:CGImage ←→ NSImage ←→ CIImage【変換Extension】 - Qiita
更新履歴
2020.01.27 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]