/* $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: jmol-developers@lists.sf.net * * 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 hansonr@stolaf.edu 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: * * david@catcode.com * * @author J. David Eisenberg * @author http://catcode.com/pngencoder/ * @author Christian Ribeaud (christian.ribeaud@genedata.com) * @author Bob Hanson (hansonr@stolaf.edu) * * @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; // } //} }