風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

エクセルでピザカッター!(画像を等しい角度で分割)

PowerPointで、丸い画像を強引に等分する方法というツイートをみかけて、ふと「エクセルでもできるんだろうか?」と思い、試してみたくなったしだいです。
残念ながらPowerPointのように手作業でも簡単にできる方法は思いつかなかったためVBAの力を借りておりますが……
動作イメージはこんな感じです*1



手っ取り早く試してみたい方へ

マクロ有効ワークシートを共有してありますので、ダウンロードしてお試し下さい。
drive.google.com
ソースコードだけ見たい方は、こちらに晒してあります

きっともっと簡便な方法もある気がするので、アドバイス歓迎です!

備忘録

はっきり言って役に立つのか何なのかという一発ネタな感じですが、作成過程で何回もはまりいくつか知見を得られましたので、おおよそはまった気づいた順に書き留めておきます。

画像はなべて96dpi

いくつかの画像をエクセルに挿入して試していたところ、元の画像のサイズ(ピクセル)通りに表示されるものと(図の書式設定でサイズをリセットしても)異なった大きさで表示されるものとがありました。
エクセルに画像を挿入する場合は元のラスタ画像ファイル(JPEG・PNG等)は解像度を96dpiで保存しておくのが良いようです。
画像を挿入した後、図の書式設定でサイズをリセット(高さ/幅の倍率を100%にする)することで元の画像と同じ大きさにできるので、その後の調整も楽になります*2

f:id:furyu-tei:20210530114458p:plain
エクセルに挿入する画像は解像度によって見え方が異なる
画像を図形でトリミングする方法

エクセルに挿入した画像は、これを選択しつつ「図の形式(Alt+JP)→トリミング(V)→図形に合わせてトリミング(S)」から図形を選ぶことで、選んだ図形でトリミングすることができます。
f:id:furyu-tei:20210530125901p:plain
また、先に図形を挿入しておいてこれを選択しつつ「図形の書式(Alt+JD)→図形の塗りつぶし(FS)→図(P)」から画像ファイルを指定して塗りつぶすことにより、図形の形に合わせて画像をトリミングすることもできます。
f:id:furyu-tei:20210530125917p:plain

手動でやる分にはどちらでも構いません。
前者の方が直感的で、いちいち画像ファイルを選択する必要もなく(一度画像ファイルを挿入しておけば後はコピペで済む)デフォルトの塗りつぶしや枠線に煩わされることもないのでおすすめでしょうか。

今回はVBAでやるためにコピー&ペーストを極力使いたくなかったので(クリップボード周りの機能は結構な頻度で失敗するため処理が煩雑になりがち・後述)後者の方法を用いています。
このため、指定したパスに実際に画像ファイルが存在する必要があります
【追記】
最初にシート間で一回だけコピー&ペーストしておけば後はShape.Duplicateで複製できることに気づいたので、マクロ有効ワークシートの方でも前者の方法に変更してあります。

#Const DuplicatePictureToMakePiece = True ' True: 元の画像を複製 / False: 1ピース毎に画像読み込み

のTrueをFalseに変更すれば、後者の方法にも戻せます。

トリミングの位置調整はむずかしい

任意の矩形を取るラスタ画像(円形に見える画像も実際のデータは通常は矩形)に対応させたかったので、矩形の外接円(半径が\frac{\sqrt{短辺^2+長辺^2}}2)を考えて、これを等しい角度の部分円でトリミングしていくことで分割する方法を取ることにしました。

とりあえず部分円のサイズを計算して調整した上で画像を挿入すると、直後は下記のようになります。
f:id:furyu-tei:20210530131933p:plain
どうやらデフォルトは図形にフィットするように画像がサイズ調整されるようですので、図の書式設定→画像の位置で幅・高さを元の画像と同じにしてやります。
f:id:furyu-tei:20210530132345p:plain
うまく切り取られているように見えます。お、これなら簡単にいくんじゃない……? と思っていた頃が自分にもありました。

同じ操作を180°よりも小さい角度の部分円でやってやると……
f:id:furyu-tei:20210530135033p:plain
こうなります……おや、画像の表示位置が……?(わかりやすいように期待している位置を半透明で重ねています)
どうやらトリミングのときには、デフォルト(トリミングの画像位置の横方向/縦方向に移動の値が0cm)では画像の中心を図形の見えている部分(部分円だと扇型の部分)を囲む矩形の中心に合わせるようになっているようです。
つまり、図形の見えている部分(扇型の部分)の表示範囲を計算して、それに合わせて画像位置の横方向/縦方向に移動の値を調整してやる必要があったのです……め、面倒くさい……。
なんでや……図形の大きさ自体は変わってないんやし、図形の中心でええやないか……せめてそっちの設定を選ばせてくれ
f:id:furyu-tei:20210530135428p:plain

部分円を囲む矩形の求め方(三角関数なんて忘れていたかった)

部分円(扇型)を囲む矩形か……とりあえず扇形の外周上の座標X・Yそれぞれの最大値と最小値を求めればよいのだろうけれど、円弧の部分はどうすれば……? ある角度範囲をとるときのsinとcosの最大値・最小値を求める必要がありそう?🤔 そんなの、数学が苦手な自分にできるわけないだろっ!😫
最初は力技で(角度を少しずつ変化させていって最大値・最小値を取得)やっていましたが、いき.xls[互換モード]@aero_iki さん助けを求めていろいろとアドバイスをいただきつつ、

なんとか解決にいたりました。

いき.xls[互換モード]さん、ありがとうございました!

図形のままだとシートの端には寄せられない

ここまでで、なんとか画像を分割できるようになりました。
このままでもよかったのですが、図形のままだと
f:id:furyu-tei:20210530143234p:plain:w350
のように、見た目には余白があるにも関わらず実際の図形はより大きい領域を占めているため、シートの端の方に寄せられない状態になります(図形や画像の座標はシートの左上が原点となっており、負の値は指定できません)。
図形のままで任意の矩形にトリミングできればよいのですが、たぶんできません(もし方法があれば教えて下さい)。

図形を画像に変換するときの問題点

そこで、図形を画像に変換してから矩形でトリミングすることを考えたのですが。
図形を選択してコピー→図 (PNG) として貼り付けると……
f:id:furyu-tei:20210530144020p:plain
のように、見えている部分だけが残る形で画像化されてしまいます。
え、待って……これってまた面倒な座標計算をしないといけない予感?🤔

そろそろ疲れていたので💦「見えている部分だけが残るのなら……残したい部分を見えるようにすればいいじゃない!」という発想のもと、図形の正方形/長方形を基準となる外接円と同じ高さ&幅にして挿入、部分円に重ね合わせてグループ化したものをコピー→図(PNG)として貼り付ける手段を取りました。
f:id:furyu-tei:20210530145329p:plain
実際の処理では矩形は透明にしていますが、矩形の場合はその状態でコピペしても元と同じ大きさとなりました

あとは、元の画像の矩形とそれぞれの部分円が重なる領域が収まる矩形領域で(実際には先の部分円の円弧座標の最大・最小を求める処理と同じ処理で、元画像の矩形と円弧の重なり部分の座標の最大・最小もいっしょに計算しています)画像を切り取れば(ShapeのPictureFormatでCrop◯◯を設定)完成です。
f:id:furyu-tei:20210530152824p:plain
まぁ画像に変換してしまうと今度は透明部分もクリックできてしまうので、これはこれで操作しづらいのですが💦

追記:トリミングしても元画像のデータは残る

「完成です」と書いておいてあれですが💦ひとつ忘れていました。
画像をトリミング(ShapeのPictureFormatでCrop○○を設定)した場合ですが、この状態で画像を選択して「図の形式(Alt+JP)→トリミング(V)→トリミング(C)」してみるとわかるように、
f:id:furyu-tei:20210531212058p:plain
切り取られた部分(灰色になっている箇所)もデータとしては残ってしまっています。

手動でなら「図の形式(Alt+JP)→図の圧縮(M)」を選択することで
f:id:furyu-tei:20210531212304p:plain
のようなダイアログが表示され、「☑ 図のトリミング部分を削除する(D)」という便利な機能も使えるのですが、残念ながらVBAからはうまく使えません。
一応「Application.CommandBars.ExecuteMso "PicturesCompress"」を実行すればダイアログが呼び出せるので、SendKeysと組み合わせることで圧縮できなくもないのですが、オプションの選択がやりにくかったりときどき失敗する等、使い勝手がよくありません

どうしようかと思ったのですが、図形を画像に変換するときの問題点ではやっかいだった「コピー→図 (PNG) として貼り付け」をもう一回行えば、見えている部分だけが残る形で画像化されるんだからこれでいいじゃない! ということに気が付きました。
実際に試してみると……
f:id:furyu-tei:20210531213302p:plain
「コピー→図 (PNG) として貼り付け」した画像(右側)はトリミングで見ても余分な箇所は残っていません。
これで無事、みかけだけではなくてちゃんとトリミングされた画像にすることができました。

VBAでクリップボードが絡む処理は鬼門

図形を画像に変換する際には、Shapeの.Copy&Worksheetの.PasteSpecialを実行しているのですが、これをそのまま行うと高確率で.Copyもしくは.PasteSpecialのいずれかでエラーが発生してしまいます。
場合によってはエラーが発生していないのにも関わらず、画像が正常に貼付けされないケースもあります。

これはコピー→ペーストの過程で行われているクリップボードの操作で発生する問題のようです。

踊るエクセル@ExcelVBAerさんのアドバイス


に参考にクリップボード関係のWinAPIを使い、クリップボードのクリア→コピー→データがクリップボードに入ったことを確認→ペースト…という手順(エラーが発生した場合はリトライ)にしてみました。
踊るエクセルさん、ありがとうございました!
追記:記事作成後のアドバイスも追加しています

ただ、いかにも冗長な処理だし、これが適切な処理なのかも自信がないしで(クリップボード関係のWinAPIをよくわかっていない)もやもやします。よりよいやり方があれば教えて下さい。
標準のメソッドを使っているのにクリップボード関係の処理は高頻度に失敗するというのがそもそも納得いかないというのもありますが