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