Package net.sourceforge.jiu.codecs

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

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

package net.sourceforge.jiu.codecs;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.RandomAccessFile;
import net.sourceforge.jiu.codecs.ImageCodec;
import net.sourceforge.jiu.codecs.InvalidFileStructureException;
import net.sourceforge.jiu.codecs.UnsupportedTypeException;
import net.sourceforge.jiu.codecs.WrongFileFormatException;
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.ByteChannelImage;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.MemoryBilevelImage;
import net.sourceforge.jiu.data.MemoryGray8Image;
import net.sourceforge.jiu.data.MemoryPaletted8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;
import net.sourceforge.jiu.ops.WrongParameterException;
import net.sourceforge.jiu.util.ArrayConverter;
import net.sourceforge.jiu.util.SeekableByteArrayOutputStream;

/**
* A codec to read and write image files in the native image file format of
* <a target="_top" href="http://www.palmos.com/">Palm OS</a>,
* an operating system for handheld devices.
*
* <h3>Supported file types when loading</h3>
* This codec reads uncompressed, scan line compressed and RLE compressed Palm files
* with bit depths of 1, 2, 4, 8 and 16 bits per pixel.
* Not supported are the Packbits compression algorithm or any color depths other
* then the aforementioned.
*
* <h3>Supported image types when saving</h3>
* Compression types <em>Uncompressed</em>, <em>Scan line</em> and <em>RLE</em> are written.
* When saving an image as a Palm, the image data classes will be mapped to file types as follows:
* <ul>
* <li>{@link BilevelImage}: will be saved as a 1 bit per pixel, monochrome file.</li>
* <li>{@link Gray8Image}: will be saved as an 8 bits per pixel file with a custom
*  palette which will contain the 256 shades of gray from black - (0, 0, 0) - to
*  white - (255, 255, 255).</li>
* <li>{@link Paletted8Image}: it is first checked if the image is using the
*  Palm system 8 bits per pixel palette. If so, an 8 bits per pixel file
*  with no custom palette is written, otherwise an 8 bits per pixel file
*  with a custom palette (of the original length) is written.
* </li>
* <li>{@link RGB24Image}: will be saved as a 16 bits per pixel, direct color file.
*   Some information will get lost when converting from 24 to 16 bits per pixel.
*   Instead of 256 shades for each red, green and blue (and thus, 256<sup>3</sup> = 16,777,216
*   possible colors) the resulting file will only
*   use 32 shades of red and blue and 64 shades of green (65,536 possible colors).</li>
* </ul>
*
* <h3>I/O objects</h3>
* This codec supports all the I/O classes that are considered in ImageCodec.
* If you save images and want a correct <em>compressed size</em> field
* in the resulting Palm file, make sure to give a RandomAccessFile object to
* the codec.
* Or simply use {@link #setFile} which does that automatically.
*
* <h3>File extension</h3>
* This codec suggests <code>.palm</code> as file extension for this file format.
* This is by no means official, but I find it helpful.
*
* <h3>Transparency information</h3>
* The transparency index in a Palm file is saved and loaded, but a loaded index
* is not stored in the image object as there is no support for transparency information of
* any kind in PixelImage yet.
* The RGB transparency color that is present in a file only in direct color mode
* is read but not written.
*
* <h3>Bounds</h3>
* The bounds concept of ImageCodec is supported so that you can load or save
* only part of an image.
*
* <h3>Open questions on the Palm file format</h3>
* <ul>
* <li>How does Packbits compression work? Where can I get sample files or a Windows
*  converter that writes those?</li>
* <li>How is FLAG_4_BYTE_FIELDS interpreted? When are four byte fields used?</li>
* <li>When loading a 4 bpp Palm image file without a custom palette,
*  how is the decoder supposed to know whether to take the predefined
*  color or grayscale palette with 16 entries?</li>
* </ul>
*
* <h3>Known problems</h3>
* <ul>
* <li>Unfortunately, the Palm image file format does not include a signature that
*  makes it easy to identify such a file. Various checks on allowed combinations of
*  color depth, compression type etc. will prevent the codec from trying to interpret
*  all files as Palm image files, but there is still a probability of false
*  identification.</li>
* </ul>
*
* <h3>Usage examples</h3>
* Load an image from a Palm image file:
* <pre>
* PalmCodec codec = new PalmCodec();
* codec.setFile("test.palm", CodecMode.LOAD);
* codec.process();
* PixelImage image = codec.getImage();
* codec.close();
* </pre>
* Save an image to a Palm file using RLE compression:
* <pre>
* PalmCodec codec = new PalmCodec();
* codec.setImage(image);
* codec.setCompression(PalmCodec.COMPRESSION_RLE);
* codec.setFile("out.palm", CodecMode.SAVE);
* codec.process();
* codec.close();
* </pre>
*
* <h3>Background</h3>
* The code is based on:
* <ul>
* <li>the specification
* <a target="_top" href="http://www.kawt.de/doc/palmimage.html">Palm
* Native Image Format</a>,</li>
* <li>the source code of the utilities <code>pnmtopalm</code> and
* <code>palmtopnm</code> that are part of the
* <a href="http://netpbm.sourceforge.net" target="_top">Netpbm</a> package,</li>
* <li><a href="http://oasis.palm.com/dev/kb/papers/1831.cfm" target="_top">Palm OS Compressed Bitmaps</a> by Ken Krugler,
*  a Palm Developer Knowledge Base article on the scan line compression algorithm and</li>
* <li><a href="http://oasis.palm.com/dev/kb/manuals/sdk/Bitmap.cfm" target="_top">Palm OS Bitmaps</a>,
*  also part of the Palm Developer Knowledge Base, contains general information on the
*  structure of Palm images.</li>
* </ul>
* I also received helpful feedback and test images from Bill Janssen.
*
* @author Marco Schmidt
*/
public class PalmCodec extends ImageCodec
{
  /**
   * Constant for compression type <em>Uncompressed</em>.
   */
  public static final int COMPRESSION_NONE = 255;

  /**
   * Constant for compression type <em>Packbits</em>.
   */
  public static final int COMPRESSION_PACKBITS = 2;

  /**
   * Constant for compression type <em>RLE (run length encoding)</em>.
   */
  public static final int COMPRESSION_RLE = 1;

  /**
   * Constant for compression type <em>Scanline</em>.
   */
  public static final int COMPRESSION_SCANLINE = 0;

  private static final int FLAG_COMPRESSED = 0x8000;
  private static final int FLAG_COLOR_TABLE = 0x4000;
  private static final int FLAG_TRANSPARENCY = 0x2000;
  //private static final int FLAG_INDIRECT = 0x1000;
  //private static final int FLAG_FOR_SCREEN = 0x0800;
  private static final int FLAG_DIRECT_COLOR = 0x0400;
  //private static final int FLAG_4_BYTE_FIELD = 0x0200;

  // following the Palm OS default palettes
  // instead of short we could use byte but that would require converting
  // all values > 127 to byte representation (-128 .. 128)

  private static final short[][] PALM_SYSTEM_PALETTE_4_GRAY = new short[][]
  {
    { 255, 255, 255}, { 192, 192, 192}, { 128, 128, 128 }, {   0,   0,   0 }
  };

  private static final short[][] PALM_SYSTEM_PALETTE_16_COLOR = new short[][]
  {
    { 255, 255, 255}, { 128, 128, 128 }, { 128,   0,   0 }, { 128, 128,   0 },
    {   0, 128,   0}, {   0, 128, 128 }, {   0,   0, 128 }, { 128,   0, 128 },
    { 255,   0, 255}, { 192, 192, 192 }, { 255,   0,   0 }, { 255, 255,   0 },
    {   0, 255,   0}, {   0, 255, 255 }, {   0,   0, 255 }, {   0,   0,   0 }
  };

  private static final short[][] PALM_SYSTEM_PALETTE_16_GRAY = new short[][]
  {
    { 255, 255, 255}, { 238, 238, 238 }, { 221, 221, 221 }, { 204, 204, 204 },
    { 187, 187, 187}, { 170, 170, 170 }, { 153, 153, 153 }, { 136, 136, 136 },
    { 119, 119, 119}, { 102, 102, 102 }, 858585 }, 686868 },
    515151}, 343434 }, 171717 }, {   0,   0,   0 }
  };

  private static final short[][] PALM_SYSTEM_PALETTE_256 = new short[][]
  {
    { 255, 255, 255 }, { 255, 204, 255 }, { 255, 153, 255 }, { 255, 102, 255 },
    { 25551, 255 }, { 255,   0, 255 }, { 255, 255, 204 }, { 255, 204, 204 },
    { 255, 153, 204 }, { 255, 102, 204 }, { 25551, 204 }, { 255,   0, 204 },
    { 255, 255, 153 }, { 255, 204, 153 }, { 255, 153, 153 }, { 255, 102, 153 },
    { 25551, 153 }, { 255,   0, 153 }, { 204, 255, 255 }, { 204, 204, 255 },
    { 204, 153, 255 }, { 204, 102, 255 }, { 20451, 255 }, { 204,   0, 255 },
    { 204, 255, 204 }, { 204, 204, 204 }, { 204, 153, 204 }, { 204, 102, 204 },
    { 20451, 204 }, { 204,   0, 204 }, { 204, 255, 153 }, { 204, 204, 153 },
    { 204, 153, 153 }, { 204, 102, 153 }, { 20451, 153 }, { 204,   0, 153 },
    { 153, 255, 255 }, { 153, 204, 255 }, { 153, 153, 255 }, { 153, 102, 255 },
    { 15351, 255 }, { 153,   0, 255 }, { 153, 255, 204 }, { 153, 204, 204 },
    { 153, 153, 204 }, { 153, 102, 204 }, { 15351, 204 }, { 153,   0, 204 },
    { 153, 255, 153 }, { 153, 204, 153 }, { 153, 153, 153 }, { 153, 102, 153 },
    { 15351, 153 }, { 153,   0, 153 }, { 102, 255, 255 }, { 102, 204, 255 },
    { 102, 153, 255 }, { 102, 102, 255 }, { 10251, 255 }, { 102,   0, 255 },
    { 102, 255, 204 }, { 102, 204, 204 }, { 102, 153, 204 }, { 102, 102, 204 },
    { 10251, 204 }, { 102,   0, 204 }, { 102, 255, 153 }, { 102, 204, 153 },
    { 102, 153, 153 }, { 102, 102, 153 }, { 10251, 153 }, { 102,   0, 153 },
    51, 255, 255 }, 51, 204, 255 }, 51, 153, 255 }, 51, 102, 255 },
    5151, 255 }, 51,   0, 255 }, 51, 255, 204 }, 51, 204, 204 },
    51, 153, 204 }, 51, 102, 204 }, 5151, 204 }, 51,   0, 204 },
    51, 255, 153 }, 51, 204, 153 }, 51, 153, 153 }, 51, 102, 153 },
    5151, 153 }, 51,   0, 153 }, {   0, 255, 255 }, {   0, 204, 255 },
    {   0, 153, 255 }, {   0, 102, 255 }, {   051, 255 }, {   0,   0, 255 },
    {   0, 255, 204 }, {   0, 204, 204 }, {   0, 153, 204 }, {   0, 102, 204 },
    {   051, 204 }, {   0,   0, 204 }, {   0, 255, 153 }, {   0, 204, 153 },
    {   0, 153, 153 }, {   0, 102, 153 }, {   051, 153 }, {   0,   0, 153 },
    { 255, 255, 102 }, { 255, 204, 102 }, { 255, 153, 102 }, { 255, 102, 102 },
    { 25551, 102 }, { 255,   0, 102 }, { 255, 25551 }, { 255, 20451 },
    { 255, 15351 }, { 255, 10251 }, { 2555151 }, { 255,   051 },
    { 255, 255,   0 }, { 255, 204,   0 }, { 255, 153,   0 }, { 255, 102,   0 },
    { 25551,   0 }, { 255,   0,   0 }, { 204, 255, 102 }, { 204, 204, 102 },
    { 204, 153, 102 }, { 204, 102, 102 }, { 20451, 102 }, { 204,   0, 102 },
    { 204, 25551 }, { 204, 20451 }, { 204, 15351 }, { 204, 10251 },
    { 2045151 }, { 204,   051 }, { 204, 255,   0 }, { 204, 204,   0 },
    { 204, 153,   0 }, { 204, 102,   0 }, { 20451,   0 }, { 204,   0,   0 },
    { 153, 255, 102 }, { 153, 204, 102 }, { 153, 153, 102 }, { 153, 102, 102 },
    { 15351, 102 }, { 153,   0, 102 }, { 153, 25551 }, { 153, 20451 },
    { 153, 15351 }, { 153, 10251 }, { 1535151 }, { 153,   051 },
    { 153, 255,   0 }, { 153, 204,   0 }, { 153, 153,   0 }, { 153, 102,   0 },
    { 15351,   0 }, { 153,   0,   0 }, { 102, 255, 102 }, { 102, 204, 102 },
    { 102, 153, 102 }, { 102, 102, 102 }, { 10251, 102 }, { 102,   0, 102 },
    { 102, 25551 }, { 102, 20451 }, { 102, 15351 }, { 102, 10251 },
    { 1025151 }, { 102,   051 }, { 102, 255,   0 }, { 102, 204,   0 },
    { 102, 153,   0 }, { 102, 102,   0 }, { 10251,   0 }, { 102,   0,   0 },
    51, 255, 102 }, 51, 204, 102 }, 51, 153, 102 }, 51, 102, 102 },
    5151, 102 }, 51,   0, 102 }, 51, 25551 }, 51, 20451 },
    51, 15351 }, 51, 10251 }, 515151 }, 51,   051 },
    51, 255,   0 }, 51, 204,   0 }, 51, 153,   0 }, 51, 102,   0 },
    5151,   0 }, 51,   0,   0 }, {   0, 255, 102 }, {   0, 204, 102 },
    {   0, 153, 102 }, {   0, 102, 102 }, {   051, 102 }, {   0,   0, 102 },
    {   0, 25551 }, {   0, 20451 }, {   0, 15351 }, {   0, 10251 },
    {   05151 }, {   0,   0 , 51 }, {   0, 255,   0 }, {   0, 204,   0 },
    {   0, 153,   0 }, {   0, 102,   0 }, {   051,   0 }, 171717 },
    343434 }, 686868 }, 858585 }, { 119, 119, 119 },
    { 136, 136, 136 }, { 170, 170, 170 }, { 187, 187, 187 }, { 221, 221, 221 },
    { 238, 238, 238 }, { 192, 192, 192 }, { 128,   0,   0 }, { 128,   0, 128 },
    {   0, 128,   0 }, {   0, 128, 128 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 },
    {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }, {   0,   0,   0 }
  };

  private int bitsPerPixel;
  private int blueBits;
  private int bytesPerRow;
  private int compression;
  private long compressedDataOffset;
  private int flags;
  private int greenBits;
  private int height;
  private Palette palette;
  //private int nextImageOffset;
  private int redBits;
  private byte[] rgb;
  private byte[] transColor;
  private int transparencyIndex = -1;
  private int version;
  private int width;

  private static Palette createPalette(short[][] data)
  {
    Palette result = new Palette(data.length);
    for (int i = 0; i < data.length; i++)
    {
      result.put(i, data[i][0], data[i][1], data[i][2]);
    }
    return result;
  }

  /**
   * Creates the 2 bits per pixel Palm system palette with grayscale values.
   * This palette is used when no custom palette is defined in a 2 bpp image.
   * @return Palm's default palette for 2 bits per pixel (grayscale), with 4 entries
   */
  public static Palette createSystem2BitGrayscalePalette()
  {
    return createPalette(PALM_SYSTEM_PALETTE_4_GRAY);
  }

  /**
   * Creates the 4 bits per pixel Palm system palette with color values.
   * This palette (or the 4 bpp grayscale palette) is used when no custom palette is defined in a 4 bpp image.
   * @return Palm's default palette for 4 bits per pixel (color), with 16 entries
   */
  public static Palette createSystem4BitColorPalette()
  {
    return createPalette(PALM_SYSTEM_PALETTE_16_COLOR);
  }

  /**
   * Creates the 4 bits per pixel Palm system palette with grayscale values.
   * This palette (or the 4 bpp color palette) is used when no custom palette is defined in a 4 bpp image.
   * @return Palm's default palette for 4 bits per pixel (grayscale), with 16 entries
   */
  public static Palette createSystem4BitGrayscalePalette()
  {
    return createPalette(PALM_SYSTEM_PALETTE_16_GRAY);
  }

  /**
   * Creates the 8 bits per pixel Palm system palette.
   * This palette is used when no custom palette is defined in an 8 bpp image.
   * @return Palm's default palette for 8 bits per pixel, with 256 entries
   */
  public static Palette createSystem8BitPalette()
  {
    return createPalette(PALM_SYSTEM_PALETTE_256);
  }

  /**
   * Returns the Palm compression method.
   * This should be one of the COMPRESSION_xyz constants of this class.
   * @return integer value with the compression method (found in a file when
   *  loading or to be used for saving)
   * @see #setCompression
   */
  public int getCompression()
  {
    return compression;
  }

  public String getFormatName()
  {
    return "Palm image file format";
  }

  public String[] getMimeTypes()
  {
    return null;
  }

  /**
   * Returns the transpareny index if one is available ({@link #hasTransparencyIndex}
   * returns <code>true</code>) or an undefined value otherwise.
   * @see #hasTransparencyIndex
   * @see #removeTransparencyIndex
   * @see #setTransparencyIndex
   */
  public int getTransparencyIndex()
  {
    return transparencyIndex;
  }

  /**
   * Returns whether a transpareny index is available and can be
   * retrieved via {@link #getTransparencyIndex}.
   * @return transparency index, a positive value that is a valid index into the palette
   * @see #getTransparencyIndex
   * @see #removeTransparencyIndex
   * @see #setTransparencyIndex
   */
  public boolean hasTransparencyIndex()
  {
    return transparencyIndex >= 0;
  }

  private void invertBilevelData(byte[] row)
  {
    if (row != null)
    {
      for (int i = 0; i < row.length; i++)
      {
        row[i] = (byte)~row[i];
      }
    }
  }

  private static boolean isEqualPalette(Palette palette, short[][] data)
  {
    if (palette == null || data == null)
    {
      return false;
    }
    if (palette.getNumEntries() != data.length)
    {
      return false;
    }
    for (int i = 0; i < data.length; i++)
    {
      int red = palette.getSample(RGBIndex.INDEX_RED, i);
      int green = palette.getSample(RGBIndex.INDEX_GREEN, i);
      int blue = palette.getSample(RGBIndex.INDEX_BLUE, i);
      short[] color = data[i];
      if (color[0] != red || color[1] != green || color[2] != blue)
      {
        return false;
      }
    }
    return true;
  }

  public boolean isLoadingSupported()
  {
    return true;
  }

  /**
   * Returns if the argument palette is the Palm system grayscale palette
   * with 4 entries.
   * @param palette to be checked
   * @see #createSystem2BitGrayscalePalette
   */
  public static boolean isPalmSystemPaletteGray4(Palette palette)
  {
    return isEqualPalette(palette, PALM_SYSTEM_PALETTE_4_GRAY);
  }

  /**
   * Returns if the argument palette is the Palm system grayscale palette
   * with 16 entries.
   * @param palette to be checked
   * @see #createSystem4BitGrayscalePalette
   */
  public static boolean isPalmSystemPaletteGray16(Palette palette)
  {
    return isEqualPalette(palette, PALM_SYSTEM_PALETTE_16_GRAY);
  }

  /**
   * Returns if the argument palette is the Palm system color palette
   * with 16 entries.
   * @param palette to be checked
   * @see #createSystem4BitColorPalette
   */
  public static boolean isPalmSystemPaletteColor16(Palette palette)
  {
    return isEqualPalette(palette, PALM_SYSTEM_PALETTE_16_COLOR);
  }

  /**
   * Returns if the argument palette is the Palm system palette
   * with 256 colors.
   * @param palette to be checked
   * @see #createSystem8BitPalette
   * @return if the argument is an 8 bits per pixel Palm system palette
   */
  public static boolean isPalmSystemPalette256(Palette palette)
  {
    return isEqualPalette(palette, PALM_SYSTEM_PALETTE_256);
  }

  public boolean isSavingSupported()
  {
    return true;
  }

  private void load() throws
    InvalidFileStructureException,
    IOException,
    OperationFailedException,
    UnsupportedTypeException,
    WrongFileFormatException
  {
    DataInput in = getInputAsDataInput();
    loadHeader(in);
    loadPalette(in);
    loadImage(in);
  }

  private void loadHeader(DataInput in) throws
    InvalidFileStructureException,
    IOException,
    UnsupportedTypeException,
    WrongFileFormatException
  {
    width = in.readShort() & 0xffff;
    height = in.readShort() & 0xffff;
    bytesPerRow = in.readShort() & 0xffff;
    flags = in.readShort() & 0xffff;
    bitsPerPixel = in.readUnsignedByte();
    version = in.readUnsignedByte();
    //nextImageOffset = in.readShort() & 0xffff;
    in.readShort();
    transparencyIndex = in.readUnsignedByte() & 0xffff;
    compression = in.readUnsignedByte() & 0xffff;
    in.skipBytes(2); // reserved
    if ((flags & FLAG_COMPRESSED) == 0)
    {
      compression = COMPRESSION_NONE;
    }
    boolean unsupportedDirectColor = false;
    if ((flags & FLAG_DIRECT_COLOR) != 0)
    {
      // read direct color information (8 bytes)
      redBits = in.readUnsignedByte();
      greenBits = in.readUnsignedByte();
      blueBits = in.readUnsignedByte();
      unsupportedDirectColor = redBits != 5 || greenBits != 6 || blueBits != 5;
      in.skipBytes(2);
      transColor = new byte[3];
      in.readFully(transColor);
    }
    if (width < 1 || height < 1 ||
        unsupportedDirectColor ||
        (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 4 && bitsPerPixel != 8 && bitsPerPixel != 16) ||
        (compression != COMPRESSION_NONE && compression != COMPRESSION_RLE && compression != COMPRESSION_SCANLINE))
    {
      throw new WrongFileFormatException("Not a file in Palm image file format.");
    }
    /*System.out.println("width=" + width + ", height=" + height + ", bytes per row=" +
      bytesPerRow + ", flags=" + flags + ", bpp=" + bitsPerPixel + ", version=" +
      version + ", palette=" + (((flags & FLAG_COLOR_TABLE) != 0) ? "y" : "n") +
      ", transparent=" + transparencyIndex + ", compression=" + compression);*/
  }

  private void loadImage(DataInput in) throws
    InvalidFileStructureException,
    IOException,
    UnsupportedTypeException,
    WrongFileFormatException,
    WrongParameterException
  {
    setBoundsIfNecessary(width, height);
    checkBounds(width, height);
    PixelImage image = getImage();
    /* if there is no image to be reused (image == null), create one;
       otherwise check if the provided image is of the right type
       and throw an exception if not */
    if (palette != null)
    {
      // paletted image
      if (image == null)
      {
        image = new MemoryPaletted8Image(getBoundsWidth(), getBoundsHeight(), palette);
      }
      else
      {
        if (!(image instanceof Paletted8Image))
        {
          throw new WrongParameterException("Image to be used for loading must be paletted for this file.");
        }
        ((Paletted8Image)image).setPalette(palette);
      }
    }
    else
    {
      switch(bitsPerPixel)
      {
        case(1): // bilevel image (black and white)
        {
          if (image == null)
          {
            image = new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight());
          }
          else
          {
            if (!(image instanceof BilevelImage))
            {
              throw new WrongParameterException("Image to be used for " +
                "loading must implement BilevelImage for this file.");
            }
          }
          break;
        }
        case(16): // RGB direct color
        {
          if (image == null)
          {
            image = new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight());
          }
          else
          {
            if (!(image instanceof RGB24Image))
            {
              throw new WrongParameterException("Image to be used for " +
                "loading must implement RGB24Image.");
            }
          }
          rgb = new byte[width * 3];
          break;
        }
        default: // grayscale, 2, 4 or 8 bits per pixel
        {
          if (image == null)
          {
            image = new MemoryGray8Image(getBoundsWidth(), getBoundsHeight());
          }
          else
          {
            if (!(image instanceof Gray8Image))
            {
              throw new WrongParameterException("Image to be used for " +
                "loading must implement Gray8Image for this file.");
            }
          }
        }
      }
    }
    setImage(image);
    // check if image has the correct pixel resolution
    if (image.getWidth() != getBoundsWidth() || image.getHeight() != getBoundsHeight())
    {
      throw new WrongParameterException("Image to be reused has wrong resolution (must have " +
        getBoundsWidth() + " x " + getBoundsHeight() + " pixels).");
    }
    loadImageData(in);
  }

  private void loadImageData(DataInput in) throws
    InvalidFileStructureException,
    IOException
  {
    PixelImage image = getImage();
    // if compression is used, read a short with the compressed data size
    if (compression != COMPRESSION_NONE)
    {
      //int compressedDataSize = in.readShort() & 0xffff;
      in.readShort();
    }
    byte[] row = new byte[bytesPerRow];
    final int NUM_ROWS = getBoundsY2() + 1;
    for (int y = 0; y < NUM_ROWS; y++)
    {
      switch(compression)
      {
        case(COMPRESSION_NONE):
        {
          in.readFully(row, 0, bytesPerRow);
          break;
        }
        case(COMPRESSION_RLE):
        {
          int index = 0;
          do
          {
            int num = in.readUnsignedByte();
            if (num < 1 || index + num > bytesPerRow)
            {
              String message = "At index=" + index + ", y=" + y + " there is a run length of " + num;
              System.err.println("ERROR decoding RLE: " + message);
              throw new InvalidFileStructureException(message);
            }
            byte value = in.readByte();
            while (num-- > 0)
            {
              row[index++] = value;
            }
          }
          while (index < bytesPerRow);
          break;
        }
        case(COMPRESSION_SCANLINE):
        {
          int index = 0;
          int pixelMask = 0;
          int mask = 0;
          do
          {
            if (mask == 0)
            {
              pixelMask = in.readUnsignedByte();
              mask = 0x80;
            }
            if ((pixelMask & mask) == 0)
            {
              index++;
            }
            else
            {
              row[index++] = in.readByte();
            }
            mask >>= 1;
          }
          while (index < bytesPerRow);
          break;
        }
        case(COMPRESSION_PACKBITS):
        {
          // compression algorithm unknown, thus not implemented
          // this statement cannot be reached, the codec makes
          // sure that an exception gets thrown when the packbits
          // algorithm is actually encountered in a file;
          // if you have a description of the algorithm, please
          // contact the JIU maintainers
          break;
        }
      }
      store(image, y, row);
      setProgress(y, NUM_ROWS);
    }
  }

  private void loadPalette(DataInput in) throws
    InvalidFileStructureException,
    IOException,
    UnsupportedTypeException,
    WrongFileFormatException
  {
    if ((flags & FLAG_COLOR_TABLE) == 0)
    {
      switch(bitsPerPixel)
      {
        case(2):
        {
          palette = createSystem2BitGrayscalePalette();
          break;
        }
        case(4):
        {
          palette = createSystem4BitGrayscalePalette(); // or color?
          break;
        }
        case(8):
        {
          palette = createSystem8BitPalette();
          break;
        }
      }
      return;
    }
    int numEntries = in.readShort() & 0xffff;
    if (numEntries < 1 || numEntries > 256)
    {
      throw new WrongFileFormatException("Not a Palm image file, invalid number of palette entries: "  + numEntries);
    }
    palette = new Palette(numEntries, 255);
    for (int i = 0; i < numEntries; i++)
    {
      //int reserved = in.readUnsignedByte();
      in.readUnsignedByte();
      int red = in.readUnsignedByte();
      int green = in.readUnsignedByte();
      int blue = in.readUnsignedByte();
      palette.putSample(RGBIndex.INDEX_RED, i, red);
      palette.putSample(RGBIndex.INDEX_GREEN, i, green);
      palette.putSample(RGBIndex.INDEX_BLUE, i, blue);
    }
  }

  public void process() throws
    InvalidFileStructureException,
    MissingParameterException,
    OperationFailedException,
    WrongParameterException
  {
    try
    {
      initModeFromIOObjects();
      if (getMode() == CodecMode.LOAD)
      {
        load();
      }
      else
      if (getMode() == CodecMode.SAVE)
      {
        save();
      }
      else
      {
        throw new WrongParameterException("Could find neither objects for loading nor for saving.");
      }
    }
    catch (IOException ioe)
    {
      throw new OperationFailedException("I/O error in Palm codec: " + ioe.toString());
    }
  }

  /**
   * Removes the transparency index if one has been set.
   * @see #getTransparencyIndex
   * @see #hasTransparencyIndex
   * @see #setTransparencyIndex
   */
  public void removeTransparencyIndex()
  {
    transparencyIndex = -1;
  }

  private void save() throws
    IOException,
    OperationFailedException,
    UnsupportedTypeException
  {
    // get image, set bounds if necessary and check existing bounds
    PixelImage image = getImage();
    if (image == null)
    {
      throw new MissingParameterException("Need image to save.");
    }
    setBoundsIfNecessary(image.getWidth(), image.getHeight());
    checkBounds(image.getWidth(), image.getHeight());
    // get output object
    DataOutput out = getOutputAsDataOutput();
    if (out == null)
    {
      throw new MissingParameterException("Could not get DataOutput object when saving in Palm file format.");
    }
    // initialize fields to be written to the header
    width = getBoundsWidth();
    height = getBoundsHeight();
    flags = 0;
    if (hasTransparencyIndex())
    {
      flags |= FLAG_TRANSPARENCY;
    }
    if (compression != COMPRESSION_NONE)
    {
      flags |= FLAG_COMPRESSED;
    }
    version = 0;
    if (bitsPerPixel > 1)
    {
      version = 1;
    }
    if (hasTransparencyIndex() || compression != COMPRESSION_NONE)
    {
      version = 2;
    }
    //nextImageOffset = 0;
    compressedDataOffset = 0;
    // check image types
    if (image instanceof BilevelImage)
    {
      save(out, (BilevelImage)image);
    }
    else
    if (image instanceof Gray8Image)
    {
      save(out, (Gray8Image)image);
    }
    else
    if (image instanceof Paletted8Image)
    {
      save(out, (Paletted8Image)image);
    }
    else
    if (image instanceof RGB24Image)
    {
      save(out, (RGB24Image)image);
    }
    else
    {
      throw new UnsupportedTypeException("Unsupported image type: " + image.getClass().getName());
    }
  }

  private void save(DataOutput out, BilevelImage image) throws IOException
  {
    bytesPerRow = (width + 7) / 8;
    if ((bytesPerRow % 2) == 1)
    {
      bytesPerRow++;
    }
    bitsPerPixel = 1;
    setCorrectVersion();
    saveHeader(out);
    byte[] row = new byte[bytesPerRow];
    byte[] prev = null;
    if (compression == COMPRESSION_SCANLINE)
    {
      prev = new byte[row.length];
    }
    final int X1 = getBoundsX1();
    final int Y1 = getBoundsY1();
    for (int y = 0; y < height; y++)
    {
      image.getPackedBytes(X1, y + Y1, width, row, 0, 0);
      invertBilevelData(row);
      saveRow(out, y == 0, row, prev);
      if (compression == COMPRESSION_SCANLINE)
      {
        System.arraycopy(row, 0, prev, 0, row.length);
      }
      setProgress(y, height);
    }
    saveFinalCompressedSize(out);
  }

  private void save(DataOutput out, Gray8Image image) throws IOException
  {
    bytesPerRow = width;
    if ((bytesPerRow % 2) == 1)
    {
      bytesPerRow++;
    }
    bitsPerPixel = 8;
    flags |= FLAG_COLOR_TABLE;
    setCorrectVersion();
    saveHeader(out);
    out.writeShort(256); // palette length
    for (int i = 0; i < 256; i++)
    {
      out.writeByte(0); // reserved
      out.writeByte(i); // red
      out.writeByte(i); // green
      out.writeByte(i); // blue
    }
    compressedDataOffset += 2 + 4 * 256;
    saveInitialCompressedSize(out);
    byte[] row = new byte[width];
    byte[] prev = null;
    if (compression == COMPRESSION_SCANLINE)
    {
      prev = new byte[width];
    }
    final int X1 = getBoundsX1();
    final int Y1 = getBoundsY1();
    for (int y = 0; y < height; y++)
    {
      image.getByteSamples(0, X1, y + Y1, width, 1, row, 0);
      saveRow(out, y == 0, row, prev);
      if (compression == COMPRESSION_SCANLINE)
      {
        System.arraycopy(row, 0, prev, 0, row.length);
      }
      setProgress(y, height);
    }
    saveFinalCompressedSize(out);
  }

  private void save(DataOutput out, Paletted8Image image) throws IOException
  {
    Palette palette = image.getPalette();
    boolean system256Palette = isPalmSystemPalette256(palette);
    boolean system16GrayPalette = isPalmSystemPaletteGray16(palette);
    boolean system16ColorPalette = isPalmSystemPaletteColor16(palette);
    boolean system4GrayPalette = isPalmSystemPaletteGray4(palette);
    boolean customPalette = !(system256Palette || system16GrayPalette || system16ColorPalette || system4GrayPalette);
    if (customPalette)
    {
      flags |= FLAG_COLOR_TABLE;
    }
    // determine bits per pixel, bytesPerRow
    if (palette.getNumEntries() <= 4)
    {
      bitsPerPixel = 2;
      bytesPerRow = (width + 3) / 4;
    }
    else
    if (palette.getNumEntries() <= 16)
    {
      bitsPerPixel = 4;
      bytesPerRow = (width + 1) / 2;
    }
    else
    {
      bitsPerPixel = 8;
      bytesPerRow = width;
    }
    //System.out.println("initial bytesPerRow=" + bytesPerRow);
    // make sure number of bytes per row is even
    if ((bytesPerRow % 2) == 1)
    {
      bytesPerRow++;
    }
    setCorrectVersion();
    saveHeader(out);
    // write the custom palette if necessary
    if (customPalette)
    {
      savePalette(out, palette);
    }
    // if compression type != uncompressed write two bytes with compressed size to output
    saveInitialCompressedSize(out);
    // initialize row buffers
    byte[] row = new byte[width];
    byte[] prev = null;
    if (compression == COMPRESSION_SCANLINE)
    {
      prev = new byte[row.length];
    }
    byte[] temp = null;
    if (bitsPerPixel < 8)
    {
      temp = new byte[width];
    }
    // get position of upper left corner of image part to be written
    final int X1 = getBoundsX1();
    final int Y1 = getBoundsY1();
    // write all rows to file, top to bottom
    for (int y = 0; y < height; y++)
    {
      switch(bitsPerPixel)
      {
        case(2):
        {
          image.getByteSamples(0, X1, y + Y1, width, 1, temp, 0);
          ArrayConverter.encodePacked2Bit(temp, 0, row, 0, width);
          break;
        }
        case(4):
        {
          image.getByteSamples(0, X1, y + Y1, width, 1, temp, 0);
          ArrayConverter.encodePacked4Bit(temp, 0, row, 0, width);
          break;
        }
        case(8):
        {
          image.getByteSamples(0, X1, y + Y1, width, 1, row, 0);
          break;
        }
      }
      saveRow(out, y == 0, row, prev);
      if (compression == COMPRESSION_SCANLINE)
      {
        System.arraycopy(row, 0, prev, 0, row.length);
      }
      setProgress(y, height);
    }
    saveFinalCompressedSize(out);
  }

  private void save(DataOutput out, RGB24Image image) throws IOException
  {
    bytesPerRow = width * 2;
    bitsPerPixel = 16;
    flags |= FLAG_DIRECT_COLOR;
    setCorrectVersion();
    saveHeader(out);
    // write 8 bytes for direct color information to file
    out.write(5); // red bits
    out.write(6); // green bits
    out.write(5); // blue bits
    int i = 5;
    while (i-- > 0)
    {
      out.write(0);
    }
    compressedDataOffset += 8;
    // allocate row buffer(s)
    byte[] row = new byte[width * 2];
    byte[] prev = null;
    if (compression == COMPRESSION_SCANLINE)
    {
      prev = new byte[row.length];
    }
    byte[] red = new byte[width];
    byte[] green = new byte[width];
    byte[] blue = new byte[width];
   
    final int X1 = getBoundsX1();
    final int Y1 = getBoundsY1();
    for (int y = 0; y < height; y++)
    {
      // get samples for each channel of the row to be written out
      image.getByteSamples(RGBIndex.INDEX_RED, X1, y + Y1, width, 1, red, 0);
      image.getByteSamples(RGBIndex.INDEX_GREEN, X1, y + Y1, width, 1, green, 0);
      image.getByteSamples(RGBIndex.INDEX_BLUE, X1, y + Y1, width, 1, blue, 0);
      // encode row as 16 bit samples, big endian, 5-6-5
      ArrayConverter.encodeRGB24ToPackedRGB565BigEndian(
        red, 0,
        green, 0,
        blue, 0,
        row, 0,
        width);
      saveRow(out, y == 0, row, prev);
      if (compression == COMPRESSION_SCANLINE)
      {
        System.arraycopy(row, 0, prev, 0, row.length);
      }
      setProgress(y, height);
    }
    saveFinalCompressedSize(out);
  }

  private void saveFinalCompressedSize(DataOutput out) throws IOException
  {
    if ((flags & FLAG_COMPRESSED) == 0)
    {
      return;
    }
    if (!(out instanceof RandomAccessFile || out instanceof SeekableByteArrayOutputStream))
    {
      return;
    }
    long pos = -1;
    if (out instanceof RandomAccessFile)
    {
      RandomAccessFile raf = (RandomAccessFile)out;
      pos = raf.length();
    }
    else
    if (out instanceof SeekableByteArrayOutputStream)
    {
      SeekableByteArrayOutputStream sbaos = (SeekableByteArrayOutputStream)out;
      pos = sbaos.getPosition();
    }
    long compressedSize = pos - compressedDataOffset;
    compressedSize = Math.min(0xffff, compressedSize);
    /*
    System.out.println("compressed data offset=" + compressedDataOffset);
    System.out.println("position after compression=" + pos);
    System.out.println("compressed size=" + compressedSize + " / " + Integer.toHexString((int)compressedSize));
    */
    if (out instanceof RandomAccessFile)
    {
      RandomAccessFile raf = (RandomAccessFile)out;
      raf.seek(compressedDataOffset);
      raf.writeShort((int)compressedSize);
    }
    else
    if (out instanceof SeekableByteArrayOutputStream)
    {
      SeekableByteArrayOutputStream sbaos = (SeekableByteArrayOutputStream)out;
      sbaos.seek((int)compressedDataOffset);
      sbaos.write((int)(compressedSize >> 8) & 0xff);
      sbaos.write((int)compressedSize & 0xff);
    }
  }

  private void saveHeader(DataOutput out) throws IOException
  {
    out.writeShort(width);
    out.writeShort(height);
    out.writeShort(bytesPerRow);
    out.writeShort(flags);
    out.writeByte(bitsPerPixel);
    out.writeByte(version);
    out.writeShort(0); // next image offset
    out.writeByte(transparencyIndex);
    out.writeByte(compression);
    out.writeShort(0); // reserved
    compressedDataOffset = 16;
  }

  private void saveInitialCompressedSize(DataOutput out) throws IOException
  {
    if ((flags & FLAG_COMPRESSED) == 0)
    {
      return;
    }
    out.writeShort(bytesPerRow * height); // just a guess
  }

  private void savePalette(DataOutput out, Palette palette) throws IOException
  {
    out.writeShort(palette.getNumEntries());
    for (int i = 0; i < palette.getNumEntries(); i++)
    {
      out.writeByte(0); // reserved
      out.writeByte(palette.getSample(RGBIndex.INDEX_RED, i));
      out.writeByte(palette.getSample(RGBIndex.INDEX_GREEN, i));
      out.writeByte(palette.getSample(RGBIndex.INDEX_BLUE, i));
    }
    compressedDataOffset += 2 + 4 * palette.getNumEntries();
  }

  private void saveRow(DataOutput out, boolean firstRow, byte[] row, byte[] prev) throws IOException
  {
    switch(compression)
    {
      case(COMPRESSION_NONE):
      {
        out.write(row, 0, bytesPerRow);
        break;
      }
      case(COMPRESSION_RLE):
      {
        saveRowRLE(out, row);
        break;
      }
      case(COMPRESSION_SCANLINE):
      {
        saveRowScanLine(out, firstRow, row, prev);
        break;
      }
    }
  }

/*    int srcOffset = 0; // points into uncompressed data array "row"
    do
    {
      // determine length of next run, between 1 and 255
      byte value = row[srcOffset];
      int lookAheadOffset = srcOffset + 1;
      int bytesLeft = bytesPerRow - lookAheadOffset;
      if (bytesLeft > 255)
      {
        bytesLeft = 255;
      }
      while (bytesLeft != 0 && value == row[lookAheadOffset])
      {
        lookAheadOffset++;
        bytesLeft--;
      }
      int runLength = lookAheadOffset - srcOffset;
      if (runLength < 1)
      {
        System.err.println("FATAL: RUN LENGTH <0");
        System.exit(1);
      }
      if (srcOffset + runLength > bytesPerRow)
      {
        System.err.println("FATAL: srcOffset=" + srcOffset+ " runLength=" + runLength + " bytesPerRow=" + bytesPerRow);
        System.exit(1);
      }
      if (srcOffset == 13 && runLength == 2)
      {
        System.err.println("FATAL: 13 2 ");
        System.exit(1);
      }
      // write pair (length-of-run, value) to output
      out.writeByte(runLength);
      out.writeByte(value & 0xff);
      // update srcOffset to point to the next byte in row to be encoded
      srcOffset += runLength;
    }
    while (srcOffset < bytesPerRow);*/

  private void saveRowRLE(DataOutput out, byte[] row) throws IOException
  {
    int srcOffset = 0; // points into uncompressed data array "row"
    do
    {
      // determine length of next run, between 1 and 255
      int runLength = 1;
      int bytesLeft = bytesPerRow - srcOffset;
      byte value = row[srcOffset];
      while (bytesLeft != 0 && srcOffset + runLength < row.length && value == row[srcOffset + runLength])
      {
        bytesLeft--;
        runLength++;
        if (runLength == 255)
        {
          bytesLeft = 0;
        }
      }
      srcOffset += runLength;
      out.writeByte(runLength);
      out.writeByte(value & 0xff);
    }
    while (srcOffset < bytesPerRow);
  }

  private void saveRowScanLine(DataOutput out, boolean firstRow, byte[] row, byte[] prev) throws IOException
  {
    int bytesLeft = bytesPerRow;
    int srcOffset = 0;
    byte[] bytes = new byte[8];
    do
    {
      int pixelMask = 0;
      int bitMask = 128;
      int numBytesToCheck = Math.min(8, bytesLeft);
      int numOutputBytes = 0;
      bytesLeft -= numBytesToCheck;
      while (numBytesToCheck-- != 0)
      {
        if (row[srcOffset] != prev[srcOffset])
        {
          pixelMask |= bitMask;
          bytes[numOutputBytes++] = row[srcOffset];
        }
        srcOffset++;
        bitMask >>= 1;
      }
      out.writeByte(pixelMask);
      out.write(bytes, 0, numOutputBytes);
    }
    while (bytesLeft != 0);
  }

  /**
   * Sets the compression algorithm to be used for saving an image.
   * @see #getCompression
   * @param newCompressionType int value that is one of the COMPRESSION_xyz constants of this class
   * @throws IllegalArgumentException if the compression type is unsupported
   */
  public void setCompression(int newCompressionType)
  {
    if (newCompressionType != COMPRESSION_NONE &&
        newCompressionType != COMPRESSION_RLE &&
        newCompressionType != COMPRESSION_SCANLINE)
    {
      throw new IllegalArgumentException("Unsupported Palm compression type for writing.");
    }
    compression = newCompressionType;
  }

  private void setCorrectVersion()
  {
    version = 0;
    if (bitsPerPixel > 1)
    {
      version = 1;
    }
    if (hasTransparencyIndex() || getCompression() == COMPRESSION_SCANLINE || getCompression() == COMPRESSION_RLE)
    {
      version = 2;
    }
  }

  /**
   * Reuses super.setFile when used for CodecMode.LOAD, but
   * creates a RandomAccessFile instead of a FileOutputStream
   * in write mode so that the compressed size can be written
   * correcly (requires a seek operation).
   * @param fileName name of the file to be opened
   * @param codecMode defines whether this codec object is to be used for loading or saving
   */
  public void setFile(String fileName, CodecMode codecMode) throws
    IOException,
    UnsupportedCodecModeException
  {
    if (codecMode == CodecMode.LOAD)
    {
      super.setFile(fileName, codecMode);
    }
    else
    {
      setRandomAccessFile(new RandomAccessFile(fileName, "rw"), CodecMode.SAVE);
    }
  }
     
  /**
   * Sets a new transparency index when saving an image.
   * If this method is called, the argument value is used as an index
   * into the palette for a color that is supposed to be transparent.
   * When the resulting Palm image file is drawn onto some background,
   * all pixels in the color pointed to by the transparency index are not
   * supposed to be overdrawn so that the background is visisble at
   * those places.
   * @param newIndex the new transparency index, must be smaller than the number of entries in the palette
   * @see #getTransparencyIndex
   * @see #hasTransparencyIndex
   * @see #removeTransparencyIndex
   */
  public void setTransparencyIndex(int newIndex)
  {
    if (newIndex < 0)
    {
      throw new IllegalArgumentException("Transparency index must be 0 or larger.");
    }
    transparencyIndex = newIndex;
  }

  private void store(PixelImage image, int y, byte[] row)
  {
    if (!isRowRequired(y))
    {
      return;
    }
    y -= getBoundsY1();
    switch(bitsPerPixel)
    {
      case(1):
      {
        BilevelImage bimage = (BilevelImage)image;
        invertBilevelData(row);
        bimage.putPackedBytes(0, y, getBoundsWidth(), row, getBoundsX1() / 8, getBoundsX1() % 8);
        break;
      }
      case(2):
      {
        byte[] dest = new byte[bytesPerRow * 4];
        ArrayConverter.decodePacked2Bit(row, 0, dest, 0, bytesPerRow);
        ByteChannelImage bcimg = (ByteChannelImage)image;
        bcimg.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(4):
      {
        byte[] dest = new byte[bytesPerRow * 2];
        ArrayConverter.decodePacked4Bit(row, 0, dest, 0, bytesPerRow);
        ByteChannelImage bcimg = (ByteChannelImage)image;
        bcimg.putByteSamples(0, 0, y, getBoundsWidth(), 1, dest, getBoundsX1());
        break;
      }
      case(8):
      {
        ByteChannelImage bcimg = (ByteChannelImage)image;
        bcimg.putByteSamples(0, 0, y, getBoundsWidth(), 1, row, getBoundsX1());
        break;
      }
      case(16):
      {
        ArrayConverter.decodePackedRGB565BigEndianToRGB24(
          row, getBoundsX1() * 2,
          rgb, 0,
          rgb, width,
          rgb, width * 2,
          getBoundsWidth());
        RGB24Image img = (RGB24Image)image;
        img.putByteSamples(RGBIndex.INDEX_RED, 0, y, getBoundsWidth(), 1, rgb, 0);
        img.putByteSamples(RGBIndex.INDEX_GREEN, 0, y, getBoundsWidth(), 1, rgb, width);
        img.putByteSamples(RGBIndex.INDEX_BLUE, 0, y, getBoundsWidth(), 1, rgb, width * 2);
        break;
      }
    }
  }

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

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

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.