File: AVI_Writer.java

package info (click to toggle)
imagej 1.51i%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 5,244 kB
  • ctags: 13,220
  • sloc: java: 113,144; sh: 285; xml: 50; makefile: 8
file content (631 lines) | stat: -rw-r--r-- 33,393 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
package ij.plugin.filter;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.Animator;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;

/**
This plugin implements the File/Save As/AVI command.
Supported formats:
  Uncompressed 8-bit (gray or indexed color), 24-bit (RGB),
  JPEG and PNG compression of individual frames
  16-bit and 32-bit (float) images are converted to 8-bit
The plugin is based on the FileAvi class written by William Gandler,
part of Matthew J. McAuliffe's MIPAV program, available from
http://mipav.cit.nih.gov/.
2008-06-05: Support for jpeg and png-compressed output and
composite images by Michael Schmid.
2015-09-28: Writes AVI 2.0 if the file size would be above approx. 0.9 GB

* The AVI format written looks like this:
* RIFF AVI            RIFF HEADER, AVI CHUNK					
*   | LIST hdrl       MAIN AVI HEADER
*   | | avih          AVI HEADER
*   | | LIST strl     STREAM LIST(s) (One per stream)
*   | | | strh        STREAM HEADER (Required after above; fourcc type is 'vids' for video stream)
*   | | | strf        STREAM FORMAT (for video: BitMapInfo; may also contain palette)
*   | | | strn        STREAM NAME
*   | | | indx        MAIN 'AVI 2.0' INDEX of 'ix00' indices
*   | LIST movi       MOVIE DATA (maximum approx. 0.95 GB)
*   | | 00db or 00dc  FRAME (b=uncompressed, c=compressed)
*   | | 00db or 00dc  FRAME
*   | | ...
*   | | ix00          AVI 2.0-style index of frames within this 'movi' list
* RIFF AVIX	          Only if required by size (this is AVI 2.0 extension)
*   | LIST movi       MOVIE DATA (maximum approx. 0.95 GB)
*   | | 00db or 00dc  FRAME
*   | | ...
*   | | ix00          AVI 2.0-style index of frames within this 'movi' list
* RIFF AVIX	          further chunks, each approx 0.95 GB (AVI 2.0)
* ...
*/

public class AVI_Writer implements PlugInFilter {
    //four-character codes for compression
    // Note: byte sequence in four-cc is reversed - ints in Intel (little endian) byte order.
    // Note that compression codes BI_JPEG=4 and BI_PNG=5 are not understood by avi players
    // (even not by MediaPlayer, even though these codes are specified by Microsoft).
    public final static int  NO_COMPRESSION   = 0;            //no compression, also named BITMAPINFO.BI_RGB
    public final static int  JPEG_COMPRESSION = 0x47504a4d;   //'MJPG' JPEG compression of individual frames
    public final static int  PNG_COMPRESSION  = 0x20676e70;   //'png ' PNG compression of individual frames
    private final static int FOURCC_00db = 0x62643030;        //'00db' uncompressed frame
    private final static int FOURCC_00dc = 0x63643030;        //'00dc' compressed frame
    private final static int MAX_INDX_SIZE = 3072;            //max length of index of indices 'indx'
    private final static int JUNK_SIZE_THRESHOLD = 950*1024*1024;   //if size exceeds this, makes a new RIFF AVIX chunk
    //compression options: dialog parameters
    private int      compressionIndex = 2; //0=none, 1=PNG, 2=JPEG
    private static int      jpegQuality = 90;    //0 is worst, 100 best (not currently used)
    private final static String[] COMPRESSION_STRINGS = new String[] {"None", "PNG", "JPEG"};
    private final static int[] COMPRESSION_TYPES = new int[] {NO_COMPRESSION, PNG_COMPRESSION, JPEG_COMPRESSION};

    private ImagePlus imp;
    private RandomAccessFile raFile;
    private int             xDim,yDim;      //image size
    private int             zDim;           //number of movie frames (stack size)
    private int             bytesPerPixel;  //8 or 24
    private int             frameDataSize;  //in bytes (uncompressed)
    private int             biCompression;  //compression type (0, 'JPEG, 'PNG')
    private int             linePad;        //no. of bytes to add for padding of data lines to 4*n length
    private byte[]          bufferWrite;    //output buffer for image data
    private BufferedImage   bufferedImage;  //data source for writing compressed images
    private RaOutputStream  raOutputStream; //output stream for writing compressed images
    private long[]          sizePointers =  //a stack of the pointers to the chunk sizes (pointers are
                                new long[5];//  remembered to write the sizes later, when they are known)
    private int             stackPointer;   //points to first free position in sizePointers stack
    private int             endHeadPointer; //position of first 'movi' chunk, i.e., end of the space reserved for indx
    // AVI-2 related:
    private long            pointer2indx;   //points to main index-of-indices 'indx'
    private int             nIndxEntries=0; //number of 'indx' entries
    private long            pointer2indxNEntriesInUse;  //points to 'nEntriesInUse' of 'indx'
    private long            pointer2indxNextEntry;   //points to next free slot of 'indx'

    public int setup(String arg, ImagePlus imp) {
        this.imp = imp;
        return DOES_ALL+NO_CHANGES;
    }

    /** Asks for the compression type and filename; then saves as AVI file */
    public void run(ImageProcessor ip) {
        if (!showDialog(imp)) return;          //compression type dialog
        SaveDialog sd = new SaveDialog("Save as AVI...", imp.getTitle(), ".avi");
        String fileName = sd.getFileName();
        if (fileName == null)
            return;
        String fileDir = sd.getDirectory();
        FileInfo fi = imp.getOriginalFileInfo();
        if (fi!=null && imp.getStack().isVirtual() && fileDir.equals(fi.directory) && fileName.equals(fi.fileName)) {
            IJ.error("AVI Writer", "Virtual stacks cannot be saved in place.");
            return;
        }
        try {
            writeImage(imp, fileDir + fileName, COMPRESSION_TYPES[compressionIndex], jpegQuality);
            IJ.showStatus("");
        } catch (IOException e) {
            IJ.error("AVI Writer", "An error occured writing the file.\n \n" + e);
        }
        IJ.showStatus("");
    }

    private boolean showDialog(ImagePlus imp) {
    	String options = Macro.getOptions();
    	if (options!=null) {
    		if (!options.contains("compression="))
    			options = "compression=JPEG "+options;
    		options = options.replace("compression=Uncompressed", "compression=None");
    		Macro.setOptions(options);
    	}
  		double fps = getFrameRate(imp);
 		int decimalPlaces = (int) fps == fps?0:1;
        GenericDialog gd = new GenericDialog("Save as AVI...");
        gd.addChoice("Compression:", COMPRESSION_STRINGS, COMPRESSION_STRINGS[compressionIndex]);
		gd.addNumericField("Frame Rate:", fps, decimalPlaces, 3, "fps");
        gd.showDialog();                            // user input (or reading from macro) happens here
        if (gd.wasCanceled())                       // dialog cancelled?
            return false;
        compressionIndex = gd.getNextChoiceIndex();
        fps = gd.getNextNumber();
        if (fps<=0.5) fps = 0.5;
		imp.getCalibration().fps = fps;
		return true;
    }

    /** Writes an ImagePlus (stack) as AVI file. */
    public void writeImage (ImagePlus imp, String path, int compression, int jpegQuality)
            throws IOException {
        if (compression!=NO_COMPRESSION && compression!=JPEG_COMPRESSION && compression!=PNG_COMPRESSION)
            throw new IllegalArgumentException("Unsupported Compression 0x"+Integer.toHexString(compression));
        this.biCompression = compression;
        if (jpegQuality < 0) jpegQuality = 0;
        if (jpegQuality > 100) jpegQuality = 100;
        this.jpegQuality = jpegQuality;
        File file = new File(path);
        raFile = new RandomAccessFile(file, "rw");
        raFile.setLength(0);
        imp.startTiming();

        //  G e t   s t a c k   p r o p e r t i e s
        boolean isComposite = imp.isComposite();
        boolean isHyperstack = imp.isHyperStack();
        boolean isOverlay = imp.getOverlay()!=null && !imp.getHideOverlay();
        xDim = imp.getWidth();   //image width
        yDim = imp.getHeight();   //image height
        zDim = imp.getStackSize(); //number of frames in video
		boolean saveFrames=false, saveSlices=false, saveChannels=false;
        int channels = imp.getNChannels();
		int slices = imp.getNSlices();
		int frames = imp.getNFrames();
		int channel = imp.getChannel();
		int slice = imp.getSlice();
		int frame = imp.getFrame();
		if (isHyperstack || isComposite) {
			if (frames>1) {
				saveFrames = true;
				zDim = frames;
			} else if (slices>1) {
				saveSlices = true;
				zDim = slices;
			} else if (channels>1) {
				saveChannels = true;
				zDim = channels;
			} else
				isHyperstack = false;
		}

        if (imp.getType()==ImagePlus.COLOR_RGB || isComposite || biCompression==JPEG_COMPRESSION || isOverlay)
            bytesPerPixel = 3;  //color and JPEG-compressed files
        else
            bytesPerPixel = 1;  //gray 8, 16, 32 bit and indexed color: all written as 8 bit
        boolean writeLUT = bytesPerPixel==1; // QuickTime reads the avi palette also for PNG
        linePad = 0;
        int minLineLength = bytesPerPixel*xDim;
        if (biCompression==NO_COMPRESSION && minLineLength%4!=0)
            linePad = 4 - minLineLength%4; //uncompressed lines written must be a multiple of 4 bytes
        frameDataSize = (bytesPerPixel*xDim+linePad)*yDim;
        int microSecPerFrame = (int)Math.round((1.0/getFrameRate(imp))*1.0e6);
        int dwChunkId = biCompression==NO_COMPRESSION ? FOURCC_00db : FOURCC_00dc;
        long sizeEstimate = bytesPerPixel*xDim*yDim*(long)zDim;
        //boolean writeAVI2index = true;//frameDataSize*zDim > 1000000000;
        int nAvixChunksEstimate = (int)(sizeEstimate/JUNK_SIZE_THRESHOLD);  //estimated number of AVIX junks
        endHeadPointer = 4096+((nAvixChunksEstimate*16+1000)/1024)*1024;    //reserve plenty of space for 'indx'

        //  W r i t e   A V I   f i l e   h e a d e r
        writeString("RIFF");    // signature
        chunkSizeHere();        // size of file (nesting level 0)
        writeString("AVI ");    // RIFF type
        writeString("LIST");    // first LIST chunk, which contains information on data decoding
        chunkSizeHere();        // size of LIST (nesting level 1)
        writeString("hdrl");    // LIST chunk type
        writeString("avih");    // Write the avih sub-CHUNK
        writeInt(0x38);         // length of the avih sub-CHUNK (38H) not including the
                                // the first 8 bytes for avihSignature and the length
        writeInt(microSecPerFrame); // dwMicroSecPerFrame - Write the microseconds per frame
        writeInt(0);            // dwMaxBytesPerSec (maximum data rate of the file in bytes per second)
        writeInt(0);            // dwPaddingGranularity (for header length?), previously dwReserved1, usually set to zero.
        writeInt(0x10);         // dwFlags - just set the bit for AVIF_HASINDEX
                                //   10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing
                                //   an index at the end of the file.  For good performance, all
                                //   AVI files should contain an index.
        writeInt(zDim);         // dwTotalFrames - total frame number
        writeInt(0);            // dwInitialFrames -Initial frame for interleaved files.
                                // Noninterleaved files should specify 0.
        writeInt(1);            // dwStreams - number of streams in the file - here 1 video and zero audio.
        writeInt(0);      // dwSuggestedBufferSize 
         writeInt(xDim);         // dwWidth - image width in pixels
        writeInt(yDim);         // dwHeight - image height in pixels
        writeInt(0);            // dwReserved[4]
        writeInt(0);
        writeInt(0);
        writeInt(0);

        //  W r i t e   s t r e a m   i n f o r m a t i o n
        writeString("LIST");    // List of stream headers
        chunkSizeHere();        // size of LIST (nesting level 2)
        writeString("strl");    // LIST chunk type: stream list
        writeString("strh");    // stream header 
        writeInt(56);           // Write the length of the strh sub-CHUNK
        writeString("vids");    // fccType - type of data stream - here 'vids' for video stream
        writeString("DIB ");    // 'DIB ' for Microsoft Device Independent Bitmap.
        writeInt(0);            // dwFlags
        writeInt(0);            // wPriority, wLanguage
        writeInt(0);            // dwInitialFrames
        writeInt(1);            // dwScale
        writeInt((int)Math.round(getFrameRate(imp))); //  dwRate - frame rate for video streams
        writeInt(0);            // dwStart - this field is usually set to zero
        writeInt(zDim);         // dwLength - playing time of AVI file as defined by scale and rate
                                // Set equal to the number of frames
        writeInt(0);            // dwSuggestedBufferSize for reading the stream.
                                // Typically, this contains a value corresponding to the largest chunk
                                // in a stream.
        writeInt(-1);           // dwQuality - encoding quality given by an integer between
                                // 0 and 10,000.  If set to -1, drivers use the default
                                // quality value.
        writeInt(0);            // dwSampleSize. 0 means that each frame is in its own chunk
        writeShort((short)0);   // left of rcFrame if stream has a different size than dwWidth*dwHeight(unused)
        writeShort((short)0);   // top
        writeShort((short)0);   // right
        writeShort((short)0);   // bottom
        // end of 'strh' chunk, stream format follows
        writeString("strf");    // stream format chunk
        chunkSizeHere();        // size of 'strf' chunk (nesting level 3)
        writeInt(40);           // biSize - Write header size of BITMAPINFO header structure
                                // Applications should use this size to determine which BITMAPINFO header structure is
                                // being used.  This size includes this biSize field.
        writeInt(xDim);         // biWidth - width in pixels
        writeInt(yDim);         // biHeight - image height in pixels. (May be negative for uncompressed
                                // video to indicate vertical flip).
        writeShort(1);          // biPlanes - number of color planes in which the data is stored
        writeShort((short)(8*bytesPerPixel)); // biBitCount - number of bits per pixel #
        writeInt(biCompression); // biCompression - type of compression used (uncompressed: NO_COMPRESSION=0)
        int biSizeImage =       // Image Buffer. Quicktime needs 3 bytes also for 8-bit png
                (biCompression==NO_COMPRESSION)?0:xDim*yDim*bytesPerPixel;
        writeInt(biSizeImage);  // biSizeImage (buffer size for decompressed mage) may be 0 for uncompressed data
        writeInt(0);            // biXPelsPerMeter - horizontal resolution in pixels per meter
        writeInt(0);            // biYPelsPerMeter - vertical resolution in pixels per meter
        writeInt(writeLUT ? 256:0); // biClrUsed (color table size; for 8-bit only)
        writeInt(0);            // biClrImportant - specifies that the first x colors of the color table
                                // are important to the DIB.  If the rest of the colors are not available,
                                // the image still retains its meaning in an acceptable manner.  When this
                                // field is set to zero, all the colors are important, or, rather, their
                                // relative importance has not been computed.
        if (writeLUT)           // write color lookup table
            writeLUT(imp.getProcessor());
        chunkEndWriteSize();    //'strf' chunk finished (nesting level 3)
        
        writeString("strn");    // Use 'strn' to provide a zero terminated text string describing the stream
        writeInt(16);           // length of the strn sub-CHUNK (must be even)
        writeString("ImageJ AVI     \0"); //must be 16 bytes as given above (including the terminating 0 byte)
        pointer2indx = raFile.getFilePointer();
        writeString("indx");    // 'indx' chunk type: Index of indices
        chunkSizeHere();        // size of 'indx' (nesting level 3)
        writeShort(4);          // wLongsPerEntry = 4 ('Longs' are 32-bit here!)
        writeByte(0);           // bIndexSubType=0
        writeByte(0);           // bIndexType=0: AVI_INDEX_OF_INDEXES
        pointer2indxNEntriesInUse = raFile.getFilePointer();
        writeInt(0);            // nEntriesInUse, will be filled in later
        writeInt(dwChunkId);    // dwChunkId, '00dc' or '00db'
        writeInt(0); writeInt(0); writeInt(0); // dwReserved[3]
        pointer2indxNextEntry = raFile.getFilePointer();
        chunkEndWriteSize();    //'indx' chunk finished (nesting level 3), will be modified by writeMainIndxEntry
        writeString("JUNK");    // write a JUNK chunk for padding (will be moved and shortened by writeMainIndxEntry)
        chunkSizeHere();        // size of 'JUNK' for padding (nesting level 3)
        raFile.seek(endHeadPointer);      // we continue here
        chunkEndWriteSize();    // 'JUNK' finished (nesting level 3)
        chunkEndWriteSize();    // LIST 'strl' finished (nesting level 2)
        chunkEndWriteSize();    // LIST 'hdrl' finished (nesting level 1)

        //  P r e p a r e   f o r   w r i t i n g   d a t a
        if (biCompression == NO_COMPRESSION)
            bufferWrite = new byte[frameDataSize];
        else
            raOutputStream = new RaOutputStream(raFile); //needed for writing compressed formats
        //int maxChunkLength = 0;                 // needed for dwSuggestedBufferSize
        int[] dataChunkOffset = new int[zDim];  // remember chunk positions...
        int[] dataChunkLength = new int[zDim];  // ... and sizes for the index

        int currentFilePart = 0;// 0 is inside RIFF AVI (AVI 1.0 compatible), >0 is RIFF AVIX (data chunk of AVI 2.0)

        //  W r i t e   f r a m e   d a t a   a n d   i n d i c e s
        boolean writeAVI2index = false; // see whether we need an AVI2 index (large files only)
        int iFrame = 0;
        while (iFrame < zDim) {
            if (currentFilePart > 0) {  // open new RIFF AVIX chunk
                writeString("RIFF");
                chunkSizeHere();        // size of chunk (nesting level 0)
                writeString("AVIX");    // RIFF type
                //IJ.log("AVIX starts at iFrame="+iFrame);
            }
            writeString("LIST");        // this LIST chunk contains the AVI-2 style index and the actual data
            chunkSizeHere();            // size of LIST (nesting level 1)
            long moviPointer = raFile.getFilePointer();
            writeString("movi");        // write LIST type 'movi'

            int firstFrameInChunk = iFrame;

            //   W r i t e   s i n g l e   f r a m e
            while (iFrame<zDim) {
                if (iFrame %10==0) {
                    IJ.showProgress(iFrame, zDim);
                    IJ.showStatus(iFrame+"/"+zDim);
                }
                ImageProcessor ip = null;      // get the image to write ...
                if (isComposite || isHyperstack || isOverlay) {
                    if (saveFrames)
                        imp.setPositionWithoutUpdate(channel, slice, iFrame+1);
                    else if (saveSlices)
                        imp.setPositionWithoutUpdate(channel, iFrame+1, frame);
                    else if (saveChannels)
                        imp.setPositionWithoutUpdate(iFrame+1, slice, frame);
                    ImagePlus imp2 = imp;
                    if (isOverlay) {
                        if (!(saveFrames||saveSlices||saveChannels))
                            imp.setSliceWithoutUpdate(iFrame+1);
                        imp2 = imp.flatten();
                    }
                    ip = new ColorProcessor(imp2.getImage());
                } else
                    ip = zDim==1 ? imp.getProcessor() : imp.getStack().getProcessor(iFrame+1);
                int chunkPointer = (int)raFile.getFilePointer();
                writeInt(dwChunkId);            // start writing chunk: '00db' or '00dc'
                chunkSizeHere();                // size of '00db' or '00dc' chunk (nesting level 2)
                if (biCompression == NO_COMPRESSION) {
                    if (bytesPerPixel==1)
                        writeByteFrame(ip);
                    else
                        writeRGBFrame(ip);
                } else
                    writeCompressedFrame(ip);
                dataChunkOffset[iFrame] = (int)(chunkPointer - moviPointer);
                dataChunkLength[iFrame] = (int)(raFile.getFilePointer() - chunkPointer - 8); //size excludes '00db' and size fields
                chunkEndWriteSize();            // '00db' or '00dc' chunk finished (nesting level 2)
                //if (IJ.escapePressed()) {
                //    IJ.showStatus("Save as Avi INTERRUPTED");
                //    break;
                //}
                iFrame++;
                if (raFile.getFilePointer() - moviPointer > JUNK_SIZE_THRESHOLD)
                    break;                      // make sure we don't get over 1GB
            } // while (iFrame<zDim)
            int nFramesInChunk = iFrame - firstFrameInChunk;

            //  W r i t e   A V I - 2   I n d e x
            if (iFrame < zDim)
                writeAVI2index = true;      //can't write everything the first time? Then we need the AVI 2 format.
            if (writeAVI2index) {
                long ix00pointer = raFile.getFilePointer();
                writeString("ix00");        // AVI 2.0 style index of frames within the chunk
                chunkSizeHere();            // size of ix00 chunk (nesting level 2)
                writeShort(2);              // wLongsPerEntry = 2 ('Longs' are 32-bit here!)
                writeByte(0);               // bIndexSubType=0
                writeByte(1);               // bIndexType=1: AVI_INDEX_OF_CHUNKS
                writeInt(nFramesInChunk);   // nEntriesInUse
                writeInt(dwChunkId);        // dwChunkId, '00dc' or '00db'
                writeLong(moviPointer);     // qwBaseOffset
                writeInt(0);                // dwReserved, first two are qwBaseOffset?
                for (int z=firstFrameInChunk; z<iFrame; z++) {
                    writeInt(dataChunkOffset[z]+8); //note: AVI--2 index points to chunk data, not chunk header
                    writeInt(dataChunkLength[z]);   //length without chunk header
                }
                //IJ.log("write ix00: frames "+firstFrameInChunk+"-"+(iFrame-1)+" offset "+Long.toHexString(dataChunkOffset[firstFrameInChunk])+"-"+Long.toHexString(dataChunkOffset[iFrame-1]));
                //enter this ix00 index to index of indices:
                writeMainIndxEntry(ix00pointer, (int)(raFile.getFilePointer()-ix00pointer), nFramesInChunk);

                chunkEndWriteSize();        // 'ix00' finished (nesting level 2)
            }
            chunkEndWriteSize();        // LIST 'movi' finished (nesting level 1)

            //  W r i t e   A V I - 1   I n d e x
            if (currentFilePart == 0) {
                writeString("idx1");    // Write the idx1 chunk
                chunkSizeHere();        // size of 'idx1' chunk (nesting level 1)
                for (int z = 0; z < iFrame; z++) {
                    writeInt(dwChunkId);// ckid field: '00db' or '00dc'
                    writeInt(0x10);     // flags: select AVIIF_KEYFRAME
                                 // AVIIF_KEYFRAME 0x00000010
                                 // The flag indicates key frames in the video sequence.
                                 // Key frames do not need previous video information to be decompressed.
                                 // AVIIF_NOTIME 0x00000100 The CHUNK does not influence video timing (for
                                 //   example a palette change CHUNK).
                                 // AVIIF_LIST 0x00000001 marks a LIST CHUNK.
                                 // AVIIF_TWOCC 2L
                                 // AVIIF_COMPUSE 0x0FFF0000 These bits are for compressor use.
                     writeInt(dataChunkOffset[z]); // offset to the chunk header (not data)
                                 // offset can be relative to file start or 'movi'
                     writeInt(dataChunkLength[z]); // length without chunk header
                }  // for (z = 0; z < zDim; z++)
                chunkEndWriteSize();    // 'idx1' finished (nesting level 1)
            }
            chunkEndWriteSize();    // 'RIFF' File finished (nesting level 0)
            currentFilePart++;
        } //while (iFrame < zDim)

        if (!writeAVI2index) {      //delete main AVI 2 index prepared previously
            raFile.seek(pointer2indx);
            writeString("JUNK");        // overwrite 'indx'
            chunkSizeHere();            // size of 'JUNK' for padding goes here
            raFile.seek(endHeadPointer);// end of the padded range
            chunkEndWriteSize();        // 'JUNK' finished              
        }

        raFile.close();
        IJ.showProgress(1.0);
		if (isComposite || isHyperstack)
			imp.setPosition(channel, slice, frame);
    }

    /** Reserve space to write the size of chunk and remember the position
     *  for a later call to chunkEndWriteSize().
     *  Several levels of chunkSizeHere() and chunkEndWriteSize() may be nested.
     */
    private void chunkSizeHere() throws IOException {
        sizePointers[stackPointer] = raFile.getFilePointer();
        writeInt(0);    //for now, write 0 to reserve space for "size" item
        stackPointer++;
    }
    
    /** At the end of a chunk, calculate its size and write it to the
     *  position remembered previously. Also pads to 2-byte boundaries.
     */
    private void chunkEndWriteSize() throws IOException {
        stackPointer--;
        long position = raFile.getFilePointer();
        raFile.seek(sizePointers[stackPointer]);
        writeInt((int)(position - (sizePointers[stackPointer]+4)));
        raFile.seek(((position+1)/2)*2);    //pad to 2-byte boundary
        //IJ.log("chunk at 0x"+Long.toHexString(sizePointers[stackPointer]-4)+"-0x"+Long.toHexString(position));
    }

    /** Enter a local index 'ix00' to 'indx', the index of indices */
    private void writeMainIndxEntry(long ix00pointer, int dwSize, int nFrames) throws IOException {
        if (pointer2indxNextEntry + 16 + 8 > MAX_INDX_SIZE) {
            raFile.close();
            throw new RuntimeException("AVI_Writer ERROR: Index Size Overflow");
        }
        long savePosition = raFile.getFilePointer();
        raFile.seek(pointer2indxNextEntry);
        writeLong(ix00pointer);
        writeInt(dwSize);
        writeInt(nFrames);
        pointer2indxNextEntry += 16;
        nIndxEntries++;
        writeString("JUNK");        // write a JUNK chunk for padding
        chunkSizeHere();            // size of 'JUNK' for padding goes here
        raFile.seek(endHeadPointer);// end of the padded range
        chunkEndWriteSize();        // 'JUNK' finished (nesting level 3)
        raFile.seek(pointer2indx+4);
        writeInt((int)(pointer2indxNextEntry - pointer2indx - 8)); //write new size of 'indx'
        raFile.seek(pointer2indxNEntriesInUse);
        writeInt(nIndxEntries);     //write new number of 'indx' entries
        raFile.seek(savePosition);
    }

    /** Write Grayscale (or indexed color) data. Lines are  
     *  padded to a length that is a multiple of 4 bytes. */
    private void writeByteFrame(ImageProcessor ip) throws IOException {
        ip = ip.convertToByte(true);
        byte[] pixels = (byte[])ip.getPixels();
        int width = ip.getWidth();
        int height = ip.getHeight();
        int c, offset, index = 0;
        for (int y=height-1; y>=0; y--) {
            offset = y*width;
            for (int x=0; x<width; x++)
                bufferWrite[index++] = pixels[offset++];
            for (int i = 0; i<linePad; i++)
                bufferWrite[index++] = (byte)0;
        }
        raFile.write(bufferWrite);
    }

    /** Write RGB data. Each 3-byte triplet in the bitmap array represents
     *  blue, green, and red, respectively, for a pixel.  The color bytes are
     *  in reverse order (Windows convention). Lines are padded to a length
     *  that is a multiple of 4 bytes. */
    private void writeRGBFrame(ImageProcessor ip) throws IOException {
        ip = ip.convertToRGB();
        int[] pixels = (int[])ip.getPixels();
        int width = ip.getWidth();
        int height = ip.getHeight();
        int c, offset, index = 0;
        for (int y=height-1; y>=0; y--) {
            offset = y*width;
            for (int x=0; x<width; x++) {
                c = pixels[offset++];
                bufferWrite[index++] = (byte)(c&0xff); // blue
                bufferWrite[index++] = (byte)((c&0xff00)>>8); //green
                bufferWrite[index++] = (byte)((c&0xff0000)>>16); // red
            }
            for (int i = 0; i<linePad; i++)
                bufferWrite[index++] = (byte)0;
        }
        raFile.write(bufferWrite);
    }

    /** Write a frame as jpeg- or png-compressed image */
	private void writeCompressedFrame(ImageProcessor ip) throws IOException {
		//IJ.log("BufferdImage Type="+bufferedImage.getType()); // 1=RGB, 13=indexed
		if (biCompression==JPEG_COMPRESSION) {
			BufferedImage bi = getBufferedImage(ip);
			ImageIO.write(bi, "jpeg", raOutputStream);
		} else { //if (biCompression==PNG_COMPRESSION) {
			BufferedImage bi = ip.getBufferedImage();
			ImageIO.write(bi, "png", raOutputStream);
		}
	}

	private BufferedImage getBufferedImage(ImageProcessor ip) {
		BufferedImage bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_INT_RGB);
		Graphics2D g = (Graphics2D)bi.getGraphics();
		g.drawImage(ip.createImage(), 0, 0, null);
		return bi;
	}

    /** Write the color table entries (for 8 bit grayscale or indexed color).
     *  Byte order or LUT entries: blue byte, green byte, red byte, 0 byte */
    private void writeLUT(ImageProcessor ip) throws IOException {
        IndexColorModel cm = (IndexColorModel)(ip.getCurrentColorModel());
        int mapSize = cm.getMapSize();
        byte[] lutWrite = new byte[4*256];
        for (int i = 0; i<256; i++) {
            if (i<mapSize) {
                lutWrite[4*i] = (byte)cm.getBlue(i);
                lutWrite[4*i+1] = (byte)cm.getGreen(i);
                lutWrite[4*i+2] = (byte)cm.getRed(i);
                lutWrite[4*i+3] = (byte)0;
            }
        }
        raFile.write(lutWrite);
    }

    private double getFrameRate(ImagePlus imp) {
        double rate = imp.getCalibration().fps;
        if (rate==0.0)
            rate = Animator.getFrameRate();
        if (rate<=0.5) rate = 0.5;
        //if (rate>60.0) rate = 60.0;
        return rate;
    }

    private void writeString(String s) throws IOException {
        byte[] bytes =  s.getBytes("UTF8");
        raFile.write(bytes);
    }

    /** Write 8-byte int with Intel (little-endian) byte order
     * (note: RandomAccessFile.writeInt has other byte order than AVI) */
    private void writeLong(long v) throws IOException {
        for (int i=0; i<8; i++) {
            raFile.write((int)(v & 0xFFL));
            v = v>>>8;
        }
        //IJ.log("long: 0x"+Long.toHexString(v)+"="+v);
    }

    /** Write 4-byte int with Intel (little-endian) byte order
     * (note: RandomAccessFile.writeInt has other byte order than AVI) */
    private void writeInt(int v) throws IOException {
        raFile.write(v & 0xFF);
        raFile.write((v >>>  8) & 0xFF);
        raFile.write((v >>> 16) & 0xFF);
        raFile.write((v >>> 24) & 0xFF);
        //IJ.log("int: 0x"+Integer.toHexString(v)+"="+v);
    }

    /** Write 2-byte short with Intel (little-endian) byte order
     * (note: RandomAccessFile.writeShort has other byte order than AVI) */
    private void writeShort(int v) throws IOException {
        raFile.write(v & 0xFF);
        raFile.write((v >>> 8) & 0xFF);
    }

    /** Write a byte */
    private void writeByte(int v) throws IOException {
        raFile.write(v & 0xFF);
    }

    /** An output stream directed to a RandomAccessFile (starting at the current position) */
    class RaOutputStream extends OutputStream {
        RandomAccessFile raFile;
        RaOutputStream (RandomAccessFile raFile) {
            this.raFile = raFile;
        }
        public void write (int b) throws IOException {
            //IJ.log("stream: byte");
            raFile.writeByte(b); //just for completeness, usually not used by image encoders
        }
        public void write (byte[] b) throws IOException {
            //IJ.log("stream: array len="+b.length);
            raFile.write(b);
        }
        public void write (byte[] b, int off, int len) throws IOException {
            //IJ.log("stream: array="+b.length+" off="+off+" len="+len);
            raFile.write(b, off, len);
        }
    }

}