Javaで画面操作説明用GIF動画を作成する
ズバット引越し比較
Posted on 2021/12/24 at 2:20
画面操作を録画してGIF動画を作成したいなと検索したところ「ScreenToGif」というフリーソフトがあるらしく、ネット上の口コミも良さそうでした。
ただ、私はJAVA使いということでJAVA製のキャプチャソフトを探してみるも見つからず・・・。 無いなら自分で作ってみようという事で、色々試行錯誤しようやく動くものが出来上がったので、サンプルコード置いておきます。
機能としてはスクリーンキャプチャを指定したフレーム分を取得して、GIF動画にします。 PCのスペックにもよると思いますが、私の環境では6fpsぐらいしか出せませんでした。それでも操作説明するには十分かと思います。 以下にサンプル動画ありますので、評価してみてください。
GIF動画サンプル
サンプルプログラムにより作成したGIF動画です。最初は8MBぐらいの容量となってしまいますが、 GIMPで必要な部分のみカットしてフィルター処理することで454kBまで下げることができました。
サンプルコード
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import org.w3c.dom.Node;
public class GifCapture {
private List<Integer> list;
private ImageWriter iw;
private Robot robot;
private Rectangle screenSize;
private IIOMetadataNode gce;
private IIOMetadataNode aes = null;
private IIOMetadata metadata;
private boolean isLoop = true;
private int loopCount = 0; //ループ回数(0:無限ループ)
private int fileNo = 1;
public static void main(String[] args) {
try {
GifCapture capture = new GifCapture();
capture.captureStart();
} catch (Exception e) {
e.printStackTrace();
}
}
public GifCapture() throws AWTException, IOException {
File tmpDir = new File("tmp/");
if (!tmpDir.exists())
tmpDir.mkdir();
robot = new Robot();
screenSize = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
list = new ArrayList<Integer>();
Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("gif");
iw = it.hasNext() ? it.next() : null;
ImageOutputStream ios = ImageIO.createImageOutputStream(new File("tmp/capture.gif"));
iw.setOutput(ios);
iw.prepareWriteSequence(null);
gce = new IIOMetadataNode("GraphicControlExtension");
gce.setAttribute("disposalMethod", "none");
gce.setAttribute("userInputFlag", "FALSE");
gce.setAttribute("transparentColorFlag", "FALSE");
gce.setAttribute("transparentColorIndex", "0");
gce.setAttribute("delayTime", "15");
// Loop Control
if (isLoop) {
IIOMetadataNode ae = new IIOMetadataNode("ApplicationExtension");
ae.setAttribute("applicationID", "NETSCAPE");
ae.setAttribute("authenticationCode", "2.0");
ae.setUserObject(new byte[] {0x1, (byte)loopCount, 0x0});
aes = new IIOMetadataNode("ApplicationExtensions");
aes.appendChild(ae);
}
}
int second = 1;
public void captureStart() throws Exception {
long startTime = System.currentTimeMillis();
ExecutorService th1 = Executors.newFixedThreadPool(3);
for (int i=0; i<60; i++) {
Capture cp = new Capture();
th1.execute(cp);
}
th1.shutdown();
while (list.isEmpty()) {
Thread.sleep(100);
}
ExecutorService th2 = Executors.newSingleThreadExecutor();
th2.execute(new Runnable() {
@Override
public void run() {
ImageWriteParam iwp = iw.getDefaultWriteParam();
int index = 0;
while (index == 0 || index < list.size()) {
int fileNo = list.get(index);
String fileNoStr = Integer.toString(fileNo);
String zero = "";
for (int j=4; j>fileNoStr.length(); j--)
zero += "0";
String filePath = "tmp/" + zero + fileNoStr+ ".gif";
File gif = new File(filePath);
if (gif.exists()) {
BufferedImage buff;
try {
buff = ImageIO.read(gif);
metadata = iw.getDefaultImageMetadata(
new ImageTypeSpecifier(buff), iwp);
String metaFormat = metadata.getNativeMetadataFormatName();
Node root = metadata.getAsTree(metaFormat);
root.appendChild(gce);
if (aes != null)
root.appendChild(aes);
metadata.setFromTree(metaFormat, root);
iw.writeToSequence(new IIOImage(buff, null, metadata), null);
System.out.println("writeToSequence : " + gif.getAbsolutePath());
gif.deleteOnExit();
index++;
} catch (IOException e) {
e.printStackTrace();
break;
}
} else {
System.out.println("not found : " + gif.getAbsolutePath() + " - continued");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
try {
iw.endWriteSequence();
System.out.println("endWriteSequence : " + (System.currentTimeMillis() - startTime) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
});
th2.shutdown();
}
private synchronized CaptureImage getCaptureImage() {
String fileNoStr = Integer.toString(fileNo);
String zero = "";
for (int j=4; j>fileNoStr.length(); j--)
zero += "0";
String filePath = "tmp/" + zero + fileNoStr+ ".tmp";
list.add(fileNo);
fileNo++;
BufferedImage buff = robot.createScreenCapture(screenSize);
PointerInfo inf = MouseInfo.getPointerInfo();
Point mp = inf.getLocation();
int x = (int) mp.getLocation().getX();
int y = (int) mp.getLocation().getY();
int w = buff.getWidth() / 2;
int h = buff.getHeight() / 2;
Graphics2D g = buff.createGraphics();
g.setColor(Color.WHITE);
g.fillOval(x, y, 7, 7);
g.setColor(Color.BLACK);
g.drawOval(x, y, 8, 8);
BufferedImage to = new BufferedImage(
w, h, BufferedImage.TYPE_3BYTE_BGR);
to.getGraphics().drawImage(buff.getScaledInstance(
w, h, Image.SCALE_AREA_AVERAGING),
0, 0, w, h, null);
CaptureImage capture = new CaptureImage();
capture.setCapture(to);
capture.setFilePath(filePath);
return capture;
}
class Capture implements Runnable {
@Override
public void run() {
try {
CaptureImage capture = getCaptureImage();
String filePath = capture.getFilePath();
BufferedImage buff = capture.getBufferedImage();
File tmp = new File(filePath);
tmp.deleteOnExit();
ImageIO.write(buff, "gif", tmp);
String renamePath = filePath.replace(".tmp", ".gif");
File gif = new File(renamePath);
gif.deleteOnExit();
System.out.println("rename: " + tmp.renameTo(gif) + "=> " + gif.getName());
tmp.delete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class CaptureImage {
private String filePath = null;
private BufferedImage buff = null;
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public BufferedImage getBufferedImage() {
return buff;
}
public void setCapture(BufferedImage capture) {
this.buff = capture;
}
}
}
サンプルプログラムの説明
BufferedImage buff = robot.createScreenCapture(screenSize);
ここでスクリーンキャプチャをBufferedImageとして取得するのですが、これをwriteToSequence()でGIFに書き込む処理が重く、最初は3fpsほどしか出せませんでした。
そこでtmpというディレクトリを作成し、そこに一時ファイルとして1枚のGIF画像を作成し、別スレッドからファイルを読み込んでGIFに書き込む方法に変更しました。
それでもやっと5~6fpsぐらいですが・・・。
gce.setAttribute("delayTime", "15");
GIF動画にはヘッダ情報を入れることができ、delayTimeはフレーム再生時間で単位は1/100秒となります。
今回のサンプルでは150m秒となります。色々試しましたがこれが一番自然に仕上がりました。
for (int i=0; i<60; i++) {
Capture cp = new Capture();
th1.execute(cp);
}
ここで何フレームキャプチャするかを設定します。私の環境では60フレーム撮り終わるのに大体11秒かかりましたので、5~6fpsというところでしょうか。
PointerInfo inf = MouseInfo.getPointerInfo();
Point mp = inf.getLocation();
int x = (int) mp.getLocation().getX();
int y = (int) mp.getLocation().getY();
Graphics2D g = buff.createGraphics();
g.setColor(Color.WHITE);
g.fillOval(x, y, 7, 7);
g.setColor(Color.BLACK);
g.drawOval(x, y, 8, 8);
キャプチャ画像にはマウスカーソルが表示されません。なので、マウス位置を取得してfillOvalで黒ふちの塗りつぶし白の丸を描画しています。
GIF動画の容量圧縮
プログラムにより出力されたGIF動画は容量大きめで出力されるため、そのままサイトに貼り付けることはおすすめできません。 それならmp4にして自動再生にした方が絵もきれいでいいと思います。
私はGIMPを使用して必要な部分のみ切り取りし、フィルター>アニメーション>GIF用最適化をすることで、容量を圧縮しました。
まとめ
数秒程度の簡単な説明動画であれば以下の点でGIF動画がおすすめです。
- きちんと処理をすればファイルサイズが軽量
- スマートフォンやPCなど、ほとんどの環境で再生可能
- 無限ループ再生が可能
しかし、GIFであるため最大256色までしか使えないため、実写には向かない。音声が入れられないなどの大きなデメリットもあるため、 動画の長さ、種類によって使い分ける必要があります。