** * pngencoder takes a Java image object and creates a byte string which can be saved as a ...
**
* pngencoder takes a Java image object and creates a byte string which can be saved as a png file.
* the image is presumed to use the directcolormodel.
*
* thanks to jay denny at keypoint software
* http://www.keypoint.com/
* who let me develop this code on company time.
*
* you may contact me with (probably very-much-needed) improvements,
* comments, and bug fixes at:
*
* david@catcode.com
*
* this library is free software; you can redistribute it and/or
* modify it under the terms of the gnu lesser general public
* license as published by the free software foundation; either
* version 2.1 of the license, or (at your option) any later version.
*
* this library is distributed in the hope that it will be useful,
* but without any warranty; without even the implied warranty of
* merchantability or fitness for a particular purpose. see the gnu
* lesser general public license for more details.
*
* you should have received a copy of the gnu lesser general public
* license along with this library; if not, write to the free software
* foundation, inc., 59 temple place, suite 330, boston, ma 02111-1307 usa
* a copy of the gnu lgpl may be found at
* http://www.gnu.org/copyleft/lesser.html,
*
* @author j. david eisenberg
* @version 1.4, 31 march 2000
*/
import java.awt.*;
import java.awt.image.*;
import java.util.*;
import java.util.zip.*;
import java.io.*;
public class pngencoder extends object
{
/** constant specifying that alpha channel should be encoded. */
public static final boolean encode_alpha=true;
/** constant specifying that alpha channel should not be encoded. */
public static final boolean no_alpha=false;
/** constants for filters */
public static final int filter_none = 0;
public static final int filter_sub = 1;
public static final int filter_up = 2;
public static final int filter_last = 2;
protected byte[] pngbytes;
protected byte[] priorrow;
protected byte[] leftbytes;
protected image image;
protected int width, height;
protected int bytepos, maxpos;
protected int hdrpos, datapos, endpos;
protected crc32 crc = new crc32();
protected long crcvalue;
protected boolean encodealpha;
protected int filter;
protected int bytesperpixel;
protected int compressionlevel;
/**
* class constructor
*
*/
public pngencoder()
{
this( null, false, filter_none, 0 );
}
/**
* class constructor specifying image to encode, with no alpha channel encoding.
*
* @param image a java image object which uses the directcolormodel
* @see java.awt.image
*/
public pngencoder( image image )
{
this(image, false, filter_none, 0);
}
/**
* class constructor specifying image to encode, and whether to encode alpha.
*
* @param image a java image object which uses the directcolormodel
* @param encodealpha encode the alpha channel? false=no; true=yes
* @see java.awt.image
*/
public pngencoder( image image, boolean encodealpha )
{
this(image, encodealpha, filter_none, 0);
}
/**
* class constructor specifying image to encode, whether to encode alpha, and filter to use.
*
* @param image a java image object which uses the directcolormodel
* @param encodealpha encode the alpha channel? false=no; true=yes
* @param whichfilter 0=none, 1=sub, 2=up
* @see java.awt.image
*/
public pngencoder( image image, boolean encodealpha, int whichfilter )
{
this( image, encodealpha, whichfilter, 0 );
}
/**
* class constructor specifying image source to encode, whether to encode alpha, filter to use, and compression level.
*
* @param image a java image object
* @param encodealpha encode the alpha channel? false=no; true=yes
* @param whichfilter 0=none, 1=sub, 2=up
* @param complevel 0..9
* @see java.awt.image
*/
public pngencoder( image image, boolean encodealpha, int whichfilter,
int complevel)
{
this.image = image;
this.encodealpha = encodealpha;
setfilter( whichfilter );
if (complevel >=0 && complevel <=9)
{
this.compressionlevel = complevel;
}
}
/**
* set the image to be encoded
*
* @param image a java image object which uses the directcolormodel
* @see java.awt.image
* @see java.awt.image.directcolormodel
*/
public void setimage( image image )
{
this.image = image;
pngbytes = null;
}
/**
* creates an array of bytes that is the png equivalent of the current image, specifying whether to encode alpha or not.
*
* @param encodealpha boolean false=no alpha, true=encode alpha
* @return an array of bytes, or null if there was a problem
*/
public byte[] pngencode( boolean encodealpha )
{
byte[] pngidbytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
int i;
if (image == null)
{
return null;
}
width = image.getwidth( null );
height = image.getheight( null );
this.image = image;
/*
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
pngbytes = new byte[((width 1) * height * 3) 200];
/*
* keep track of largest byte written to the array
*/
maxpos = 0;
bytepos = writebytes( pngidbytes, 0 );
hdrpos = bytepos;
writeheader();
datapos = bytepos;
if (writeimagedata())
{
writeend();
pngbytes = resizebytearray( pngbytes, maxpos );
}
else
{
pngbytes = null;
}
return pngbytes;
}
/**
* creates an array of bytes that is the png equivalent of the current image.
* alpha encoding is determined by its setting in the constructor.
*
* @return an array of bytes, or null if there was a problem
*/
public byte[] pngencode()
{
return pngencode( encodealpha );
}
/**
* set the alpha encoding on or off.
*
* @param encodealpha false=no, true=yes
*/
public void setencodealpha( boolean encodealpha )
{
this.encodealpha = encodealpha;
}
/**
* retrieve alpha encoding status.
*
* @return boolean false=no, true=yes
*/
public boolean getencodealpha()
{
return encodealpha;
}
/**
* set the filter to use
*
* @param whichfilter from constant list
*/
public void setfilter( int whichfilter )
{
this.filter = filter_none;
if ( whichfilter <= filter_last )
{
this.filter = whichfilter;
}
}
/**
* retrieve filtering scheme
*
* @return int (see constant list)
*/
public int getfilter()
{
return filter;
}
/**
* set the compression level to use
*
* @param level 0 through 9
*/
public void setcompressionlevel( int level )
{
if ( level >= 0 && level <= 9)
{
this.compressionlevel = level;
}
}
/**
* retrieve compression level
*
* @return int in range 0-9
*/
public int getcompressionlevel()
{
return compressionlevel;
}
/**
* increase or decrease the length of a byte array.
*
* @param array the original array.
* @param newlength the length you wish the new array to have.
* @return array of newly desired length. if shorter than the
* original, the trailing elements are truncated.
*/
protected byte[] resizebytearray( byte[] array, int newlength )
{
byte[] newarray = new byte[newlength];
int oldlength = array.length;
system.arraycopy( array, 0, newarray, 0,
math.min( oldlength, newlength ) );
return newarray;
}
/**
* write an array of bytes into the pngbytes array.
* note: this routine has the side effect of updating
* maxpos, the largest element written in the array.
* the array is resized by 1000 bytes or the length
* of the data to be written, whichever is larger.
*
* @param data the data to be written into pngbytes.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
*/
protected int writebytes( byte[] data, int offset )
{
maxpos = math.max( maxpos, offset data.length );
if (data.length offset > pngbytes.length)
{
pngbytes = resizebytearray( pngbytes, pngbytes.length
math.max( 1000, data.length ) );
}
system.arraycopy( data, 0, pngbytes, offset, data.length );
return offset data.length;
}
/**
* write an array of bytes into the pngbytes array, specifying number of bytes to write.
* note: this routine has the side effect of updating
* maxpos, the largest element written in the array.
* the array is resized by 1000 bytes or the length
* of the data to be written, whichever is larger.
*
* @param data the data to be written into pngbytes.
* @param nbytes the number of bytes to be written.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
*/
protected int writebytes( byte[] data, int nbytes, int offset )
{
maxpos = math.max( maxpos, offset nbytes );
if (nbytes offset > pngbytes.length)
{
pngbytes = resizebytearray( pngbytes, pngbytes.length
math.max( 1000, nbytes ) );
}
system.arraycopy( data, 0, pngbytes, offset, nbytes );
return offset nbytes;
}
/**
* write a two-byte integer into the pngbytes array at a given position.
*
* @param n the integer to be written into pngbytes.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
*/
protected int writeint2( int n, int offset )
{
byte[] temp = { (byte)((n >> 8) & 0xff),
(byte) (n & 0xff) };
return writebytes( temp, offset );
}
/**
* write a four-byte integer into the pngbytes array at a given position.
*
* @param n the integer to be written into pngbytes.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
*/
protected int writeint4( int n, int offset )
{
byte[] temp = { (byte)((n >> 24) & 0xff),
(byte) ((n >> 16) & 0xff ),
(byte) ((n >> 8) & 0xff ),
(byte) ( n & 0xff ) };
return writebytes( temp, offset );
}
/**
* write a single byte into the pngbytes array at a given position.
*
* @param n the integer to be written into pngbytes.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
*/
protected int writebyte( int b, int offset )
{
byte[] temp = { (byte) b };
return writebytes( temp, offset );
}
/**
* write a string into the pngbytes array at a given position.
* this uses the getbytes method, so the encoding used will
* be its default.
*
* @param n the integer to be written into pngbytes.
* @param offset the starting point to write to.
* @return the next place to be written to in the pngbytes array.
* @see java.lang.string#getbytes()
*/
protected int writestring( string s, int offset )
{
return writebytes( s.getbytes(), offset );
}
/**
* write a png "ihdr" chunk into the pngbytes array.
*/
protected void writeheader()
{
int startpos;
startpos = bytepos = writeint4( 13, bytepos );
bytepos = writestring( "ihdr", bytepos );
width = image.getwidth( null );
height = image.getheight( null );
bytepos = writeint4( width, bytepos );
bytepos = writeint4( height, bytepos );
bytepos = writebyte( 8, bytepos ); // bit depth
bytepos = writebyte( (encodealpha) ? 6 : 2, bytepos ); // direct model
bytepos = writebyte( 0, bytepos ); // compression method
bytepos = writebyte( 0, bytepos ); // filter method
bytepos = writebyte( 0, bytepos ); // no interlace
crc.reset();
crc.update( pngbytes, startpos, bytepos-startpos );
crcvalue = crc.getvalue();
bytepos = writeint4( (int) crcvalue, bytepos );
}
/**
* perform "sub" filtering on the given row.
* uses temporary array leftbytes to store the original values
* of the previous pixels. the array is 16 bytes long, which
* will easily hold two-byte samples plus two-byte alpha.
*
* @param pixels the array holding the scan lines being built
* @param startpos starting position within pixels of bytes to be filtered.
* @param width width of a scanline in pixels.
*/
protected void filtersub( byte[] pixels, int startpos, int width )
{
int i;
int offset = bytesperpixel;
int actualstart = startpos offset;
int nbytes = width * bytesperpixel;
int leftinsert = offset;
int leftextract = 0;
byte current_byte;
for (i=actualstart; i < startpos nbytes; i )
{
leftbytes[leftinsert] = pixels[i];
pixels[i] = (byte) ((pixels[i] - leftbytes[leftextract]) % 256);
leftinsert = (leftinsert 1) % 0x0f;
leftextract = (leftextract 1) % 0x0f;
}
}
/**
* perform "up" filtering on the given row.
* side effect: refills the prior row with current row
*
* @param pixels the array holding the scan lines being built
* @param startpos starting position within pixels of bytes to be filtered.
* @param width width of a scanline in pixels.
*/
protected void filterup( byte[] pixels, int startpos, int width )
{
int i, nbytes;
byte current_byte;
nbytes = width * bytesperpixel;
for (i=0; i < nbytes; i )
{
current_byte = pixels[startpos i];
pixels[startpos i] = (byte) ((pixels[startpos i] - priorrow[i]) % 256);
priorrow[i] = current_byte;
}
}
/**
* write the image data into the pngbytes array.
* this will write one or more png "idat" chunks. in order
* to conserve memory, this method grabs as many rows as will
* fit into 32k bytes, or the whole image; whichever is less.
*
*
* @return true if no errors; false if error grabbing pixels
*/
protected boolean writeimagedata()
{
int rowsleft = height; // number of rows remaining to write
int startrow = 0; // starting row to process this time through
int nrows; // how many rows to grab at a time
byte[] scanlines; // the scan lines to be compressed
int scanpos; // where we are in the scan lines
int startpos; // where this line’s actual pixels start (used for filtering)
byte[] compressedlines; // the resultant compressed lines
int ncompressed; // how big is the compressed area?
int depth; // color depth ( handle only 8 or 32 )
pixelgrabber pg;
bytesperpixel = (encodealpha) ? 4 : 3;
deflater scrunch = new deflater( compressionlevel );
bytearrayoutputstream outbytes =
new bytearrayoutputstream(1024);
deflateroutputstream compbytes =
new deflateroutputstream( outbytes, scrunch );
try
{
while (rowsleft > 0)
{
nrows = math.min( 32767 / (width*(bytesperpixel 1)), rowsleft );
// nrows = rowsleft;
int[] pixels = new int[width * nrows];
pg = new pixelgrabber(image, 0, startrow,
width, nrows, pixels, 0, width);
try {
pg.grabpixels();
}
catch (exception e) {
system.err.println("interrupted waiting for pixels!");
return false;
}
if ((pg.getstatus() & imageobserver.abort) != 0) {
system.err.println("image fetch aborted or errored");
return false;
}
/*
* create a data chunk. scanlines adds "nrows" for
* the filter bytes.
*/
scanlines = new byte[width * nrows * bytesperpixel nrows];
if (filter == filter_sub)
{
leftbytes = new byte[16];
}
if (filter == filter_up)
{
priorrow = new byte[width*bytesperpixel];
}
scanpos = 0;
startpos = 1;
for (int i=0; i
{
if (i % width == 0)
{
scanlines[scanpos ] = (byte) filter;
startpos = scanpos;
}
scanlines[scanpos ] = (byte) ((pixels[i] >> 16) & 0xff);
scanlines[scanpos ] = (byte) ((pixels[i] >> 8) & 0xff);
scanlines[scanpos ] = (byte) ((pixels[i] ) & 0xff);
if (encodealpha)
{
scanlines[scanpos ] = (byte) ((pixels[i] >> 24) & 0xff );
}
if ((i % width == width-1) && (filter != filter_none))
{
if (filter == filter_sub)
{
filtersub( scanlines, startpos, width );
}
if (filter == filter_up)
{
filterup( scanlines, startpos, width );
}
}
}
/*
* write these lines to the output area
*/
compbytes.write( scanlines, 0, scanpos );
startrow = nrows;
rowsleft -= nrows;
}
compbytes.close();
/*
* write the compressed bytes
*/
compressedlines = outbytes.tobytearray();
ncompressed = compressedlines.length;
crc.reset();
bytepos = writeint4( ncompressed, bytepos );
bytepos = writestring("idat", bytepos );
crc.update("idat".getbytes());
bytepos = writebytes( compressedlines, ncompressed, bytepos );
crc.update( compressedlines, 0, ncompressed );
crcvalue = crc.getvalue();
bytepos = writeint4( (int) crcvalue, bytepos );
scrunch.finish();
return true;
}
catch (ioexception e)
{
system.err.println( e.tostring());
return false;
}
}
/**
* write a png "iend" chunk into the pngbytes array.
*/
protected void writeend()
{
bytepos = writeint4( 0, bytepos );
bytepos = writestring( "iend", bytepos );
crc.reset();
crc.update("iend".getbytes());
crcvalue = crc.getvalue();
bytepos = writeint4( (int) crcvalue, bytepos );
}
}