Package net.sourceforge.jiu.codecs

Source Code of net.sourceforge.jiu.codecs.PNGCodec

/*
* PNGCodec
*
* Copyright (c) 2003, 2004, 2005, 2006 Marco Schmidt.
* All rights reserved.
*/

package net.sourceforge.jiu.codecs;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;
import java.util.zip.CheckedInputStream;
import java.util.zip.Deflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.CRC32;
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.Gray16Image;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.IntegerImage;
import net.sourceforge.jiu.data.MemoryBilevelImage;
import net.sourceforge.jiu.data.MemoryGray16Image;
import net.sourceforge.jiu.data.MemoryGray8Image;
import net.sourceforge.jiu.data.MemoryPaletted8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.MemoryRGB48Image;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.data.RGB48Image;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;
import net.sourceforge.jiu.util.ArrayConverter;

/**
* An input stream that reads from an underlying stream of PNG
* IDAT chunks and skips all header information.
* PNG uses one or more IDAT chunks to store image data.
* The resulting stream looks like that:
* <code>IDAT [chunk size] [compressed data] [checksum]
* IDAT [chunk size] [compressed data] [checksum] ...</code>
* This stream class expects an input stream where the first IDAT chunk name and chunk
* size have been read already, the stream is thus pointing to the
* first byte of the first [compressed data] section.
* The size of that section is given to the constructor.
* This class then returns calls to read(), counts the bytes it has given
* away and, whenever a compressed data section has been consumed, it reads
* the IDAT chunk and stores its size, using it to determine when the
* next compressed data section will end.
* That way, for the caller the stream appears to be one large compressed
* section.
* <p>
* According to the PNG specs the reason for multiple IDAT chunks is as
* follows:
* <blockquote>
* (Multiple IDAT chunks are allowed so that encoders can work in a fixed
* amount of memory; typically the chunk size will correspond to the encoder's
* buffer size.)
* </blockquote>
* <a target="_top" href="http://www.w3.org/TR/PNG#C.IDAT">4.1.3. IDAT Image data</a>
* <p>
* If there is a more elegant approach to read multiple IDAT chunks, please
* let me know.
* However, reading everything into memory is not an option.
* @author Marco Schmidt
* @since 0.12.0
*/
class PngIdatInputStream extends InputStream
{
  private static final int IDAT = 0x49444154;
  private DataInputStream in;
  private long bytesLeft;

  public PngIdatInputStream(DataInputStream input, long bytes)
  {
    in = input;
    bytesLeft = bytes;
  }

  public int read() throws IOException
  {
    if (bytesLeft == 0)
    {
      skipHeaders();
    }
    bytesLeft--;
    return in.read();
  }

  private void skipHeaders() throws IOException
  {
    do
    {
      //int crc = in.readInt();
      in.readInt(); // skip CRC
      bytesLeft = in.readInt() & 0xffffffffL;
      int type = in.readInt();
      if (IDAT != type)
      {
        throw new IOException("Expected IDAT chunk type, got " +
          Integer.toHexString(type));
      }
    }
    while (bytesLeft == 0);
  }
}

/**
* A codec for the Portable Network Graphics (PNG) format.
* Supports both loading and saving of images.
* <h3>Usage examples</h3>
* <h4>Load an image</h4>
* The following example code loads an image from a PNG file.
* Note that you could also use {@link ImageLoader} or {@link net.sourceforge.jiu.gui.awt.ToolkitLoader}
* which require only a single line of code and can load all formats
* supported by JIU, including PNG.
* <pre>  PNGCodec codec = new PNGCodec();
*  codec.setFile("image.png", CodecMode.LOAD);
*  codec.process();
*  PixelImage image = codec.getImage();</pre>
* <h4>Save an image</h4>
* <pre>  PNGCodec codec = new PNGCodec();
*  codec.setFile("out.png", CodecMode.SAVE);
*  codec.setImage(image);
*  codec.setCompressionLevel(Deflater.BEST_COMPRESSION);
*  codec.appendComment("Copyright (c) 1992 John Doe");
*  // sets last modification time to current time
*  codec.setModification(new GregorianCalendar(
*   new SimpleTimeZone(0, "UTC")));
*  codec.process();</pre>
* <h3>Supported storage order types</h3>
* <h4>Loading</h4>
* This codec reads both non-interlaced and Adam7 interlaced PNG files.
* <h4>Saving</h4>
* This codec only writes non-interlaced PNG files.
* <h3>Supported color types</h3>
* <h4>Loading</h4>
* <ul>
* <li>Grayscale 1 bit streams are loaded as {@link net.sourceforge.jiu.data.BilevelImage} objects,
*  2, 4 and 8 bit streams as {@link net.sourceforge.jiu.data.Gray8Image} and 16 bit as
{@link net.sourceforge.jiu.data.Gray16Image} objects.</li>
* <li>Indexed 1, 2, 4 and 8 bit streams are all loaded as {@link net.sourceforge.jiu.data.Paletted8Image}.</li>
* <li>RGB truecolor 24 bit streams are loaded as {@link net.sourceforge.jiu.data.RGB24Image},
*  48 bit streams as {@link net.sourceforge.jiu.data.RGB48Image} objects.</li>
* </ul>
* <h4>Saving</h4>
* <ul>
* <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as grayscale 1 bit PNG streams.</li>
* <li>{@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as indexed 8 bit PNG streams.
*  Images will always be stored as 8 bit files, even if the palette has only 16, 4 or 2 entries.
* </li>
* <li>{@link net.sourceforge.jiu.data.Gray8Image} objects are stored as 8 bit grayscale PNG streams.</li>
* <li>{@link net.sourceforge.jiu.data.Gray16Image} objects are stored as 16 bit grayscale PNG streams.</li>
* <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bit RGB truecolor PNG streams.</li>
* <li>{@link net.sourceforge.jiu.data.RGB48Image} objects are stored as 48 bit RGB truecolor PNG streams.</li>
* </ul>
* <h3>Transparency information</h3>
* PNG allows to store different types of transparency information.
* Full alpha channels, transparent index values, and more.
* Right now, this JIU codec does not make use of this information and simply
* skips over it when encountered.
* <h3>Bounds</h3>
* This codec regards the bounds concept.
* If bounds are specified with {@link #setBounds}, the codec will only load or save
* part of an image.
* <h3>Metadata</h3>
* <h4>Loading</h4>
* <ul>
* <li>Physical resolution information is loaded from <code>pHYs</code> chunks.
*  Use {@link #getDpiX} and {@link #getDpiY} to retrieve that information.
*  after the call to {@link #process}.</li>
* <li>Textual comments are read from <code>tEXt</code> chunks and can be retrieved
*  with {@link #getComment} after the call to {@link #process}.</li>
* </ul>
* <h4>Saving</h4>
* <ul>
<li>Physical resolution information (specified with {@link #setDpi})
*    is stored in a <code>pHYs</code> chunk.</li>
<li>Textual comments (specified with {@link #appendComment}) are stored as <code>tEXt</code> chunks.
*   The keyword used is <code>Comment</code>.
*   Each of the {@link #getNumComments} is stored in a chunk of its own.</li>
<li>Time of modification is stored in a <code>tIME</code> chunk.
*   Use {@link #setModification(Calendar)} to give a point in time to this codec.</li>
* </ul>
* <h3>Implementation details</h3>
* This class relies heavily on the Java runtime library for decompression and
* checksum creation.
* <h3>Background</h3>
* To learn more about the PNG file format, visit its
* <a target="_top" href="http://www.libpng.org/pub/png/">official homepage</a>.
* There you can find a detailed specification,
* test images and existing PNG libraries and PNG-aware applications.
* The book <em>PNG - The Definitive Guide</em> by Greg Roelofs, published by O'Reilly, 1999,
* ISBN 1-56592-542-4 is a valuable source of information on PNG.
* It is out of print, but it can be viewed online and downloaded for offline reading
* in its entirety from the site.
* @author Marco Schmidt
* @since 0.12.0
*/
public class PNGCodec extends ImageCodec
{
  private final int CHUNK_CRC32_IEND = 0xae426082;
  private final int CHUNK_SIZE_IHDR = 0x0000000d;
  private final int CHUNK_TYPE_IDAT = 0x49444154;
  private final int CHUNK_TYPE_IEND = 0x49454e44;
  private final int CHUNK_TYPE_IHDR = 0x49484452;
  private final int CHUNK_TYPE_PHYS = 0x70485973;
  private final int CHUNK_TYPE_PLTE = 0x504c5445;
  private final int CHUNK_TYPE_TEXT = 0x74455874;
  private final int CHUNK_TYPE_TIME = 0x74494d45;
  private final int COLOR_TYPE_GRAY = 0;
  private final int COLOR_TYPE_GRAY_ALPHA = 4;
  private final int COLOR_TYPE_INDEXED = 3;
  private final int COLOR_TYPE_RGB = 2;
  private final int COLOR_TYPE_RGB_ALPHA = 6;
  private final int COLOR_TYPE_ALPHA = 4;
  private final int FILTER_TYPE_NONE = 0;
  private final int FILTER_TYPE_SUB = 1;
  private final int FILTER_TYPE_UP = 2;
  private final int FILTER_TYPE_AVERAGE = 3;
  private final int FILTER_TYPE_PAETH = 4;
  private final int COMPRESSION_DEFLATE = 0;
  private final int INTERLACING_NONE = 0;
  private final int INTERLACING_ADAM7 = 1;
  private final int FILTERING_ADAPTIVE = 0;
  private final int MAX_TEXT_SIZE = 512;
  private final int ADAM7_NUM_PASSES = 7;
  private final int DEFAULT_ENCODING_MIN_IDAT_SIZE = 32 * 1024;
  private final int[] ADAM7_COLUMN_INCREMENT = {8, 8, 4, 4, 2, 2, 1};
  private final int[] ADAM7_FIRST_COLUMN = {0, 4, 0, 2, 0, 1, 0};
  private final int[] ADAM7_FIRST_ROW = {0, 0, 4, 0, 2, 0, 1};
  private final int[] ADAM7_ROW_INCREMENT = {8, 8, 8, 4, 4, 2, 2};
  private final byte[] MAGIC_BYTES =
    {(byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47,
     (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a};

  private boolean alpha;
  private byte[][] buffers;
  private int bpp;
  private CRC32 checksum;
  private CheckedInputStream checkedIn;
  private int chunkCounter;
  private int colorType;
  private int compressionType;
  private int currentBufferIndex;
  private int deflateLevel = Deflater.DEFAULT_COMPRESSION;
  private int deflateStrategy = Deflater.DEFAULT_STRATEGY;
  private int encodingMinIdatSize = DEFAULT_ENCODING_MIN_IDAT_SIZE;
  private int filterType;
  private boolean hasIhdr;
  private int height;
  private IntegerImage image;
  private DataInputStream in;
  private InflaterInputStream infl;
  private int interlaceType;
  private Calendar modification;
  private int numChannels;
  private DataOutput out;
  private Palette palette;
  private int precision;
  private int previousBufferIndex;
  private int width;

  /**
   * Allocates the right image to private field <code>image</code>,
   * taking into consideration the fields width, height, precision and colorType.
   * Assumes that an IHDR chunk has been read and the above mentioned
   * fields have been initialized and checked for their validity.
   */
  private void allocateImage() throws InvalidFileStructureException, UnsupportedTypeException
  {
    setBoundsIfNecessary(width, height);
    int w = getBoundsWidth();
    int h = getBoundsHeight();
    if (colorType == COLOR_TYPE_GRAY || colorType == COLOR_TYPE_GRAY_ALPHA)
    {
      if (precision == 1)
      {
        image = new MemoryBilevelImage(w, h);
      }
      else
      if (precision <= 8)
      {
        image = new MemoryGray8Image(w, h);
      }
      else
      if (precision == 16)
      {
        image = new MemoryGray16Image(w, h);
      }
    }
    else
    if (colorType == COLOR_TYPE_INDEXED)
    {
      if (palette == null)
      {
        throw new InvalidFileStructureException("No palette found when trying to load indexed image.");
      }
      image = new MemoryPaletted8Image(w, h, palette);
    }
    else
    if (colorType == COLOR_TYPE_RGB || colorType == COLOR_TYPE_RGB_ALPHA)
    {
      if (precision == 8)
      {
        image = new MemoryRGB24Image(w, h);
      }
      else
      {
        image = new MemoryRGB48Image(w, h);
      }
    }
    else
    {
      throw new UnsupportedTypeException("Unsupported image type encountered");
    }
  }

  /**
   * Checks values {@link #precision} and {@link #colorType}.
   * A lot of combinations possibly found in an IHDR chunk
   * are invalid.
   * Also initializes {@link #alpha} and {@link #numChannels}.
   * @throws UnsupportedTypeException if an invalid combination
   *  of precision and colorType is found
   */
  private void checkColorTypeAndPrecision() throws UnsupportedTypeException
  {
    if (colorType != COLOR_TYPE_GRAY &&
        colorType != COLOR_TYPE_RGB &&
        colorType != COLOR_TYPE_INDEXED &&
        colorType != COLOR_TYPE_GRAY_ALPHA &&
        colorType != COLOR_TYPE_RGB_ALPHA)
    {
      throw new UnsupportedTypeException("Not a valid color type: " + colorType);
    }
    if (precision != 1 && precision != 2 && precision != 4 && precision != 8 && precision != 16)
    {
      throw new UnsupportedTypeException("Invalid precision value: " + precision);
    }
    if (colorType == COLOR_TYPE_INDEXED && precision > 8)
    {
      throw new UnsupportedTypeException("More than eight bits of precision are not allowed for indexed images.");
    }
    if (colorType == COLOR_TYPE_RGB && precision < 8)
    {
      throw new UnsupportedTypeException("Less than eight bits of precision are not allowed for RGB images.");
    }
    alpha = (colorType & COLOR_TYPE_ALPHA) != 0;
    if (colorType == COLOR_TYPE_RGB ||
        colorType == COLOR_TYPE_RGB_ALPHA)
    {
      numChannels = 3;
    }
    else
    {
      numChannels = 1;
    }
    bpp = computeBytesPerRow(1);
  }

  /**
   * Computes a number of bytes for a given number of pixels,
   * regarding precision and availability of an alpha channel.
   * @param numPixels the number of pixels for which the number
   *  of bytes necessary to store them is to be computed
   * @return number of bytes
   */
  private int computeBytesPerRow(int numPixels)
  {
    if (precision < 8)
    {
      return (numPixels + ((8 / precision) - 1)) / (8 / precision);
    }
    else
    {
      return (numChannels + (alpha ? 1 : 0)) * (precision / 8) * numPixels;
    }
  }

  private int computeColumnsAdam7(int pass)
  {
    switch(pass)
    {
      case(0): return (width + 7) / 8;
      case(1): return (width + 3) / 8;
      case(2): return (width + 3) / 4;
      case(3): return (width + 1) / 4;
      case(4): return (width + 1) / 2;
      case(5): return width / 2;
      case(6): return width;
      default: throw new IllegalArgumentException("Not a valid pass index: " + pass);
    }
  }

  private void fillRowBuffer(int y, byte[] row, int offs)
  {
    PixelImage image = getImage();
    int x1 = getBoundsX1();
    int w = getBoundsWidth();
    if (image instanceof BilevelImage)
    {
      BilevelImage bilevelImage = (BilevelImage)image;
      bilevelImage.getPackedBytes(x1, y, w, row, offs, 0);
    }
    else
    if (image instanceof Gray16Image)
    {
      Gray16Image grayImage = (Gray16Image)image;
      while (w-- > 0)
      {
        short sample = grayImage.getShortSample(x1++, y);
        ArrayConverter.setShortBE(row, offs, sample);
        offs += 2;
      }
    }
    else
    if (image instanceof Gray8Image)
    {
      Gray8Image grayImage = (Gray8Image)image;
      grayImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs);
    }
    else
    if (image instanceof Paletted8Image)
    {
      Paletted8Image palImage = (Paletted8Image)image;
      palImage.getByteSamples(0, getBoundsX1(), y, getBoundsWidth(), 1, row, offs);
    }
    else
    if (image instanceof RGB24Image)
    {
      RGB24Image rgbImage = (RGB24Image)image;
      while (w-- > 0)
      {
        row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_RED, x1, y);
        row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_GREEN, x1, y);
        row[offs++] = rgbImage.getByteSample(RGBIndex.INDEX_BLUE, x1, y);
        x1++;
      }
    }
    else
    if (image instanceof RGB48Image)
    {
      RGB48Image rgbImage = (RGB48Image)image;
      while (w-- > 0)
      {
        short sample = rgbImage.getShortSample(RGBIndex.INDEX_RED, x1, y);
        ArrayConverter.setShortBE(row, offs, sample);
        offs += 2;

        sample = rgbImage.getShortSample(RGBIndex.INDEX_GREEN, x1, y);
        ArrayConverter.setShortBE(row, offs, sample);
        offs += 2;

        sample = rgbImage.getShortSample(RGBIndex.INDEX_BLUE, x1, y);
        ArrayConverter.setShortBE(row, offs, sample);
        offs += 2;

        x1++;
      }
    }
  }

  /**
    * Creates a four-letter String from the parameter, an <code>int</code>
    * value, supposed to be storing a chunk name.
    * @return the chunk name
    */
  private static String getChunkName(int chunk)
  {
    StringBuffer result = new StringBuffer(4);
    for (int i = 24; i >= 0; i -= 8)
    {
      result.append((char)((chunk >> i) & 0xff));
    }
    return result.toString();
  }

  public String getFormatName()
  {
    return "Portable Network Graphics (PNG)";
  }

  public String[] getMimeTypes()
  {
    return new String[] {"image/png"};
  }

  private static int getPaeth(byte l, byte u, byte nw)
  {
    int a = l & 0xff;
    int b = u & 0xff;
    int c = nw & 0xff;
    int p = a + b - c;
    int pa = p - a;
    if (pa < 0)
    {
      pa = -pa;
    }
    int pb = p - b;
    if (pb < 0)
    {
      pb = -pb;
    }
    int pc = p - c;
    if (pc < 0)
    {
      pc = -pc;
    }
    if (pa <= pb && pa <= pc)
    {
      return a;
    }
    if (pb <= pc)
    {
      return b;
    }
    return c;
  }

  private void inflateBytes(byte[] buffer, int numBytes) throws InvalidFileStructureException, IOException
  {
    int offset = 0;
    do
    {
      try
      {
        int toRead = numBytes - offset;
        int numRead = infl.read(buffer, offset, toRead);
        if (numRead < 0)
        {
          throw new InvalidFileStructureException("Cannot fill buffer");
        }
        offset += numRead;
      }
      catch (IOException ioe)
      {
        throw new InvalidFileStructureException("Stopped decompressing " + ioe.toString());
      }
    }
    while (offset != numBytes);
  }

  public boolean isLoadingSupported()
  {
    return true;
  }

  public boolean isSavingSupported()
  {
    return true;
  }

  private void load() throws
    InvalidFileStructureException,
    IOException,
    UnsupportedTypeException,
    WrongFileFormatException
  {
    byte[] magic = new byte[MAGIC_BYTES.length];
    in.readFully(magic);
    for (int i = 0; i < MAGIC_BYTES.length; i++)
    {
      if (magic[i] != MAGIC_BYTES[i])
      {
        throw new WrongFileFormatException("Not a valid PNG input " +
          "stream, wrong magic byte sequence.");
      }
    }
    chunkCounter = 0;
    do
    {
      loadChunk();
      chunkCounter++;
    }
    while (image == null);
    close();
    setImage(image);
  }

  private void loadChunk() throws InvalidFileStructureException, IOException, UnsupportedTypeException
  {
    /*
     * read chunk size; according to the PNG specs, the size value must not be larger
     * than 2^31 - 1; to be safe, we treat the value as an unsigned
     * 32 bit value anyway
     */
    long chunkSize = in.readInt() & 0xffffffffL;
    checksum.reset();
    int chunkName = in.readInt();
    // first chunk must be IHDR
    if (chunkCounter == 0 && chunkName != CHUNK_TYPE_IHDR)
    {
      throw new InvalidFileStructureException("First chunk was not IHDR but " + getChunkName(chunkName));
    }
    switch (chunkName)
    {
      // image data chunk
      case(CHUNK_TYPE_IDAT):
      {
        loadImage(chunkSize);
        break;
      }
      // end of image chunk
      case(CHUNK_TYPE_IEND):
      {
        throw new InvalidFileStructureException("Reached IEND chunk but could not load image.");
      }
      case(CHUNK_TYPE_IHDR):
      {
        if (hasIhdr)
        {
          throw new InvalidFileStructureException("More than one IHDR chunk found.");
        }
        if (chunkCounter != 0)
        {
          throw new InvalidFileStructureException("IHDR chunk must be first; found to be chunk #" + (chunkCounter + 1));
        }
        if (chunkSize != CHUNK_SIZE_IHDR)
        {
          throw new InvalidFileStructureException("Expected PNG " +
            "IHDR chunk length to be " + CHUNK_SIZE_IHDR + ", got " +
            chunkSize + ".");
        }
        hasIhdr = true;
        loadImageHeader();
        break;
      }
      case(CHUNK_TYPE_PHYS):
      {
        if (chunkSize == 9)
        {
          byte[] phys = new byte[9];
          in.readFully(phys);
          int x = ArrayConverter.getIntBE(phys, 0);
          int y = ArrayConverter.getIntBE(phys, 4);
          if (phys[8] == 1)
          {
            // unit is meters
            final double INCHES_PER_METER = 100 / 2.54;
            setDpi((int)(x / INCHES_PER_METER), (int)(y / INCHES_PER_METER));
          }
        }
        else
        {
          skip(chunkSize);
        }
        break;
      }
      case(CHUNK_TYPE_PLTE):
      {
        if ((chunkSize % 3) != 0)
        {
          throw new InvalidFileStructureException("Not a valid palette chunk size: " + chunkSize);
        }
        loadPalette(chunkSize / 3);
        break;
      }
      case(CHUNK_TYPE_TEXT):
      {
        if (chunkSize == 0)
        {
        }
        else
        if (chunkSize > MAX_TEXT_SIZE)
        {
          skip(chunkSize);
        }
        else
        {
          StringBuffer text = new StringBuffer((int)chunkSize);
          int i = 0;
          char c;
          do
          {
            c = (char)in.read();
            if (c == 0)
            {
              skip(chunkSize - i - 1);
              break;
            }
            text.append(c);
            i++;
          }
          while (i < chunkSize);
          //System.out.println("text=\"" + text.toString() + "\"");
        }
        break;
      }
      default:
      {
        skip(chunkSize);
      }
    }
    int createdChecksum = (int)checksum.getValue();
    if (image == null)
    {
      // this code doesn't work anymore if we have just read an image
      int chunkChecksum = in.readInt();
      if (createdChecksum != chunkChecksum)
      {
        throw new InvalidFileStructureException("Checksum created on chunk " +
          getChunkName(chunkName) + " " + Integer.toHexString(createdChecksum) +
          " is not equal to checksum read from stream " +
          Integer.toHexString(chunkChecksum) +
          "; file is corrupted.");
      }
    }
  }

  /**
   * Load an image from the current position in the file.
   * Assumes the last things read from input are an IDAT chunk type and
   * its size, which is the sole argument of this method.
   * @param chunkSize size of the IDAT chunk that was just read
   * @throws InvalidFileStructureException if there are values in the PNG stream that make it invalid
   * @throws IOException if there were I/O errors when reading
   * @throws UnsupportedTypeException if something was encountered in the stream that is valid but not supported by this codec
   */
  private void loadImage(long chunkSize) throws InvalidFileStructureException, IOException, UnsupportedTypeException
  {
    // allocate two byte buffers for current and previous row
    buffers = new byte[2][];
    int numBytes = computeBytesPerRow(width);
    currentBufferIndex = 0;
    previousBufferIndex = 1;
    buffers[currentBufferIndex] = new byte[numBytes];
    buffers[previousBufferIndex] = new byte[numBytes];
    for (int i = 0; i < buffers[previousBufferIndex].length; i++)
    {
      buffers[previousBufferIndex][i] = (byte)0;
    }
    // allocate the correct type of image object for the image type read in the IHDR chunk
    allocateImage();
    // create a PngIdatInputStream which will skip header information when
    // multiple IDAT chunks are in the input stream
    infl = new InflaterInputStream(new PngIdatInputStream(in, chunkSize));
    switch(interlaceType)
    {
      case(INTERLACING_NONE):
      {
        loadImageNonInterlaced();
        break;
      }
      case(INTERLACING_ADAM7):
      {
        loadImageInterlacedAdam7();
        break;
      }
    }
  }

  /**
   * Reads data from an IHDR chunk and initializes private fields with it.
   * Does a lot of checking if read values are valid and supported by this class.
   * @throws IOException
   * @throws InvalidFileStructureException
   * @throws UnsupportedTypeException
   */
  private void loadImageHeader() throws IOException, InvalidFileStructureException, UnsupportedTypeException
  {
    // WIDTH -- horizontal resolution
    width = in.readInt();
    if (width < 1)
    {
      throw new InvalidFileStructureException("Width must be larger than 0; got " + width);
    }
    // HEIGHT -- vertical resolution
    height = in.readInt();
    if (height < 1)
    {
      throw new InvalidFileStructureException("Height must be larger than 0; got " + height);
    }
    // PRECISION -- bits per sample
    precision = in.read();
    // COLOR TYPE -- indexed, paletted, grayscale, optionally alpha
    colorType = in.read();
    // check for invalid combinations of color type and precision
    // and initialize alpha and numChannels
    checkColorTypeAndPrecision();
    // COMPRESSION TYPE -- only Deflate is defined
    compressionType = in.read();
    if (compressionType != COMPRESSION_DEFLATE)
    {
      throw new UnsupportedTypeException("Unsupported compression type: " +
        compressionType + ".");
    }
    // FILTER TYPE -- only Adaptive is defined
    filterType = in.read();
    if (filterType != FILTERING_ADAPTIVE)
    {
      throw new UnsupportedTypeException("Only 'adaptive filtering' is supported right now; got " + filterType);
    }
    // INTERLACE TYPE -- order of storage of image data
    interlaceType = in.read();
    if (interlaceType != INTERLACING_NONE &&
        interlaceType != INTERLACING_ADAM7)
    {
      throw new UnsupportedTypeException("Only 'no interlacing' and 'Adam7 interlacing' are supported; got " + interlaceType);
    }
  }

  private void loadImageInterlacedAdam7() throws InvalidFileStructureException, IOException, UnsupportedTypeException
  {
    final int TOTAL_LINES = ADAM7_NUM_PASSES * height;
    for (int pass = 0; pass < ADAM7_NUM_PASSES; pass++)
    {
      currentBufferIndex = 0;
      previousBufferIndex = 1;
      byte[] previousBuffer = buffers[previousBufferIndex];
      for (int x = 0; x < previousBuffer.length; x++)
      {
        previousBuffer[x] = 0;
      }
      int y = ADAM7_FIRST_ROW[pass];
      int destY = y - getBoundsY1();
      int numColumns = computeColumnsAdam7(pass);
      if (numColumns == 0)
      {
        // this pass contains no data; skip to next pass
        setProgress((pass + 1) * height, TOTAL_LINES);
        continue;
      }
      int numBytes = computeBytesPerRow(numColumns);
      while (y < height)
      {
        previousBuffer = buffers[previousBufferIndex];
        byte[] currentBuffer = buffers[currentBufferIndex];
        int rowFilterType = readFilterType();
        inflateBytes(currentBuffer, numBytes);
        reverseFilter(rowFilterType, currentBuffer, previousBuffer, numBytes);
        if (isRowRequired(y))
        {
          storeInterlacedAdam7(pass, destY, currentBuffer);
        }
        int progressY = y;
        if (pass > 0)
        {
          progressY += pass * height;
        }
        setProgress(progressY, TOTAL_LINES);
        y += ADAM7_ROW_INCREMENT[pass];
        destY += ADAM7_ROW_INCREMENT[pass];
        currentBufferIndex = 1 - currentBufferIndex;
        previousBufferIndex = 1 - previousBufferIndex;
      }
    }
  }

  private void loadImageNonInterlaced() throws InvalidFileStructureException, IOException, UnsupportedTypeException
  {
    int linesToRead = getBoundsY2() + 1;
    int rowLength = computeBytesPerRow(width);
    for (int y = 0, destY = - getBoundsY1(); y <= getBoundsY2(); y++, destY++)
    {
      byte[] currentBuffer = buffers[currentBufferIndex];
      byte[] previousBuffer = buffers[previousBufferIndex];
      int rowFilterType = readFilterType();
      inflateBytes(currentBuffer, rowLength);
      reverseFilter(rowFilterType, currentBuffer, previousBuffer, rowLength);
      if (isRowRequired(y))
      {
        storeNonInterlaced(destY, currentBuffer);
      }
      setProgress(y, linesToRead);
      previousBufferIndex = 1 - previousBufferIndex;
      currentBufferIndex = 1 - currentBufferIndex;
    }
  }

  private void loadPalette(long numEntries) throws InvalidFileStructureException, IOException
  {
    if (palette != null)
    {
      throw new InvalidFileStructureException("More than one palette in input stream.");
    }
    if (numEntries < 1)
    {
      throw new InvalidFileStructureException("Number of palette entries must be at least 1.");
    }
    if (numEntries > 256)
    {
      throw new InvalidFileStructureException("Number of palette entries larger than 256: " + numEntries);
    }
    palette = new Palette((int)numEntries);
    int index = 0;
    do
    {
      palette.putSample(Palette.INDEX_RED, index, in.read() & 0xff);
      palette.putSample(Palette.INDEX_GREEN, index, in.read() & 0xff);
      palette.putSample(Palette.INDEX_BLUE, index, in.read() & 0xff);
      index++;
    }
    while (index != numEntries);
  }

  public static void main(String[] args) throws Exception
  {
    PNGCodec codec = new PNGCodec();
    codec.setFile(args[0], CodecMode.LOAD);
    codec.process();
    codec.close();
    PixelImage image = codec.getImage();
    codec = new PNGCodec();
    codec.setFile(args[1], CodecMode.SAVE);
    codec.setImage(image);
    codec.setDpi(300, 300);
    codec.appendComment("Test comment #1.");
    codec.appendComment("And test comment #2.");
    codec.setModification(new GregorianCalendar(new SimpleTimeZone(0, "UTC")));
    codec.process();
    codec.close();
  }

  public void process() throws
    InvalidFileStructureException,
    MissingParameterException,
    OperationFailedException,
    UnsupportedTypeException,
    WrongFileFormatException
  {
    initModeFromIOObjects();
    if (getMode() == CodecMode.LOAD)
    {
      try
      {
        if (getImageIndex() != 0)
        {
          throw new InvalidImageIndexException("PNG streams can only store one image; " +
            "index " + getImageIndex() + " is thus not valid.");
        }
        InputStream input = getInputStream();
        if (input == null)
        {
          throw new MissingParameterException("InputStream object missing.");
        }
        checksum = new CRC32();
        checkedIn = new CheckedInputStream(input, checksum);
        in = new DataInputStream(checkedIn);
        load();
      }
      catch (IOException ioe)
      {
        throw new OperationFailedException("I/O failure: " + ioe.toString());
      }
    }
    else
    if (getMode() == CodecMode.SAVE)
    {
      try
      {
        PixelImage image = getImage();
        if (image == null)
        {
          throw new MissingParameterException("Need image for saving.");
        }
        out = getOutputAsDataOutput();
        if (out == null)
        {
          throw new MissingParameterException("Could not retrieve non-null DataOutput object for saving.");
        }
        setBoundsIfNecessary(image.getWidth(), image.getHeight());
        save();
      }
      catch (IOException ioe)
      {
        throw new OperationFailedException("I/O failure: " + ioe.toString());
      }
    }
    else
    {
      throw new OperationFailedException("Unknown codec mode: " + getMode());
    }
  }

  private int readFilterType() throws InvalidFileStructureException, IOException
  {
    int filterType = infl.read();
    if (filterType >= 0 && filterType <= 4)
    {
      return filterType;
    }
    else
    {
      throw new InvalidFileStructureException("Valid filter types are from 0 to 4; got " + filterType);
    }
  }

  private void reverseFilter(int rowFilterType, byte[] buffer, byte[] prev, int numBytes) throws UnsupportedTypeException
  {
    switch(rowFilterType)
    {
      case(FILTER_TYPE_NONE):
      {
        break;
      }
      case(FILTER_TYPE_SUB):
      {
        for (int x = 0, px = -bpp; x < numBytes; x++, px++)
        {
          byte currXMinusBpp;
          if (px < 0)
          {
            currXMinusBpp = 0;
          }
          else
          {
            currXMinusBpp = buffer[px];
          }
          buffer[x] = (byte)(buffer[x] + currXMinusBpp);
        }
        break;
      }
      case(FILTER_TYPE_UP):
      {
        for (int x = 0; x < numBytes; x++)
        {
          buffer[x] = (byte)(buffer[x] + prev[x]);
        }
        break;
      }
      case(FILTER_TYPE_AVERAGE):
      {
        for (int x = 0, px = -bpp; x < numBytes; x++, px++)
        {
          int currX = buffer[x] & 0xff;
          int currXMinus1;
          if (px < 0)
          {
            currXMinus1 = 0;
          }
          else
          {
            currXMinus1 = buffer[px] & 0xff;
          }
          int prevX = prev[x] & 0xff;
          int result = currX + ((currXMinus1 + prevX) / 2);
          byte byteResult = (byte)result;
          buffer[x] = byteResult;
        }
        break;
      }
      case(FILTER_TYPE_PAETH):
      {
        for (int x = 0, px = -bpp; x < numBytes; x++, px++)
        {
          byte currXMinusBpp;
          byte prevXMinusBpp;
          if (px < 0)
          {
            currXMinusBpp = 0;
            prevXMinusBpp = 0;
          }
          else
          {
            currXMinusBpp = buffer[px];
            prevXMinusBpp = prev[px];
          }
          buffer[x] = (byte)(buffer[x] + getPaeth(currXMinusBpp, prev[x], prevXMinusBpp));
        }
        break;
      }
      default:
      {
        throw new UnsupportedTypeException("Unknown filter type: " + rowFilterType);
      }
    }
  }

  private void save() throws IOException
  {
    // write 8 byte PNG signature
    out.write(MAGIC_BYTES);
    // write IHDR (image header) chunk
    saveIhdrChunk();
    // write pHYs chunk (physical resolution) if data is available
    savePhysChunk();
    // write tEXt chunks if comments are available
    saveTextChunks();
    // write tIME chunk if modification time was set
    saveTimeChunk();
    // write PLTE chunk if necessary
    savePlteChunk();
    // write IDAT chunk
    saveImage();
    // write IEND chunk
    saveIendChunk();
    close();   
  }

  private void saveChunk(int chunkType, int chunkSize, byte[] data) throws IOException
  {
    // set up array with chunk size and type
    byte[] intArray = new byte[8];
    ArrayConverter.setIntBE(intArray, 0, chunkSize);
    ArrayConverter.setIntBE(intArray, 4, chunkType);
    // write chunk size, type and data
    out.write(intArray, 0, 8);
    out.write(data, 0, chunkSize);
    // create checksum on type and data
    CRC32 checksum = new CRC32();
    checksum.reset();
    checksum.update(intArray, 4, 4);
    checksum.update(data, 0, chunkSize);
    // put checksum into byte array
    ArrayConverter.setIntBE(intArray, 0, (int)checksum.getValue());
    // and write it to output
    out.write(intArray, 0, 4);
  }

  private void saveIendChunk() throws IOException
  {
    out.writeInt(0);
    out.writeInt(CHUNK_TYPE_IEND);
    out.writeInt(CHUNK_CRC32_IEND);
  }

  private void saveIhdrChunk() throws IOException
  {
    byte[] buffer = new byte[CHUNK_SIZE_IHDR];
    width = getBoundsWidth();
    ArrayConverter.setIntBE(buffer, 0, width);
    height = getBoundsHeight();
    ArrayConverter.setIntBE(buffer, 4, height);
    PixelImage image = getImage();
    alpha = false;
    numChannels = 1;
    if (image instanceof BilevelImage)
    {
      precision = 1;
      colorType = COLOR_TYPE_GRAY;
    }
    else
    if (image instanceof Gray16Image)
    {
      precision = 16;
      colorType = COLOR_TYPE_GRAY;
    }
    else
    if (image instanceof Gray8Image)
    {
      precision = 8;
      colorType = COLOR_TYPE_GRAY;
    }
    else
    if (image instanceof Paletted8Image)
    {
      precision = 8;
      colorType = COLOR_TYPE_INDEXED;
    }
    else
    if (image instanceof RGB24Image)
    {
      numChannels = 3;
      precision = 8;
      colorType = COLOR_TYPE_RGB;
    }
    else
    if (image instanceof RGB48Image)
    {
      numChannels = 3;
      precision = 16;
      colorType = COLOR_TYPE_RGB;
    }
    buffer[8] = (byte)precision;
    buffer[9] = (byte)colorType;
    compressionType = COMPRESSION_DEFLATE;
    buffer[10] = (byte)compressionType;
    filterType = FILTERING_ADAPTIVE;
    buffer[11] = (byte)filterType;
    interlaceType = INTERLACING_NONE;
    buffer[12] = (byte)interlaceType;
    saveChunk(CHUNK_TYPE_IHDR, CHUNK_SIZE_IHDR, buffer);
  }

  private void saveImage() throws IOException
  {
    switch(interlaceType)
    {
      case(INTERLACING_NONE):
      {
        saveImageNonInterlaced();
        break;
      }
    }
  }

  private void saveImageNonInterlaced() throws IOException
  {
    int bytesPerRow = computeBytesPerRow(getBoundsWidth());
    byte[] rowBuffer = new byte[bytesPerRow + 1];
    byte[] outBuffer = new byte[Math.max(encodingMinIdatSize, bytesPerRow + 1)];
    int outOffset = 0;
    int numDeflated;
    Deflater defl = new Deflater(deflateLevel);
    defl.setStrategy(deflateStrategy);
    for (int y = getBoundsY1(); y <= getBoundsY2(); y++)
    {
      // fill row buffer
      rowBuffer[0] = (byte)FILTER_TYPE_NONE;
      fillRowBuffer(y, rowBuffer, 1);
      // give it to compressor
      defl.setInput(rowBuffer);
      // store compressed data in outBuffer
      do
      {
        numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset);
        outOffset += numDeflated;
        if (outOffset == outBuffer.length)
        {
          saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
          outOffset = 0;
        }
      }
      while (numDeflated > 0);
      setProgress(y - getBoundsY1(), getBoundsHeight());
    }
    // tell Deflater that it got all the input
    defl.finish();
    // retrieve remaining compressed data from defl to outBuffer 
    do
    {
      numDeflated = defl.deflate(outBuffer, outOffset, outBuffer.length - outOffset);
      outOffset += numDeflated;
      if (outOffset == outBuffer.length)
      {
        saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
        outOffset = 0;
      }
    }
    while (numDeflated > 0);
    // write final IDAT chunk if necessary
    if (outOffset > 0)
    {
      saveChunk(CHUNK_TYPE_IDAT,  outOffset, outBuffer);
    }
  }

  private void savePhysChunk() throws IOException
  {
    int dpiX = getDpiX();
    int dpiY = getDpiY();
    if (dpiX < 1 || dpiY < 1)
    {
      return;
    }
    byte[] data = new byte[9];
    int ppuX = (int)(dpiX * (100 / 2.54));
    int ppuY = (int)(dpiY * (100 / 2.54));
    ArrayConverter.setIntBE(data, 0, ppuX);
    ArrayConverter.setIntBE(data, 4, ppuY);
    data[8] = 1; // unit is the meter
    saveChunk(CHUNK_TYPE_PHYS, data.length, data);
  }

  private void savePlteChunk() throws IOException
  {
    if (colorType != COLOR_TYPE_INDEXED)
    {
      return;
    }
    Paletted8Image image = (Paletted8Image)getImage();
    Palette pal = image.getPalette();
    int numEntries = pal.getNumEntries();
    byte[] data = new byte[numEntries * 3];
    for (int i = 0, j = 0; i < numEntries; i++, j += 3)
    {
      data[j] = (byte)pal.getSample(RGBIndex.INDEX_RED, i);
      data[j + 1] = (byte)pal.getSample(RGBIndex.INDEX_GREEN, i);
      data[j + 2] = (byte)pal.getSample(RGBIndex.INDEX_BLUE, i);
    }
    saveChunk(CHUNK_TYPE_PLTE, data.length, data);
  }

  private void saveTextChunks() throws IOException
  {
    int index = 0;
    while (index < getNumComments())
    {
      String comment = getComment(index++);
      comment = "Comment\000" + comment;
      byte[] data = comment.getBytes("ISO-8859-1");
      saveChunk(CHUNK_TYPE_TEXT, data.length, data);
    }
  }

  private void saveTimeChunk() throws IOException
  {
    if (modification == null)
    {
      return;
    }
    byte[] data = new byte[7];
    ArrayConverter.setShortBE(data, 0, (short)modification.get(Calendar.YEAR));
    data[2] = (byte)(modification.get(Calendar.MONTH) + 1);
    data[3] = (byte)modification.get(Calendar.DAY_OF_MONTH);
    data[4] = (byte)modification.get(Calendar.HOUR_OF_DAY);
    data[5] = (byte)modification.get(Calendar.MINUTE);
    data[6] = (byte)modification.get(Calendar.SECOND);
    saveChunk(CHUNK_TYPE_TIME, data.length, data);
  }

  /**
   * Sets the compression level to be used with the underlying
   * {@link java.util.zip.Deflater} object which does the compression.
   * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_COMPRESSION}
   * is used. 
   * @param newLevel compression level, from 0 to 9, 0 being fastest
   *  and compressing worst and 9 offering highest compression and taking
   *  the most time
   */
  public void setCompressionLevel(int newLevel)
  {
    if (newLevel >= 0 && newLevel <= 9)
    {
      deflateLevel = newLevel;
    }
    else
    {
      throw new IllegalArgumentException("Compression level must be from 0..9; got " + newLevel);
    }
  }

  /**
   * Sets the compression strategy to be used with the underlying
   * {@link java.util.zip.Deflater} object which does the compression.
   * If no value is specified, {@link java.util.zip.Deflater#DEFAULT_STRATEGY}
   * is used. 
   * @param newStrategy one of Deflater's strategy values:
   *  {@link java.util.zip.Deflater#DEFAULT_STRATEGY},
   *  {@link java.util.zip.Deflater#FILTERED},
   *  {@link java.util.zip.Deflater#HUFFMAN_ONLY}
   */
  public void setCompressionStrategy(int newStrategy)
  {
    if (newStrategy == Deflater.FILTERED ||
        newStrategy == Deflater.DEFAULT_STRATEGY ||
        newStrategy == Deflater.HUFFMAN_ONLY)
    {
      deflateStrategy = newStrategy;
    }
    else
    {
      throw new IllegalArgumentException("Unknown compression strategy: " + newStrategy);
    }
  }

  /**
   * Sets the size of IDAT chunks generated when encoding.
   * If this method is never called, a default value of 32768 bytes (32 KB) is used.
   * Note that a byte array of the size of the value you specify here is allocated,
   * so make sure that you keep the value small enough to stay within a
   * system's memory.
   * <p>
   * Compressed image data is spread over several IDAT chunks by this codec.
   * The length of the compressed data of a complete image is known only after the complete image
   * has been encoded.
   * With PNG, that length value has to be stored before the compressed data as a chunk size value.
   * This codec is supposed to work with {@link java.io.OutputStream} objects,
   * so seeking back to adjust the chunk size value of an IDAT chunk is not
   * possible.
   * That's why all data of a chunk is compressed into a memory buffer.
   * Whenever the buffer gets full, it is written to output as an IDAT chunk.
   * <p>
   * Note that the last IDAT chunk may be smaller than the size defined here.
   * @param newSize size of encoding compressed data buffer
   */
  public void setEncodingIdatSize(int newSize)
  {
    if (newSize < 1)
    {
      throw new IllegalArgumentException("Minimum IDAT chunk size must be 1 or larger.");
    }
    encodingMinIdatSize = newSize;
  }

  public void setFile(String fileName, CodecMode codecMode) throws IOException, UnsupportedCodecModeException
  {
    if (codecMode == CodecMode.LOAD)
    {
      setInputStream(new BufferedInputStream(new FileInputStream(fileName)));
    }
    else
    {
      super.setFile(fileName, codecMode);
    }
  }

  /**
   * Sets date and time of last modification of the image to be stored in a PNG stream
   * when saving.
   * Make sure the argument object has UTC as time zone
   * (<a target="_top" href="http://www.w3.org/TR/PNG#C.tIME">as
   * demanded by the PNG specs)</a>.
   * If you want the current time and date, use
   * <code>new GregorianCalendar(new SimpleTimeZone(0, "UTC"))</code>
   * as parameter for this method.
   * @param time time of last modification of the image
   */
  public void setModification(Calendar time)
  {
    modification = time;
  }

  /**
   * Skips a number of bytes in the input stream.
   * @param num number of bytes to be skipped
   * @throws IOException if there were I/O errors
   */
  private void skip(long num) throws IOException
  {
    while (num > 0)
    {
      long numSkipped = in.skip(num);
      if (numSkipped > 0)
      {
        num -= numSkipped;
      }
    }
  }

  private void storeInterlacedAdam7(int pass, int y, byte[] buffer)
  {
    switch(colorType)
    {
      case(COLOR_TYPE_GRAY):
      {
        storeInterlacedAdam7Gray(pass, y, buffer);
        break;
      }
      case(COLOR_TYPE_RGB):
      {
        storeInterlacedAdam7Rgb(pass, y, buffer);
        break;
      }
      case(COLOR_TYPE_RGB_ALPHA):
      {
        storeInterlacedAdam7RgbAlpha(pass, y, buffer);
        break;
      }
      case(COLOR_TYPE_GRAY_ALPHA):
      {
        storeInterlacedAdam7GrayAlpha(pass, y, buffer);
        break;
      }
      case(COLOR_TYPE_INDEXED):
      {
        storeInterlacedAdam7Indexed(pass, y, buffer);
        break;
      }
    }
  }

  private void storeInterlacedAdam7Gray(int pass, int y, byte[] buffer)
  {
    int x = ADAM7_FIRST_COLUMN[pass];
    final int incr = ADAM7_COLUMN_INCREMENT[pass];
    final int x1 = getBoundsX1();
    final int x2 = getBoundsX2();
    int offset = 0;
    int numColumns = computeColumnsAdam7(pass);
    int numPackedBytes = computeBytesPerRow(numColumns);
    byte[] dest = new byte[numColumns + 7];
    switch(precision)
    {
      case(1):
      {
        BilevelImage bilevelImage = (BilevelImage)image;
        ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            if (dest[offset] == 0)
            {
              bilevelImage.putBlack(x - x1, y);
            }
            else
            {
              bilevelImage.putWhite(x - x1, y);
            }
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(2):
      {
        Gray8Image grayImage = (Gray8Image)image;
        ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            grayImage.putByteSample(x - x1, y, dest[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(4):
      {
        Gray8Image grayImage = (Gray8Image)image;
        ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            grayImage.putByteSample(x - x1, y, dest[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(8):
      {
        Gray8Image grayImage = (Gray8Image)image;
        while (x <= x2)
        {
          if (x >= x1)
          {
            grayImage.putSample(x - x1, y, buffer[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(16):
      {
        Gray16Image grayImage = (Gray16Image)image;
        while (x <= x2)
        {
          if (x >= x1)
          {
            int sample = (buffer[offset] & 0xff) << 8;
            sample |= (buffer[offset + 1] & 0xff);
            grayImage.putSample(x, y, sample);
          }
          x += incr;
          offset += 2;
        }
        break;
      }
    }
  }

  private void storeInterlacedAdam7GrayAlpha(int pass, int y, byte[] buffer)
  {
    int x = ADAM7_FIRST_COLUMN[pass];
    final int incr = ADAM7_COLUMN_INCREMENT[pass];
    final int x1 = getBoundsX1();
    final int x2 = getBoundsX2();
    int offset = 0;
    switch(precision)
    {
      case(8):
      {
        Gray8Image grayImage = (Gray8Image)image;
        while (x <= x2)
        {
          if (x >= x1)
          {
            grayImage.putSample(x - x1, y, buffer[offset]);
            // alpha
          }
          x += incr;
          offset += 2;
        }
        break;
      }
      case(16):
      {
        Gray16Image grayImage = (Gray16Image)image;
        while (x <= x2)
        {
          if (x >= x1)
          {
            int sample = (buffer[offset] & 0xff) << 8;
            sample |= (buffer[offset + 1] & 0xff);
            grayImage.putSample(x, y, sample);
            // store alpha
          }
          x += incr;
          offset += 4;
        }
        break;
      }
    }
  }

  private void storeInterlacedAdam7Indexed(int pass, int y, byte[] buffer)
  {
    Paletted8Image palImage = (Paletted8Image)image;
    int x = ADAM7_FIRST_COLUMN[pass];
    final int incr = ADAM7_COLUMN_INCREMENT[pass];
    final int x1 = getBoundsX1();
    final int x2 = getBoundsX2();
    int offset = 0;
    int numColumns = computeColumnsAdam7(pass);
    int numPackedBytes = computeBytesPerRow(numColumns);
    byte[] dest = new byte[numColumns + 7];
    switch(precision)
    {
      case(1):
      {
        ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            palImage.putByteSample(x - x1, y, dest[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(2):
      {
        ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            palImage.putByteSample(x - x1, y, dest[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(4):
      {
        ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, numPackedBytes);
        while (x <= x2)
        {
          if (x >= x1)
          {
            palImage.putByteSample(x - x1, y, dest[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
      case(8):
      {
        while (x <= x2)
        {
          if (x >= x1)
          {
            palImage.putSample(x - x1, y, buffer[offset]);
          }
          x += incr;
          offset++;
        }
        break;
      }
    }
  }

  private void storeInterlacedAdam7Rgb(int pass, int y, byte[] buffer)
  {
    int x = ADAM7_FIRST_COLUMN[pass];
    final int x1 = getBoundsX1();
    final int x2 = getBoundsX2();
    final int incr = ADAM7_COLUMN_INCREMENT[pass];
    int offset = 0;
    if (precision == 8)
    {
      RGB24Image rgbImage = (RGB24Image)image;
      while (x <= x2)
      {
        if (x >= x1)
        {
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]);
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]);
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]);
        }
        x += incr;
        offset += 3;
      }
    }
    else
    if (precision == 16)
    {
      RGB48Image rgbImage = (RGB48Image)image;
      while (x <= x2)
      {
        if (x >= x1)
        {
          int red = (buffer[offset] & 0xff) << 8;
          red |= buffer[offset + 1] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
 
          int green = (buffer[offset + 2] & 0xff) << 8;
          green |= buffer[offset + 3] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
   
          int blue = (buffer[offset + 4] & 0xff) << 8;
          blue |= buffer[offset + 5] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
        }
        x += incr;
        offset += 6;
      }
    }
  }

  private void storeInterlacedAdam7RgbAlpha(int pass, int y, byte[] buffer)
  {
    int x = ADAM7_FIRST_COLUMN[pass];
    final int x1 = getBoundsX1();
    final int x2 = getBoundsX2();
    final int incr = ADAM7_COLUMN_INCREMENT[pass];
    int offset = 0;
    if (precision == 8)
    {
      RGB24Image rgbImage = (RGB24Image)image;
      while (x <= x2)
      {
        if (x >= x1)
        {
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset]);
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset + 1]);
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset + 2]);
          // store alpha
        }
        x += incr;
        offset += 4;
      }
    }
    else
    if (precision == 16)
    {
      RGB48Image rgbImage = (RGB48Image)image;
      while (x <= x2)
      {
        if (x >= x1)
        {
          int red = (buffer[offset] & 0xff) << 8;
          red |= buffer[offset + 1] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
 
          int green = (buffer[offset + 2] & 0xff) << 8;
          green |= buffer[offset + 3] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
   
          int blue = (buffer[offset + 4] & 0xff) << 8;
          blue |= buffer[offset + 5] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
         
          // store alpha
        }
        x += incr;
        offset += 8;
      }
    }
  }


  private void storeNonInterlaced(int y, byte[] buffer)
  {
    switch(colorType)
    {
      case(COLOR_TYPE_GRAY):
      {
        storeNonInterlacedGray(y, buffer);
        break;
      }
      case(COLOR_TYPE_GRAY_ALPHA):
      {
        storeNonInterlacedGrayAlpha(y, buffer);
        break;
      }
      case(COLOR_TYPE_INDEXED):
      {
        storeNonInterlacedIndexed(y, buffer);
        break;
      }
      case(COLOR_TYPE_RGB):
      {
        storeNonInterlacedRgb(y, buffer);
        break;
      }
      case(COLOR_TYPE_RGB_ALPHA):
      {
        storeNonInterlacedRgbAlpha(y, buffer);
        break;
      }
    }
  }

  private void storeNonInterlacedGray(int y, byte[] buffer)
  {
    switch(precision)
    {
      case(1):
      {
        BilevelImage bilevelImage = (BilevelImage)image;
        int x1 = getBoundsX1();
        bilevelImage.putPackedBytes(0, y, getBoundsWidth(), buffer, x1 / 8, x1 % 8);
        break;
      }
      case(2):
      {
        Gray8Image grayImage = (Gray8Image)image;
        byte[] dest = new byte[width + 3];
        ArrayConverter.convertPacked2BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length);
        grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(4):
      {
        Gray8Image grayImage = (Gray8Image)image;
        byte[] dest = new byte[width + 1];
        ArrayConverter.convertPacked4BitIntensityTo8Bit(buffer, 0, dest, 0, buffer.length);
        grayImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(8):
      {
        Gray8Image grayImage = (Gray8Image)image;
        int offset = getBoundsX1();
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          grayImage.putSample(0, x++, y, buffer[offset++]);
          k--;
        }
        break;
      }
      case(16):
      {
        Gray16Image grayImage = (Gray16Image)image;
        int offset = getBoundsX1();
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          int sample = (buffer[offset++] & 0xff) << 8;
          sample |= (buffer[offset++] & 0xff);
          grayImage.putSample(x++, y, sample);
          k--;
        }
        break;
      }
    }
  }

  private void storeNonInterlacedGrayAlpha(int y, byte[] buffer)
  {
    switch(precision)
    {
      case(8):
      {
        Gray8Image grayImage = (Gray8Image)image;
        int offset = getBoundsX1();
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          grayImage.putSample(0, x++, y, buffer[offset++]);
          offset++; // skip alpha; should be stored in a TransparencyInformation object
          k--;
        }
        break;
      }
      case(16):
      {
        Gray16Image grayImage = (Gray16Image)image;
        int offset = getBoundsX1();
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          int sample = (buffer[offset++] & 0xff) << 8;
          sample |= (buffer[offset++] & 0xff);
          grayImage.putSample(x++, y, sample);
          offset += 2; // skip alpha;  TODO: store in TransparencyInformation object
          k--;
        }
        break;
      }
    }
  }

  private void storeNonInterlacedIndexed(int y, byte[] buffer)
  {
    Paletted8Image palImage = (Paletted8Image)image;
    switch(precision)
    {
      case(1):
      {
        byte[] dest = new byte[width + 7];
        ArrayConverter.decodePacked1Bit(buffer, 0, dest, 0, buffer.length);
        palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(2):
      {
        byte[] dest = new byte[width + 3];
        ArrayConverter.decodePacked2Bit(buffer, 0, dest, 0, buffer.length);
        palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(4):
      {
        byte[] dest = new byte[width + 1];
        ArrayConverter.decodePacked4Bit(buffer, 0, dest, 0, buffer.length);
        palImage.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(8):
      {
        int offset = getBoundsX1();
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          palImage.putSample(0, x++, y, buffer[offset++]);
          k--;
        }
        break;
      }
    }
  }

  private void storeNonInterlacedRgb(int y, byte[] buffer)
  {
    if (precision == 8)
    {
      RGB24Image rgbImage = (RGB24Image)image;
      int offset = getBoundsX1() * 3;
      int x = 0;
      int k = getBoundsWidth();
      while (k > 0)
      {
        rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]);
        rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]);
        rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]);
        x++;
        k--;
      }
    }
    else
    if (precision == 16)
    {
      RGB48Image rgbImage = (RGB48Image)image;
      int offset = getBoundsX1() * 6;
      int x = 0;
      int k = getBoundsWidth();
      while (k > 0)
      {
        int red = (buffer[offset++] & 0xff) << 8;
        red |= buffer[offset++] & 0xff;
        rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);

        int green = (buffer[offset++] & 0xff) << 8;
        green |= buffer[offset++] & 0xff;
        rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
 
        int blue = (buffer[offset++] & 0xff) << 8;
        blue |= buffer[offset++] & 0xff;
        rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
 
        x++;
        k--;
      }
    }
  }

  private void storeNonInterlacedRgbAlpha(int y, byte[] buffer)
  {
    switch(precision)
    {
      case(8):
      {
        RGB24Image rgbImage = (RGB24Image)image;
        int offset = getBoundsX1() * 3;
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, buffer[offset++]);
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, buffer[offset++]);
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, buffer[offset++]);
          offset++; // skip alpha; TODO: store in TransparencyInformation object
          x++;
          k--;
        }
        break;
      }
      case(16):
      {
        RGB48Image rgbImage = (RGB48Image)image;
        int offset = getBoundsX1() * 8;
        int x = 0;
        int k = getBoundsWidth();
        while (k > 0)
        {
          int red = (buffer[offset++] & 0xff) << 8;
          red |= buffer[offset++] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_RED, x, y, red);
   
          int green = (buffer[offset++] & 0xff) << 8;
          green |= buffer[offset++] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_GREEN, x, y, green);
   
          int blue = (buffer[offset++] & 0xff) << 8;
          blue |= buffer[offset++] & 0xff;
          rgbImage.putSample(RGB24Image.INDEX_BLUE, x, y, blue);
   
          offset += 2; // skip alpha; TODO: store in TransparencyInformation object
          x++;
          k--;
        }
        break;
      }
    }
  }

  public String suggestFileExtension(PixelImage image)
  {
    return ".png";
  }
}
TOP

Related Classes of net.sourceforge.jiu.codecs.PNGCodec

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.