Custom .TIFF writer

Hi everybody,

NOTE: This code was written to be applied in an LWJGL game and as a result, offers very “specific” functionality. Nonetheless I believe it may have other uses as well. I just hope it’s going to help some of you…

Some time ago I implemented a screenshot capture method. I used ImageIO to do the screenshot writting. Of course it wasn’t good enough for my needs, as it added both memory overhead (more classes loaded, obligatory use of BufferedImage :P, etc.) and a lot of CPU overhead (in my Athlon 700MHz it took 1.5-2 seconds per capture). It did it’s job, but I had to use another method to go at least under 1 sec / capture.

I wanted to have a fast solution (1-3 captures per second), and export a cross-platform loss-less image format. From what ImageIO offers (NOTE: I used the new ImageIO extension, that adds more formats), the only formats that satisfy the cross-platform / loss-less part are .PNG and .TIFF (maybe .BMP too? I have never touched a Mac…). I tried both, using ImageIO and although the new extension adds some native code for read/write acceleration, the results were unsatisfactory when it came to speed. I even tried some other formats, with no luck :frowning:

Finally I decided to implement my own writer. I chose .TIFF, as it is the “most” cross-platform of all the others, it supports uncompressed data (no need for time consuming compression algos), and is easy to write ;).

So, here it is, for you to use and enjoy! Overall, I got a ten-fold(!) speed increase, grabbing ~5 screenshots / second. It’s not a complete .TIFF writer, you can just write 3 bytes/pixel RGB images. If you want other types, feel free to extend it’s functionality ::).


/** Screenshot count */
private static int lastCapture = 1;

/** A buffer that holds the inversed screenshot image */
private static byte[] captureData = new byte[Display.WIDTH * Display.HEIGHT * 3];

public static void capture() throws IOException {
      // Get a temp ByteBuffer
      final ByteDataBuffer captureDataBuffer = Memory.getByteDataBuffer(captureData.length);

      // Read Frame Buffer
      Scene.gl.readPixels(0, 0, Display.WIDTH, Display.HEIGHT, GL.RGB, GL.UNSIGNED_BYTE, captureDataBuffer.address);

      int bytesPerRow = Display.WIDTH * 3;
      int lastRow = Display.HEIGHT - 1;

      // Inverse captured image
      ByteBuffer captureBuffer = captureDataBuffer.data;
      for ( int i = 0; i < Display.HEIGHT; i++ )
            captureBuffer.get(captureData, ( lastRow - i ) * bytesPerRow, bytesPerRow);

      // Write screenshot
      TIFFWriter.writeImage(Display.WIDTH, Display.HEIGHT, captureData, new File("screenshot" + lastCapture++ + ".tiff"));

      // Release temp ByteBuffer
      Memory.releaseDataBuffer(captureDataBuffer);
}

Memory.getByteDataBuffer() is just a method that grabs a ByteBuffer from another huge ByteBuffer. Saves a lot of overhead ( Cas, thanks for your precious tips ;-). Always helpful. ). Memory.releaseDataBuffer() releases the allocated memory. I strongly suggest you go implement sth like this (for those who haven’t yet). Otherwise, all you have to do is provide a direct ByteBuffer to put the pixel data from the Frame Buffer.

After grabbing the pixels, the image rows get inverted (0,0 coordinate bottom-left :o? What were they thinking?? Just kidding, I’ve gotten used to it…) and then we give the data to the TIFFWriter.

NOTE: I’ve used Display.WIDTH and Display.HEIGHT. This is the screen resolution. Modify according to your design.

Look at the next post for TIFFWriter implementation and comments…


import java.nio.*;
import java.io.*;
import java.util.Calendar;

/**
 * TIFFWriter implements .TIFF image writting.
 * The only image type it supports is uncompressed RGB.
 * 
 * @author Tsakpinis Ioannis <b>spasi@hiphop.gr</b>
 */
public final class TIFFWriter {

      // ------------------- TIFF CONSTANTS -------------------
      //private final static short BYTE = 1;
      private final static short ASCII = 2;
      private final static short SHORT = 3;
      private final static short LONG = 4;
      private final static short RATIONAL = 5;

      private final static short HEADER_TIFF_MAGIC_NUMBER = 42;

      // ------------------- HEADER -------------------
      private final static byte[] HEADER_BYTEORDER = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? new byte[]{'I', 'I'} : new byte[]{'M', 'M'};
      private final static int HEADER_FIRST_IFD_OFFSET = 8;

      // ------------------- IMAGE FILE DIRECTORY -------------------

      private final static short IFD_ENTRY_COUNT = 16;

      // IFD ENTRIES FOR FULL-RGB IMAGES
      private final static short IFD_NEW_SUBFILE_TYPE = 254;
      private final static short IFD_IMAGE_WIDTH = 256;
      private final static short IFD_IMAGE_LENGTH = 257;
      private final static short IFD_BITS_PER_SAMPLE = 258;
      private final static short IFD_COMPRESSION = 259;
      private final static short IFD_PHOTOMETRIC_INTERPOLATION = 262;
      private final static short IFD_STRIP_OFFSETS = 273;
      private final static short IFD_SAMPLES_PER_PIXEL = 277;
      private final static short IFD_ROWS_PER_STRIP = 278;
      private final static short IFD_STRIP_BYTE_COUNTS = 279;
      private final static short IFD_X_RESOLUTION = 282;
      private final static short IFD_Y_RESOLUTION = 283;
      private final static short IFD_RESOLUTION_UNIT = 296;

      // OPTIONAL IFD ENTRIES
      private final static short IFD_SOFTWARE = 305;
      private final static short IFD_DATE_TIME = 306;
      private final static short IFD_IMAGE_DESCRIPTION = 270;

      private final static byte[] SOFTWARE = "Put your software name here".getBytes();
      private final static byte[] IMAGE_DESCRIPTION = "Put an image description here".getBytes();

      private final static int HEADER_SIZE = ( 2 * 2 ) + ( 4 * 1 );
      private final static int IFD_SIZE = 2 + 12 * IFD_ENTRY_COUNT + 4;
      private final static int LONG_VARIABLES_SIZE = 6 + 8 + 8 + 20 + SOFTWARE.length + 1 + IMAGE_DESCRIPTION.length + 1;

      // IFD OFFSETS
      private final static int BITS_PER_SAMPLE_OFFSET = 206;
      private final static int STRIPS_OFFSETS = 248 + SOFTWARE.length + 1 + IMAGE_DESCRIPTION.length + 1;
      private final static int X_RESOLUTION_OFFSET = 212;
      private final static int Y_RESOLUTION_OFFSET = 220;
      private final static int SOFTWARE_OFFSET = 248;
      private final static int IMAGE_DESCRIPTION_OFFSET = 248 + SOFTWARE.length + 1;

      // IFD VARIABLES' OFFSETS
      private final static int IMAGE_WIDTH_OFFSET = 30;
      private final static int IMAGE_HEIGHT_OFFSET = 42;
      private final static int ROWS_PER_STRIP_OFFSET = 114;
      private final static int STRIP_BYTE_COUNTS_OFFSET = 126;
      private final static int DATE_TIME_OFFSET = 228;

      /** This ByteBuffer will hold the TIFF header, the IFD and some of the other metadata (>4 bytes values) */
      private final static ByteBuffer metadata = ByteBuffer.allocate(HEADER_SIZE + IFD_SIZE + LONG_VARIABLES_SIZE).order(ByteOrder.nativeOrder());

      /** A Calendar instance that gets update on every capture */
      private final static Calendar calendar = Calendar.getInstance();

      /** A buffer to hold the formatted current date & time */
      private final static byte[] dateTime = new byte[20];

      static {
            // INITIALISE METADATA BUFFER
            metadata.put(HEADER_BYTEORDER);
            metadata.putShort(HEADER_TIFF_MAGIC_NUMBER);
            metadata.putInt(HEADER_FIRST_IFD_OFFSET);

            metadata.putShort(IFD_ENTRY_COUNT);

            metadata.putShort(IFD_NEW_SUBFILE_TYPE);
            metadata.putShort(LONG);
            metadata.putInt(1);
            metadata.putInt(0);

            metadata.putShort(IFD_IMAGE_WIDTH);
            metadata.putShort(LONG);
            metadata.putInt(1);
            metadata.putInt(0); // PUT IMAGE WIDTH HERE

            metadata.putShort(IFD_IMAGE_LENGTH);
            metadata.putShort(LONG);
            metadata.putInt(1);
            metadata.putInt(0); // PUT IMAGE LENGTH HERE

            metadata.putShort(IFD_BITS_PER_SAMPLE);
            metadata.putShort(SHORT);
            metadata.putInt(3);
            metadata.putInt(BITS_PER_SAMPLE_OFFSET);

            metadata.putShort(IFD_COMPRESSION);
            metadata.putShort(SHORT);
            metadata.putInt(1);
            metadata.putInt(1);

            metadata.putShort(IFD_PHOTOMETRIC_INTERPOLATION);
            metadata.putShort(SHORT);
            metadata.putInt(1);
            metadata.putInt(2);

            metadata.putShort(IFD_STRIP_OFFSETS);
            metadata.putShort(LONG);
            metadata.putInt(1);
            metadata.putInt(STRIPS_OFFSETS);

            metadata.putShort(IFD_SAMPLES_PER_PIXEL);
            metadata.putShort(SHORT);
            metadata.putInt(1);
            metadata.putInt(3);

            metadata.putShort(IFD_ROWS_PER_STRIP);
            metadata.putShort(SHORT);
            metadata.putInt(1);
            metadata.putInt(0); // PUT ROWS PER STRIP HERE

            metadata.putShort(IFD_STRIP_BYTE_COUNTS);
            metadata.putShort(LONG);
            metadata.putInt(1);
            metadata.putInt(0); // PUT STRIP BYTE COUNTS HERE

            metadata.putShort(IFD_RESOLUTION_UNIT);
            metadata.putShort(SHORT);
            metadata.putInt(1);
            metadata.putInt(1);

            metadata.putShort(IFD_X_RESOLUTION);
            metadata.putShort(RATIONAL);
            metadata.putInt(1);
            metadata.putInt(X_RESOLUTION_OFFSET);

            metadata.putShort(IFD_Y_RESOLUTION);
            metadata.putShort(RATIONAL);
            metadata.putInt(1);
            metadata.putInt(Y_RESOLUTION_OFFSET);

            metadata.putShort(IFD_DATE_TIME);
            metadata.putShort(ASCII);
            metadata.putInt(20);
            metadata.putInt(DATE_TIME_OFFSET);

            metadata.putShort(IFD_SOFTWARE);
            metadata.putShort(ASCII);
            metadata.putInt(SOFTWARE.length + 1);
            metadata.putInt(SOFTWARE_OFFSET);

            metadata.putShort(IFD_IMAGE_DESCRIPTION);
            metadata.putShort(ASCII);
            metadata.putInt(IMAGE_DESCRIPTION.length + 1);
            metadata.putInt(IMAGE_DESCRIPTION_OFFSET);

            // We have only one IFD
            metadata.putInt(0);

            // Bits per Sample
            metadata.putShort((short)8).putShort((short)8).putShort((short)8);

            // X,Y Resolution
            metadata.putLong(1).putLong(1);

            // Skip DATE-TIME
            metadata.position(metadata.position() + 20);

            metadata.put(SOFTWARE);
            metadata.put((byte)0);
            metadata.put(IMAGE_DESCRIPTION);
            metadata.put((byte)0);
      }

      private TIFFWriter() {
      }

      /**
       * Writes an image to a TIFF image format file.
       * @param w the image width
       * @param h the image height
       * @param pixels the image pixel data
       * @param output the file to write the image to
       * @throws IOException If an IO error occurs
       */
      public final static void writeImage(int w, int h, byte[] pixels, File output) throws IOException {
            FileOutputStream fos = new FileOutputStream(output);

            metadata.putInt(IMAGE_WIDTH_OFFSET, w);
            metadata.putInt(IMAGE_HEIGHT_OFFSET, h);
            metadata.putInt(ROWS_PER_STRIP_OFFSET, h);
            metadata.putInt(STRIP_BYTE_COUNTS_OFFSET, w * h * 3);

            updateTime();
            metadata.position(DATE_TIME_OFFSET);
            metadata.put(dateTime);

            fos.write(metadata.array());
            fos.write(pixels);

            fos.close();
      }

      /**
       * Updates the calendar and puts the current
       * date and time to a buffer, specially formatted
       * to the TIFF Date & Time format.
       * Example: "2003:04:02 18:30:15\0"
       */
      private final static void updateTime() {
            // Reset current date and time
            for ( int i = 0; i < dateTime.length; i++ )
                  dateTime[i] = 0;

            calendar.setTimeInMillis(System.currentTimeMillis());

            int value = calendar.get(Calendar.YEAR);
            dateTime[0] = (byte)( '0' + ( value / 1000 ) );
            dateTime[1] = (byte)( '0' + ( ( value % 1000 ) / 100 ) );
            dateTime[2] = (byte)( '0' + ( ( value % 100 ) / 10 ) );
            dateTime[3] = (byte)( '0' + ( value % 10 ) );

            dateTime[4] = ':';

            value = calendar.get(Calendar.MONTH);
            dateTime[5] = (byte)( '0' + ( value / 10 ) );
            dateTime[6] = (byte)( '0' + ( value % 10 ) );

            dateTime[7] = ':';

            value = calendar.get(Calendar.DAY_OF_MONTH);
            dateTime[8] = (byte)( '0' + ( value / 10 ) );
            dateTime[9] = (byte)( '0' + ( value % 10 ) );

            dateTime[10] = ' ';

            value = calendar.get(Calendar.HOUR_OF_DAY);
            dateTime[11] = (byte)( '0' + ( value / 10 ) );
            dateTime[12] = (byte)( '0' + ( value % 10 ) );

            dateTime[13] = ':';

            value = calendar.get(Calendar.MINUTE);
            dateTime[14] = (byte)( '0' + ( value / 10 ) );
            dateTime[15] = (byte)( '0' + ( value % 10 ) );

            dateTime[16] = ':';

            value = calendar.get(Calendar.MINUTE);
            dateTime[17] = (byte)( '0' + ( value / 10 ) );
            dateTime[18] = (byte)( '0' + ( value % 10 ) );
      }
}

This code writes the .TIFF image according to the specification. It puts 3 more entries to the image meta-data, although not necessary at all, just for fun. These are SOFTWARE, which is the name of the program used to write the image, IMAGE DESCRIPTION, which is a description of the image content and DATE_TIME, which is the date and time of the image writting. You can put anything you like as SOFTWARE and IMAGE DESCRIPTION. The updateTime method gets the current date and time from a Calendar and formats the result as the .TIFF specification dictates (don’t modify). If anyone wants to modify this code, I strongly urge him/her to download the spec, as I’ve hard-coded many byte offsets that will mess up if something gets changed. As I said in my previous post, this is a very “specific” implementation…

Please let me know if it was of any use to you. And I’d appreciate any optimization suggestions…