JAL-1807 Bob's JalviewJS prototype first commit
[jalviewjs.git] / src / javajs / img / GifEncoder.java
1 /* $RCSfile$
2  * $Author: hansonr $
3  * $Date: 2007-06-02 12:14:13 -0500 (Sat, 02 Jun 2007) $
4  * $Revision: 7831 $
5  *
6  * Copyright (C) 2000-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
25 //  Final encoding code from http://acme.com/resources/classes/Acme/JPM/Encoders/GifEncoder.java
26 //
27 //  GifEncoder - write out an image as a GIF
28 // 
29 // 
30 //  Transparency handling and variable bit size courtesy of Jack Palevich.
31 //  
32 //  Copyright (C)1996,1998 by Jef Poskanzer <jef@mail.acme.com>. All rights reserved.
33 //  
34 //  Redistribution and use in source and binary forms, with or without
35 //  modification, are permitted provided that the following conditions
36 //  are met:
37 //  1. Redistributions of source code must retain the above copyright
38 //     notice, this list of conditions and the following disclaimer.
39 //  2. Redistributions in binary form must reproduce the above copyright
40 //     notice, this list of conditions and the following disclaimer in the
41 //     documentation and/or other materials provided with the distribution.
42 // 
43 //  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
44 //  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
45 //  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
46 //  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
47 //  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
48 //  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
49 //  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
50 //  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
51 //  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
52 //  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
53 //  SUCH DAMAGE.
54 // 
55 //  Visit the ACME Labs Java page for up-to-date versions of this and other
56 //  fine Java utilities: http://www.acme.com/java/
57 // 
58 /// Write out an image as a GIF.
59 // <P>
60 // <A HREF="/resources/classes/Acme/JPM/Encoders/GifEncoder.java">Fetch the software.</A><BR>
61 // <A HREF="/resources/classes/Acme.tar.gz">Fetch the entire Acme package.</A>
62 // <P>
63 // @see ToGif
64
65 package javajs.img;
66
67 import javajs.util.CU;
68 import javajs.util.Lst;
69 import javajs.util.M3;
70 import javajs.util.P3;
71
72 import java.util.Hashtable;
73 import java.util.Map;
74 import java.io.IOException;
75
76 /**
77  * 
78  * GifEncoder extensively adapted for Jmol by Bob Hanson
79  * 
80  * Color quantization roughly follows the GIMP method
81  * "dither Floyd-Steinberg (normal)" but with some twists. (For example, we
82  * exclude the background color.)
83  * 
84  * Note that although GIMP code annotation refers to "median-cut", it is really
85  * using MEAN-cut. That is what I use here as well.
86  * 
87  * -- commented code allows visualization of the color space using Jmol. Very
88  * enlightening!
89  * 
90  * -- much simplified interface with ImageEncoder
91  * 
92  * -- uses simple Hashtable with Integer() to catalog colors
93  * 
94  * -- allows progressive production of animated GIF via Jmol CAPTURE command
95  * 
96  * -- uses general purpose javajs.util.OutputChannel for byte-handling options
97  * such as posting to a server, writing to disk, and retrieving bytes.
98  * 
99  * -- allows JavaScript port
100  * 
101  * -- Bob Hanson, first try: 24 Sep 2013; final coding: 9 Nov 2014
102  * 
103  * 
104  * @author Bob Hanson hansonr@stolaf.edu
105  */
106
107 public class GifEncoder extends ImageEncoder {
108
109   private Map<String, Object> params;
110   private P3[] palette;
111   private int backgroundColor;
112
113   private boolean interlaced;
114   private boolean addHeader = true;
115   private boolean addImage = true;
116   private boolean addTrailer = true;
117   private boolean isTransparent;
118   private boolean floydSteinberg = true;
119   private boolean capturing;
120   private boolean looping;
121
122   private int delayTime100ths = -1;
123   private int bitsPerPixel = 1;
124
125   private int byteCount;
126
127   /**
128    * we allow for animated GIF by being able to re-enter the code with different
129    * parameters held in params
130    * 
131    * 
132    */
133   @Override
134   protected void setParams(Map<String, Object> params) {
135     this.params = params;
136     Integer ic = (Integer) params.get("transparentColor");
137     if (ic == null) {
138       ic = (Integer) params.get("backgroundColor");
139       if (ic != null)
140         backgroundColor = ic.intValue();
141     } else {
142       backgroundColor = ic.intValue();
143       isTransparent = true;
144     }
145
146     interlaced = (Boolean.TRUE == params.get("interlaced"));
147     if (params.containsKey("captureRootExt") // file0000.gif 
148         || !params.containsKey("captureMode")) // animated gif
149       return;
150     interlaced = false;
151     capturing = true;
152     try {
153       byteCount = ((Integer) params.get("captureByteCount")).intValue();
154     } catch (Exception e) {
155       // ignore
156     }
157     switch ("maec"
158         .indexOf(((String) params.get("captureMode")).substring(0, 1))) {
159     case 0: //"movie"
160       params.put("captureMode", "add");
161       addImage = false;
162       addTrailer = false;
163       break;
164     case 1: // add 
165       addHeader = false;
166       addTrailer = false;
167       int fps = Math.abs(((Integer) params.get("captureFps")).intValue());
168       delayTime100ths = (fps == 0 ? 0 : 100 / fps);
169       looping = (Boolean.FALSE != params.get("captureLooping"));
170       break;
171     case 2: // end
172       addHeader = false;
173       addImage = false;
174       break;
175     case 3: // cancel
176       addHeader = false;
177       addImage = false;
178       out.cancel();
179       break;
180     }
181   }
182
183   @Override
184   protected void generate() throws IOException {
185     if (addHeader)
186       writeHeader();
187     addHeader = false; // only one header
188     if (addImage) {
189       createPalette();
190       writeGraphicControlExtension();
191       if (delayTime100ths >= 0 && looping)
192         writeNetscapeLoopExtension();
193       writeImage();
194     }
195   }
196
197   @Override
198   protected void close() {
199     if (addTrailer) {
200       writeTrailer();
201     } else {
202       doClose = false;
203     }
204     if (capturing)
205       params.put("captureByteCount", Integer.valueOf(byteCount));
206   }
207
208   //////////////  256-color quantization  //////////////
209
210   /**
211    * a color point in normalized L*a*b space with a flag indicating whether it
212    * is the background color
213    */
214   private class ColorItem extends P3 {
215     protected boolean isBackground;
216
217     ColorItem(int rgb, boolean isBackground) {
218       this.isBackground = isBackground;
219       setT(toLABnorm(rgb));
220     }
221   }
222
223   /**
224    * A list of normalized L*a*b points with an index and a center and volume
225    * 
226    */
227   private class ColorCell extends Lst<P3> {
228
229     protected int index;
230     protected P3 center;
231
232     private float volume;
233
234     ColorCell(int index) {
235       this.index = index;
236     }
237
238     /**
239      * @param doVisualize
240      *        debugging only
241      * @return volume in normalized L*a*b space
242      */
243     public float getVolume(boolean doVisualize) {
244       if (volume != 0)
245         return volume;
246       if (size() < 2)
247         return -1;
248       //if (true)
249       //return lst.size();
250       //float d;
251       float maxx = -Integer.MAX_VALUE;
252       float minx = Integer.MAX_VALUE;
253       float maxy = -Integer.MAX_VALUE;
254       float miny = Integer.MAX_VALUE;
255       float maxz = -Integer.MAX_VALUE;
256       float minz = Integer.MAX_VALUE;
257       int n = size();
258       for (int i = n; --i >= 0;) {
259         P3 xyz = get(i);
260         if (xyz.x < minx)
261           minx = xyz.x;
262         if (xyz.y < miny)
263           miny = xyz.y;
264         if (xyz.z < minz)
265           minz = xyz.z;
266         if (xyz.x > maxx)
267           maxx = xyz.x;
268         if (xyz.y > maxy)
269           maxy = xyz.y;
270         if (xyz.z > maxz)
271           maxz = xyz.z;
272       }
273       float dx = (maxx - minx);
274       float dy = (maxy - miny);
275       float dz = (maxz - minz);
276       // Jmol visualization only
277       //      if (doVisualize) {
278       //        P3 ptRGB = toRGB(center);
279       //        drawPt(index, -size(), ptRGB);
280       //        //for (int i = n; --i >= 0;)
281       //        //drawPt(index, i, toRGB(get(i)));
282       //        P3 pt0 = toRGB(P3.new3(Math.max(minx, 0), Math.max(miny, 0),
283       //            Math.max(minz, 0)));
284       //        P3 pt1 = toRGB(P3.new3(Math.min(maxx, 100), Math.min(maxy, 100),
285       //            Math.min(maxz, 100)));
286       //        rgbToXyz(pt0, pt0);
287       //        xyzToLab(pt0, pt0);
288       //        rgbToXyz(pt1, pt1);
289       //        xyzToLab(pt1, pt1);
290       //        System.out.println("boundbox corners " + pt0 + " " + pt1);
291       //        System.out.println("draw d" + index + " boundbox color " + ptRGB
292       //            + " mesh nofill");
293       //      }
294       return volume = dx * dx + dy * dy + dz * dz;
295     }
296
297     //    // Jmol visualization only
298     //      private void drawPt(int index, int i, P3 rgb) {
299     //        boolean isMain = (i < 0);
300     //      P3 lab = rgbToXyz(rgb, null);
301     //      xyzToLab(lab, lab);
302     //      System.out.println("draw d" + index + (isMain ? "_" : "_" + i) + " width "
303     //          + (isMain ? 1.0 : 0.2) + " " + lab
304     //          + " color " + rgb + (isMain ? " '" + -i + "'" : ""));
305     //      }
306
307     /**
308      * Set the average normalized L*a*b value for this cell and return its RGB point
309      * 
310      * @return RGB point
311      * 
312      */
313     protected P3 setColor() {
314       int count = size();
315       center = new P3();
316       for (int i = count; --i >= 0;)
317         center.add(get(i));
318       center.scale(1f / count);
319       // Jmol visualization only
320       //volume = 0;
321       //getVolume(true); 
322       return toRGB(center);
323     }
324
325     /**
326      * use median_cut algorithm to split the cell, creating a doubly linked
327      * list.
328      * 
329      * Paul Heckbert, MIT thesis COLOR IMAGE QUANTIZATION FOR FRAME BUFFER
330      * DISPLAY https://www.cs.cmu.edu/~ph/ciq_thesis
331      * 
332      * except, as in GIMP, we use center (not median) here.
333      * 
334      * @param cells
335      * @return true if split
336      */
337     protected boolean splitCell(Lst<ColorCell> cells) {
338       int n = size();
339       if (n < 2)
340         return false;
341       int newIndex = cells.size();
342       ColorCell newCell = new ColorCell(newIndex);
343       cells.addLast(newCell);
344       float[][] ranges = new float[3][3];
345       for (int ic = 0; ic < 3; ic++) {
346         float low = Float.MAX_VALUE;
347         float high = -Float.MAX_VALUE;
348         for (int i = n; --i >= 0;) {
349           P3 lab = get(i);
350           float v = (ic == 0 ? lab.x : ic == 1 ? lab.y : lab.z);
351           if (low > v)
352             low = v;
353           if (high < v)
354             high = v;
355         }
356         ranges[0][ic] = low;
357         ranges[1][ic] = high;
358         ranges[2][ic] = high - low;
359       }
360       float[] r = ranges[2];
361       int mode = (r[0] >= r[1] ? (r[0] >= r[2] ? 0 : 2) : r[1] >= r[2] ? 1 : 2);
362       float val = ranges[0][mode] + ranges[2][mode] / 2;
363       volume = 0; // recalculate volume if needed
364       switch (mode) {
365       case 0:
366         for (int i = n; --i >= 0;)
367           if (get(i).x >= val)
368             newCell.addLast(remove(i));
369         break;
370       case 1:
371         for (int i = n; --i >= 0;)
372           if (get(i).y >= val)
373             newCell.addLast(remove(i));
374         break;
375       case 2:
376         for (int i = size(); --i >= 0;)
377           if (get(i).z >= val)
378             newCell.addLast(remove(i));
379         break;
380       }
381       return true;
382     }
383   }
384
385   /**
386    * Quantize all colors and create the final palette;
387    * replace pixels[] with an array of color indices.
388    * 
389    */
390   private void createPalette() {
391
392     // catalog all pixel colors
393
394     Lst<ColorItem> tempColors = new Lst<ColorItem>();
395     Map<Integer, ColorItem> ciHash = new Hashtable<Integer, ColorItem>();
396     for (int i = 0, n = pixels.length; i < n; i++) {
397       int rgb = pixels[i];
398       Integer key = Integer.valueOf(rgb);
399       ColorItem item = ciHash.get(key);
400       if (item == null) {
401         item = new ColorItem(rgb, rgb == backgroundColor);
402         ciHash.put(key, item);
403         tempColors.addLast(item);
404       }
405     }
406     int nColors = tempColors.size();
407     System.out.println("GIF total image colors: " + nColors);
408     ciHash = null;
409
410     // create a set of <= 256 color cells
411
412     Lst<ColorCell> cells = quantizeColors(tempColors);
413     nColors = cells.size();
414     System.out.println("GIF final color count: " + nColors);
415
416     // generate the palette and map each cell's rgb color to itself
417
418     Map<Integer, ColorCell> colorMap = new Hashtable<Integer, ColorCell>();
419     bitsPerPixel = (nColors <= 2 ? 1 : nColors <= 4 ? 2 : nColors <= 16 ? 4 : 8);
420     palette = new P3[1 << bitsPerPixel];
421     for (int i = 0; i < nColors; i++) {
422       ColorCell c = cells.get(i);
423       colorMap.put(
424           Integer.valueOf(CU.colorPtToFFRGB(palette[i] = c.setColor())), c);
425     }
426
427     // index all pixels to a pallete color
428
429     pixels = indexPixels(cells, colorMap);
430   }
431
432   /**
433    * Quantize colors by generating a set of cells in normalized L*a*b space
434    * containing all colors. Start with just two cells -- fixed background color
435    * and all others. Keep splitting cells while there are fewer than 256 and
436    * some with multiple colors in them.
437    * 
438    * It is possible that we will end up with fewer than 256 colors.
439    * 
440    * @param tempColors
441    * @return final list of colors
442    */
443   private Lst<ColorCell> quantizeColors(Lst<ColorItem> tempColors) {
444     int n = tempColors.size();
445     Lst<ColorCell> cells = new Lst<ColorCell>();
446     ColorCell cc = new ColorCell(0);
447     cc.addLast(new ColorItem(backgroundColor, true));
448     cells.addLast(cc);
449     cc = new ColorCell(1);
450     if (n > 256)
451       cells.addLast(cc);
452     for (int i = 0; i < n; i++) {
453       ColorItem c = tempColors.get(i);
454       if (c.isBackground)
455         continue;
456       cc.addLast(c);
457       if (n <= 256) {
458         cells.addLast(cc);
459         cc = new ColorCell(cells.size());
460       }
461     }
462     tempColors.clear();
463     if (n > 256)
464       while ((n = cells.size()) < 256) {
465         float maxVol = 0;
466         ColorCell maxCell = null;
467         for (int i = n; --i >= 1;) {
468           ColorCell c = cells.get(i);
469           float v = c.getVolume(false);
470           if (v > maxVol) {
471             maxVol = v;
472             maxCell = c;
473           }
474         }
475         if (maxCell == null || !maxCell.splitCell(cells))
476           break;
477       }
478     return cells;
479   }
480
481   /**
482    * 
483    * Assign all colors to their closest approximation and return an array of
484    * color indexes.
485    * 
486    * Uses Floyd-Steinberg dithering, finding the closest known color and then
487    * spreading out the error over four leading pixels. Limits error to +/- 75
488    * percent in normalized L*a*b space.
489    * 
490    * @param cells
491    *        quantized color cells
492    * @param colorMap
493    *        map of quantized rgb to its cell
494    * @return array of color indexes, one for each pixel
495    * 
496    */
497   private int[] indexPixels(Lst<ColorCell> cells,
498                             Map<Integer, ColorCell> colorMap) {
499     // We need a strip only width+2 wide to process all the errors.
500     // Errors are added to the next pixel and the next row's pixels 
501     // only through p + width + 1:
502     //         p  +1
503     //   +w-1 +w  +w+1
504     // so including p as well, we need a total of width + 2 errors.
505     //
506     // as p moves through the pixels, we just use mod to cycle through
507     // this strip.
508     //
509     int w2 = width + 2;
510     P3[] errors = new P3[w2];
511     // We should replace, not overwrite, pixels 
512     // as this may be the raw canvas.buf32.
513     int[] newPixels = new int[pixels.length];
514     P3 err = new P3();
515     P3 lab;
516     int rgb;
517     Map<Integer, ColorCell> nearestCell = new Hashtable<Integer, ColorCell>();
518     for (int i = 0, p = 0; i < height; ++i) {
519       boolean notLastRow = (i != height - 1);
520       for (int j = 0; j < width; ++j, p++) {
521         if (pixels[p] == backgroundColor) {
522           // leave as 0
523           continue;
524         }
525         P3 pe = errors[p % w2];
526         if (pe == null || pe.x == Float.MAX_VALUE) {
527           lab = null;
528           rgb = pixels[p];
529         } else {
530           lab = toLABnorm(pixels[p]);
531           err = pe;
532           // important not to round the clamp here -- full floating precision
533           err.x = clamp(err.x, -75, 75);
534           err.y = clamp(err.y, -75, 75);
535           err.z = clamp(err.z, -75, 75);
536           lab.add(err);
537           rgb = CU.colorPtToFFRGB(toRGB(lab));
538         }
539         Integer key = Integer.valueOf(rgb);
540         ColorCell cell = colorMap.get(key);
541         if (cell == null) {
542           // critical to generate normalized L*a*b from RGB here for nearestCell mapping.
543           // otherwise future RGB keys may match the wrong cell
544           lab = toLABnorm(rgb);
545           cell = nearestCell.get(key);
546           if (cell == null) {
547             // find nearest cell
548             float maxerr = Float.MAX_VALUE;
549             // skip 0 0 0
550             for (int ib = cells.size(); --ib >= 1;) {
551               ColorCell c = cells.get(ib);
552               err.sub2(lab, c.center);
553               float d = err.lengthSquared();
554               if (d < maxerr) {
555                 maxerr = d;
556                 cell = c;
557               }
558             }
559             nearestCell.put(key, cell);
560           }
561           if (floydSteinberg) {
562             // dither
563             err.sub2(lab, cell.center);
564             boolean notLastCol = (j < width - 1);
565             if (notLastCol)
566               addError(err, 7, errors, p + 1, w2);
567             if (notLastRow) {
568               if (j > 0)
569                 addError(err, 3, errors, p + width - 1, w2);
570               addError(err, 5, errors, p + width, w2);
571               if (notLastCol)
572                 addError(err, 1, errors, p + width + 1, w2);
573             }
574           }
575           err.x = Float.MAX_VALUE; // used; flag for resetting to 0
576         }
577         newPixels[p] = cell.index;
578       }
579     }
580     return newPixels;
581   }
582
583   private void addError(P3 err, int f, P3[] errors, int p, int w2) {
584     // GIMP will allow changing the background color.
585     if (pixels[p] == backgroundColor)
586       return;
587     p %= w2;
588     P3 errp = errors[p];
589     if (errp == null)
590       errp = errors[p] = new P3();
591     else if (errp.x == Float.MAX_VALUE) // reuse
592       errp.set(0, 0, 0);
593     errp.scaleAdd2(f / 16f, err, errp);
594   }
595
596   ///////////////////////// CIE L*a*b / XYZ / sRGB conversion methods /////////
597
598   // these could be static, but that just makes for more JavaScript code
599
600   protected P3 toLABnorm(int rgb) {
601     P3 lab = CU.colorPtFromInt(rgb, null);
602     rgbToXyz(lab, lab);
603     xyzToLab(lab, lab);
604     // normalize to 0-100
605     lab.y = (lab.y + 86.185f) / (98.254f + 86.185f) * 100f;
606     lab.z = (lab.z + 107.863f) / (94.482f + 107.863f) * 100f;
607     return lab;
608   }
609
610   protected P3 toRGB(P3 lab) {
611     P3 xyz = P3.newP(lab);
612     // normalized to 0-100
613     xyz.y = xyz.y / 100f * (98.254f + 86.185f) - 86.185f;
614     xyz.z = xyz.z / 100f * (94.482f + 107.863f) - 107.863f;
615     labToXyz(xyz, xyz);
616     return xyzToRgb(xyz, xyz);
617   }
618
619   private static M3 xyz2rgb;
620   private static M3 rgb2xyz;
621
622   static {
623     rgb2xyz = M3.newA9(new float[] { 0.4124f, 0.3576f, 0.1805f, 0.2126f,
624         0.7152f, 0.0722f, 0.0193f, 0.1192f, 0.9505f });
625
626     xyz2rgb = M3.newA9(new float[] { 3.2406f, -1.5372f, -0.4986f, -0.9689f,
627         1.8758f, 0.0415f, 0.0557f, -0.2040f, 1.0570f });
628   }
629
630   public P3 rgbToXyz(P3 rgb, P3 xyz) {
631     // http://en.wikipedia.org/wiki/CIE_1931_color_space
632     // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
633     if (xyz == null)
634       xyz = new P3();
635     xyz.x = sxyz(rgb.x);
636     xyz.y = sxyz(rgb.y);
637     xyz.z = sxyz(rgb.z);
638     rgb2xyz.rotate(xyz);
639     return xyz;
640   }
641
642   private float sxyz(float x) {
643     x /= 255;
644     return (float) (x <= 0.04045 ? x / 12.92 : Math.pow(((x + 0.055) / 1.055),
645         2.4)) * 100;
646   }
647
648   public P3 xyzToRgb(P3 xyz, P3 rgb) {
649     // http://en.wikipedia.org/wiki/CIE_1931_color_space
650     // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
651     if (rgb == null)
652       rgb = new P3();
653     rgb.setT(xyz);
654     rgb.scale(0.01f);
655     xyz2rgb.rotate(rgb);
656     rgb.x = clamp(srgb(rgb.x), 0, 255);
657     rgb.y = clamp(srgb(rgb.y), 0, 255);
658     rgb.z = clamp(srgb(rgb.z), 0, 255);
659     return rgb;
660   }
661
662   private float srgb(float x) {
663     return (float) (x > 0.0031308f ? (1.055 * Math.pow(x, 1.0 / 2.4)) - 0.055
664         : x * 12.92) * 255;
665   }
666
667   public P3 xyzToLab(P3 xyz, P3 lab) {
668     // http://en.wikipedia.org/wiki/Lab_color_space
669     // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
670     // Lab([0..100], [-86.185..98.254], [-107.863..94.482])
671     // XYZn = D65 = {95.0429, 100.0, 108.8900};
672     if (lab == null)
673       lab = new P3();
674     float x = flab(xyz.x / 95.0429f);
675     float y = flab(xyz.y / 100);
676     float z = flab(xyz.z / 108.89f);
677     lab.x = (116 * y) - 16;
678     lab.y = 500 * (x - y);
679     lab.z = 200 * (y - z);
680     return lab;
681   }
682
683   private float flab(float t) {
684     return (float) (t > 8.85645168E-3 /* (24/116)^3 */? Math.pow(t,
685         0.333333333) : 7.78703704 /* 1/3*116/24*116/24 */* t + 0.137931034 /* 16/116 */
686     );
687   }
688
689   public P3 labToXyz(P3 lab, P3 xyz) {
690     // http://en.wikipedia.org/wiki/Lab_color_space
691     // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
692     // XYZn = D65 = {95.0429, 100.0, 108.8900};
693     if (xyz == null)
694       xyz = new P3();
695
696     xyz.setT(lab);
697     float y = (xyz.x + 16) / 116;
698     float x = xyz.y / 500 + y;
699     float z = y - xyz.z / 200;
700     xyz.x = fxyz(x) * 95.0429f;
701     xyz.y = fxyz(y) * 100;
702     xyz.z = fxyz(z) * 108.89f;
703
704     return xyz;
705   }
706
707   private float fxyz(float t) {
708     return (float) (t > 0.206896552 /* (24/116) */? t * t * t
709         : 0.128418549 /* 3*24/116*24/116 */* (t - 0.137931034 /* 16/116 */));
710   }
711
712   private float clamp(float c, float min, float max) {
713     c = (c < min ? min : c > max ? max : c);
714     return (min == 0 ? Math.round(c) : c);
715   }
716
717   ///////////////////////// GifEncoder writing methods ////////////////////////
718
719   /**
720    * includes logical screen descriptor
721    * 
722    * @throws IOException
723    */
724   private void writeHeader() throws IOException {
725     putString("GIF89a");
726     putWord(width);
727     putWord(height);
728     putByte(0); // no global color table -- using local instead
729     putByte(0); // no background
730     putByte(0); // no pixel aspect ratio given
731   }
732
733   private void writeGraphicControlExtension() {
734     if (isTransparent || delayTime100ths >= 0) {
735       putByte(0x21); // graphic control extension
736       putByte(0xf9); // graphic control label
737       putByte(4); // block size
738       putByte((isTransparent ? 9 : 0) | (delayTime100ths > 0 ? 2 : 0)); // packed bytes 
739       putWord(delayTime100ths > 0 ? delayTime100ths : 0);
740       putByte(0); // transparent index
741       putByte(0); // end-of-block
742     }
743   }
744
745   // see  http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
746   //      +---------------+
747   //   0  |     0x21      |  Extension Label
748   //      +---------------+
749   //   1  |     0xFF      |  Application Extension Label
750   //      +---------------+
751   //   2  |     0x0B      |  Block Size
752   //      +---------------+
753   //   3  |               | 
754   //      +-             -+
755   //   4  |               | 
756   //      +-             -+
757   //   5  |               | 
758   //      +-             -+
759   //   6  |               | 
760   //      +-  NETSCAPE   -+  Application Identifier (8 bytes)
761   //   7  |               | 
762   //      +-             -+
763   //   8  |               | 
764   //      +-             -+
765   //   9  |               | 
766   //      +-             -+
767   //  10  |               | 
768   //      +---------------+
769   //  11  |               | 
770   //      +-             -+
771   //  12  |      2.0      |  Application Authentication Code (3 bytes)
772   //      +-             -+
773   //  13  |               | 
774   //      +===============+                      --+
775   //  14  |     0x03      |  Sub-block Data Size   |
776   //      +---------------+                        |
777   //  15  |     0x01      |  Sub-block ID          |
778   //      +---------------+                        | Application Data Sub-block
779   //  16  |               |                        |
780   //      +-             -+  Loop Count (2 bytes)  |
781   //  17  |               |                        |
782   //      +===============+                      --+
783   //  18  |     0x00      |  Block Terminator
784   //      +---------------+
785
786   private void writeNetscapeLoopExtension() {
787     putByte(0x21); // graphic control extension
788     putByte(0xff); // netscape loop extension
789     putByte(0x0B); // block size
790     putString("NETSCAPE2.0");
791     putByte(3);
792     putByte(1);
793     putWord(0); // loop indefinitely
794     putByte(0); // end-of-block
795
796   }
797
798   private int initCodeSize;
799   private int curpt;
800
801   private void writeImage() {
802     putByte(0x2C);
803     putWord(0); //left
804     putWord(0); //top
805     putWord(width);
806     putWord(height);
807
808     //    <Packed Fields>  =      LISx xZZZ
809
810     //    L Local Color Table Flag
811     //    I Interlace Flag
812     //    S Sort Flag
813     //    x Reserved
814     //    ZZZ Size of Local Color Table
815
816     int packedFields = 0x80 | (interlaced ? 0x40 : 0) | (bitsPerPixel - 1);
817     putByte(packedFields);
818     int colorMapSize = 1 << bitsPerPixel;
819     P3 p = new P3();
820     for (int i = 0; i < colorMapSize; i++) {
821       if (palette[i] != null)
822         p = palette[i];
823       putByte((int) p.x);
824       putByte((int) p.y);
825       putByte((int) p.z);
826     }
827     putByte(initCodeSize = (bitsPerPixel <= 1 ? 2 : bitsPerPixel));
828     compress();
829     putByte(0);
830   }
831
832   private void writeTrailer() {
833     // Write the GIF file terminator
834     putByte(0x3B);
835   }
836
837   ///// compression routines /////
838
839   private static final int EOF = -1;
840
841   // Return the next pixel from the image
842   private int nextPixel() {
843     if (countDown-- == 0)
844       return EOF;
845     int colorIndex = pixels[curpt];
846     // Bump the current X position
847     ++curx;
848     if (curx == width) {
849       // If we are at the end of a scan line, set curx back to the beginning
850       // If we are interlaced, bump the cury to the appropriate spot,
851       // otherwise, just increment it.
852       curx = 0;
853       if (interlaced)
854         updateY(INTERLACE_PARAMS[pass], INTERLACE_PARAMS[pass + 4]);
855       else
856         ++cury;
857     }
858     curpt = cury * width + curx;
859     return colorIndex & 0xff;
860   }
861
862   private static final int[] INTERLACE_PARAMS = { 8, 8, 4, 2, 4, 2, 1, 0 };
863
864   /**
865    * 
866    * Group 1 : Every 8th. row, starting with row 0. (Pass 1)
867    * 
868    * Group 2 : Every 8th. row, starting with row 4. (Pass 2)
869    * 
870    * Group 3 : Every 4th. row, starting with row 2. (Pass 3)
871    * 
872    * Group 4 : Every 2nd. row, starting with row 1. (Pass 4)
873    * 
874    * @param yNext
875    * @param yNew
876    */
877   private void updateY(int yNext, int yNew) {
878     cury += yNext;
879     if (yNew >= 0 && cury >= height) {
880       cury = yNew;
881       ++pass;
882     }
883   }
884
885   // Write out a word to the GIF file
886   private void putWord(int w) {
887     putByte(w);
888     putByte(w >> 8);
889   }
890
891   // GIFCOMPR.C       - GIF Image compression routines
892   //
893   // Lempel-Ziv compression based on 'compress'.  GIF modifications by
894   // David Rowley (mgardi@watdcsu.waterloo.edu)
895
896   // General DEFINEs
897
898   private static final int BITS = 12;
899
900   private static final int HSIZE = 5003; // 80% occupancy
901
902   // GIF Image compression - modified 'compress'
903   //
904   // Based on: compress.c - File compression ala IEEE Computer, June 1984.
905   //
906   // By Authors:  Spencer W. Thomas      (decvax!harpo!utah-cs!utah-gr!thomas)
907   //              Jim McKie              (decvax!mcvax!jim)
908   //              Steve Davies           (decvax!vax135!petsd!peora!srd)
909   //              Ken Turkowski          (decvax!decwrl!turtlevax!ken)
910   //              James A. Woods         (decvax!ihnp4!ames!jaw)
911   //              Joe Orost              (decvax!vax135!petsd!joe)
912
913   private int nBits; // number of bits/code
914   private int maxbits = BITS; // user settable max # bits/code
915   private int maxcode; // maximum code, given n_bits
916   private int maxmaxcode = 1 << BITS; // should NEVER generate this code
917
918   private final static int MAXCODE(int nBits) {
919     return (1 << nBits) - 1;
920   }
921
922   private int[] htab = new int[HSIZE];
923   private int[] codetab = new int[HSIZE];
924
925   private int hsize = HSIZE; // for dynamic table sizing
926
927   private int freeEnt = 0; // first unused entry
928
929   // block compression parameters -- after all codes are used up,
930   // and compression rate changes, start over.
931   private boolean clearFlag = false;
932
933   // Algorithm:  use open addressing double hashing (no chaining) on the
934   // prefix code / next character combination.  We do a variant of Knuth's
935   // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
936   // secondary probe.  Here, the modular division first probe is gives way
937   // to a faster exclusive-or manipulation.  Also do block compression with
938   // an adaptive reset, whereby the code table is cleared when the compression
939   // ratio decreases, but after the table fills.  The variable-length output
940   // codes are re-sized at this point, and a special CLEAR code is generated
941   // for the decompressor.  Late addition:  construct the table according to
942   // file size for noticeable speed improvement on small files.  Please direct
943   // questions about this implementation to ames!jaw.
944
945   private int clearCode;
946   private int EOFCode;
947
948   private int countDown;
949   private int pass = 0;
950   private int curx, cury;
951
952   private void compress() {
953
954     // Calculate number of bits we are expecting
955     countDown = width * height;
956
957     // Indicate which pass we are on (if interlace)
958     pass = 0;
959     // Set up the current x and y position
960     curx = 0;
961     cury = 0;
962
963     // Set up the necessary values
964     clearFlag = false;
965     nBits = initCodeSize + 1;
966     maxcode = MAXCODE(nBits);
967
968     clearCode = 1 << initCodeSize;
969     EOFCode = clearCode + 1;
970     freeEnt = clearCode + 2;
971
972     // Set up the 'byte output' routine
973     bufPt = 0;
974
975     int ent = nextPixel();
976
977     int hshift = 0;
978     int fcode;
979     for (fcode = hsize; fcode < 65536; fcode *= 2)
980       ++hshift;
981     hshift = 8 - hshift; // set hash code range bound
982
983     int hsizeReg = hsize;
984     clearHash(hsizeReg); // clear hash table
985
986     output(clearCode);
987
988     int c;
989     outer_loop: while ((c = nextPixel()) != EOF) {
990       fcode = (c << maxbits) + ent;
991       int i = (c << hshift) ^ ent; // xor hashing
992
993       if (htab[i] == fcode) {
994         ent = codetab[i];
995         continue;
996       } else if (htab[i] >= 0) // non-empty slot
997       {
998         int disp = hsizeReg - i; // secondary hash (after G. Knott)
999         if (i == 0)
1000           disp = 1;
1001         do {
1002           if ((i -= disp) < 0)
1003             i += hsizeReg;
1004
1005           if (htab[i] == fcode) {
1006             ent = codetab[i];
1007             continue outer_loop;
1008           }
1009         } while (htab[i] >= 0);
1010       }
1011       output(ent);
1012       ent = c;
1013       if (freeEnt < maxmaxcode) {
1014         codetab[i] = freeEnt++; // code -> hashtable
1015         htab[i] = fcode;
1016       } else {
1017         clearBlock();
1018       }
1019     }
1020     // Put out the final code.
1021     output(ent);
1022     output(EOFCode);
1023   }
1024
1025   // output
1026   //
1027   // Output the given code.
1028   // Inputs:
1029   //      code:   A n_bits-bit integer.  If == -1, then EOF.  This assumes
1030   //              that n_bits =< wordsize - 1.
1031   // Outputs:
1032   //      Outputs code to the file.
1033   // Assumptions:
1034   //      Chars are 8 bits long.
1035   // Algorithm:
1036   //      Maintain a BITS character long buffer (so that 8 codes will
1037   // fit in it exactly).  Use the VAX insv instruction to insert each
1038   // code in turn.  When the buffer fills up empty it and start over.
1039
1040   private int curAccum = 0;
1041   private int curBits = 0;
1042
1043   private int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
1044       0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF,
1045       0x7FFF, 0xFFFF };
1046
1047   private void output(int code) {
1048     curAccum &= masks[curBits];
1049
1050     if (curBits > 0)
1051       curAccum |= (code << curBits);
1052     else
1053       curAccum = code;
1054
1055     curBits += nBits;
1056
1057     while (curBits >= 8) {
1058       byteOut((byte) (curAccum & 0xff));
1059       curAccum >>= 8;
1060       curBits -= 8;
1061     }
1062
1063     // If the next entry is going to be too big for the code size,
1064     // then increase it, if possible.
1065     if (freeEnt > maxcode || clearFlag) {
1066       if (clearFlag) {
1067         maxcode = MAXCODE(nBits = initCodeSize + 1);
1068         clearFlag = false;
1069       } else {
1070         ++nBits;
1071         if (nBits == maxbits)
1072           maxcode = maxmaxcode;
1073         else
1074           maxcode = MAXCODE(nBits);
1075       }
1076     }
1077
1078     if (code == EOFCode) {
1079       // At EOF, write the rest of the buffer.
1080       while (curBits > 0) {
1081         byteOut((byte) (curAccum & 0xff));
1082         curAccum >>= 8;
1083         curBits -= 8;
1084       }
1085       flushBytes();
1086     }
1087   }
1088
1089   // Clear out the hash table
1090
1091   // table clear for block compress
1092   private void clearBlock() {
1093     clearHash(hsize);
1094     freeEnt = clearCode + 2;
1095     clearFlag = true;
1096
1097     output(clearCode);
1098   }
1099
1100   // reset code table
1101   private void clearHash(int hsize) {
1102     for (int i = 0; i < hsize; ++i)
1103       htab[i] = -1;
1104   }
1105
1106   // GIF-specific routines (byte array buffer)
1107
1108   // Number of bytes so far in this 'packet'
1109   private int bufPt;
1110
1111   // Define the storage for the packet accumulator
1112   final private byte[] buf = new byte[256];
1113
1114   // Add a byte to the end of the current packet, and if it is 254
1115   // byte, flush the packet to disk.
1116   private void byteOut(byte c) {
1117     buf[bufPt++] = c;
1118     if (bufPt >= 254)
1119       flushBytes();
1120   }
1121
1122   // Flush the packet to disk, and reset the accumulator
1123   protected void flushBytes() {
1124     if (bufPt > 0) {
1125       putByte(bufPt);
1126       out.write(buf, 0, bufPt);
1127       byteCount += bufPt;
1128       bufPt = 0;
1129     }
1130   }
1131
1132 }