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