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