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:     gifTimer.cancel ();
   200:     Settings.sgsPutInt ("gifwaitingtime", gifWaitingTime);
   201:     Settings.sgsPutInt ("gifrecordingtime", gifRecordingTime);
   202:     Settings.sgsPutInt ("gifmagnification", gifMagnification);
   203:     Settings.sgsPutString ("gifinterpolation",
   204:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ? "nearest" :
   205:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BILINEAR ? "bilinear" :
   206:                            gifInterpolation == RenderingHints.VALUE_INTERPOLATION_BICUBIC ? "bicubic" :
   207:                            "bilinear");
   208:   }
   209: 
   210:   //gifStartRecording ()
   211:   //  録画開始
   212:   public static void gifStartRecording () {
   213:     if (gifNowRecording) {  //録画中
   214:       return;
   215:     }
   216:     //設定をコピーする
   217:     gifScreenWidth = XEiJ.pnlScreenWidth;
   218:     gifScreenHeight = XEiJ.pnlScreenHeight;
   219:     gifStretchWidth = XEiJ.pnlStretchWidth;
   220:     gifStretchHeight = XEiJ.pnlStretchHeight;
   221:     gifStereoscopicFactor = XEiJ.pnlStereoscopicFactor;
   222:     gifStereoscopicOn = XEiJ.pnlStereoscopicOn;
   223:     gifStereoscopicMethod = XEiJ.pnlStereoscopicMethod;
   224:     //フレームレートを求める
   225: /*
   226:     int htotal = CRTC.crtR00HFrontEndCurr + 1;
   227:     int vtotal = CRTC.crtR04VFrontEndCurr + 1;
   228:     if (htotal <= 0 || vtotal <= 0) {
   229:       return;
   230:     }
   231:     int k = CRTC.crtHRLCurr << 3 | CRTC.crtHighResoCurr << 2 | CRTC.crtHResoCurr;
   232:     double osc = (double) CRTC.crtFreqs[CRTC.CRT_OSCS[k]];
   233:     int ratio = CRTC.CRT_DIVS[k];
   234:     double hfreq = osc / (ratio * htotal << 3);
   235:     double vfreq = hfreq / vtotal;
   236:     gifDelayTime = 100.0 / vfreq;  //10ms単位
   237: */
   238:     gifDelayTime = Math.max (2.0, (double) CRTC.crtTotalLength * 1e-1 + (double) CRTC.crtTotalLengthMNP * 1e-10);  //10ms単位、最小20ms
   239:     //フレーム数を求める
   240: /*
   241:     gifWaitingFrames = (int) Math.floor (vfreq * (double) gifWaitingTime + 0.5);
   242:     gifRecordingFrames = (int) Math.floor (vfreq * (double) gifRecordingTime + 0.5);
   243: */
   244:     gifWaitingFrames = (int) Math.floor ((double) gifWaitingTime * 100.0 / gifDelayTime + 0.5);
   245:     gifRecordingFrames = (int) Math.floor ((double) gifRecordingTime * 100.0 / gifDelayTime + 0.5);
   246: 
   247:     int fullSize = gifScreenWidth * gifScreenHeight * gifStereoscopicFactor;
   248:     int maxNumberOfFrames = 0x7fffffff / fullSize;
   249:     if (maxNumberOfFrames < gifRecordingFrames) {
   250:       return;
   251:     }
   252:     //バッファを確保する
   253:     int bufferSize = fullSize * gifRecordingFrames;
   254:     try {
   255:       gifBuffer = new int[bufferSize];
   256:     } catch (OutOfMemoryError oome) {
   257:       oome.printStackTrace ();
   258:       return;
   259:     }
   260:     //録画開始
   261:     gifStartRecordingMenuItem.setEnabled (false);
   262:     gifPointer = 0;
   263:     gifWaitingCounter = 0;
   264:     gifRecordingCounter = 0;
   265:     gifNowRecording = true;
   266:     CRTC.crtCaptureClock = XEiJ.mpuClockTime;
   267:     CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
   268:   }
   269: 
   270:   //gifCaptureFrame ()
   271:   //  フレームを取り込む
   272:   public static void gifCaptureFrame () {
   273:     if (gifWaitingCounter < gifWaitingFrames) {
   274:       gifWaitingCounter++;
   275:       return;
   276:     }
   277:     //ビットマップからバッファへコピーする
   278:     if (XEiJ.PNL_USE_THREAD) {
   279:       int[] bitmap = XEiJ.pnlBMLeftArray[XEiJ.pnlBMWrite & 3];
   280:       for (int y = 0; y < gifScreenHeight; y++) {
   281:         System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
   282:                           gifBuffer, gifPointer,
   283:                           gifScreenWidth);
   284:         gifPointer += gifScreenWidth;
   285:       }
   286:       if (gifStereoscopicFactor == 2) {
   287:         bitmap = XEiJ.pnlBMRightArray[XEiJ.pnlBMWrite & 3];
   288:         for (int y = 0; y < gifScreenHeight; y++) {
   289:           System.arraycopy (bitmap, XEiJ.PNL_BM_WIDTH * y,
   290:                             gifBuffer, gifPointer,
   291:                             gifScreenWidth);
   292:           gifPointer += gifScreenWidth;
   293:         }
   294:       }
   295:     } else {
   296:       for (int y = 0; y < gifScreenHeight; y++) {
   297:         System.arraycopy (XEiJ.pnlBMLeft, XEiJ.PNL_BM_WIDTH * y,
   298:                           gifBuffer, gifPointer,
   299:                           gifScreenWidth);
   300:         gifPointer += gifScreenWidth;
   301:       }
   302:       if (gifStereoscopicFactor == 2) {
   303:         for (int y = 0; y < gifScreenHeight; y++) {
   304:           System.arraycopy (XEiJ.pnlBMRight, XEiJ.PNL_BM_WIDTH * y,
   305:                             gifBuffer, gifPointer,
   306:                             gifScreenWidth);
   307:           gifPointer += gifScreenWidth;
   308:         }
   309:       }
   310:     }
   311:     gifRecordingCounter++;
   312:     if (gifRecordingCounter == gifRecordingFrames) {
   313:       //録画終了
   314:       CRTC.crtCaptureClock = XEiJ.FAR_FUTURE;
   315:       CRTC.crtFrameTaskClock = Math.min (CRTC.crtContrastClock, CRTC.crtCaptureClock);
   316:       //別スレッドで圧縮してファイルに出力する
   317:       gifTimer.schedule (new TimerTask () {
   318:         @Override public void run () {
   319:           gifOutput ();
   320:         }
   321:       }, 0L);
   322:     }
   323:   }
   324: 
   325:   //gifOutput ()
   326:   //  圧縮してファイルに出力する
   327:   public static void gifOutput () {
   328:     System.out.println (Multilingual.mlnJapanese ? "画像を圧縮しています" : "Compressing images");
   329:     //サイズ
   330:     double zoomRatio = (double) gifMagnification / 100.0;
   331:     int zoomWidth = (int) Math.floor ((double) gifStretchWidth * zoomRatio + 0.5);
   332:     int zoomHeight = (int) Math.floor ((double) gifStretchHeight * zoomRatio + 0.5);
   333:     //入力画像
   334:     BufferedImage imageLeft = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
   335:     BufferedImage imageRight = new BufferedImage (XEiJ.PNL_BM_WIDTH, gifScreenHeight, BufferedImage.TYPE_INT_RGB);
   336:     int[] bmLeft = ((DataBufferInt) imageLeft.getRaster ().getDataBuffer ()).getData ();
   337:     int[] bmRight = ((DataBufferInt) imageRight.getRaster ().getDataBuffer ()).getData ();
   338:     //出力画像
   339:     BufferedImage image = new BufferedImage (zoomWidth * gifStereoscopicFactor, zoomHeight, BufferedImage.TYPE_INT_RGB);
   340:     Graphics2D g2 = image.createGraphics ();
   341:     g2.setRenderingHint (RenderingHints.KEY_INTERPOLATION, gifInterpolation);
   342:     //イメージライタ
   343:     ImageWriter imageWriter = ImageIO.getImageWritersBySuffix ("gif").next ();
   344:     ImageWriteParam writeParam = imageWriter.getDefaultWriteParam ();
   345:     try {
   346:       //ファイル名を決める
   347:       String dirName = "capture";
   348:       File dir = new File (dirName);
   349:       if (dir.exists () ? !dir.isDirectory () : !dir.mkdir ()) {  //ディレクトリを作れない
   350:         gifBuffer = null;
   351:         gifNowRecording = false;
   352:         gifStartRecordingMenuItem.setEnabled (true);
   353:         return;
   354:       }
   355:       HashSet<String> nameSet = new HashSet<String> ();  //ディレクトリにあるファイル名のセット
   356:       for (String name : dir.list ()) {
   357:         nameSet.add (name);
   358:       }
   359:       int number = 0;
   360:       String name;
   361:       do {
   362:         number++;
   363:         name = number + ".gif";
   364:       } while (!nameSet.add (name));  //セットにない番号を探す
   365:       name = dirName + "/" + name;
   366:       //出力開始
   367:       File file = new File (name);
   368:       if (file.exists ()) {  //ないはず
   369:         file.delete ();
   370:       }
   371:       ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream (file);
   372:       imageWriter.setOutput (imageOutputStream);
   373:       imageWriter.prepareWriteSequence (null);
   374:       //フレーム毎の処理
   375:       int halfSize = gifScreenWidth * gifScreenHeight;
   376:       int fullSize = halfSize * gifStereoscopicFactor;
   377: 
   378:       double lastTime = 0.0;  //前回の終了時刻(10ms単位)
   379:       double lastIntTime = 0.0;  //前回の終了時刻を四捨五入した時刻(10ms単位)
   380: 
   381:       for (int counter = 0; counter < gifRecordingFrames; ) {
   382:         int pointer = fullSize * counter;
   383:         //同じフレームをまとめる
   384:         int span = 1;
   385:         while (counter + span < gifRecordingFrames &&
   386:                Arrays.equals (gifBuffer, pointer, pointer + fullSize,
   387:                               gifBuffer, pointer + fullSize * span, pointer + fullSize * (span + 1))) {
   388:           span++;
   389:         }
   390:         counter += span;
   391: 
   392:         double time = lastTime + gifDelayTime * (double) span;  //今回の終了時刻(10ms単位)
   393:         double intTime = Math.floor (time + 0.5);  //今回の終了時刻を四捨五入した時刻(10ms単位)
   394: 
   395:         IIOMetadata metadata = makeMetadata (imageWriter,
   396:                                              writeParam,
   397:                                              image,
   398: /*
   399:                                              String.valueOf ((int) Math.floor (gifDelayTime * (double) span + 0.5)));
   400: */
   401:                                              String.valueOf ((int) (intTime - lastIntTime)));
   402:         lastTime = time;
   403:         lastIntTime = intTime;
   404: 
   405:         //バッファからビットマップへコピーする
   406:         for (int y = 0; y < gifScreenHeight; y++) {
   407:           System.arraycopy (gifBuffer, pointer,
   408:                             bmLeft, XEiJ.PNL_BM_WIDTH * y,
   409:                             gifScreenWidth);
   410:           pointer += gifScreenWidth;
   411:         }
   412:         if (gifStereoscopicFactor == 2) {
   413:           for (int y = 0; y < gifScreenHeight; y++) {
   414:             System.arraycopy (gifBuffer, pointer,
   415:                               bmRight, XEiJ.PNL_BM_WIDTH * y,
   416:                               gifScreenWidth);
   417:             pointer += gifScreenWidth;
   418:           }
   419:         }
   420:         //ビットマップを使って画面を再構築する
   421:         g2.setColor (Color.black);
   422:         g2.fillRect (0, 0, zoomWidth * gifStereoscopicFactor, zoomHeight);
   423:         if (XEiJ.PNL_STEREOSCOPIC_ON && gifStereoscopicOn) {  //立体視ON
   424:           if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_CROSSING) {
   425:             g2.drawImage (imageRight,
   426:                           0, 0, zoomWidth, zoomHeight,
   427:                           0, 0, gifScreenWidth, gifScreenHeight,
   428:                           null);
   429:             g2.drawImage (imageLeft,
   430:                           zoomWidth, 0, zoomWidth * 2, zoomHeight,
   431:                           0, 0, gifScreenWidth, gifScreenHeight,
   432:                           null);
   433:           } else if (gifStereoscopicMethod == XEiJ.PNL_NAKED_EYE_PARALLEL ||
   434:                      gifStereoscopicMethod == XEiJ.PNL_SIDE_BY_SIDE) {
   435:             g2.drawImage (imageLeft,
   436:                           0, 0, zoomWidth, zoomHeight,
   437:                           0, 0, gifScreenWidth, gifScreenHeight,
   438:                           null);
   439:             g2.drawImage (imageRight,
   440:                           zoomWidth, 0, zoomWidth * 2, zoomHeight,
   441:                           0, 0, gifScreenWidth, gifScreenHeight,
   442:                           null);
   443:           } else {  //gifStereoscopicMethod == XEiJ.PNL_TOP_AND_BOTTOM
   444:             g2.drawImage (imageLeft,
   445:                           0, 0, zoomWidth, zoomHeight >> 1,
   446:                           0, 0, gifScreenWidth, gifScreenHeight,
   447:                           null);
   448:             g2.drawImage (imageRight,
   449:                           0, zoomHeight >> 1, zoomWidth, zoomHeight,
   450:                           0, 0, gifScreenWidth, gifScreenHeight,
   451:                           null);
   452:           }
   453:         } else {  //立体視OFF
   454:           g2.drawImage (imageLeft,
   455:                         0, 0, zoomWidth, zoomHeight,
   456:                         0, 0, gifScreenWidth, gifScreenHeight,
   457:                         null);
   458:         }
   459:         //ファイルに出力する
   460:         imageWriter.writeToSequence (new IIOImage (image, null, metadata), writeParam);
   461:       }
   462:       //出力終了
   463:       imageWriter.endWriteSequence ();
   464:       imageOutputStream.close ();
   465:       System.out.println (Multilingual.mlnJapanese ? name + " を更新しました" : name + " was updated");
   466:     } catch (IOException ioe) {
   467:       ioe.printStackTrace ();
   468:     }
   469:     gifBuffer = null;
   470:     gifNowRecording = false;
   471:     gifStartRecordingMenuItem.setEnabled (true);
   472:   }
   473: 
   474:   public static IIOMetadata makeMetadata (ImageWriter imageWriter, ImageWriteParam writeParam, BufferedImage image, String delayTime) {
   475:     IIOMetadata metadata = imageWriter.getDefaultImageMetadata (new ImageTypeSpecifier (image), writeParam);
   476:     String metaFormat = metadata.getNativeMetadataFormatName ();
   477:     Node root = metadata.getAsTree (metaFormat);
   478:     IIOMetadataNode gce = new IIOMetadataNode ("GraphicControlExtension");
   479:     gce.setAttribute ("delayTime", delayTime);
   480:     gce.setAttribute ("disposalMethod", "none");
   481:     gce.setAttribute ("transparentColorFlag", "FALSE");
   482:     gce.setAttribute ("transparentColorIndex", "0");
   483:     gce.setAttribute ("userInputFlag", "FALSE");
   484:     root.appendChild (gce);
   485:     try {
   486:       metadata.setFromTree (metaFormat, root);
   487:     } catch (IIOInvalidTreeException ite) {
   488:       ite.printStackTrace ();
   489:     }
   490:     return metadata;
   491:   }
   492: 
   493: }