javajs.img.PngEncoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jmol Show documentation
Show all versions of jmol Show documentation
Jmol: an open-source Java viewer for chemical structures in 3D
/* $RCSfile$
* $Author: nicove $
* $Date: 2007-03-30 12:26:16 -0500 (Fri, 30 Mar 2007) $
* $Revision: 7275 $
*
* Copyright (C) 2002-2005 The Jmol Development Team
*
* Contact: [email protected]
*
* 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package javajs.img;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
*
* Modified by Bob Hanson [email protected] to be a subclass of ImageEncoder
* and to use javajs.util.OutputChannel instead of just returning bytes. Also includes:
*
* -- JavaScript-compatible image processing
*
* -- transparent background option
*
* -- more efficient calculation of needs for pngBytes
*
* -- option to use pre-created PNGJ image data (3/19/14; Jmol 14.1.12)
*
* -- PNGJ format:
*
* // IHDR chunk
*
* // tEXt chunk "Jmol type - <0000000pt>+<000000len>"
*
* // tEXt chunk "Software - Jmol "
*
* // tEXt chunk "Creation Time - "
*
* // tRNS chunk transparent color, if desired
*
* // IDAT chunk (image data)
*
* // IEND chunk
*
* // [JMOL ZIP FILE APPENDIX]
*
* Original Comment:
*
* 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:
*
* [email protected]
*
* @author J. David Eisenberg
* @author http://catcode.com/pngencoder/
* @author Christian Ribeaud ([email protected])
* @author Bob Hanson ([email protected])
*
* @version 1.4, 31 March 2000
*/
public class PngEncoder extends CRCEncoder {
/** 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;
private static final int PT_FIRST_TAG = 37;
private boolean encodeAlpha;
private int filter = FILTER_NONE;
private int bytesPerPixel;
private int compressionLevel;
private String type;
private Integer transparentColor;
private byte[] appData;
private String appPrefix;
private String comment;
private byte[] bytes;
public PngEncoder() {
super();
}
@Override
protected void setParams(Map params) {
if (quality < 0)
quality = (params.containsKey("qualityPNG") ? ((Integer) params
.get("qualityPNG")).intValue() : 2);
if (quality > 9)
quality = 9;
encodeAlpha = false;
filter = FILTER_NONE;
compressionLevel = quality;
transparentColor = (Integer) params.get("transparentColor");
comment = (String) params.get("comment");
type = (params.get("type") + "0000").substring(0, 4);
bytes = (byte[]) params.get("pngImgData");
appData = (byte[]) params.get("pngAppData");
appPrefix = (String) params.get("pngAppPrefix");
}
@Override
protected void generate() throws IOException {
if (bytes == null) {
if (!pngEncode()) {
out.cancel();
return;
}
bytes = getBytes();
} else {
dataLen = bytes.length;
}
int len = dataLen;
if (appData != null) {
setJmolTypeText(appPrefix, bytes, len, appData.length,
type);
out.write(bytes, 0, len);
len = (bytes = appData).length;
}
out.write(bytes, 0, len);
}
/**
* Creates an array of bytes that is the PNG equivalent of the current image,
* specifying whether to encode alpha or not.
*
* @return true if successful
*
*/
private boolean pngEncode() {
byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
writeBytes(pngIdBytes);
//hdrPos = bytePos;
writeHeader();
writeText(getApplicationText(appPrefix, type, 0, 0));
writeText("Software\0Jmol " + comment);
writeText("Creation Time\0" + date);
if (!encodeAlpha && transparentColor != null)
writeTransparentColor(transparentColor.intValue());
//dataPos = bytePos;
return writeImageData();
}
/**
* Fill in the Jmol type text area with number of bytes of PNG data and number
* of bytes of Jmol state data and fix checksum.
*
* If we do not do this, then the checksum will be wrong, and Jmol and some
* other programs may not be able to read the PNG image.
*
* This was corrected for Jmol 12.3.30. Between 12.3.7 and 12.3.29, PNG files
* created by Jmol have incorrect checksums.
*
* @param prefix
*
* @param b
* @param nPNG
* @param nState
* @param type
*/
private static void setJmolTypeText(String prefix, byte[] b, int nPNG, int nState, String type) {
String s = "tEXt" + getApplicationText(prefix, type, nPNG, nState);
CRCEncoder encoder = new PngEncoder();
byte[] test = s.substring(0, 4 + prefix.length()).getBytes();
for (int i = test.length; -- i >= 0;)
if (b[i + PT_FIRST_TAG] != test[i]) {
System.out.println("image is not of the right form; appending data, but not adding tEXt tag.");
return;
}
encoder.setData(b, PT_FIRST_TAG);
encoder.writeString(s);
encoder.writeCRC();
}
private static String getApplicationText(String prefix, String type, int nPNG, int nState) {
String sPNG = "000000000" + nPNG;
sPNG = sPNG.substring(sPNG.length() - 9);
String sState = "000000000" + nState;
sState = sState.substring(sState.length() - 9);
return prefix + "\0" + type + (type.equals("PNG") ? "0" : "") + sPNG + "+"
+ sState;
}
// /**
// * Set the filter to use
// *
// * @param whichFilter from constant list
// */
// public void setFilter(int whichFilter) {
// this.filter = (whichFilter <= FILTER_LAST ? whichFilter : FILTER_NONE);
// }
// /**
// * 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;
// }
/**
* Write a PNG "IHDR" chunk into the pngBytes array.
*/
private void writeHeader() {
writeInt4(13);
startPos = bytePos;
writeString("IHDR");
writeInt4(width);
writeInt4(height);
writeByte(8); // bit depth
writeByte(encodeAlpha ? 6 : 2); // color type or direct model
writeByte(0); // compression method
writeByte(0); // filter method
writeByte(0); // no interlace
writeCRC();
}
private void writeText(String msg) {
writeInt4(msg.length());
startPos = bytePos;
writeString("tEXt" + msg);
writeCRC();
}
/**
* Write a PNG "tRNS" chunk into the pngBytes array.
*
* @param icolor
*/
private void writeTransparentColor(int icolor) {
writeInt4(6);
startPos = bytePos;
writeString("tRNS");
writeInt2((icolor >> 16) & 0xFF);
writeInt2((icolor >> 8) & 0xFF);
writeInt2(icolor & 0xFF);
writeCRC();
}
private byte[] scanLines; // the scan lines to be compressed
private int byteWidth; // width * bytesPerPixel
//private int hdrPos, dataPos, endPos;
//private byte[] priorRow;
//private byte[] leftBytes;
/**
* 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
*/
private boolean writeImageData() {
bytesPerPixel = (encodeAlpha ? 4 : 3);
byteWidth = width * bytesPerPixel;
int scanWidth = byteWidth + 1; // the added 1 is for the filter byte
//boolean doFilter = (filter != FILTER_NONE);
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
int scanPos; // where we are in the scan lines
Deflater deflater = new Deflater(compressionLevel);
ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
deflater);
int pt = 0; // overall image byte pointer
// Jmol note: The entire image has been stored in pixels[] already
try {
while (rowsLeft > 0) {
nRows = Math.max(1, Math.min(32767 / scanWidth, rowsLeft));
scanLines = new byte[scanWidth * nRows];
// if (doFilter)
// switch (filter) {
// case FILTER_SUB:
// leftBytes = new byte[16];
// break;
// case FILTER_UP:
// priorRow = new byte[scanWidth - 1];
// break;
// }
int nPixels = width * nRows;
scanPos = 0;
//startPos = 1;
for (int i = 0; i < nPixels; i++, pt++) {
if (i % width == 0) {
scanLines[scanPos++] = (byte) filter;
//startPos = scanPos;
}
scanLines[scanPos++] = (byte) ((pixels[pt] >> 16) & 0xff);
scanLines[scanPos++] = (byte) ((pixels[pt] >> 8) & 0xff);
scanLines[scanPos++] = (byte) ((pixels[pt]) & 0xff);
if (encodeAlpha) {
scanLines[scanPos++] = (byte) ((pixels[pt] >> 24) & 0xff);
}
// if (doFilter && i % width == width - 1) {
// switch (filter) {
// case FILTER_SUB:
// filterSub();
// break;
// case FILTER_UP:
// filterUp();
// break;
// }
// }
}
/*
* Write these lines to the output area
*/
compBytes.write(scanLines, 0, scanPos);
//startRow += nRows;
rowsLeft -= nRows;
}
compBytes.close();
/*
* Write the compressed bytes
*/
byte[] compressedLines = outBytes.toByteArray();
writeInt4(compressedLines.length);
startPos = bytePos;
writeString("IDAT");
writeBytes(compressedLines);
writeCRC();
writeEnd();
deflater.finish();
return true;
} catch (IOException e) {
System.err.println(e.toString());
return false;
}
}
/**
* Write a PNG "IEND" chunk into the pngBytes array.
*/
private void writeEnd() {
writeInt4(0);
startPos = bytePos;
writeString("IEND");
writeCRC();
}
///**
//* 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.
//*
//*/
//private void filterSub() {
// int offset = bytesPerPixel;
// int actualStart = startPos + offset;
// int leftInsert = offset;
// int leftExtract = 0;
// //byte current_byte;
//
// for (int i = actualStart; i < startPos + byteWidth; i++) {
// leftBytes[leftInsert] = scanLines[i];
// scanLines[i] = (byte) ((scanLines[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
//*
//*/
//private void filterUp() {
// int nBytes = width * bytesPerPixel;
// for (int i = 0; i < nBytes; i++) {
// int pt = startPos + i;
// byte b = scanLines[pt];
// scanLines[pt] = (byte) ((scanLines[pt] - priorRow[i]) % 256);
// priorRow[i] = b;
// }
//}
}