Java Code:
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.Iterator;

import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;

import org.w3c.dom.Node;

/**
 * ch5ImageReader.java -- this class provides the functionality to read an image
 * of format ch5.
 */
public class ch5ImageReader extends ImageReader {
  private ImageInputStream iis;

  private ch5ImageMetadata[] imagemd;

  private ch5StreamMetadata streammd;

  public ch5ImageReader(ImageReaderSpi originatingProvider) {
    super(originatingProvider);
  }

  /**
   * return the ch5StreamMetadata object instantiated in the setStreamMetadata
   * method
   */
  public IIOMetadata getStreamMetadata() {
    return streammd;
  }

  /**
   * return the ch5ImageMetadata object instantiated in the setImageMetadata
   * method
   */
  public IIOMetadata getImageMetadata(int imageIndex) {
    return imagemd[imageIndex];
  }

  /**
   * this method sets the input for this ImageReader and also calls the
   * setStreamMetadata method so that the numberImages field is available
   */
  public void setInput(Object object, boolean seekForwardOnly) {
    super.setInput(object, seekForwardOnly);
    if (object == null)
      throw new IllegalArgumentException("input is null");

    if (!(object instanceof ImageInputStream)) {
      String argString = "input not an ImageInputStream";
      throw new IllegalArgumentException(argString);
    }
    iis = (ImageInputStream) object;
    setStreamMetadata(iis);
  }

  /**
   * this method provides suggestions for possible image types that will be
   * used to decode the image specified by index imageIndex. By default, the
   * first image type returned by this method will be the image type of the
   * BufferedImage returned by the ImageReader's getDestination method. In
   * this case, we are suggesting using an 8 bit grayscale image with no alpha
   * component.
   */
  public Iterator getImageTypes(int imageIndex) {
    java.util.List l = new java.util.ArrayList();
    ;
    int bits = 8;

    /*
     * can convert ch5 format into 8 bit grayscale image with no alpha
     */
    l.add(ImageTypeSpecifier.createGrayscale(bits, DataBuffer.TYPE_BYTE,
        false));
    return l.iterator();
  }

  /**
   * read in the input image specified by index imageIndex using the
   * parameters specified by the ImageReadParam object param
   */
  public BufferedImage read(int imageIndex, ImageReadParam param) {

    checkIndex(imageIndex);

    if (isSeekForwardOnly())
      minIndex = imageIndex;
    else
      minIndex = 0;

    BufferedImage bimage = null;
    WritableRaster raster = null;

    /*
     * this method sets the image metadata so that we can use the getWidth
     * and getHeight methods
     */
    setImageMetadata(iis, imageIndex);

    int srcWidth = getWidth(imageIndex);
    int srcHeight = getHeight(imageIndex);

    // initialize values to -1
    int dstWidth = -1;
    int dstHeight = -1;
    int srcRegionWidth = -1;
    int srcRegionHeight = -1;
    int srcRegionXOffset = -1;
    int srcRegionYOffset = -1;
    int xSubsamplingFactor = -1;
    int ySubsamplingFactor = -1;
    if (param == null)
      param = getDefaultReadParam();

    Iterator imageTypes = getImageTypes(imageIndex);
    try {
      /*
       * get the destination BufferedImage which will be filled using the
       * input image's pixel data
       */
      bimage = getDestination(param, imageTypes, srcWidth, srcHeight);

      /*
       * get Rectangle object which will be used to clip the source
       * image's dimensions.
       */
      Rectangle srcRegion = param.getSourceRegion();
      if (srcRegion != null) {
        srcRegionWidth = (int) srcRegion.getWidth();
        srcRegionHeight = (int) srcRegion.getHeight();
        srcRegionXOffset = (int) srcRegion.getX();
        srcRegionYOffset = (int) srcRegion.getY();

        /*
         * correct for overextended source regions
         */
        if (srcRegionXOffset + srcRegionWidth > srcWidth)
          dstWidth = srcWidth - srcRegionXOffset;
        else
          dstWidth = srcRegionWidth;

        if (srcRegionYOffset + srcRegionHeight > srcHeight)
          dstHeight = srcHeight - srcRegionYOffset;
        else
          dstHeight = srcRegionHeight;
      } else {
        dstWidth = srcWidth;
        dstHeight = srcHeight;
        srcRegionXOffset = srcRegionYOffset = 0;
      }
      /*
       * get subsampling factors
       */
      xSubsamplingFactor = param.getSourceXSubsampling();
      ySubsamplingFactor = param.getSourceYSubsampling();

      /**
       * dstWidth and dstHeight should be equal to bimage.getWidth() and
       * bimage.getHeight() after these next two instructions
       */
      dstWidth = (dstWidth - 1) / xSubsamplingFactor + 1;
      dstHeight = (dstHeight - 1) / ySubsamplingFactor + 1;
    } catch (IIOException e) {
      System.err.println("Can't create destination BufferedImage");
    }
    raster = bimage.getWritableTile(0, 0);

    /*
     * using the parameters specified by the ImageReadParam object, read the
     * image image data into the destination BufferedImage
     */
    byte[] srcBuffer = new byte[srcWidth];
    byte[] dstBuffer = new byte[dstWidth];
    int jj;
    int index;
    try {
      for (int j = 0; j < srcHeight; j++) {
        iis.readFully(srcBuffer, 0, srcWidth);

        jj = j - srcRegionYOffset;
        if (jj % ySubsamplingFactor == 0) {
          jj /= ySubsamplingFactor;
          if ((jj >= 0) && (jj < dstHeight)) {
            for (int i = 0; i < dstWidth; i++) {
              index = srcRegionXOffset + i * xSubsamplingFactor;
              dstBuffer[i] = srcBuffer[index];
            }
            raster.setDataElements(0, jj, dstWidth, 1, dstBuffer);
          }
        }
      }
    } catch (IOException e) {
      bimage = null;
    }
    return bimage;
  }

  /**
   * this method sets the image metadata for the image indexed by index
   * imageIndex. This method is specific for the ch5 format and thus only sets
   * the image width and image height
   */
  private void setImageMetadata(ImageInputStream iis, int imageIndex) {
    imagemd[imageIndex] = new ch5ImageMetadata();
    try {
      String s;
      s = iis.readLine();
      while (s.length() == 0)
        s = iis.readLine();
      imagemd[imageIndex].imageWidth = Integer.parseInt(s.trim());
      s = iis.readLine();
      imagemd[imageIndex].imageHeight = Integer.parseInt(s.trim());
    } catch (IOException exception) {
    }
  }

  /**
   * this method sets the stream metadata for the images represented by the
   * ImageInputStream iis. This method is specific for the ch5 format and thus
   * only sets the numberImages field.
   */
  private void setStreamMetadata(ImageInputStream iis) {
    streammd = new ch5StreamMetadata();
    try {
      String magicNumber = iis.readLine();
      int numImages = Integer.parseInt(iis.readLine().trim());
      streammd.numberImages = numImages;
      imagemd = new ch5ImageMetadata[streammd.numberImages];
    } catch (IOException exception) {
    }
  }

  /**
   * This method can only be used after the stream metadata has been set
   * (which occurs in the setInput method). Else it will return a -1
   */
  public int getNumImages(boolean allowSearch) {
    return streammd.numberImages;
  }

  /**
   * This method can only be used after the stream metadata has been set
   * (which occurs in the setInput method). Else it will return a -1
   */
  public int getHeight(int imageIndex) {
    if (imagemd == null)
      return -1;
    checkIndex(imageIndex);

    return imagemd[imageIndex].imageHeight;
  }

  /**
   * This method can only be used after the stream metadata has been set
   * (which occurs in the setInput method). Else it will return a -1
   */
  public int getWidth(int imageIndex) {
    if (imagemd == null)
      return -1;
    checkIndex(imageIndex);

    return imagemd[imageIndex].imageWidth;
  }

  private void checkIndex(int imageIndex) {
    if (imageIndex >= streammd.numberImages) {
      String argString = "imageIndex >= number of images";
      throw new IndexOutOfBoundsException(argString);
    }
    if (imageIndex < minIndex) {
      String argString = "imageIndex < minIndex";
      throw new IndexOutOfBoundsException(argString);
    }
  }
}

/**
 * ch5StreamMetadata.java -- holds stream metadata for the ch5 format. The
 * internal tree for holding this metadata is read only
 */

class ch5StreamMetadata extends IIOMetadata {
  static final String nativeMetadataFormatName = "ch5.imageio.ch5stream_1.00";

  static final String nativeMetadataFormatClassName = "ch5.imageio.ch5stream";

  static final String[] extraMetadataFormatNames = null;

  static final String[] extraMetadataFormatClassNames = null;

  static final boolean standardMetadataFormatSupported = false;

  public int numberImages;

  public ch5StreamMetadata() {
    super(standardMetadataFormatSupported, nativeMetadataFormatName,
        nativeMetadataFormatClassName, extraMetadataFormatNames,
        extraMetadataFormatClassNames);
    numberImages = -1;
  }

  public boolean isReadOnly() {
    return true;
  }

  /**
   * IIOMetadataFormat objects are meant to describe the structure of metadata
   * returned from the getAsTree method. In this case, no such description is
   * available
   */
  public IIOMetadataFormat getMetadataFormat(String formatName) {
    if (formatName.equals(nativeMetadataFormatName)) {
      return null;
    } else {
      throw new IllegalArgumentException("Unrecognized format!");
    }
  }

  /**
   * returns the stream metadata in a tree corresponding to the provided
   * formatName
   */
  public Node getAsTree(String formatName) {
    if (formatName.equals(nativeMetadataFormatName)) {
      return getNativeTree();
    } else {
      throw new IllegalArgumentException("Unrecognized format!");
    }
  }

  /**
   * returns the stream metadata in a tree using the following format
   * <!ELEMENT ch5.imageio.ch5stream_1.00 (imageDimensions)> <!ATTLIST
   * imageDimensions numberImages CDATA #REQUIRED
   */
  private Node getNativeTree() {
    IIOMetadataNode node; // scratch node

    IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);

    // Image descriptor
    node = new IIOMetadataNode("imageDimensions");
    node.setAttribute("numberImages", Integer.toString(numberImages));
    root.appendChild(node);

    return root;
  }

  public void setFromTree(String formatName, Node root) {
    throw new IllegalStateException("Metadata is read-only!");
  }

  public void mergeTree(String formatName, Node root) {
    throw new IllegalStateException("Metadata is read-only!");
  }

  public void reset() {
    throw new IllegalStateException("Metadata is read-only!");
  }

  /**
   * initialize the stream metadata element numberImages
   */
  public void initialize(int numberImages) {
    this.numberImages = numberImages;
  }
}

/**
 * ch5ImageMetadata.java -- holds image metadata for the ch5 format. The
 * internal tree for holding this metadata is read only
 */

class ch5ImageMetadata extends IIOMetadata {
  static final String nativeMetadataFormatName = "ch5.imageio.ch5image_1.00";

  static final String nativeMetadataFormatClassName = "ch5.imageio.ch5image";

  static final String[] extraMetadataFormatNames = null;

  static final String[] extraMetadataFormatClassNames = null;

  static final boolean standardMetadataFormatSupported = false;

  public int imageWidth;

  public int imageHeight;

  public ch5ImageMetadata() {
    super(standardMetadataFormatSupported, nativeMetadataFormatName,
        nativeMetadataFormatClassName, extraMetadataFormatNames,
        extraMetadataFormatClassNames);
    imageWidth = -1;
    imageHeight = -1;
  }

  public boolean isReadOnly() {
    return true;
  }

  /**
   * IIOMetadataFormat objects are meant to describe the structure of metadata
   * returned from the getAsTree method. In this case, no such description is
   * available
   */
  public IIOMetadataFormat getMetadataFormat(String formatName) {
    if (formatName.equals(nativeMetadataFormatName)) {
      return null;
    } else {
      throw new IllegalArgumentException("Unrecognized format!");
    }
  }

  /**
   * returns the image metadata in a tree corresponding to the provided
   * formatName
   */
  public Node getAsTree(String formatName) {
    if (formatName.equals(nativeMetadataFormatName)) {
      return getNativeTree();
    } else {
      throw new IllegalArgumentException("Unrecognized format!");
    }
  }

  /**
   * returns the image metadata in a tree using the following format <!ELEMENT
   * ch5.imageio.ch5image_1.00 (imageDimensions)> <!ATTLIST imageDimensions
   * imageWidth CDATA #REQUIRED imageHeight CDATA #REQUIRED
   */
  private Node getNativeTree() {
    IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);

    IIOMetadataNode node = new IIOMetadataNode("imageDimensions");
    node.setAttribute("imageWidth", Integer.toString(imageWidth));
    node.setAttribute("imageHeight", Integer.toString(imageHeight));
    root.appendChild(node);

    return root;
  }

  public void setFromTree(String formatName, Node root) {
    throw new IllegalStateException("Metadata is read-only!");
  }

  public void mergeTree(String formatName, Node root) {
    throw new IllegalStateException("Metadata is read-only!");
  }

  public void reset() {
    throw new IllegalStateException("Metadata is read-only!");
  }

  /**
   * initialize the image metadata elements width and height
   */
  public void initialize(int width, int height) {
    imageWidth = width;
    imageHeight = height;
  }
}