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