3 * $Date: 2007-03-30 12:26:16 -0500 (Fri, 30 Mar 2007) $
6 * Some portions of this file have been modified by Robert Hanson hansonr.at.stolaf.edu 2012-2017
7 * for use in SwingJS via transpilation into JavaScript using Java2Script.
9 * Copyright (C) 2002-2005 The Jmol Development Team
11 * Contact: jmol-developers@lists.sf.net
13 * This library is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU Lesser General Public
15 * License as published by the Free Software Foundation; either
16 * version 2.1 of the License, or (at your option) any later version.
18 * This library is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * Lesser General Public License for more details.
23 * You should have received a copy of the GNU Lesser General Public
24 * License along with this library; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
29 import java.io.ByteArrayOutputStream;
30 import java.io.IOException;
32 import java.util.zip.Deflater;
33 import java.util.zip.DeflaterOutputStream;
39 * Modified by Bob Hanson hansonr@stolaf.edu to be a subclass of ImageEncoder
40 * and to use javajs.util.OutputChannel instead of just returning bytes. Also includes:
42 * -- JavaScript-compatible image processing
44 * -- transparent background option
46 * -- more efficient calculation of needs for pngBytes
48 * -- option to use pre-created PNGJ image data (3/19/14; Jmol 14.1.12)
54 * // tEXt chunk "Jmol type - <PNG0|PNGJ|PNGT><0000000pt>+<000000len>"
56 * // tEXt chunk "Software - Jmol <version>"
58 * // tEXt chunk "Creation Time - <date>"
60 * // tRNS chunk transparent color, if desired
62 * // IDAT chunk (image data)
66 * // [JMOL ZIP FILE APPENDIX]
70 * PngEncoder takes a Java Image object and creates a byte string which can be
71 * saved as a PNG file. The Image is presumed to use the DirectColorModel.
73 * Thanks to Jay Denny at KeyPoint Software http://www.keypoint.com/ who let me
74 * develop this code on company time.
76 * You may contact me with (probably very-much-needed) improvements, comments,
81 * @author J. David Eisenberg
82 * @author http://catcode.com/pngencoder/
83 * @author Christian Ribeaud (christian.ribeaud@genedata.com)
84 * @author Bob Hanson (hansonr@stolaf.edu)
86 * @version 1.4, 31 March 2000
88 public class PngEncoder extends CRCEncoder {
90 /** Constants for filters */
91 public static final int FILTER_NONE = 0;
92 public static final int FILTER_SUB = 1;
93 public static final int FILTER_UP = 2;
94 public static final int FILTER_LAST = 2;
96 private static final int PT_FIRST_TAG = 37;
98 private boolean encodeAlpha;
99 private int filter = FILTER_NONE;
100 private int bytesPerPixel;
101 private int compressionLevel;
103 private Integer transparentColor;
105 private byte[] appData;
106 private String appPrefix;
107 private String comment;
108 private byte[] bytes;
111 public PngEncoder() {
116 protected void setParams(Map<String, Object> params) {
118 quality = (params.containsKey("qualityPNG") ? ((Integer) params
119 .get("qualityPNG")).intValue() : 2);
123 filter = FILTER_NONE;
124 compressionLevel = quality;
125 transparentColor = (Integer) params.get("transparentColor");
126 comment = (String) params.get("comment");
127 type = (params.get("type") + "0000").substring(0, 4);
128 bytes = (byte[]) params.get("pngImgData");
129 appData = (byte[]) params.get("pngAppData");
130 appPrefix = (String) params.get("pngAppPrefix");
136 protected void generate() throws IOException {
144 dataLen = bytes.length;
147 if (appData != null) {
148 setJmolTypeText(appPrefix, bytes, len, appData.length,
150 out.write(bytes, 0, len);
151 len = (bytes = appData).length;
153 out.write(bytes, 0, len);
158 * Creates an array of bytes that is the PNG equivalent of the current image,
159 * specifying whether to encode alpha or not.
161 * @return true if successful
164 private boolean pngEncode() {
166 byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
168 writeBytes(pngIdBytes);
171 writeText(getApplicationText(appPrefix, type, 0, 0));
173 writeText("Software\0" + comment);
174 writeText("Creation Time\0" + date);
176 if (!encodeAlpha && transparentColor != null)
177 writeTransparentColor(transparentColor.intValue());
179 return writeImageData();
183 * Fill in the Jmol type text area with number of bytes of PNG data and number
184 * of bytes of Jmol state data and fix checksum.
186 * If we do not do this, then the checksum will be wrong, and Jmol and some
187 * other programs may not be able to read the PNG image.
189 * This was corrected for Jmol 12.3.30. Between 12.3.7 and 12.3.29, PNG files
190 * created by Jmol have incorrect checksums.
199 private static void setJmolTypeText(String prefix, byte[] b, int nPNG, int nState, String type) {
200 String s = "tEXt" + getApplicationText(prefix, type, nPNG, nState);
201 CRCEncoder encoder = new PngEncoder();
202 byte[] test = s.substring(0, 4 + prefix.length()).getBytes();
203 for (int i = test.length; -- i >= 0;)
204 if (b[i + PT_FIRST_TAG] != test[i]) {
205 System.out.println("image is not of the right form; appending data, but not adding tEXt tag.");
208 encoder.setData(b, PT_FIRST_TAG);
209 encoder.writeString(s);
214 * Generate the PNGJ directory identifier:
216 * xxxxxxxxx\0ttttiiiiiiiii+ddddddddd
220 * xxxxxxxxx is a unique 9-character software identifier
221 * tttt is a four-byte software-specific type indicator (PNG0, PNGJ, PNGT, etc.)
222 * iiiiiiiii is the file pointer to the start of app data
223 * ddddddddd is the length of the app data
225 * @param prefix up to 9 characters to allow software to recognize itself
226 * @param type PNGx, where x is J or T for Jmol; original type "PNG" is now "PNG0"
231 private static String getApplicationText(String prefix, String type,
232 int nPNG, int nData) {
233 String sPNG = "000000000" + nPNG;
234 sPNG = sPNG.substring(sPNG.length() - 9);
235 String sData = "000000000" + nData;
236 sData = sData.substring(sData.length() - 9);
238 prefix = "#SwingJS.";
239 if (prefix.length() < 9)
240 prefix = (prefix + ".........");
241 if (prefix.length() > 9)
242 prefix = prefix.substring(0, 9);
243 return prefix + "\0" + type + sPNG + "+" + sData;
247 // * Set the filter to use
249 // * @param whichFilter from constant list
251 // public void setFilter(int whichFilter) {
252 // this.filter = (whichFilter <= FILTER_LAST ? whichFilter : FILTER_NONE);
256 // * Retrieve filtering scheme
258 // * @return int (see constant list)
260 // public int getFilter() {
265 // * Set the compression level to use
267 // * @param level 0 through 9
269 // public void setCompressionLevel(int level) {
270 // if ((level >= 0) && (level <= 9)) {
271 // this.compressionLevel = level;
276 // * Retrieve compression level
278 // * @return int in range 0-9
280 // public int getCompressionLevel() {
281 // return compressionLevel;
285 * Write a PNG "IHDR" chunk into the pngBytes array.
287 private void writeHeader() {
294 writeByte(8); // bit depth
295 writeByte(encodeAlpha ? 6 : 2); // color type or direct model
296 writeByte(0); // compression method
297 writeByte(0); // filter method
298 writeByte(0); // no interlace
302 private void writeText(String msg) {
303 writeInt4(msg.length());
305 writeString("tEXt" + msg);
310 * Write a PNG "tRNS" chunk into the pngBytes array.
314 private void writeTransparentColor(int icolor) {
319 writeInt2((icolor >> 16) & 0xFF);
320 writeInt2((icolor >> 8) & 0xFF);
321 writeInt2(icolor & 0xFF);
325 private byte[] scanLines; // the scan lines to be compressed
326 private int byteWidth; // width * bytesPerPixel
328 //private int hdrPos, dataPos, endPos;
329 //private byte[] priorRow;
330 //private byte[] leftBytes;
334 * Write the image data into the pngBytes array. This will write one or more
335 * PNG "IDAT" chunks. In order to conserve memory, this method grabs as many
336 * rows as will fit into 32K bytes, or the whole image; whichever is less.
339 * @return true if no errors; false if error grabbing pixels
341 private boolean writeImageData() {
343 bytesPerPixel = (encodeAlpha ? 4 : 3);
344 byteWidth = width * bytesPerPixel;
346 int scanWidth = byteWidth + 1; // the added 1 is for the filter byte
348 //boolean doFilter = (filter != FILTER_NONE);
350 int rowsLeft = height; // number of rows remaining to write
351 //int startRow = 0; // starting row to process this time through
352 int nRows; // how many rows to grab at a time
354 int scanPos; // where we are in the scan lines
356 Deflater deflater = new Deflater(compressionLevel);
357 ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
359 DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
362 int pt = 0; // overall image byte pointer
364 // Jmol note: The entire image has been stored in pixels[] already
367 while (rowsLeft > 0) {
368 nRows = Math.max(1, Math.min(32767 / scanWidth, rowsLeft));
369 scanLines = new byte[scanWidth * nRows];
373 // leftBytes = new byte[16];
376 // priorRow = new byte[scanWidth - 1];
379 int nPixels = width * nRows;
382 for (int i = 0; i < nPixels; i++, pt++) {
383 if (i % width == 0) {
384 scanLines[scanPos++] = (byte) filter;
385 //startPos = scanPos;
387 scanLines[scanPos++] = (byte) ((pixels[pt] >> 16) & 0xff);
388 scanLines[scanPos++] = (byte) ((pixels[pt] >> 8) & 0xff);
389 scanLines[scanPos++] = (byte) ((pixels[pt]) & 0xff);
391 scanLines[scanPos++] = (byte) ((pixels[pt] >> 24) & 0xff);
393 // if (doFilter && i % width == width - 1) {
406 * Write these lines to the output area
408 compBytes.write(scanLines, 0, scanPos);
416 * Write the compressed bytes
418 byte[] compressedLines = outBytes.toByteArray();
419 writeInt4(compressedLines.length);
422 writeBytes(compressedLines);
427 } catch (IOException e) {
428 System.err.println(e.toString());
434 * Write a PNG "IEND" chunk into the pngBytes array.
436 private void writeEnd() {
444 //* Perform "sub" filtering on the given row.
445 //* Uses temporary array leftBytes to store the original values
446 //* of the previous pixels. The array is 16 bytes long, which
447 //* will easily hold two-byte samples plus two-byte alpha.
450 //private void filterSub() {
451 // int offset = bytesPerPixel;
452 // int actualStart = startPos + offset;
453 // int leftInsert = offset;
454 // int leftExtract = 0;
455 // //byte current_byte;
457 // for (int i = actualStart; i < startPos + byteWidth; i++) {
458 // leftBytes[leftInsert] = scanLines[i];
459 // scanLines[i] = (byte) ((scanLines[i] - leftBytes[leftExtract]) % 256);
460 // leftInsert = (leftInsert + 1) % 0x0f;
461 // leftExtract = (leftExtract + 1) % 0x0f;
466 //* Perform "up" filtering on the given row. Side effect: refills the prior row
470 //private void filterUp() {
471 // int nBytes = width * bytesPerPixel;
472 // for (int i = 0; i < nBytes; i++) {
473 // int pt = startPos + i;
474 // byte b = scanLines[pt];
475 // scanLines[pt] = (byte) ((scanLines[pt] - priorRow[i]) % 256);