3 * $Date: 2007-03-30 12:26:16 -0500 (Fri, 30 Mar 2007) $
6 * Copyright (C) 2002-2005 The Jmol Development Team
8 * Contact: jmol-developers@lists.sf.net
10 * This library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Lesser General Public
12 * License as published by the Free Software Foundation; either
13 * version 2.1 of the License, or (at your option) any later version.
15 * This library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * Lesser General Public License for more details.
20 * You should have received a copy of the GNU Lesser General Public
21 * License along with this library; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27 import java.util.zip.Deflater;
28 import java.util.zip.DeflaterOutputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.IOException;
36 * Modified by Bob Hanson hansonr@stolaf.edu to be a subclass of ImageEncoder
37 * and to use javajs.util.OutputChannel instead of just returning bytes. Also includes:
39 * -- JavaScript-compatible image processing
41 * -- transparent background option
43 * -- more efficient calculation of needs for pngBytes
45 * -- option to use pre-created PNGJ image data (3/19/14; Jmol 14.1.12)
51 * // tEXt chunk "Jmol type - <PNG0|PNGJ><0000000pt>+<000000len>"
53 * // tEXt chunk "Software - Jmol <version>"
55 * // tEXt chunk "Creation Time - <date>"
57 * // tRNS chunk transparent color, if desired
59 * // IDAT chunk (image data)
63 * // [JMOL ZIP FILE APPENDIX]
67 * PngEncoder takes a Java Image object and creates a byte string which can be
68 * saved as a PNG file. The Image is presumed to use the DirectColorModel.
70 * Thanks to Jay Denny at KeyPoint Software http://www.keypoint.com/ who let me
71 * develop this code on company time.
73 * You may contact me with (probably very-much-needed) improvements, comments,
78 * @author J. David Eisenberg
79 * @author http://catcode.com/pngencoder/
80 * @author Christian Ribeaud (christian.ribeaud@genedata.com)
81 * @author Bob Hanson (hansonr@stolaf.edu)
83 * @version 1.4, 31 March 2000
85 public class PngEncoder extends CRCEncoder {
87 /** Constants for filters */
88 public static final int FILTER_NONE = 0;
89 public static final int FILTER_SUB = 1;
90 public static final int FILTER_UP = 2;
91 public static final int FILTER_LAST = 2;
93 private static final int PT_FIRST_TAG = 37;
95 private boolean encodeAlpha;
96 private int filter = FILTER_NONE;
97 private int bytesPerPixel;
98 private int compressionLevel;
100 private Integer transparentColor;
102 private byte[] appData;
103 private String appPrefix;
104 private String comment;
105 private byte[] bytes;
108 public PngEncoder() {
113 protected void setParams(Map<String, Object> params) {
115 quality = (params.containsKey("qualityPNG") ? ((Integer) params
116 .get("qualityPNG")).intValue() : 2);
120 filter = FILTER_NONE;
121 compressionLevel = quality;
122 transparentColor = (Integer) params.get("transparentColor");
123 comment = (String) params.get("comment");
124 type = (params.get("type") + "0000").substring(0, 4);
125 bytes = (byte[]) params.get("pngImgData");
126 appData = (byte[]) params.get("pngAppData");
127 appPrefix = (String) params.get("pngAppPrefix");
133 protected void generate() throws IOException {
141 dataLen = bytes.length;
144 if (appData != null) {
145 setJmolTypeText(appPrefix, bytes, len, appData.length,
147 out.write(bytes, 0, len);
148 len = (bytes = appData).length;
150 out.write(bytes, 0, len);
155 * Creates an array of bytes that is the PNG equivalent of the current image,
156 * specifying whether to encode alpha or not.
158 * @return true if successful
161 private boolean pngEncode() {
163 byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
165 writeBytes(pngIdBytes);
168 writeText(getApplicationText(appPrefix, type, 0, 0));
170 writeText("Software\0Jmol " + comment);
171 writeText("Creation Time\0" + date);
173 if (!encodeAlpha && transparentColor != null)
174 writeTransparentColor(transparentColor.intValue());
176 return writeImageData();
180 * Fill in the Jmol type text area with number of bytes of PNG data and number
181 * of bytes of Jmol state data and fix checksum.
183 * If we do not do this, then the checksum will be wrong, and Jmol and some
184 * other programs may not be able to read the PNG image.
186 * This was corrected for Jmol 12.3.30. Between 12.3.7 and 12.3.29, PNG files
187 * created by Jmol have incorrect checksums.
196 private static void setJmolTypeText(String prefix, byte[] b, int nPNG, int nState, String type) {
197 String s = "tEXt" + getApplicationText(prefix, type, nPNG, nState);
198 CRCEncoder encoder = new PngEncoder();
199 byte[] test = s.substring(0, 4 + prefix.length()).getBytes();
200 for (int i = test.length; -- i >= 0;)
201 if (b[i + PT_FIRST_TAG] != test[i]) {
202 System.out.println("image is not of the right form; appending data, but not adding tEXt tag.");
205 encoder.setData(b, PT_FIRST_TAG);
206 encoder.writeString(s);
210 private static String getApplicationText(String prefix, String type, int nPNG, int nState) {
211 String sPNG = "000000000" + nPNG;
212 sPNG = sPNG.substring(sPNG.length() - 9);
213 String sState = "000000000" + nState;
214 sState = sState.substring(sState.length() - 9);
215 return prefix + "\0" + type + (type.equals("PNG") ? "0" : "") + sPNG + "+"
220 // * Set the filter to use
222 // * @param whichFilter from constant list
224 // public void setFilter(int whichFilter) {
225 // this.filter = (whichFilter <= FILTER_LAST ? whichFilter : FILTER_NONE);
229 // * Retrieve filtering scheme
231 // * @return int (see constant list)
233 // public int getFilter() {
238 // * Set the compression level to use
240 // * @param level 0 through 9
242 // public void setCompressionLevel(int level) {
243 // if ((level >= 0) && (level <= 9)) {
244 // this.compressionLevel = level;
249 // * Retrieve compression level
251 // * @return int in range 0-9
253 // public int getCompressionLevel() {
254 // return compressionLevel;
258 * Write a PNG "IHDR" chunk into the pngBytes array.
260 private void writeHeader() {
267 writeByte(8); // bit depth
268 writeByte(encodeAlpha ? 6 : 2); // color type or direct model
269 writeByte(0); // compression method
270 writeByte(0); // filter method
271 writeByte(0); // no interlace
275 private void writeText(String msg) {
276 writeInt4(msg.length());
278 writeString("tEXt" + msg);
283 * Write a PNG "tRNS" chunk into the pngBytes array.
287 private void writeTransparentColor(int icolor) {
292 writeInt2((icolor >> 16) & 0xFF);
293 writeInt2((icolor >> 8) & 0xFF);
294 writeInt2(icolor & 0xFF);
298 private byte[] scanLines; // the scan lines to be compressed
299 private int byteWidth; // width * bytesPerPixel
301 //private int hdrPos, dataPos, endPos;
302 //private byte[] priorRow;
303 //private byte[] leftBytes;
307 * Write the image data into the pngBytes array. This will write one or more
308 * PNG "IDAT" chunks. In order to conserve memory, this method grabs as many
309 * rows as will fit into 32K bytes, or the whole image; whichever is less.
312 * @return true if no errors; false if error grabbing pixels
314 private boolean writeImageData() {
316 bytesPerPixel = (encodeAlpha ? 4 : 3);
317 byteWidth = width * bytesPerPixel;
319 int scanWidth = byteWidth + 1; // the added 1 is for the filter byte
321 //boolean doFilter = (filter != FILTER_NONE);
323 int rowsLeft = height; // number of rows remaining to write
324 //int startRow = 0; // starting row to process this time through
325 int nRows; // how many rows to grab at a time
327 int scanPos; // where we are in the scan lines
329 Deflater deflater = new Deflater(compressionLevel);
330 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
332 DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
335 int pt = 0; // overall image byte pointer
337 // Jmol note: The entire image has been stored in pixels[] already
340 while (rowsLeft > 0) {
341 nRows = Math.max(1, Math.min(32767 / scanWidth, rowsLeft));
342 scanLines = new byte[scanWidth * nRows];
346 // leftBytes = new byte[16];
349 // priorRow = new byte[scanWidth - 1];
352 int nPixels = width * nRows;
355 for (int i = 0; i < nPixels; i++, pt++) {
356 if (i % width == 0) {
357 scanLines[scanPos++] = (byte) filter;
358 //startPos = scanPos;
360 scanLines[scanPos++] = (byte) ((pixels[pt] >> 16) & 0xff);
361 scanLines[scanPos++] = (byte) ((pixels[pt] >> 8) & 0xff);
362 scanLines[scanPos++] = (byte) ((pixels[pt]) & 0xff);
364 scanLines[scanPos++] = (byte) ((pixels[pt] >> 24) & 0xff);
366 // if (doFilter && i % width == width - 1) {
379 * Write these lines to the output area
381 compBytes.write(scanLines, 0, scanPos);
389 * Write the compressed bytes
391 byte[] compressedLines = outBytes.toByteArray();
392 writeInt4(compressedLines.length);
395 writeBytes(compressedLines);
400 } catch (IOException e) {
401 System.err.println(e.toString());
407 * Write a PNG "IEND" chunk into the pngBytes array.
409 private void writeEnd() {
417 //* Perform "sub" filtering on the given row.
418 //* Uses temporary array leftBytes to store the original values
419 //* of the previous pixels. The array is 16 bytes long, which
420 //* will easily hold two-byte samples plus two-byte alpha.
423 //private void filterSub() {
424 // int offset = bytesPerPixel;
425 // int actualStart = startPos + offset;
426 // int leftInsert = offset;
427 // int leftExtract = 0;
428 // //byte current_byte;
430 // for (int i = actualStart; i < startPos + byteWidth; i++) {
431 // leftBytes[leftInsert] = scanLines[i];
432 // scanLines[i] = (byte) ((scanLines[i] - leftBytes[leftExtract]) % 256);
433 // leftInsert = (leftInsert + 1) % 0x0f;
434 // leftExtract = (leftExtract + 1) % 0x0f;
439 //* Perform "up" filtering on the given row. Side effect: refills the prior row
443 //private void filterUp() {
444 // int nBytes = width * bytesPerPixel;
445 // for (int i = 0; i < nBytes; i++) {
446 // int pt = startPos + i;
447 // byte b = scanLines[pt];
448 // scanLines[pt] = (byte) ((scanLines[pt] - priorRow[i]) % 256);