GIFAnimation.java
     1: //========================================================================================
     2: //  GIFAnimation.java
     3: //    en:GIF animation recording
     4: //    ja:GIFアニメーション録画
     5: //  Copyright (C) 2003-2026 Makoto Kamada
     6: //
     7: //  This file is part of the XEiJ (X68000 Emulator in Java).
     8: //  You can use, modify and redistribute the XEiJ if the conditions are met.
     9: //  Read the XEiJ License for more details.
    10: //  https://stdkmd.net/xeij/
    11: //========================================================================================
    12: 
    13: package xeij;
    14: 
    15: import java.awt.*;  //Graphics2D,RenderingHints
    16: import java.awt.event.*;  //ActionEvent,ActionListener
    17: import java.awt.image.*;  //BufferedImage,DataBufferInt
    18: import java.io.*;  //File
    19: import java.util.*;  //HashSet,Timer
    20: import javax.imageio.*;  //ImageIO
    21: import javax.imageio.metadata.*;  //IIOMetadata
    22: import javax.imageio.stream.*;  //ImageOutputStream
    23: import javax.swing.*;  //JSpinner,SpinnerNumberModel
    24: import javax.swing.event.*;  //ChangeEvent,ChangeListener
    25: import org.w3c.dom.*;  //Node
    26: 
    27: public class GIFAnimation {
    28: 
    29:   //GIFアニメーション録画
    30: 
    31:   //待ち時間
    32:   //  例えばゲームの画面を録画するとき、ポーズをかけた状態で録画ボタンを押してポーズを解除してプレイを再開した後に録画を開始できる
    33: 
    34:   public static final int GIF_WAITING_TIME_MIN = 0;  //待ち時間(s)の最小値
    35:   public static final int GIF_WAITING_TIME_MAX = 30;  //待ち時間(s)の最大値
    36:   public static final int GIF_RECORDING_TIME_MIN = 1;  //録画時間(s)の最小値
    37:   public static final int GIF_RECORDING_TIME_MAX = 30;  //録画時間(s)の最大値
    38:   public static final int GIF_MAGNIFICATION_MIN = 10;  //倍率(%)の最小値
    39:   public static final int GIF_MAGNIFICATION_MAX = 200;  //倍率(%)の最大値
    40: 
    41:   public static int gifWaitingTime;  //待ち時間(s)
    42:   public static int gifRecordingTime;  //録画時間(s)
    43:   public static int gifMagnification;  //倍率(%)
    44:   public static Object gifInterpolation;  //補間アルゴリズム
    45: 
    46:   public static SpinnerNumberModel gifWaitingTimeModel;  //待ち時間(s)のスピナーモデル
    47:   public static SpinnerNumberModel gifRecordingTimeModel;  //録画時間(s)のスピナーモデル
    48:   public static SpinnerNumberModel gifMagnificationModel;  //倍率(%)のスピナーモデル
    49: 
    50:   public static JMenuItem gifStartRecordingMenuItem;  //録画開始メニューアイテム
    51:   public static JMenu gifSettingsMenu;  //GIFアニメーション録画設定メニュー
    52: 
    53:   public static java.util.Timer gifTimer;  //出力用のスレッド
    54: 
    55:   public static int gifScreenWidth;  //pnlScreenWidthのコピー
    56:   public static int gifScreenHeight;  //pnlScreenHeightのコピー
    57:   public static int gifStretchWidth;  //pnlStretchWidthのコピー
    58:   public static int gifStretchHeight;  //pnlStretchHeightのコピー
    59:   public static int gifStereoscopicFactor;  //pnlStereoscopicFactorのコピー
    60:   public static boolean gifStereoscopicOn;  //pnlStereoscopicOnのコピー
    61:   public static int gifStereoscopicMethod;  //pnlStereoscopicMethodのコピー
    62: 
    63:   public static double gifDelayTime;  //フレームの間隔(10ms単位)
    64:   public static int gifWaitingFrames;  //待ち時間のフレーム数
    65:   public static int gifRecordingFrames;  //録画時間のフレーム数
    66:   public static int[] gifBuffer;  //フレームバッファ
    67:   public static int gifPointer;  //フレームバッファのポインタ
    68:   public static int gifWaitingCounter;  //待ち時間のフレームカウンタ
    69:   public static int gifRecordingCounter;  //録画時間のフレームカウンタ
    70:   public static boolean gifNowRecording;  //true=録画中
    71: 
    72:   //gifInit ()
    73:   //  初期化
    74:   public static void gifInit () {
    75: 
    76:     gifWaitingTime = Math.max (GIF_WAITING_TIME_MIN, Math.min (GIF_WAITING_TIME_MAX, Settings.sgsGetInt ("gifwaitingtime")));
    77:     gifRecordingTime = Math.max (GIF_RECORDING_TIME_MIN, Math.min (GIF_RECORDING_TIME_MAX, Settings.sgsGetInt ("gifrecordingtime")));
    78:     gifMagnification = Math.max (GIF_MAGNIFICATION_MIN, Math.min (GIF_MAGNIFICATION_MAX, Settings.sgsGetInt ("gifmagnification")));
    79:     switch (Settings.sgsGetString ("gifinterpolation").toLowerCase ()) {
    80:     case "nearest":  //最近傍補間
    81:       gifInterpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
    82:       break;
    83:     case "bilinear":  //線形補間
    84:       gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
    85:       break;
    86:     case "bicubic":  //三次補間
    87:       gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
    88:       break;
    89:     default:
    90:       gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
    91:     }
    92: 
    93:     gifWaitingTimeModel = new SpinnerNumberModel (gifWaitingTime, GIF_WAITING_TIME_MIN, GIF_WAITING_TIME_MAX, 1);
    94:     gifRecordingTimeModel = new SpinnerNumberModel (gifRecordingTime, GIF_RECORDING_TIME_MIN, GIF_RECORDING_TIME_MAX, 1);
    95:     gifMagnificationModel = new SpinnerNumberModel (gifMagnification, GIF_MAGNIFICATION_MIN, GIF_MAGNIFICATION_MAX, 1);
    96: 
    97:     ButtonGroup interpolationGroup = new ButtonGroup ();
    98: 
    99:     gifStartRecordingMenuItem =
   100:       Multilingual.mlnText (
   101:         ComponentFactory.createMenuItem (
   102:           "Start recording",
   103:           new ActionListener () {
   104:             @Override public void actionPerformed (ActionEvent ae) {
   105:               gifStartRecording ();
   106:             }
   107:           }),
   108:         "ja", "録画開始");
   109: 
   110:     gifSettingsMenu =
   111:       Multilingual.mlnText (
   112:         ComponentFactory.createMenu (
   113:           "GIF animation recording settings",
   114:           ComponentFactory.createHorizontalBox (
   115:             Multilingual.mlnText (ComponentFactory.createLabel ("Waiting time"), "ja", "待ち時間"),
   116:             Box.createHorizontalGlue ()
   117:             ),
   118:           ComponentFactory.createHorizontalBox (
   119:             Box.createHorizontalStrut (20),
   120:             ComponentFactory.createNumberSpinner (gifWaitingTimeModel, 4, new ChangeListener () {
   121:               @Override public void stateChanged (ChangeEvent ce) {
   122:                 gifWaitingTime = gifWaitingTimeModel.getNumber ().intValue ();
   123:               }
   124:             }),
   125:             Multilingual.mlnText (ComponentFactory.createLabel ("seconds"), "ja", "秒"),
   126:             Box.createHorizontalGlue ()
   127:             ),
   128:           ComponentFactory.createHorizontalBox (
   129:             Multilingual.mlnText (ComponentFactory.createLabel ("Recording time"), "ja", "録画時間"),
   130:             Box.createHorizontalGlue ()
   131:             ),
   132:           ComponentFactory.createHorizontalBox (
   133:             Box.createHorizontalStrut (20),
   134:             ComponentFactory.createNumberSpinner (gifRecordingTimeModel, 4, new ChangeListener () {
   135:               @Override public void stateChanged (ChangeEvent ce) {
   136:                 gifRecordingTime = gifRecordingTimeModel.getNumber ().intValue ();
   137:               }
   138:             }),
   139:             Multilingual.mlnText (ComponentFactory.createLabel ("seconds"), "ja", "秒"),
   140:             Box.createHorizontalGlue ()
   141:             ),
   142:           ComponentFactory.createHorizontalBox (
   143:             Multilingual.mlnText (ComponentFactory.createLabel ("Magnification"), "ja", "倍率"),
   144:             Box.createHorizontalGlue ()
   145:             ),
   146:           ComponentFactory.createHorizontalBox (
   147:             Box.createHorizontalStrut (20),
   148:             ComponentFactory.createNumberSpinner (gifMagnificationModel, 4, new ChangeListener () {
   149:               @Override public void stateChanged (ChangeEvent ce) {
   150:                 gifMagnification = gifMagnificationModel.getNumber ().intValue ();
   151:               }
   152:             }),
   153:             ComponentFactory.createLabel ("%"),
   154:             Box.createHorizontalGlue ()
   155:             ),
   156:           ComponentFactory.createHorizontalSeparator (),
   157:           Multilingual.mlnText (
   158:             ComponentFactory.createRadioButtonMenuItem (
   159:               interpolationGroup,
   160:               gifInterpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
   161:               "Nearest neighbor",
   162:               new ActionListener () {
   163:                 @Override public void actionPerformed (ActionEvent ae) {
   164:                   gifInterpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
   165:                 }
   166:               }),
   167:             "ja", "最近傍補間"),
   168:           Multilingual.mlnText (
   169:             ComponentFactory.createRadioButtonMenuItem (
   170:               interpolationGroup,
   171:               gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BILINEAR,
   172:               "Bilinear",
   173:               new ActionListener () {
   174:                 @Override public void actionPerformed (ActionEvent ae) {
   175:                   gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
   176:                 }
   177:               }),
   178:             "ja", "線形補間"),
   179:           Multilingual.mlnText (
   180:             ComponentFactory.createRadioButtonMenuItem (
   181:               interpolationGroup,
   182:               gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BICUBIC,
   183:               "Bicubic",
   184:               new ActionListener () {
   185:                 @Override public void actionPerformed (ActionEvent ae) {
   186:                   gifInterpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
   187:                 }
   188:               }),
   189:             "ja", "三次補間")
   190:           ),
   191:         "ja", "GIF アニメーション録画設定");
   192: 
   193:     gifTimer = new java.util.Timer ();
   194:   }
   195: 
   196:   //gifTini ()
   197:   //  後始末
   198:   public static void gifTini () {
   199:     Settings.sgsPutInt ("gifwaitingtime", gifWaitingTime);
   200:     Settings.sgsPutInt ("gifrecordingtime", gifRecordingTime);
   201:     Settings.sgsPutInt ("gifmagnification", gifMagnification);
   202:     Settings.sgsPutString ("gifinterpolation",
   203:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ? "nearest" :
   204:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BILINEAR ? "bilinear" :
   205:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BICUBIC ? "bicubic" :
   206:                            "bilinear");
   207:   }
   208: 
   209:   //gifStartRecording ()
   210:   //  録画開始
   211:   public static void gifStartRecording () {
   212:     if (gifNowRecording) {  //録画中
   213:       return;
   214:     }
   215:     //設定をコピーする
   216:     gifScreenWidth = XEiJ.pnlScreenWidth;
   217:     gifScreenHeight = XEiJ.pnlScreenHeight;
   218:     gifStretchWidth = XEiJ.pnlStretchWidth;
   219:     gifStretchHeight = XEiJ.pnlStretchHeight;
   220:     gifStereoscopicFactor = XEiJ.pnlStereoscopicFactor;
   221:     gifStereoscopicOn = XEiJ.pnlStereoscopicOn;
   222:     gifStereoscopicMethod = XEiJ.pnlStereoscopicMethod;
   223:     //フレームレートを求める
   224: /*
   225:     int htotal = CRTC.crtR00HFrontEndCurr + 1;
   226:     int vtotal = CRTC.crtR04VFrontEndCurr + 1;
   227:     if (htotal <= 0 || vtotal <= 0) {
   228:       return;
   229:     }
   230:     int k = CRTC.crtHRLCurr << 3 | CRTC.crtHighResoCurr << 2 | CRTC.crtHResoCurr;
   231:     double osc = (double) CRTC.crtFreqs[CRTC.CRT_OSCS[k]];
   232:     int ratio = CRTC.CRT_DIVS[k];
   233:     double hfreq = osc / (ratio * htotal << 3);
   234:     double vfreq = hfreq / vtotal;
   235:     gifDelayTime = 100.0 / vfreq;  //10ms単位
   236: */
   237:     gifDelayTime = Math.max (2.0, (double) CRTC.crtTotalLength * 1e-1 + (double) CRTC.crtTotalLengthMNP * 1e-10);  //10ms単位、最小20ms
   238:     //フレーム数を求める
   239: /*
   240:     gifWaitingFrames = (int) Math.floor (vfreq * (double) gifWaitingTime + 0.5);
   241:     gifRecordingFrames = (int) Math.floor (vfreq * (double) gifRecordingTime + 0.5);
   242: */
   243:     gifWaitingFrames = (int) Math.floor ((double) gifWaitingTime * 100.0 / gifDelayTime + 0.5);
   244:     gifRecordingFrames = (int) Math.floor ((double) gifRecordingTime * 100.0 / gifDelayTime + 0.5);
   245: 
   246:     int fullSize = gifScreenWidth * gifScreenHeight * gifStereoscopicFactor;
   247:     int maxNumberOfFrames = 0x7fffffff / fullSize;
   248:     if (maxNumberOfFrames < gifRecordingFrames) {
   249:       return;
   250:     }
   251:     //バッファを確保する
   252:     int bufferSize = fullSize * gifRecordingFrames;
   253:     try {
   254:       gifBuffer = new int[bufferSize];
   255:     } catch (OutOfMemoryError oome) {
   256:       oome.printStackTrace ();
   257:       return;
   258:     }
   259:     //録画開始
   260:     gifStartRecordingMenuItem.setEnabled (false);
   261:     gifPointer = 0;
   262:     gifWaitingCounter = 0;
   263:     gifRecordingCounter = 0;
   264:     gifNowRecording = true;
   265:     CRTC.crtCaptureClock = XEiJ.mpuClockTime;
   266:     CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
   267:   }
   268: 
   269:   //gifCaptureFrame ()
   270:   //  フレームを取り込む
   271:   public static void gifCaptureFrame () {
   272:     if (gifWaitingCounter < gifWaitingFrames) {
   273:       gifWaitingCounter++;
   274:       return;
   275:     }
   276:     //ビットマップからバッファへコピーする
   277:     if (XEiJ.PNL_USE_THREAD) {
   278:       int[] bitmap = XEiJ.pnlBMLeftArray[XEiJ.pnlBMWrite & 3];
   279:       for (int y = 0; y < gifScreenHeight; y++) {
   280:         System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
   281:                           gifBuffer, gifPointer,
   282:                           gifScreenWidth);
   283:         gifPointer += gifScreenWidth;
   284:       }
   285:       if (gifStereoscopicFactor == 2) {
   286:         bitmap = XEiJ.pnlBMRightArray[XEiJ.pnlBMWrite & 3];
   287:         for (int y = 0; y < gifScreenHeight; y++) {
   288:           System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
   289:                             gifBuffer, gifPointer,
   290:                             gifScreenWidth);
   291:           gifPointer += gifScreenWidth;
   292:         }
   293:       }
   294:     } else {
   295:       for (int y = 0; y < gifScreenHeight; y++) {
   296:         System.arraycopy (XEiJ.pnlBMLeft, XEiJ.PNL_BM_WIDTH * y,
   297:                           gifBuffer, gifPointer,
   298:                           gifScreenWidth);
   299:         gifPointer += gifScreenWidth;
   300:       }
   301:       if (gifStereoscopicFactor == 2) {
   302:         for (int y = 0; y < gifScreenHeight; y++) {
   303:           System.arraycopy (XEiJ.pnlBMRight, XEiJ.PNL_BM_WIDTH * y,
   304:                             gifBuffer, gifPointer,
   305:                             gifScreenWidth);
   306:           gifPointer += gifScreenWidth;
   307:         }
   308:       }
   309:     }
   310:     gifRecordingCounter++;
   311:     if (gifRecordingCounter == gifRecordingFrames) {
   312:       //録画終了
   313:       CRTC.crtCaptureClock = XEiJ.FAR_FUTURE;
   314:       CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
   315:       //別スレッドで圧縮してファイルに出力する
   316:       gifTimer.schedule (new TimerTask () {
   317:         @Override public void run () {
   318:           gifOutput ();
   319:         }
   320:       }, 0L);
   321:     }
   322:   }
   323: 
   324:   //gifOutput ()
   325:   //  圧縮してファイルに出力する
   326:   public static void gifOutput () {
   327:     System.out.println (Multilingual.mlnJapanese ? "画像を圧縮しています" : "Compressing images");
   328:     //サイズ
   329:     double zoomRatio = (double) gifMagnification / 100.0;
   330:     int zoomWidth = (int) Math.floor ((double) gifStretchWidth * zoomRatio + 0.5);
   331:     int zoomHeight = (int) Math.floor ((double) gifStretchHeight * zoomRatio + 0.5);
   332:     //入力画像
   333:     BufferedImage imageLeft = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
   334:     BufferedImage imageRight = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
   335:     int[] bmLeft = ((DataBufferInt) imageLeft.getRaster ().getDataBuffer ()).getData ();
   336:     int[] bmRight = ((DataBufferInt) imageRight.getRaster ().getDataBuffer ()).getData ();
   337:     //出力画像
   338:     BufferedImage image = new BufferedImage (zoomWidth * gifStereoscopicFactor, zoomHeight, BufferedImage.TYPE_INT_RGB);
   339:     Graphics2D g2 = image.createGraphics ();
   340:     g2.setRenderingHint (RenderingHints.KEY_INTERPOLATION, gifInterpolation);
   341:     //イメージライタ
   342:     ImageWriter imageWriter = ImageIO.getImageWritersBySuffix ("gif").next ();
   343:     ImageWriteParam writeParam = imageWriter.getDefaultWriteParam ();
   344:     try {
   345:       //ファイル名を決める
   346:       String dirName = "capture";
   347:       File dir = new File (dirName);
   348:       if (dir.exists () ? !dir.isDirectory () : !dir.mkdir ()) {  //ディレクトリを作れない
   349:         gifBuffer = null;
   350:         gifNowRecording = false;
   351:         gifStartRecordingMenuItem.setEnabled (true);
   352:         return;
   353:       }
   354:       HashSet<String> nameSet = new HashSet<String> ();  //ディレクトリにあるファイル名のセット
   355:       for (String name : dir.list ()) {
   356:         nameSet.add (name);
   357:       }
   358:       int number = 0;
   359:       String name;
   360:       do {
   361:         number++;
   362:         name = number + ".gif";
   363:       } while (!nameSet.add (name));  //セットにない番号を探す
   364:       name = dirName + "/" + name;
   365:       //出力開始
   366:       File file = new File (name);
   367:       if (file.exists ()) {  //ないはず
   368:         file.delete ();
   369:       }
   370:       ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream (file);
   371:       imageWriter.setOutput (imageOutputStream);
   372:       imageWriter.prepareWriteSequence (null);
   373:       //フレーム毎の処理
   374:       int halfSize = gifScreenWidth * gifScreenHeight;
   375:       int fullSize = halfSize * gifStereoscopicFactor;
   376: 
   377:       double lastTime = 0.0;  //前回の終了時刻(10ms単位)
   378:       double lastIntTime = 0.0;  //前回の終了時刻を四捨五入した時刻(10ms単位)
   379: 
   380:       for (int counter = 0; counter < gifRecordingFrames; ) {
   381:         int pointer = fullSize * counter;
   382:         //同じフレームをまとめる
   383:         int span = 1;
   384:         while (counter + span < gifRecordingFrames &&
   385:                Arrays.equals (gifBuffer, pointer, pointer + fullSize,
   386:                               gifBuffer, pointer + fullSize * span, pointer + fullSize * (span + 1))) {
   387:           span++;
   388:         }
   389:         counter += span;
   390: 
   391:         double time = lastTime + gifDelayTime * (double) span;  //今回の終了時刻(10ms単位)
   392:         double intTime = Math.floor (time + 0.5);  //今回の終了時刻を四捨五入した時刻(10ms単位)
   393: 
   394:         IIOMetadata metadata = makeMetadata (imageWriter,
   395:                                              writeParam,
   396:                                              image,
   397: /*
   398:                                              String.valueOf ((int) Math.floor (gifDelayTime * (double) span + 0.5)));
   399: */
   400:                                              String.valueOf ((int) (intTime - lastIntTime)));
   401:         lastTime = time;
   402:         lastIntTime = intTime;
   403: 
   404:         //バッファからビットマップへコピーする
   405:         for (int y = 0; y < gifScreenHeight; y++) {
   406:           System.arraycopy (gifBuffer, pointer,
   407:                             bmLeft, XEiJ.PNL_BM_WIDTH * y,
   408:                             gifScreenWidth);
   409:           pointer += gifScreenWidth;
   410:         }
   411:         if (gifStereoscopicFactor == 2) {
   412:           for (int y = 0; y < gifScreenHeight; y++) {
   413:             System.arraycopy (gifBuffer, pointer,
   414:                               bmRight, XEiJ.PNL_BM_WIDTH * y,
   415:                               gifScreenWidth);
   416:             pointer += gifScreenWidth;
   417:           }
   418:         }
   419:         //ビットマップを使って画面を再構築する
   420:         g2.setColor (Color.black);
   421:         g2.fillRect (0, 0, zoomWidth * gifStereoscopicFactor, zoomHeight);
   422:         if (XEiJ.PNL_STEREOSCOPIC_ON && gifStereoscopicOn) {  //立体視ON
   423:           if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_CROSSING) {
   424:             g2.drawImage (imageRight,
   425:                           0, 0, zoomWidth, zoomHeight,
   426:                           0, 0, gifScreenWidth, gifScreenHeight,
   427:                           null);
   428:             g2.drawImage (imageLeft,
   429:                           zoomWidth, 0, zoomWidth * 2, zoomHeight,
   430:                           0, 0, gifScreenWidth, gifScreenHeight,
   431:                           null);
   432:           } else if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_PARALLEL ||
   433:                      gifStereoscopicMethod == XEiJ.PNL_SIDE_BY_SIDE) {
   434:             g2.drawImage (imageLeft,
   435:                           0, 0, zoomWidth, zoomHeight,
   436:                           0, 0, gifScreenWidth, gifScreenHeight,
   437:                           null);
   438:             g2.drawImage (imageRight,
   439:                           zoomWidth, 0, zoomWidth * 2, zoomHeight,
   440:                           0, 0, gifScreenWidth, gifScreenHeight,
   441:                           null);
   442:           } else {  //gifStereoscopicMethod == XEiJ.PNL_TOP_AND_BOTTOM
   443:             g2.drawImage (imageLeft,
   444:                           0, 0, zoomWidth, zoomHeight >> 1,
   445:                           0, 0, gifScreenWidth, gifScreenHeight,
   446:                           null);
   447:             g2.drawImage (imageRight,
   448:                           0, zoomHeight >> 1, zoomWidth, zoomHeight,
   449:                           0, 0, gifScreenWidth, gifScreenHeight,
   450:                           null);
   451:           }
   452:         } else {  //立体視OFF
   453:           g2.drawImage (imageLeft,
   454:                         0, 0, zoomWidth, zoomHeight,
   455:                         0, 0, gifScreenWidth, gifScreenHeight,
   456:                         null);
   457:         }
   458:         //ファイルに出力する
   459:         imageWriter.writeToSequence (new IIOImage (image, null, metadata), writeParam);
   460:       }
   461:       //出力終了
   462:       imageWriter.endWriteSequence ();
   463:       imageOutputStream.close ();
   464:       System.out.println (Multilingual.mlnJapanese ? name + " を更新しました" : name + " was updated");
   465:     } catch (IOException ioe) {
   466:       ioe.printStackTrace ();
   467:     }
   468:     gifBuffer = null;
   469:     gifNowRecording = false;
   470:     gifStartRecordingMenuItem.setEnabled (true);
   471:   }
   472: 
   473:   public static IIOMetadata makeMetadata (ImageWriter imageWriter, ImageWriteParam writeParam, BufferedImage image, String delayTime) {
   474:     IIOMetadata metadata = imageWriter.getDefaultImageMetadata (new ImageTypeSpecifier (image), writeParam);
   475:     String metaFormat = metadata.getNativeMetadataFormatName ();
   476:     Node root = metadata.getAsTree (metaFormat);
   477:     IIOMetadataNode gce = new IIOMetadataNode ("GraphicControlExtension");
   478:     gce.setAttribute ("delayTime", delayTime);
   479:     gce.setAttribute ("disposalMethod", "none");
   480:     gce.setAttribute ("transparentColorFlag", "FALSE");
   481:     gce.setAttribute ("transparentColorIndex", "0");
   482:     gce.setAttribute ("userInputFlag", "FALSE");
   483:     root.appendChild (gce);
   484:     try {
   485:       metadata.setFromTree (metaFormat, root);
   486:     } catch (IIOInvalidTreeException ite) {
   487:       ite.printStackTrace ();
   488:     }
   489:     return metadata;
   490:   }
   491: 
   492: }