/Users/lyon/j4p/src/ip/gif/gifAnimation/Gif89Encoder.java

1    //****************************************************************************** 
2    // Gif89Encoder.java 
3    //****************************************************************************** 
4    package ip.gif.gifAnimation; 
5     
6    import java.awt.*; 
7    import java.io.*; 
8    import java.util.Vector; 
9     
10   //============================================================================== 
11    
12   /** 
13    * This is the central class of a JDK 1.1 compatible GIF encoder that, 
14    * AFAIK, supports more features of the extended GIF spec than any other 
15    * Java open source encoder.  Some sections of the source are lifted or 
16    * adapted from Jef Poskanzer's <cite>Acme GifEncoder</cite> (so please see 
17    * the <a href="../readme.txt">readme</a> containing his notice), but much 
18    * of it, including nearly all of the present class, is original code.  My 
19    * main motivation for writing a new encoder was to support animated GIFs, 
20    * but the package also adds support for embedded textual comments. 
21    * <p/> 
22    * There are still some limitations.  For instance, animations are limited 
23    * to a single global color table.  But that is usually what you want 
24    * anyway, so as to avoid irregularities on some displays.  (So this is not 
25    * really a limitation, but a "disciplinary feature" :)  Another rather 
26    * more serious restriction is that the total number of RGB colors in a 
27    * given input-batch mustn't exceed 256.  Obviously, there is an opening 
28    * here for someone who would like to add a color-reducing preprocessor. 
29    * <p/> 
30    * The encoder, though very usable in its present form, is at bottom only a 
31    * partial implementation skewed toward my own particular needs.  Hence a 
32    * couple of caveats are in order.  (1) During development it was in the 
33    * back of my mind that an encoder object should be reusable - i.e., you 
34    * should be able to make multiple calls to encode() on the same object, 
35    * with or without intervening frame additions or changes to options.  But 
36    * I haven't reviewed the code with such usage in mind, much less tested 
37    * it, so it's likely I overlooked something.  (2) The encoder classes 
38    * aren't thread safe, so use caution in a context where access is shared 
39    * by multiple threads.  (Better yet, finish the library and re-release it 
40    * :) 
41    * <p/> 
42    * There follow a couple of simple examples illustrating the most common 
43    * way to use the encoder, i.e., to encode AWT Image objects created 
44    * elsewhere in the program.  Use of some of the most popular format 
45    * options is also shown, though you will want to peruse the API for 
46    * additional features. 
47    * <p/> 
48    * <p/> 
49    * <strong>Animated GIF Example</strong> 
50    * <pre> 
51    *  import net.jmge.gif.Gif89Encoder; 
52    *  // ... 
53    *  void writeAnimatedGIF(Image[] still_images, 
54    *                        String annotation, 
55    *                        boolean looped, 
56    *                        double frames_per_second, 
57    *                        OutputStream out) throws IOException 
58    *  { 
59    *    Gif89Encoder gifenc = new Gif89Encoder(); 
60    *    for (int i = 0; i < still_images.length; ++i) 
61    *      gifenc.addFrame(still_images[i]); 
62    *    gifenc.setComments(annotation); 
63    *    gifenc.setLoopCount(looped ? 0 : 1); 
64    *    gifenc.setUniformDelay((int) Math.round(100 / frames_per_second)); 
65    *    gifenc.encode(out); 
66    *  } 
67    *  </pre> 
68    * <p/> 
69    * <strong>Static GIF Example</strong> 
70    * <pre> 
71    *  import net.jmge.gif.Gif89Encoder; 
72    *  // ... 
73    *  void writeNormalGIF(Image img, 
74    *                      String annotation, 
75    *                      int transparent_index,  // pass -1 for none 
76    *                      boolean interlaced, 
77    *                      OutputStream out) throws IOException 
78    *  { 
79    *    Gif89Encoder gifenc = new Gif89Encoder(img); 
80    *    gifenc.setComments(annotation); 
81    *    gifenc.setTransparentIndex(transparent_index); 
82    *    gifenc.getFrameAt(0).setInterlaced(interlaced); 
83    *    gifenc.encode(out); 
84    *  } 
85    *  </pre> 
86    * 
87    * @author J. M. G. Elliott (tep@jmge.net) 
88    * @version 0.90 beta (15-Jul-2000) 
89    * @see Gif89Frame 
90    * @see DirectGif89Frame 
91    * @see IndexGif89Frame 
92    */ 
93   public class Gif89Encoder { 
94    
95       private Dimension dispDim = new Dimension(0, 0); 
96       private GifColorTable colorTable; 
97       private int bgIndex = 0; 
98       private int loopCount = 1; 
99       private String theComments; 
100      private Vector vFrames = new Vector(); 
101   
102      //---------------------------------------------------------------------------- 
103      /** 
104       * Use this default constructor if you'll be adding multiple frames 
105       * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel 
106       * arrays). 
107       */ 
108      public Gif89Encoder() { 
109          // empty color table puts us into "palette autodetect" mode 
110          colorTable = new GifColorTable(); 
111      } 
112   
113      //---------------------------------------------------------------------------- 
114      /** 
115       * Like the default except that it also adds a single frame, for 
116       * conveniently encoding a static GIF from an image. 
117       * 
118       * @param static_image Any Image object that supports pixel-grabbing. 
119       * @throws IOException See the addFrame() methods. 
120       */ 
121      public Gif89Encoder(Image static_image) throws IOException { 
122          this(); 
123          addFrame(static_image); 
124      } 
125   
126      //---------------------------------------------------------------------------- 
127      /** 
128       * This constructor installs a user color table, overriding the 
129       * detection of of a palette from ARBG pixels. 
130       * <p/> 
131       * Use of this constructor imposes a couple of restrictions: (1) Frame 
132       * objects can't be of type DirectGif89Frame (2) Transparency, if 
133       * desired, must be set explicitly. 
134       * 
135       * @param colors Array of color values; no more than 256 colors will be 
136       *               read, since that's the limit for a GIF. 
137       */ 
138      public Gif89Encoder(Color[] colors) { 
139          colorTable = new GifColorTable(colors); 
140      } 
141   
142      //---------------------------------------------------------------------------- 
143      /** 
144       * Convenience constructor for encoding a static GIF from index-model 
145       * data. Adds a single frame as specified. 
146       * 
147       * @param colors    Array of color values; no more than 256 colors will 
148       *                  be read, since that's the limit for a GIF. 
149       * @param width     Width of the GIF bitmap. 
150       * @param height    Height of same. 
151       * @param ci_pixels Array of color-index pixels no less than width * 
152       *                  height in length. 
153       * @throws IOException See the addFrame() methods. 
154       */ 
155      public Gif89Encoder(Color[] colors, 
156                          int width, 
157                          int height, 
158                          byte ci_pixels[]) 
159              throws IOException { 
160          this(colors); 
161          addFrame(width, height, ci_pixels); 
162      } 
163   
164      //---------------------------------------------------------------------------- 
165      /** 
166       * Get the number of frames that have been added so far. 
167       * 
168       * @return Number of frame items. 
169       */ 
170      public int getFrameCount() { 
171          return vFrames.size(); 
172      } 
173   
174      //---------------------------------------------------------------------------- 
175      /** 
176       * Get a reference back to a Gif89Frame object by position. 
177       * 
178       * @param index Zero-based index of the frame in the sequence. 
179       * @return Gif89Frame object at the specified position (or null if no 
180       *         such frame). 
181       */ 
182      public Gif89Frame getFrameAt(int index) { 
183          return isOk(index) ? (Gif89Frame) vFrames.elementAt(index) : null; 
184      } 
185   
186      //---------------------------------------------------------------------------- 
187      /** 
188       * Add a Gif89Frame frame to the end of the internal sequence.  Note 
189       * that there are restrictions on the Gif89Frame type: if the encoder 
190       * object was constructed with an explicit color table, an attempt to 
191       * add a DirectGif89Frame will throw an exception. 
192       * 
193       * @param gf An externally constructed Gif89Frame. 
194       * @throws IOException If Gif89Frame can't be accommodated.  This could 
195       *                     happen if either (1) the aggregate cross-frame 
196       *                     RGB color count exceeds 256, or (2) the 
197       *                     Gif89Frame subclass is incompatible with the 
198       *                     present encoder object. 
199       */ 
200      public void addFrame(Gif89Frame gf) throws IOException { 
201          accommodateFrame(gf); 
202          vFrames.addElement(gf); 
203      } 
204   
205      //---------------------------------------------------------------------------- 
206      /** 
207       * Convenience version of addFrame() that takes a Java Image, 
208       * internally constructing the requisite DirectGif89Frame. 
209       * 
210       * @param image Any Image object that supports pixel-grabbing. 
211       * @throws IOException If either (1) pixel-grabbing fails, (2) the 
212       *                     aggregate cross-frame RGB color count exceeds 
213       *                     256, or (3) this encoder object was constructed 
214       *                     with an explicit color table. 
215       */ 
216      public void addFrame(Image image) throws IOException { 
217          addFrame(new DirectGif89Frame(image)); 
218      } 
219   
220      //---------------------------------------------------------------------------- 
221      /** 
222       * The index-model convenience version of addFrame(). 
223       * 
224       * @param width     Width of the GIF bitmap. 
225       * @param height    Height of same. 
226       * @param ci_pixels Array of color-index pixels no less than width * 
227       *                  height in length. 
228       * @throws IOException Actually, in the present implementation, there 
229       *                     aren't any unchecked exceptions that can be 
230       *                     thrown when adding an IndexGif89Frame <i>per 
231       *                     se</i>.  But I might add some pedantic check 
232       *                     later, to justify the generality :) 
233       */ 
234      public void addFrame(int width, int height, byte ci_pixels[]) 
235              throws IOException { 
236          addFrame(new IndexGif89Frame(width, height, ci_pixels)); 
237      } 
238   
239      //---------------------------------------------------------------------------- 
240      /** 
241       * Like addFrame() except that the frame is inserted at a specific 
242       * point in the sequence rather than appended. 
243       * 
244       * @param index Zero-based index at which to insert frame. 
245       * @param gf    An externally constructed Gif89Frame. 
246       * @throws IOException If Gif89Frame can't be accommodated.  This could 
247       *                     happen if either (1) the aggregate cross-frame 
248       *                     RGB color count exceeds 256, or (2) the 
249       *                     Gif89Frame subclass is incompatible with the 
250       *                     present encoder object. 
251       */ 
252      public void insertFrame(int index, Gif89Frame gf) throws IOException { 
253          accommodateFrame(gf); 
254          vFrames.insertElementAt(gf, index); 
255      } 
256   
257      //---------------------------------------------------------------------------- 
258      /** 
259       * Set the color table index for the transparent color, if any. 
260       * 
261       * @param index Index of the color that should be rendered as 
262       *              transparent, if any. A value of -1 turns off 
263       *              transparency.  (Default: -1) 
264       */ 
265      public void setTransparentIndex(int index) { 
266          colorTable.setTransparent(index); 
267      } 
268   
269      //---------------------------------------------------------------------------- 
270      /** 
271       * Sets attributes of the multi-image display area, if applicable. 
272       * 
273       * @param dim        Width/height of display.  (Default: largest 
274       *                   detected frame size) 
275       * @param background Color table index of background color.  (Default: 
276       *                   0) 
277       * @see Gif89Frame#setPosition 
278       */ 
279      public void setLogicalDisplay(Dimension dim, int background) { 
280          dispDim = new Dimension(dim); 
281          bgIndex = background; 
282      } 
283   
284      //---------------------------------------------------------------------------- 
285      /** 
286       * Set animation looping parameter, if applicable. 
287       * 
288       * @param count Number of times to play sequence.  Special value of 0 
289       *              specifies indefinite looping.  (Default: 1) 
290       */ 
291      public void setLoopCount(int count) { 
292          loopCount = count; 
293      } 
294   
295      //---------------------------------------------------------------------------- 
296      /** 
297       * Specify some textual comments to be embedded in GIF. 
298       * 
299       * @param comments String containing ASCII comments. 
300       */ 
301      public void setComments(String comments) { 
302          theComments = comments; 
303      } 
304   
305      //---------------------------------------------------------------------------- 
306      /** 
307       * A convenience method for setting the "animation speed".  It simply 
308       * sets the delay parameter for each frame in the sequence to the 
309       * supplied value. Since this is actually frame-level rather than 
310       * animation-level data, take care to add your frames before calling 
311       * this method. 
312       * 
313       * @param interval Interframe interval in centiseconds. 
314       */ 
315      public void setUniformDelay(int interval) { 
316          for (int i = 0; i < vFrames.size(); ++i) 
317              ((Gif89Frame) vFrames.elementAt(i)).setDelay(interval); 
318      } 
319   
320      //---------------------------------------------------------------------------- 
321      /** 
322       * After adding your frame(s) and setting your options, simply call 
323       * this method to write the GIF to the passed stream.  Multiple calls 
324       * are permissible if for some reason that is useful to your 
325       * application.  (The method simply encodes the current state of the 
326       * object with no thought to previous calls.) 
327       * 
328       * @param out The stream you want the GIF written to. 
329       * @throws IOException If a write error is encountered. 
330       */ 
331      public void encode(OutputStream out) throws IOException { 
332          int nframes = getFrameCount(); 
333          boolean is_sequence = nframes > 1; 
334   
335          // N.B. must be called before writing screen descriptor 
336          colorTable.closePixelProcessing(); 
337   
338          // write GIF HEADER 
339          Put.ascii("GIF89a", out); 
340   
341          // write global blocks 
342          writeLogicalScreenDescriptor(out); 
343          colorTable.encode(out); 
344          if (is_sequence && loopCount != 1) 
345              writeNetscapeExtension(out); 
346          if (theComments != null && theComments.length() > 0) 
347              writeCommentExtension(out); 
348   
349          // write out the control and rendering data for each frame 
350          for (int i = 0; i < nframes; ++i) 
351              ((Gif89Frame) vFrames.elementAt(i)).encode(out, 
352                      is_sequence, 
353                      colorTable.getDepth(), 
354                      colorTable.getTransparent()); 
355   
356          // write GIF TRAILER 
357          out.write((int) ';'); 
358   
359          out.flush(); 
360      } 
361   
362      //---------------------------------------------------------------------------- 
363      /** 
364       * A simple driver to test the installation and to demo usage.  Put the 
365       * JAR on your classpath and run ala <blockquote>java 
366       * net.jmge.gif.Gif89Encoder {filename}</blockquote> The filename must 
367       * be either (1) a JPEG file with extension 'jpg', for conversion to a 
368       * static GIF, or (2) a file containing a list of GIFs and/or JPEGs, 
369       * one per line, to be combined into an animated GIF.  The output will 
370       * appear in the current directory as 'gif89out.gif'. 
371       * <p/> 
372       * (N.B. This test program will abort if the input file(s) exceed(s) 
373       * 256 total RGB colors, so in its present form it has no value as a 
374       * generic JPEG to GIF converter.  Also, when multiple files are input, 
375       * you need to be wary of the total color count, regardless of file 
376       * type.) 
377       * 
378       * @param args Command-line arguments, only the first of which is used, 
379       *             as mentioned above. 
380       */ 
381      public static void main(String[] args) { 
382          try { 
383   
384              Toolkit tk = Toolkit.getDefaultToolkit(); 
385              OutputStream out = new BufferedOutputStream( 
386                      new FileOutputStream("gif89out.gif")); 
387   
388              if (args[0].toUpperCase().endsWith(".JPG")) 
389                  new Gif89Encoder(tk.getImage(args[0])).encode(out); 
390              else { 
391                  BufferedReader in = new BufferedReader( 
392                          new FileReader(args[0])); 
393                  Gif89Encoder ge = new Gif89Encoder(); 
394   
395                  String line; 
396                  while ((line = in.readLine()) != null) 
397                      ge.addFrame(tk.getImage(line.trim())); 
398                  ge.setLoopCount(0);  // let's loop indefinitely 
399                  ge.encode(out); 
400   
401                  in.close(); 
402              } 
403              out.close(); 
404   
405          } catch (Exception e) { 
406              e.printStackTrace(); 
407          } finally { 
408              System.exit(0); 
409          } // must kill VM explicitly (Toolkit thread?) 
410      } 
411   
412      //---------------------------------------------------------------------------- 
413      private void accommodateFrame(Gif89Frame gf) throws IOException { 
414          dispDim.width = Math.max(dispDim.width, gf.getWidth()); 
415          dispDim.height = Math.max(dispDim.height, gf.getHeight()); 
416          colorTable.processPixels(gf); 
417      } 
418   
419      //---------------------------------------------------------------------------- 
420      private void writeLogicalScreenDescriptor(OutputStream os) 
421              throws IOException { 
422          Put.leShort(dispDim.width, os); 
423          Put.leShort(dispDim.height, os); 
424   
425          // write 4 fields, packed into a byte  (bitfieldsize:value) 
426          //   global color map present?         (1:1) 
427          //   bits per primary color less 1     (3:7) 
428          //   sorted color table?               (1:0) 
429          //   bits per pixel less 1             (3:varies) 
430          os.write(0xf0 | colorTable.getDepth() - 1); 
431   
432          // write background color index 
433          os.write(bgIndex); 
434   
435          // Jef Poskanzer's notes on the next field, for our possible edification: 
436          // Pixel aspect ratio - 1:1. 
437          //Putbyte( (byte) 49, outs ); 
438          // Java's GIF reader currently has a bug, if the aspect ratio byte is 
439          // not zero it throws an ImageFormatException.  It doesn't know that 
440          // 49 means a 1:1 aspect ratio.  Well, whatever, zero works with all 
441          // the other decoders I've tried so it probably doesn't hurt. 
442   
443          // OK, if it's good enough for Jef, it's definitely good enough for us: 
444          os.write(0); 
445      } 
446   
447      //---------------------------------------------------------------------------- 
448      private void writeNetscapeExtension(OutputStream os) 
449              throws IOException { 
450          // n.b. most software seems to interpret the count as a repeat count 
451          // (i.e., interations beyond 1) rather than as an iteration count 
452          // (thus, to avoid repeating we have to omit the whole extension) 
453   
454          os.write((int) '!');           // GIF Extension Introducer 
455          os.write(0xff);                // Application Extension Label 
456   
457          os.write(11);                  // application ID block size 
458          Put.ascii("NETSCAPE2.0", os);  // application ID data 
459   
460          os.write(3);                   // data sub-block size 
461          os.write(1);                   // a looping flag? dunno 
462   
463          // we finally write the relevent data 
464          Put.leShort(loopCount > 1 ? loopCount - 1 : 0, os); 
465   
466          os.write(0);                   // block terminator 
467      } 
468   
469      //---------------------------------------------------------------------------- 
470      private void writeCommentExtension(OutputStream os) 
471              throws IOException { 
472          os.write((int) '!');     // GIF Extension Introducer 
473          os.write(0xfe);          // Comment Extension Label 
474   
475          int remainder = theComments.length() % 255; 
476          int nsubblocks_full = theComments.length() / 255; 
477          int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0); 
478          int ibyte = 0; 
479          for (int isb = 0; isb < nsubblocks; ++isb) { 
480              int size = isb < nsubblocks_full ? 255 : remainder; 
481   
482              os.write(size); 
483              Put.ascii(theComments.substring(ibyte, ibyte + size), os); 
484              ibyte += size; 
485          } 
486   
487          os.write(0);    // block terminator 
488      } 
489   
490      //---------------------------------------------------------------------------- 
491      private boolean isOk(int frame_index) { 
492          return frame_index >= 0 && frame_index < vFrames.size(); 
493      } 
494  } 
495   
496  //============================================================================== 
497   
498  class GifColorTable { 
499   
500      // the palette of ARGB colors, packed as returned by Color.getRGB() 
501      private int[] theColors = new int[256]; 
502   
503      // other basic attributes 
504      private int colorDepth; 
505      private int transparentIndex = -1; 
506   
507      // these fields track color-index info across frames 
508      private int ciCount = 0; // count of distinct color indices 
509      private ReverseColorMap ciLookup;    // cumulative rgb-to-ci lookup table 
510   
511      //---------------------------------------------------------------------------- 
512      GifColorTable() { 
513          ciLookup = new ReverseColorMap();  // puts us into "auto-detect mode" 
514      } 
515   
516      //---------------------------------------------------------------------------- 
517      GifColorTable(Color[] colors) { 
518          int n2copy = Math.min(theColors.length, colors.length); 
519          for (int i = 0; i < n2copy; ++i) 
520              theColors[i] = colors[i].getRGB(); 
521      } 
522   
523      //---------------------------------------------------------------------------- 
524      int getDepth() { 
525          return colorDepth; 
526      } 
527   
528      //---------------------------------------------------------------------------- 
529      int getTransparent() { 
530          return transparentIndex; 
531      } 
532   
533      //---------------------------------------------------------------------------- 
534      // default: -1 (no transparency) 
535      void setTransparent(int color_index) { 
536          transparentIndex = color_index; 
537      } 
538   
539      //---------------------------------------------------------------------------- 
540      void processPixels(Gif89Frame gf) throws IOException { 
541          if (gf instanceof DirectGif89Frame) 
542              filterPixels((DirectGif89Frame) gf); 
543          else 
544              trackPixelUsage((IndexGif89Frame) gf); 
545      } 
546   
547      //---------------------------------------------------------------------------- 
548      void closePixelProcessing()  // must be called before encode() 
549      { 
550          colorDepth = computeColorDepth(ciCount); 
551      } 
552   
553      //---------------------------------------------------------------------------- 
554      void encode(OutputStream os) throws IOException { 
555          // size of palette written is the smallest power of 2 that can accomdate 
556          // the number of RGB colors detected (or largest color index, in case of 
557          // index pixels) 
558          int palette_size = 1 << colorDepth; 
559          for (int i = 0; i < palette_size; ++i) { 
560              os.write(theColors[i] >> 16 & 0xff); 
561              os.write(theColors[i] >> 8 & 0xff); 
562              os.write(theColors[i] & 0xff); 
563          } 
564      } 
565   
566      //---------------------------------------------------------------------------- 
567      // This method accomplishes three things: 
568      // (1) converts the passed rgb pixels to indexes into our rgb lookup table 
569      // (2) fills the rgb table as new colors are encountered 
570      // (3) looks for transparent pixels so as to set the transparent index 
571      // The information is cumulative across multiple calls. 
572      // 
573      // (Note: some of the logic is borrowed from Jef Poskanzer's code.) 
574      //---------------------------------------------------------------------------- 
575      private void filterPixels(DirectGif89Frame dgf) throws IOException { 
576          if (ciLookup == null) 
577              throw new IOException( 
578                      "RGB frames require palette autodetection"); 
579   
580          int[] argb_pixels = (int[]) dgf.getPixelSource(); 
581          byte[] ci_pixels = dgf.getPixelSink(); 
582          int npixels = argb_pixels.length; 
583          for (int i = 0; i < npixels; ++i) { 
584              int argb = argb_pixels[i]; 
585   
586              // handle transparency 
587              if ((argb >>> 24) < 0x80)        // transparent pixel? 
588                  if (transparentIndex == -1)    // first transparent color encountered? 
589                      transparentIndex = ciCount;  // record its index 
590                  else if (argb != theColors[transparentIndex]) // different pixel value? 
591                  { 
592                      // collapse all transparent pixels into one color index 
593                      ci_pixels[i] = (byte) transparentIndex; 
594                      continue;  // CONTINUE - index already in table 
595                  } 
596   
597              // try to look up the index in our "reverse" color table 
598              int color_index = ciLookup.getPaletteIndex(argb & 0xffffff); 
599   
600              if (color_index == -1)  // if it isn't in there yet 
601              { 
602                  if (ciCount == 256) 
603                      throw new IOException( 
604                              "can't encode as GIF (> 256 colors)"); 
605   
606                  // store color in our accumulating palette 
607                  theColors[ciCount] = argb; 
608   
609                  // store index in reverse color table 
610                  ciLookup.put(argb & 0xffffff, ciCount); 
611   
612                  // send color index to our output array 
613                  ci_pixels[i] = (byte) ciCount; 
614   
615                  // increment count of distinct color indices 
616                  ++ciCount; 
617              } else  // we've already snagged color into our palette 
618                  ci_pixels[i] = (byte) color_index;  // just send filtered pixel 
619          } 
620      } 
621   
622      //---------------------------------------------------------------------------- 
623      private void trackPixelUsage(IndexGif89Frame igf) throws IOException { 
624          byte[] ci_pixels = (byte[]) igf.getPixelSource(); 
625          int npixels = ci_pixels.length; 
626          for (int i = 0; i < npixels; ++i) 
627              if (ci_pixels[i] >= ciCount) 
628                  ciCount = ci_pixels[i] + 1; 
629      } 
630   
631      //---------------------------------------------------------------------------- 
632      private int computeColorDepth(int colorcount) { 
633          // color depth = log-base-2 of maximum number of simultaneous colors, i.e. 
634          // bits per color-index pixel 
635          if (colorcount <= 2) 
636              return 1; 
637          if (colorcount <= 4) 
638              return 2; 
639          if (colorcount <= 16) 
640              return 4; 
641          return 8; 
642      } 
643  } 
644   
645  //============================================================================== 
646  // We're doing a very simple linear hashing thing here, which seems sufficient 
647  // for our needs.  I make no claims for this approach other than that it seems 
648  // an improvement over doing a brute linear search for each pixel on the one 
649  // hand, and creating a Java object for each pixel (if we were to use a Java 
650  // Hashtable) on the other.  Doubtless my little hash could be improved by 
651  // tuning the capacity (at the very least).  Suggestions are welcome. 
652  //============================================================================== 
653   
654  class ReverseColorMap { 
655   
656      private static class ColorRecord { 
657          int rgb; 
658          int ipalette; 
659   
660          ColorRecord(int rgb, int ipalette) { 
661              this.rgb = rgb; 
662              this.ipalette = ipalette; 
663          } 
664      } 
665   
666      // I wouldn't really know what a good hashing capacity is, having missed out 
667      // on data structures and algorithms class :)  Alls I know is, we've got a lot 
668      // more space than we have time.  So let's try a sparse table with a maximum 
669      // load of about 1/8 capacity. 
670      private static final int HCAPACITY = 2053;  // a nice prime number 
671   
672      // our hash table proper 
673      private ColorRecord[] hTable = new ColorRecord[HCAPACITY]; 
674   
675      //---------------------------------------------------------------------------- 
676      // Assert: rgb is not negative (which is the same as saying, be sure the 
677      // alpha transparency byte - i.e., the high byte - has been masked out). 
678      //---------------------------------------------------------------------------- 
679      int getPaletteIndex(int rgb) { 
680          ColorRecord rec; 
681   
682          for (int itable = rgb % hTable.length; 
683               (rec = hTable[itable]) != null && rec.rgb != rgb; 
684               itable = ++itable % hTable.length 
685                  ) 
686              ; 
687   
688          if (rec != null) 
689              return rec.ipalette; 
690   
691          return -1; 
692      } 
693   
694      //---------------------------------------------------------------------------- 
695      // Assert: (1) same as above; (2) rgb key not already present 
696      //---------------------------------------------------------------------------- 
697      void put(int rgb, int ipalette) { 
698          int itable; 
699   
700          for (itable = rgb % hTable.length; 
701               hTable[itable] != null; 
702               itable = ++itable % hTable.length 
703                  ) 
704              ; 
705   
706          hTable[itable] = new ColorRecord(rgb, ipalette); 
707      } 
708  }