Extending JIMI File Format Support


Contents

  • Overview
  • A note about examples
  • Structure of a JIMI Format Module
  • Decoders
  • JimiDecoderFactories
  • Encoders
  • JimiEncoderFactories
  • JimiExtensions
  • Multi-image formats
  • Tips for writing good format modules
  • Categories of Image
  • Overview

    JIMI is designed to be easily extensible to provide potential support for any file format through custom format modules.  The extensions benefit automatically from all JIMI's features, including One-Shot production, Virtual Memory Management, and rich image manipulation.  Extended format modules are treated in the same way as JIMI's core format set to ensure seamless integration.  This document describes how to write encoders and decoders, and how to integrate them with JIMI.

    A note about examples

    The image file format used for the example format module code in this document is a ficticious file format created as an example, the Activated Pseudo-Format (APF).  This is a very simple format for storing 32-bit alpha/red/green/blue images.  The structure is as follows:

    header:
        signature        ascii string "APF"
        width            big-endian 32-bit integer value indicating the image width
        height           big-endian 32-bit integer value indicating the image height

    pixel data:
        series of (width * height) big-endian 32-bit values, each containing four 8-bit channels of image data, ordered alpha, red, green, blue.

    Structure of a JIMI Format Module

    An extended format module for JIMI consists of:
  • Any number of format decoders
  • Any number of format encoders
  • Factory classes for each encoder and decoder
  • A JimiExtension class for describing the module, and using it to register your module with JIMI
  • In the following sections we step through each phase of developing a new Format Module.

    Writing a decoder

    New format decoders for JIMI usually inherit from JimiSingleImageRasterDecoder, an abstract base class which leaves two methods to be implemented by the subclass for a specific format:
  • MutableJimiRasterImage doInitDecoding(JimiImageFactory, InputStream)

  • This method is called to  have the decoder read and interpret image headers to set its options and create an appropriate type of JimiImage.  This method exits as soon as the decoder is ready to start decoding pixel data.
  • void doImageDecode()

  • This method, which is called after doInitDecoding, decodes pixel data and sets it in the previously created JimiImage.  When this method completes, the image is fully decoded and the decoding process has finished.
    These are the only methods the decoder has to implement.

    Writing your doInitDecoding method
    The doInitDecoding method has the following signature:

        public MutableJimiRasterImage doInitDecoding(JimiImageFactory factory, InputStream in)
            throws JimiException, IOException

    There are several steps involved in implementing this method.  Firstly, headers for the image are parsed to determine the width, height, and type of ColorModel required for the image.  How this is done depends on the format, but these are properties of all raster-based images and are the only information required.  Optionally, the decoder may also read additional information from the headers for parameters specific to the image format. These parameters typically include things like compression-type, whether the image was interlaced, comments embedded in the image, or anything else supported by the specific file format. The primary purpose of preserving this information with the image is so that the same options can be automatically re-used if the image is later encoded. If you choose to support format-specific options with your decoder, you must write a class implementing com.sun.jimi.core.util.FormatOptionSet, which provides objects representing the options available. By doing this, your options can be inspected in a standard way, allowing a user to interactively modify them when images are being saved.  FormatOptionSet objects can be associated with an image by calling the setOptions method.

    When these are finished, all the required information about the image has been gathered and it's time to create an object in which to store the pixel data in. JIMI provides 5 different types of these objects, each of which is an implementation of MutableJimiRasterImage. The type to choose depends on what sort of image you are decoding.  The following is a guide to which type you should choose for your decoder:

  • IntRasterImage

  • This type stores pixel values as ints, which provides 32 bits of data for storing each pixel.  This is typically used for RGB-based image data where 8 bits are assigned to each color channel.  It is appropriate to use this type for any image which requires 32 bit pixel values.
  • ByteRasterImage

  • This type stores pixel values as bytes for images which only require 8 bits for each pixel.  This is typically used in conjunction with an IndexColorModel for palette based images with 256 colors or less.  It is appropriate to use this type for any image which requires 8 bit pixel values.
  • BitRasterImage

  • This type stores pixel values each in a single bit for reduced memory consumption.  This is typically used for black and white images, but can be used for any type that requires only 2 different colors.
  • ChanneledIntRasterImage

  • This is an extension of IntRasterImage which allows image data to be set individually for each byte-channel of the image. This means that, for example, in several passes the alpha, red, green and blue channels of an image could be individually set for formats which store their data one channel at a time. Although this provides all the functionality of an IntRasterImage, it should only be used when its additional functionality is required because IntRasterImage can be more efficient.
  • LongRasterImage

  • This type uses 64-bit longs to store individual pixels. This is useful for formats which have large color channels which prevent a pixel from being represented by a 32 bit value.  These long-based images require use of special ColorModels derived from LongColorModel.
    By checking your needs against this list, you can determine which type of JimiRasterImage your decoder should use.  Having done this, the desired object can be created using one of the create<Type>() methods of the JimiImageFactory argument. The object created should then be referenced with an instance field to be accessed again by doImageDecode for setting pixel data.

    Finally, you should set the "hints" of the image object you created using the setHints method, which it inherits from MutableRasterImage.  The values of the hints are exactly the same as the ones in java.awt.image.ImageConsumer, and by default TOPDOWNLEFTRIGHT | COMPLETESCANLINES | SINGLEFRAME | SINGLEPASS will be selected.  If you are going to set data in a different order, you must set more appropriate hints.  When all this is complete, you simply return a reference to the image object you created and doInitDecoding is finished.

    Here is an example decoder.

    Writing your doImageDecode method
    This method is very straight forward. You simply read data for the image and incrementally set it in the image object created in doInitDecoding. Image data should be set in conveniently sized chunks. There is no specific required chunk size, though it is conventional to buffer one row at a time and set the entire row of data. If the format does not deal in rows, any more convenient chunk size can be used.

    As data is set, setProgress (a method inherited from the JimiSingleImageRasterDecoder) should be called to inform any interested objects where the operation is up to.  This method takes one int as its argument, which should be set to the percentage of the operation which is complete, a number between 0 and 100 inclusive.

    After all data has been read and set in the image object, the setFinished() method should be called on the image.  When this is done, the decoding process is complete and the method can return.

    Writing the JimiDecoderFactory

    With a decoder implemented, the next thing to do is create a class implementing the JimiDecoderFactory interface which JIMI can use to access the decoder.  The purpose of the JimiDecoderFactory is to provide information about the image format to allow JIMI to determine when it should be used, and to create instances of the decoder.  Typically, your Factory will be a subclass of JimiDecoderFactorySupport, which provides default implemention of some methods of the interface.  The information you need about your format is the mime-type(s) with which it is identified, any filename-extensions to associate with it, and, if available, a signature which appears near the start of the image data and identifies the format.

    These classes are very easy to implement, as this example illustrates.

    Writing an encoder

    Writing an encoder with JIMI is even easier than writing a decoder. JIMI encoders inherit from the abstract base class JimiSingleImageRasterEncoder, which has just one method to implement: doEncodeImage. The function of this method is to take a JimiRasterImage and write it to an OutputStream. The exact signature is:

        public void doImageEncode(JimiRasterImage image, OutputStream out)
            throws JimiException, IOException

    Though formats can vary greatly, this method is usually implemented in three steps: getting the image into an appropriate form, writing the image file headers, and writing the image pixel data.  Getting the image into an appropriate form involves taking the JimiRasterImage parameter and, if necessary, converting it into a form supported by the image format.  For example, a format limited to saving 256-color images, like GIF, would convert the JimiRasterImage into a ByteRasterImage that uses an IndexColorModel with <= 256 colors.  A format which writes 24-bit RGB or 32-bit ARGB data, such as JPEG, would not make any conversion, since all JimiRasterImages support access in this way.  More flexible formats like PNG which can support either type would check whether the image it had been given was already palette-based, and if it was then save it as such, or if not then save it as RGB.  JIMI provides several categories of image and the ability to check for conformance, or to convert images to conform to a certain category.  These categories include "palette image", "8-bit palette image", "mono-color image", and are easily accessible through the JimiRasterImageCategorizer class.  See the Categories of Images section for more information about these image properties.

    With the image in a form appropriate for encoding, the rest of the encoding process is format-specific.  The encoder must write all headers for the format and the image pixel data, and the encoding process is complete.  Like decoders, the encoders also inherit a setProgress(int progress) method which should be used to allow external objects to track their progress.

    Here is an example encoder.

    Writing a JimiEncoderFactory

    Encoders must have a Factory class associated with them, which is very similar to the factory for decoders but provides slightly different information.  To implement a JimiEncoderFactory for your format, simply subclass and provide implementations for the simple format-specific methods.  This example shows how easily the Factories can be implemented.

    Writing a JimiExtension

    Having developed any encoders and/or decoders, a JimiExtension class is written to describe the entire module to JIMI.  This is the easiest part of writing a format module, as this example implementation shows.  When the JimiExtension is written, the format module is complete and it need only be registered with JIMI.  This is achieved with:

        JimiControl.addExtension(new MyJimiExtension());

    With this done, your custom formats are available through JIMI in the exact same way as the core formats which JIMI handles.


    Multi-image formats

    Some image file formats can hold several images in a single file, TIFF and animated GIFs for example.  Encoders and decoders for these formats are very similar to those of single image formats, but deal with series' of images.  When dealing with these formats, use JimiMultiImageRasterDecoder and JimiMultiImageRasterEncoder.

    Tips for writing good format modules

    Here are some things you can do to help make your format modules fit in and perform well.
  • Don't Buffer Decoders

  • Decoders in JIMI should not use BufferedInputStreams for reading their input data.  JIMI will pass streams which are already buffered to the decoder if buffering is appropriate, so a second buffer in the decoder will degrade performance.
  • Finish reading at the end of the image

  • Decoders should finish decoding at the end of the data relevant to the image file.  Assume that there is important data in the stream beyond the image, and that you should read exactly the amount of image data that is there, leaving the stream aligned at the data which follows the image.

    Categories of image

    In JIMI, there are certain types of JimiRasterImage and java.awt.ColorModel pairs which are adopted as standard for certain types of image.  Through these conventions, different format modules in JIMI have common forms for image exchange to avoid unnecessary conversions of unrecognised types.  The categories are as follows.
     
    <= 8-bit palette image ByteRasterImage with java.awt.IndexColorModel
    Palette image Any JimiRasterImage with a java.awt.image.IndexColorModel
    Black and white BitRasterImage or ByteRasterImage with IndexColorModel containing 2 entries
    Grayscale (any bitsize) java.awt.image.DirectColorModel with an identical mask for each of the R,G,B channels
    > 32-bits per pixel LongRasterImage with subclass of com.sun.jimi.core.raster.LongColorModel
    RGB Any JimiRasterImage with any java.awt.ColorModel