JAL-1807 Bob's first commit -- Applet loaded; needs image
[jalview.git] / src / javajs / img / PngEncoder.java
1 /* $RCSfile$
2  * $Author: nicove $
3  * $Date: 2007-03-30 12:26:16 -0500 (Fri, 30 Mar 2007) $
4  * $Revision: 7275 $
5  *
6  * Copyright (C) 2002-2005  The Jmol Development Team
7  *
8  * Contact: jmol-developers@lists.sf.net
9  *
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.
14  *
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.
19  *
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.
23  */
24 package javajs.img;
25
26 import java.util.Map;
27 import java.util.zip.Deflater;
28 import java.util.zip.DeflaterOutputStream;
29 import java.io.ByteArrayOutputStream;
30 import java.io.IOException;
31
32
33
34 /**
35  * 
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: 
38  *  
39  * -- JavaScript-compatible image processing
40  *  
41  * -- transparent background option
42  *  
43  * -- more efficient calculation of needs for pngBytes 
44  * 
45  * -- option to use pre-created PNGJ image data (3/19/14; Jmol 14.1.12)
46  * 
47  * -- PNGJ format:
48  * 
49  * // IHDR chunk 
50  * 
51  * // tEXt chunk "Jmol type - <PNG0|PNGJ><0000000pt>+<000000len>" 
52  * 
53  * // tEXt chunk "Software - Jmol <version>"
54  * 
55  * // tEXt chunk "Creation Time - <date>"
56  * 
57  * // tRNS chunk transparent color, if desired
58  *
59  * // IDAT chunk (image data)
60  * 
61  * // IEND chunk 
62  * 
63  * // [JMOL ZIP FILE APPENDIX]
64  * 
65  * Original Comment:
66  * 
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.
69  * 
70  * Thanks to Jay Denny at KeyPoint Software http://www.keypoint.com/ who let me
71  * develop this code on company time.
72  * 
73  * You may contact me with (probably very-much-needed) improvements, comments,
74  * and bug fixes at:
75  * 
76  * david@catcode.com
77  * 
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)
82  * 
83  * @version 1.4, 31 March 2000
84  */
85 public class PngEncoder extends CRCEncoder {
86
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;
92   
93   private static final int PT_FIRST_TAG = 37;
94
95   private boolean encodeAlpha;
96   private int filter = FILTER_NONE;
97   private int bytesPerPixel;
98   private int compressionLevel;
99   private String type;
100   private Integer transparentColor;
101
102   private byte[] appData;
103   private String appPrefix;
104   private String comment;
105   private byte[] bytes;
106
107   
108   public PngEncoder() {
109     super();
110   }
111
112   @Override
113   protected void setParams(Map<String, Object> params) {
114     if (quality < 0)
115       quality = (params.containsKey("qualityPNG") ? ((Integer) params
116           .get("qualityPNG")).intValue() : 2);
117     if (quality > 9)
118       quality = 9;
119     encodeAlpha = false;
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");
128   }
129
130   
131
132   @Override
133   protected void generate() throws IOException {
134     if (bytes == null) {
135       if (!pngEncode()) {
136         out.cancel();
137         return;
138       }
139       bytes = getBytes();
140     } else {
141       dataLen = bytes.length;
142     }
143     int len = dataLen;
144     if (appData != null) {
145       setJmolTypeText(appPrefix, bytes, len, appData.length,
146           type);
147       out.write(bytes, 0, len);
148       len = (bytes = appData).length;
149     }
150     out.write(bytes, 0, len);
151   }
152
153
154   /**
155    * Creates an array of bytes that is the PNG equivalent of the current image,
156    * specifying whether to encode alpha or not.
157    * 
158    * @return        true if successful
159    * 
160    */
161   private boolean pngEncode() {
162
163     byte[] pngIdBytes = { -119, 80, 78, 71, 13, 10, 26, 10 };
164
165     writeBytes(pngIdBytes);
166     //hdrPos = bytePos;
167     writeHeader();
168     writeText(getApplicationText(appPrefix, type, 0, 0));
169
170     writeText("Software\0Jmol " + comment);
171     writeText("Creation Time\0" + date);
172
173     if (!encodeAlpha && transparentColor != null)
174       writeTransparentColor(transparentColor.intValue());
175     //dataPos = bytePos;
176     return writeImageData();
177   }
178
179   /**
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.
182    * 
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.
185    * 
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.
188    * 
189    * @param prefix 
190    * 
191    * @param b
192    * @param nPNG
193    * @param nState
194    * @param type
195    */
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.");
203         return;
204       }
205     encoder.setData(b, PT_FIRST_TAG);
206     encoder.writeString(s);
207     encoder.writeCRC();
208   }
209
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 + "+"
216         + sState;
217   }
218
219   //  /**
220   //   * Set the filter to use
221   //   *
222   //   * @param whichFilter from constant list
223   //   */
224   //  public void setFilter(int whichFilter) {
225   //    this.filter = (whichFilter <= FILTER_LAST ? whichFilter : FILTER_NONE);
226   //  }
227
228   //  /**
229   //   * Retrieve filtering scheme
230   //   *
231   //   * @return int (see constant list)
232   //   */
233   //  public int getFilter() {
234   //    return filter;
235   //  }
236
237   //  /**
238   //   * Set the compression level to use
239   //   *
240   //   * @param level 0 through 9
241   //   */
242   //  public void setCompressionLevel(int level) {
243   //    if ((level >= 0) && (level <= 9)) {
244   //      this.compressionLevel = level;
245   //    }
246   //  }
247
248   //  /**
249   //   * Retrieve compression level
250   //   *
251   //   * @return int in range 0-9
252   //   */
253   //  public int getCompressionLevel() {
254   //    return compressionLevel;
255   //  }
256
257   /**
258    * Write a PNG "IHDR" chunk into the pngBytes array.
259    */
260   private void writeHeader() {
261
262     writeInt4(13);
263     startPos = bytePos;
264     writeString("IHDR");
265     writeInt4(width);
266     writeInt4(height);
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
272     writeCRC();
273   }
274
275   private void writeText(String msg) {
276     writeInt4(msg.length());
277     startPos = bytePos;
278     writeString("tEXt" + msg);
279     writeCRC();
280   }
281
282   /**
283    * Write a PNG "tRNS" chunk into the pngBytes array.
284    * 
285    * @param icolor
286    */
287   private void writeTransparentColor(int icolor) {
288
289     writeInt4(6);
290     startPos = bytePos;
291     writeString("tRNS");
292     writeInt2((icolor >> 16) & 0xFF);
293     writeInt2((icolor >> 8) & 0xFF);
294     writeInt2(icolor & 0xFF);
295     writeCRC();
296   }
297
298   private byte[] scanLines; // the scan lines to be compressed
299   private int byteWidth; // width * bytesPerPixel
300
301   //private int hdrPos, dataPos, endPos;
302   //private byte[] priorRow;
303   //private byte[] leftBytes;
304
305
306   /**
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.
310    * 
311    * 
312    * @return true if no errors; false if error grabbing pixels
313    */
314   private boolean writeImageData() {
315
316     bytesPerPixel = (encodeAlpha ? 4 : 3);
317     byteWidth = width * bytesPerPixel;
318
319     int scanWidth = byteWidth + 1; // the added 1 is for the filter byte
320
321     //boolean doFilter = (filter != FILTER_NONE);
322
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
326
327     int scanPos; // where we are in the scan lines
328
329     Deflater deflater = new Deflater(compressionLevel);
330     ByteArrayOutputStream outBytes = new ByteArrayOutputStream(1024);
331
332     DeflaterOutputStream compBytes = new DeflaterOutputStream(outBytes,
333         deflater);
334
335     int pt = 0; // overall image byte pointer
336     
337     // Jmol note: The entire image has been stored in pixels[] already
338     
339     try {
340       while (rowsLeft > 0) {
341         nRows = Math.max(1, Math.min(32767 / scanWidth, rowsLeft));
342         scanLines = new byte[scanWidth * nRows];
343         //        if (doFilter)
344         //          switch (filter) {
345         //          case FILTER_SUB:
346         //            leftBytes = new byte[16];
347         //            break;
348         //          case FILTER_UP:
349         //            priorRow = new byte[scanWidth - 1];
350         //            break;
351         //          }
352         int nPixels = width * nRows;
353         scanPos = 0;
354         //startPos = 1;
355         for (int i = 0; i < nPixels; i++, pt++) {
356           if (i % width == 0) {
357             scanLines[scanPos++] = (byte) filter;
358             //startPos = scanPos;
359           }
360           scanLines[scanPos++] = (byte) ((pixels[pt] >> 16) & 0xff);
361           scanLines[scanPos++] = (byte) ((pixels[pt] >> 8) & 0xff);
362           scanLines[scanPos++] = (byte) ((pixels[pt]) & 0xff);
363           if (encodeAlpha) {
364             scanLines[scanPos++] = (byte) ((pixels[pt] >> 24) & 0xff);
365           }
366           //          if (doFilter && i % width == width - 1) {
367           //            switch (filter) {
368           //            case FILTER_SUB:
369           //              filterSub();
370           //              break;
371           //            case FILTER_UP:
372           //              filterUp();
373           //              break;
374           //            }
375           //          }
376         }
377
378         /*
379          * Write these lines to the output area
380          */
381         compBytes.write(scanLines, 0, scanPos);
382
383         //startRow += nRows;
384         rowsLeft -= nRows;
385       }
386       compBytes.close();
387
388       /*
389        * Write the compressed bytes
390        */
391       byte[] compressedLines = outBytes.toByteArray();
392       writeInt4(compressedLines.length);
393       startPos = bytePos;
394       writeString("IDAT");
395       writeBytes(compressedLines);
396       writeCRC();
397       writeEnd();
398       deflater.finish();
399       return true;
400     } catch (IOException e) {
401       System.err.println(e.toString());
402       return false;
403     }
404   }
405
406   /**
407    * Write a PNG "IEND" chunk into the pngBytes array.
408    */
409   private void writeEnd() {
410     writeInt4(0);
411     startPos = bytePos;
412     writeString("IEND");
413     writeCRC();
414   }
415
416   ///**
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.
421   //*
422   //*/
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;
429   //
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;
435   // }
436   //}
437   //
438   ///**
439   //* Perform "up" filtering on the given row. Side effect: refills the prior row
440   //* with current row
441   //* 
442   //*/
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);
449   //   priorRow[i] = b;
450   // }
451   //}
452
453 }