· 7 years ago · Oct 04, 2018, 04:30 AM
1/**
2The DrawingPanel class provides a simple interface for drawing persistent
3images using a Graphics object. An internal BufferedImage object is used
4to keep track of what has been drawn. A client of the class simply
5constructs a DrawingPanel of a particular size and then draws on it with
6the Graphics object, setting the background color if they so choose.
7<p>
8
9To ensure that the image is always displayed, a timer calls repaint at
10regular intervals.
11<p>
12
13This version of DrawingPanel also saves animated GIFs, though this is kind
14of hit-and-miss because animated GIFs are pretty sucky (256 color limit, large
15file size, etc).
16<p>
17
18Recent features:
19- save zoomed images (2011/10/25)
20- window no longer moves when zoom changes (2011/10/25)
21- grid lines (2011/10/11)
22
23@author Marty Stepp
24@version October 21, 2011
25*/
26
27import java.awt.AlphaComposite;
28import java.awt.BorderLayout;
29import java.awt.Color;
30import java.awt.Composite;
31import java.awt.Container;
32import java.awt.Dimension;
33import java.awt.EventQueue;
34import java.awt.FlowLayout;
35import java.awt.Font;
36import java.awt.Frame;
37import java.awt.Graphics;
38import java.awt.Graphics2D;
39import java.awt.GridLayout;
40import java.awt.Image;
41import java.awt.Point;
42import java.awt.RenderingHints;
43import java.awt.Toolkit;
44import java.awt.Window;
45import java.awt.event.ActionEvent;
46import java.awt.event.ActionListener;
47import java.awt.event.MouseEvent;
48import java.awt.event.MouseMotionListener;
49import java.awt.event.WindowEvent;
50import java.awt.event.WindowListener;
51import java.awt.image.BufferedImage;
52import java.awt.image.PixelGrabber;
53import java.io.File;
54import java.io.FileOutputStream;
55import java.io.IOException;
56import java.io.OutputStream;
57import java.io.PrintStream;
58import java.lang.Exception;
59import java.lang.Integer;
60import java.lang.InterruptedException;
61import java.lang.Math;
62import java.lang.Object;
63import java.lang.OutOfMemoryError;
64import java.lang.SecurityException;
65import java.lang.String;
66import java.lang.System;
67import java.lang.Thread;
68import java.net.URL;
69import java.net.NoRouteToHostException;
70import java.net.SocketException;
71import java.net.UnknownHostException;
72import java.util.ArrayList;
73import java.util.List;
74import java.util.Scanner;
75import java.util.Vector;
76import javax.imageio.ImageIO;
77import javax.swing.BorderFactory;
78import javax.swing.Box;
79import javax.swing.JButton;
80import javax.swing.JCheckBox;
81import javax.swing.JCheckBoxMenuItem;
82import javax.swing.JColorChooser;
83import javax.swing.JDialog;
84import javax.swing.JFileChooser;
85import javax.swing.JFrame;
86import javax.swing.JLabel;
87import javax.swing.JMenu;
88import javax.swing.JMenuBar;
89import javax.swing.JMenuItem;
90import javax.swing.JOptionPane;
91import javax.swing.JPanel;
92import javax.swing.JScrollPane;
93import javax.swing.JSlider;
94import javax.swing.KeyStroke;
95import javax.swing.SwingConstants;
96import javax.swing.Timer;
97import javax.swing.UIManager;
98import javax.swing.event.ChangeEvent;
99import javax.swing.event.ChangeListener;
100import javax.swing.event.MouseInputListener;
101import javax.swing.filechooser.FileFilter;
102
103public final class DrawingPanel extends FileFilter
104 implements ActionListener, MouseMotionListener, Runnable, WindowListener {
105 // inner class to represent one frame of an animated GIF
106 private static class ImageFrame {
107 public Image image;
108 public int delay;
109
110 public ImageFrame(Image image, int delay) {
111 this.image = image;
112 this.delay = delay / 10; // strangely, gif stores delay as sec/100
113 }
114 }
115
116 // class constants
117 public static final String ANIMATED_PROPERTY = "drawingpanel.animated";
118 public static final String AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY = "drawingpanel.animateonsleep";
119 public static final String DIFF_PROPERTY = "drawingpanel.diff";
120 public static final String HEADLESS_PROPERTY = "drawingpanel.headless";
121 public static final String MULTIPLE_PROPERTY = "drawingpanel.multiple";
122 public static final String SAVE_PROPERTY = "drawingpanel.save";
123 public static final String ANIMATION_FILE_NAME = "_drawingpanel_animation_save.txt";
124 private static final String TITLE = "Drawing Panel";
125 private static final String COURSE_WEB_SITE = "http://www.cs.washington.edu/education/courses/cse142/12sp/drawingpanel.txt";
126 private static final Color GRID_LINE_COLOR = new Color(64, 64, 64, 128);
127 private static final int GRID_SIZE = 10; // 10px between grid lines
128 private static final int DELAY = 100; // delay between repaints in millis
129 private static final int MAX_FRAMES = 100; // max animation frames
130 private static final int MAX_SIZE = 10000; // max width/height
131 private static final boolean DEBUG = false;
132 private static final boolean SAVE_SCALED_IMAGES = true; // if true, when panel is zoomed, saves images at that zoom factor
133 private static int instances = 0;
134 private static Thread shutdownThread = null;
135
136 private static void checkAnimationSettings() {
137 try {
138 File settingsFile = new File(ANIMATION_FILE_NAME);
139 if (settingsFile.exists()) {
140 Scanner input = new Scanner(settingsFile);
141 String animationSaveFileName = input.nextLine();
142 input.close();
143 // *** TODO: delete the file
144 System.out.println("***");
145 System.out.println("*** DrawingPanel saving animated GIF: " +
146 new File(animationSaveFileName).getName());
147 System.out.println("***");
148 settingsFile.delete();
149
150 System.setProperty(ANIMATED_PROPERTY, "1");
151 System.setProperty(SAVE_PROPERTY, animationSaveFileName);
152 }
153 } catch (Exception e) {
154 if (DEBUG) {
155 System.out.println("error checking animation settings: " + e);
156 }
157 }
158 }
159
160 private static boolean hasProperty(String name) {
161 try {
162 return System.getProperty(name) != null;
163 } catch (SecurityException e) {
164 if (DEBUG) System.out.println("Security exception when trying to read " + name);
165 return false;
166 }
167 }
168
169 private static boolean propertyIsTrue(String name) {
170 try {
171 String prop = System.getProperty(name);
172 return prop != null && (prop.equalsIgnoreCase("true") || prop.equalsIgnoreCase("yes") || prop.equalsIgnoreCase("1"));
173 } catch (SecurityException e) {
174 if (DEBUG) System.out.println("Security exception when trying to read " + name);
175 return false;
176 }
177 }
178
179 /*
180 private static boolean propertyIsFalse(String name) {
181 try {
182 String prop = System.getProperty(name);
183 return prop != null && (prop.equalsIgnoreCase("false") || prop.equalsIgnoreCase("no") || prop.equalsIgnoreCase("0"));
184 } catch (SecurityException e) {
185 if (DEBUG) System.out.println("Security exception when trying to read " + name);
186 return false;
187 }
188 }
189 */
190
191 // Returns whether the 'main' thread is still running.
192 private static boolean mainIsActive() {
193 ThreadGroup group = Thread.currentThread().getThreadGroup();
194 int activeCount = group.activeCount();
195
196 // look for the main thread in the current thread group
197 Thread[] threads = new Thread[activeCount];
198 group.enumerate(threads);
199 for (int i = 0; i < threads.length; i++) {
200 Thread thread = threads[i];
201 String name = ("" + thread.getName()).toLowerCase();
202 if (name.indexOf("main") >= 0 ||
203 name.indexOf("testrunner-assignmentrunner") >= 0) {
204 // found main thread!
205 // (TestRunnerApplet's main runner also counts as "main" thread)
206 return thread.isAlive();
207 }
208 }
209
210 // didn't find a running main thread; guess that main is done running
211 return false;
212 }
213
214 private static boolean usingDrJava() {
215 try {
216 return System.getProperty("drjava.debug.port") != null ||
217 System.getProperty("java.class.path").toLowerCase().indexOf("drjava") >= 0;
218 } catch (SecurityException e) {
219 // running as an applet, or something
220 return false;
221 }
222 }
223
224 private class ImagePanel extends JPanel {
225 private static final long serialVersionUID = 0;
226 private Image image;
227
228 public ImagePanel(Image image) {
229 setImage(image);
230 setBackground(Color.WHITE);
231 setPreferredSize(new Dimension(image.getWidth(this), image.getHeight(this)));
232 setAlignmentX(0.0f);
233 }
234
235 public void paintComponent(Graphics g) {
236 super.paintComponent(g);
237 Graphics2D g2 = (Graphics2D) g;
238 if (currentZoom != 1) {
239 g2.scale(currentZoom, currentZoom);
240 }
241 g2.drawImage(image, 0, 0, this);
242
243 // possibly draw grid lines for debugging
244 if (gridLines) {
245 g2.setPaint(GRID_LINE_COLOR);
246 for (int row = 1; row <= getHeight() / GRID_SIZE; row++) {
247 g2.drawLine(0, row * GRID_SIZE, getWidth(), row * GRID_SIZE);
248 }
249 for (int col = 1; col <= getWidth() / GRID_SIZE; col++) {
250 g2.drawLine(col * GRID_SIZE, 0, col * GRID_SIZE, getHeight());
251 }
252 }
253 }
254
255 public void setImage(Image image) {
256 this.image = image;
257 repaint();
258 }
259 }
260
261 // fields
262 private int width, height; // dimensions of window frame
263 private JFrame frame; // overall window frame
264 private JPanel panel; // overall drawing surface
265 private ImagePanel imagePanel; // real drawing surface
266 private BufferedImage image; // remembers drawing commands
267 private Graphics2D g2; // graphics context for painting
268 private JLabel statusBar; // status bar showing mouse position
269 private JFileChooser chooser; // file chooser to save files
270 private long createTime; // time at which DrawingPanel was constructed
271 private Timer timer; // animation timer
272 private ArrayList<ImageFrame> frames; // stores frames of animation to save
273 private Gif89Encoder encoder;
274 // private FileOutputStream stream;
275 private Color backgroundColor = Color.WHITE;
276 private String callingClassName; // name of class that constructed this panel
277 private boolean animated = false; // changes to true if sleep() is called
278 private boolean PRETTY = true; // true to anti-alias
279 private boolean gridLines = false;
280 private int instanceNumber;
281 private int currentZoom = 1;
282 private int initialPixel; // initial value in each pixel, for clear()
283
284 // construct a drawing panel of given width and height enclosed in a window
285 public DrawingPanel(int width, int height) {
286 if (width < 0 || width > MAX_SIZE || height < 0 || height > MAX_SIZE) {
287 throw new IllegalArgumentException("Illegal width/height: " + width + " x " + height);
288 }
289
290 checkAnimationSettings();
291
292 synchronized (getClass()) {
293 instances++;
294 instanceNumber = instances; // each DrawingPanel stores its own int number
295
296 if (shutdownThread == null && !usingDrJava()) {
297 shutdownThread = new Thread(new Runnable() {
298 // Runnable implementation; used for shutdown thread.
299 public void run() {
300 try {
301 while (true) {
302 // maybe shut down the program, if no more DrawingPanels are onscreen
303 // and main has finished executing
304 if ((instances == 0 || shouldSave()) && !mainIsActive()) {
305 try {
306 System.exit(0);
307 } catch (SecurityException sex) {}
308 }
309
310 Thread.sleep(250);
311 }
312 } catch (Exception e) {}
313 }
314 });
315 shutdownThread.setPriority(Thread.MIN_PRIORITY);
316 shutdownThread.start();
317 }
318 }
319 this.width = width;
320 this.height = height;
321
322 if (DEBUG) System.out.println("w=" + width + ",h=" + height + ",anim=" + isAnimated() + ",graph=" + isGraphical() + ",save=" + shouldSave());
323
324 if (isAnimated() && shouldSave()) {
325 // image must be no more than 256 colors
326 image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
327 // image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
328 PRETTY = false; // turn off anti-aliasing to save palette colors
329
330 // initially fill the entire frame with the background color,
331 // because it won't show through via transparency like with a full ARGB image
332 Graphics g = image.getGraphics();
333 g.setColor(backgroundColor);
334 g.fillRect(0, 0, width + 1, height + 1);
335 } else {
336 image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
337 }
338 initialPixel = image.getRGB(0, 0);
339
340 g2 = (Graphics2D) image.getGraphics();
341 g2.setColor(Color.BLACK);
342 if (PRETTY) {
343 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
344 }
345
346 if (isAnimated()) {
347 initializeAnimation();
348 }
349
350 if (isGraphical()) {
351 try {
352 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
353 } catch (Exception e) {}
354
355 statusBar = new JLabel(" ");
356 statusBar.setBorder(BorderFactory.createLineBorder(Color.BLACK));
357
358 panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
359 panel.setBackground(backgroundColor);
360 panel.setPreferredSize(new Dimension(width, height));
361 imagePanel = new ImagePanel(image);
362 imagePanel.setBackground(backgroundColor);
363 panel.add(imagePanel);
364
365 // listen to mouse movement
366 panel.addMouseMotionListener(this);
367
368 // main window frame
369 frame = new JFrame(TITLE);
370 // frame.setResizable(false);
371 frame.addWindowListener(this);
372 // JPanel center = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
373 JScrollPane center = new JScrollPane(panel);
374 // center.add(panel);
375 frame.getContentPane().add(center);
376 frame.getContentPane().add(statusBar, "South");
377 frame.setBackground(Color.DARK_GRAY);
378
379 // menu bar
380 setupMenuBar();
381
382 frame.pack();
383 center(frame);
384 frame.setVisible(true);
385 if (!shouldSave()) {
386 toFront(frame);
387 }
388
389 // repaint timer so that the screen will update
390 createTime = System.currentTimeMillis();
391 timer = new Timer(DELAY, this);
392 timer.start();
393 } else if (shouldSave()) {
394 // headless mode; just set a hook on shutdown to save the image
395 callingClassName = getCallingClassName();
396 try {
397 Runtime.getRuntime().addShutdownHook(new Thread(this));
398 } catch (Exception e) {
399 if (DEBUG) System.out.println("unable to add shutdown hook: " + e);
400 }
401 }
402 }
403
404 // method of FileFilter interface
405 public boolean accept(File file) {
406 return file.isDirectory() ||
407 (file.getName().toLowerCase().endsWith(".png") ||
408 file.getName().toLowerCase().endsWith(".gif"));
409 }
410
411 // used for an internal timer that keeps repainting
412 public void actionPerformed(ActionEvent e) {
413 if (e.getSource() instanceof Timer) {
414 // redraw the screen at regular intervals to catch all paint operations
415 panel.repaint();
416 if (shouldDiff() &&
417 System.currentTimeMillis() > createTime + 4 * DELAY) {
418 String expected = System.getProperty(DIFF_PROPERTY);
419 try {
420 String actual = saveToTempFile();
421 DiffImage diff = new DiffImage(expected, actual);
422 diff.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
423 } catch (IOException ioe) {
424 System.err.println("Error diffing image: " + ioe);
425 }
426 timer.stop();
427 } else if (shouldSave() && readyToClose()) {
428 // auto-save-and-close if desired
429 try {
430 if (isAnimated()) {
431 saveAnimated(System.getProperty(SAVE_PROPERTY));
432 } else {
433 save(System.getProperty(SAVE_PROPERTY));
434 }
435 } catch (IOException ioe) {
436 System.err.println("Error saving image: " + ioe);
437 }
438 exit();
439 }
440 } else if (e.getActionCommand().equals("Exit")) {
441 exit();
442 } else if (e.getActionCommand().equals("Compare to File...")) {
443 compareToFile();
444 } else if (e.getActionCommand().equals("Compare to Web File...")) {
445 new Thread(new Runnable() {
446 public void run() {
447 compareToURL();
448 }
449 }).start();
450 } else if (e.getActionCommand().equals("Save As...")) {
451 saveAs();
452 } else if (e.getActionCommand().equals("Save Animated GIF...")) {
453 saveAsAnimated();
454 } else if (e.getActionCommand().equals("Zoom In")) {
455 zoom(currentZoom + 1);
456 } else if (e.getActionCommand().equals("Zoom Out")) {
457 zoom(currentZoom - 1);
458 } else if (e.getActionCommand().equals("Zoom Normal (100%)")) {
459 zoom(1);
460 } else if (e.getActionCommand().equals("Grid Lines")) {
461 setGridLines(((JCheckBoxMenuItem) e.getSource()).isSelected());
462 } else if (e.getActionCommand().equals("About...")) {
463 JOptionPane.showMessageDialog(frame,
464 "DrawingPanel\n" +
465 "Graphical library class to support Building Java Programs textbook\n" +
466 "written by Marty Stepp and Stuart Reges\n" +
467 "University of Washington\n\n" +
468 "please visit our web site at:\n" +
469 "http://www.buildingjavaprograms.com/",
470
471 "About DrawingPanel",
472 JOptionPane.INFORMATION_MESSAGE);
473 }
474 }
475
476 public void addMouseListener(MouseInputListener listener) {
477 panel.addMouseListener(listener);
478 if (listener instanceof MouseMotionListener) {
479 panel.addMouseMotionListener((MouseMotionListener) listener);
480 }
481 }
482
483 // erases all drawn shapes/lines/colors from the panel
484 public void clear() {
485 int[] pixels = new int[width * height];
486 for (int i = 0; i < pixels.length; i++) {
487 pixels[i] = initialPixel;
488 }
489 image.setRGB(0, 0, width, height, pixels, 0, 1);
490 }
491
492 // method of FileFilter interface
493 public String getDescription() {
494 return "Image files (*.png; *.gif)";
495 }
496
497 // obtain the Graphics object to draw on the panel
498 public Graphics2D getGraphics() {
499 return g2;
500 }
501
502 // returns the drawing panel's width in pixels
503 public int getHeight() {
504 return height;
505 }
506
507 // returns the drawing panel's pixel size (width, height) as a Dimension object
508 public Dimension getSize() {
509 return new Dimension(width, height);
510 }
511
512 // returns the drawing panel's width in pixels
513 public int getWidth() {
514 return width;
515 }
516
517 // returns panel's current zoom factor
518 public int getZoom() {
519 return currentZoom;
520 }
521
522 // listens to mouse dragging
523 public void mouseDragged(MouseEvent e) {}
524
525 // listens to mouse movement
526 public void mouseMoved(MouseEvent e) {
527 int x = e.getX() / currentZoom;
528 int y = e.getY() / currentZoom;
529 setStatusBarText("(" + x + ", " + y + ")");
530 }
531
532 // run on shutdown to save the image
533 public void run() {
534 if (DEBUG) System.out.println("Running shutdown hook");
535 try {
536 String filename = System.getProperty(SAVE_PROPERTY);
537 if (filename == null) {
538 filename = callingClassName + ".png";
539 }
540
541 if (isAnimated()) {
542 saveAnimated(filename);
543 } else {
544 save(filename);
545 }
546 } catch (SecurityException e) {
547 } catch (IOException e) {
548 System.err.println("Error saving image: " + e);
549 }
550 }
551
552 // take the current contents of the panel and write them to a file
553 public void save(String filename) throws IOException {
554 BufferedImage image2 = getImage();
555
556 // if zoomed, scale image before saving it
557 if (SAVE_SCALED_IMAGES && currentZoom != 1) {
558 BufferedImage zoomedImage = new BufferedImage(width * currentZoom, height * currentZoom, image.getType());
559 Graphics2D g = (Graphics2D) zoomedImage.getGraphics();
560 g.setColor(Color.BLACK);
561 if (PRETTY) {
562 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
563 }
564 g.scale(currentZoom, currentZoom);
565 g.drawImage(image2, 0, 0, imagePanel);
566 image2 = zoomedImage;
567 }
568
569 // if saving multiple panels, append number
570 // (e.g. output_*.png becomes output_1.png, output_2.png, etc.)
571 if (isMultiple()) {
572 filename = filename.replaceAll("\\*", String.valueOf(instanceNumber));
573 }
574
575 int lastDot = filename.lastIndexOf(".");
576 String extension = filename.substring(lastDot + 1);
577
578 // write file
579 // TODO: doesn't save background color I don't think
580 ImageIO.write(image2, extension, new File(filename));
581 }
582
583 // take the current contents of the panel and write them to a file
584 public void saveAnimated(String filename) throws IOException {
585 // add one more final frame
586 if (DEBUG) System.out.println("saveAnimated(" + filename + ")");
587 frames.add(new ImageFrame(getImage(), 5000));
588 // encoder.continueEncoding(stream, getImage(), 5000);
589
590 // Gif89Encoder gifenc = new Gif89Encoder();
591
592 // add each frame of animation to the encoder
593 try {
594 for (int i = 0; i < frames.size(); i++) {
595 ImageFrame imageFrame = frames.get(i);
596 encoder.addFrame(imageFrame.image);
597 encoder.getFrameAt(i).setDelay(imageFrame.delay);
598 imageFrame.image.flush();
599 frames.set(i, null);
600 }
601 } catch (OutOfMemoryError e) {
602 System.out.println("Out of memory when saving");
603 }
604
605 // gifenc.setComments(annotation);
606 // gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
607 // gifenc.setUniformDelay(DELAY);
608 // encoder.setBackground(backgroundColor);
609 encoder.setLoopCount(0);
610 encoder.encode(new FileOutputStream(filename));
611 }
612
613 // set the background color of the drawing panel
614 public void setBackground(Color c) {
615 Color oldBackgroundColor = backgroundColor;
616 backgroundColor = c;
617 if (isGraphical()) {
618 panel.setBackground(c);
619 imagePanel.setBackground(c);
620 }
621
622 // with animated images, need to palette-swap the old bg color for the new
623 // because there's no notion of transparency in a palettized 8-bit image
624 if (isAnimated()) {
625 replaceColor(image, oldBackgroundColor, c);
626 }
627 }
628
629 // Enables or disables the drawing of grid lines on top of the image to help
630 // with debugging sizes and coordinates.
631 public void setGridLines(boolean gridLines) {
632 this.gridLines = gridLines;
633 imagePanel.repaint();
634 }
635
636 // sets the drawing panel's height in pixels to the given value
637 // After calling this method, the client must call getGraphics() again
638 // to get the new graphics context of the newly enlarged image buffer.
639 public void setHeight(int height) {
640 setSize(getWidth(), height);
641 }
642
643 // sets the drawing panel's pixel size (width, height) to the given values
644 // After calling this method, the client must call getGraphics() again
645 // to get the new graphics context of the newly enlarged image buffer.
646 public void setSize(int width, int height) {
647 // replace the image buffer for drawing
648 BufferedImage newImage = new BufferedImage(width, height, image.getType());
649 imagePanel.setImage(newImage);
650 newImage.getGraphics().drawImage(image, 0, 0, imagePanel);
651
652 this.width = width;
653 this.height = height;
654 image = newImage;
655 g2 = (Graphics2D) newImage.getGraphics();
656 g2.setColor(Color.BLACK);
657 if (PRETTY) {
658 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
659 }
660 zoom(currentZoom);
661 if (isGraphical()) {
662 frame.pack();
663 }
664 }
665
666 // show or hide the drawing panel on the screen
667 public void setVisible(boolean visible) {
668 if (isGraphical()) {
669 frame.setVisible(visible);
670 }
671 }
672
673 // sets the drawing panel's width in pixels to the given value
674 // After calling this method, the client must call getGraphics() again
675 // to get the new graphics context of the newly enlarged image buffer.
676 public void setWidth(int width) {
677 setSize(width, getHeight());
678 }
679
680 // makes the program pause for the given amount of time,
681 // allowing for animation
682 public void sleep(int millis) {
683 if (isGraphical() && frame.isVisible()) {
684 // if not even displaying, we don't actually need to sleep
685 if (millis > 0) {
686 try {
687 Thread.sleep(millis);
688 panel.repaint();
689 toFront(frame);
690 } catch (Exception e) {}
691 }
692 }
693
694 // manually enable animation if necessary
695 if (!isAnimated() && !isMultiple() && autoEnableAnimationOnSleep()) {
696 animated = true;
697 initializeAnimation();
698 }
699
700 // capture a frame of animation
701 if (isAnimated() && shouldSave() && !isMultiple()) {
702 try {
703 if (frames.size() < MAX_FRAMES) {
704 frames.add(new ImageFrame(getImage(), millis));
705 }
706
707 // reset creation timer so that we won't save/close just yet
708 createTime = System.currentTimeMillis();
709 } catch (OutOfMemoryError e) {
710 System.out.println("Out of memory after capturing " + frames.size() + " frames");
711 }
712 }
713 }
714
715 // moves window on top of other windows
716 public void toFront() {
717 toFront(frame);
718 }
719
720 // called when DrawingPanel closes, to potentially exit the program
721 public void windowClosing(WindowEvent event) {
722 frame.setVisible(false);
723 synchronized (getClass()) {
724 instances--;
725 }
726 frame.dispose();
727 }
728
729 // methods required by WindowListener interface
730 public void windowActivated(WindowEvent event) {}
731 public void windowClosed(WindowEvent event) {}
732 public void windowDeactivated(WindowEvent event) {}
733 public void windowDeiconified(WindowEvent event) {}
734 public void windowIconified(WindowEvent event) {}
735 public void windowOpened(WindowEvent event) {}
736
737 // zooms the drawing panel in/out to the given factor
738 // factor should be >= 1
739 public void zoom(int zoomFactor) {
740 currentZoom = Math.max(1, zoomFactor);
741 if (isGraphical()) {
742 Dimension size = new Dimension(width * currentZoom, height * currentZoom);
743 imagePanel.setPreferredSize(size);
744 panel.setPreferredSize(size);
745 imagePanel.validate();
746 imagePanel.revalidate();
747 panel.validate();
748 panel.revalidate();
749 // imagePanel.setSize(size);
750 frame.getContentPane().validate();
751 imagePanel.repaint();
752 setStatusBarText(" ");
753
754 // resize frame if any more space for it exists or it's the wrong size
755 Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
756 if (size.width <= screen.width || size.height <= screen.height) {
757 frame.pack();
758 }
759
760 // if (size.width <= screen.width && size.height <= screen.height) {
761 // frame.pack();
762 // center(frame);
763 // }
764 }
765 }
766
767
768 // moves given jframe to center of screen
769 private void center(Window frame) {
770 Toolkit tk = Toolkit.getDefaultToolkit();
771 Dimension screen = tk.getScreenSize();
772
773 int x = Math.max(0, (screen.width - frame.getWidth()) / 2);
774 int y = Math.max(0, (screen.height - frame.getHeight()) / 2);
775 frame.setLocation(x, y);
776 }
777
778 // constructs and initializes JFileChooser object if necessary
779 private void checkChooser() {
780 if (chooser == null) {
781 // TODO: fix security on applet mode
782 chooser = new JFileChooser(System.getProperty("user.dir"));
783 chooser.setMultiSelectionEnabled(false);
784 chooser.setFileFilter(this);
785 }
786 }
787
788 // compares current DrawingPanel image to an image file on disk
789 private void compareToFile() {
790 // save current image to a temp file
791 try {
792 String tempFile = saveToTempFile();
793
794 // use file chooser dialog to find image to compare against
795 checkChooser();
796 if (chooser.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) {
797 return;
798 }
799
800 // user chose a file; let's diff it
801 new DiffImage(chooser.getSelectedFile().toString(), tempFile);
802 } catch (IOException ioe) {
803 JOptionPane.showMessageDialog(frame,
804 "Unable to compare images: \n" + ioe);
805 }
806 }
807
808 // compares current DrawingPanel image to an image file on the web
809 private void compareToURL() {
810 // save current image to a temp file
811 try {
812 String tempFile = saveToTempFile();
813
814 // get list of images to compare against from web site
815 URL url = new URL(COURSE_WEB_SITE);
816 Scanner input = new Scanner(url.openStream());
817 List<String> lines = new ArrayList<String>();
818 List<String> filenames = new ArrayList<String>();
819 while (input.hasNextLine()) {
820 String line = input.nextLine().trim();
821 if (line.length() == 0) { continue; }
822
823 if (line.startsWith("#")) {
824 // a comment
825 if (line.endsWith(":")) {
826 // category label
827 lines.add(line);
828 line = line.replaceAll("#\\s*", "");
829 filenames.add(line);
830 }
831 } else {
832 lines.add(line);
833
834 // get filename
835 int lastSlash = line.lastIndexOf('/');
836 if (lastSlash >= 0) {
837 line = line.substring(lastSlash + 1);
838 }
839
840 // remove extension
841 int dot = line.lastIndexOf('.');
842 if (dot >= 0) {
843 line = line.substring(0, dot);
844 }
845
846 filenames.add(line);
847 }
848 }
849
850 if (filenames.isEmpty()) {
851 JOptionPane.showMessageDialog(frame,
852 "No valid web files found to compare against.",
853 "Error: no web files found",
854 JOptionPane.ERROR_MESSAGE);
855 return;
856 } else {
857 String fileURL = null;
858 if (filenames.size() == 1) {
859 // only one choice; take it
860 fileURL = lines.get(0);
861 } else {
862 // user chooses file to compare against
863 int choice = showOptionDialog(frame, "File to compare against?",
864 "Choose File", filenames.toArray(new String[0]));
865 if (choice < 0) {
866 return;
867 }
868
869 // user chose a file; let's diff it
870 fileURL = lines.get(choice);
871 }
872 if (DEBUG) System.out.println(fileURL);
873 new DiffImage(fileURL, tempFile);
874 }
875 } catch (NoRouteToHostException nrthe) {
876 JOptionPane.showMessageDialog(frame, "You do not appear to have a working internet connection.\nPlease check your internet settings and try again.\n\n" + nrthe);
877 } catch (UnknownHostException uhe) {
878 JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + uhe);
879 } catch (SocketException se) {
880 JOptionPane.showMessageDialog(frame, "Internet connection error: \n" + se);
881 } catch (IOException ioe) {
882 JOptionPane.showMessageDialog(frame, "Unable to compare images: \n" + ioe);
883 }
884 }
885
886 // closes the frame and exits the program
887 private void exit() {
888 if (isGraphical()) {
889 frame.setVisible(false);
890 frame.dispose();
891 }
892 try {
893 System.exit(0);
894 } catch (SecurityException e) {
895 // if we're running in an applet or something, can't do System.exit
896 }
897 }
898
899 // returns a best guess about the name of the class that constructed this panel
900 private String getCallingClassName() {
901 StackTraceElement[] stack = new RuntimeException().getStackTrace();
902 String className = this.getClass().getName();
903 for (StackTraceElement element : stack) {
904 String cl = element.getClassName();
905 if (!className.equals(cl)) {
906 className = cl;
907 break;
908 }
909 }
910
911 return className;
912 }
913
914 private BufferedImage getImage() {
915 // create second image so we get the background color
916 BufferedImage image2;
917 if (isAnimated()) {
918 image2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
919 } else {
920 image2 = new BufferedImage(width, height, image.getType());
921 }
922 Graphics g = image2.getGraphics();
923 if (DEBUG) System.out.println("getImage setting background to " + backgroundColor);
924 g.setColor(backgroundColor);
925 g.fillRect(0, 0, width, height);
926 g.drawImage(image, 0, 0, panel);
927 return image2;
928 }
929
930 private void initializeAnimation() {
931 frames = new ArrayList<ImageFrame>();
932 encoder = new Gif89Encoder();
933 /*
934 try {
935 if (hasProperty(SAVE_PROPERTY)) {
936 stream = new FileOutputStream(System.getProperty(SAVE_PROPERTY));
937 }
938 // encoder.startEncoding(stream);
939 } catch (IOException e) {
940 System.out.println(e);
941 }
942 */
943 }
944
945 private boolean autoEnableAnimationOnSleep() {
946 return propertyIsTrue(AUTO_ENABLE_ANIMATION_ON_SLEEP_PROPERTY);
947 }
948
949 private boolean isAnimated() {
950 return animated || propertyIsTrue(ANIMATED_PROPERTY);
951 }
952
953 private boolean isGraphical() {
954 return !hasProperty(SAVE_PROPERTY) && !hasProperty(HEADLESS_PROPERTY);
955 }
956
957 private boolean isMultiple() {
958 return propertyIsTrue(MULTIPLE_PROPERTY);
959 }
960
961 private boolean readyToClose() {
962/*
963 if (isAnimated()) {
964 // wait a little longer, in case animation is sleeping
965 return System.currentTimeMillis() > createTime + 5 * DELAY;
966 } else {
967 return System.currentTimeMillis() > createTime + 4 * DELAY;
968 }
969*/
970 return (instances == 0 || shouldSave()) && !mainIsActive();
971 }
972
973 private void replaceColor(BufferedImage image, Color oldColor, Color newColor) {
974 int oldRGB = oldColor.getRGB();
975 int newRGB = newColor.getRGB();
976 for (int y = 0; y < image.getHeight(); y++) {
977 for (int x = 0; x < image.getWidth(); x++) {
978 if (image.getRGB(x, y) == oldRGB) {
979 image.setRGB(x, y, newRGB);
980 }
981 }
982 }
983 }
984
985 // called when user presses "Save As" menu item
986 private void saveAs() {
987 String filename = saveAsHelper("png");
988 if (filename != null) {
989 try {
990 save(filename); // save the file
991 } catch (IOException ex) {
992 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
993 }
994 }
995 }
996
997 private void saveAsAnimated() {
998 String filename = saveAsHelper("gif");
999 if (filename != null) {
1000 try {
1001 // record that the file should be saved next time
1002 PrintStream out = new PrintStream(new File(ANIMATION_FILE_NAME));
1003 out.println(filename);
1004 out.close();
1005
1006 JOptionPane.showMessageDialog(frame,
1007 "Due to constraints about how DrawingPanel works, you'll need to\n" +
1008 "re-run your program. When you run it the next time, DrawingPanel will \n" +
1009 "automatically save your animated image as: " + new File(filename).getName()
1010 );
1011 } catch (IOException ex) {
1012 JOptionPane.showMessageDialog(frame, "Unable to store animation settings:\n" + ex);
1013 }
1014 }
1015 }
1016
1017 private String saveAsHelper(String extension) {
1018 // use file chooser dialog to get filename to save into
1019 checkChooser();
1020 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1021 return null;
1022 }
1023
1024 File selectedFile = chooser.getSelectedFile();
1025 String filename = selectedFile.toString();
1026 if (!filename.toLowerCase().endsWith(extension)) {
1027 // Windows is dumb about extensions with file choosers
1028 filename += "." + extension;
1029 }
1030
1031 // confirm overwrite of file
1032 if (new File(filename).exists() && JOptionPane.showConfirmDialog(
1033 frame, "File exists. Overwrite?", "Overwrite?",
1034 JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
1035 return null;
1036 }
1037
1038 return filename;
1039 }
1040
1041 // saves DrawingPanel image to a temporary file and returns file's name
1042 private String saveToTempFile() throws IOException {
1043 File currentImageFile = File.createTempFile("current_image", ".png");
1044 save(currentImageFile.toString());
1045 return currentImageFile.toString();
1046 }
1047
1048 // sets the text that will appear in the bottom status bar
1049 private void setStatusBarText(String text) {
1050 if (currentZoom != 1) {
1051 text += " (current zoom: " + currentZoom + "x" + ")";
1052 }
1053 statusBar.setText(text);
1054 }
1055
1056 // initializes DrawingPanel's menu bar items
1057 private void setupMenuBar() {
1058 // abort compare if we're running as an applet or in a secure environment
1059 boolean secure = (System.getSecurityManager() != null);
1060
1061 JMenuItem saveAs = new JMenuItem("Save As...", 'A');
1062 saveAs.addActionListener(this);
1063 saveAs.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
1064 saveAs.setEnabled(!secure);
1065
1066 JMenuItem saveAnimated = new JMenuItem("Save Animated GIF...", 'G');
1067 saveAnimated.addActionListener(this);
1068 saveAnimated.setAccelerator(KeyStroke.getKeyStroke("ctrl A"));
1069 saveAnimated.setEnabled(!secure);
1070
1071 JMenuItem compare = new JMenuItem("Compare to File...", 'C');
1072 compare.addActionListener(this);
1073 // compare.setAccelerator(KeyStroke.getKeyStroke("ctrl C"));
1074 compare.setEnabled(!secure);
1075
1076 JMenuItem compareURL = new JMenuItem("Compare to Web File...", 'U');
1077 compareURL.addActionListener(this);
1078 compareURL.setAccelerator(KeyStroke.getKeyStroke("ctrl U"));
1079 compareURL.setEnabled(!secure);
1080
1081 JMenuItem zoomIn = new JMenuItem("Zoom In", 'I');
1082 zoomIn.addActionListener(this);
1083 zoomIn.setAccelerator(KeyStroke.getKeyStroke("ctrl EQUALS"));
1084
1085 JMenuItem zoomOut = new JMenuItem("Zoom Out", 'O');
1086 zoomOut.addActionListener(this);
1087 zoomOut.setAccelerator(KeyStroke.getKeyStroke("ctrl MINUS"));
1088
1089 JMenuItem zoomNormal = new JMenuItem("Zoom Normal (100%)", 'N');
1090 zoomNormal.addActionListener(this);
1091 zoomNormal.setAccelerator(KeyStroke.getKeyStroke("ctrl 0"));
1092
1093 JMenuItem gridLinesItem = new JCheckBoxMenuItem("Grid Lines");
1094 gridLinesItem.setMnemonic('G');
1095 gridLinesItem.addActionListener(this);
1096 gridLinesItem.setAccelerator(KeyStroke.getKeyStroke("ctrl G"));
1097
1098 JMenuItem exit = new JMenuItem("Exit", 'x');
1099 exit.addActionListener(this);
1100
1101 JMenuItem about = new JMenuItem("About...", 'A');
1102 about.addActionListener(this);
1103
1104 JMenu file = new JMenu("File");
1105 file.setMnemonic('F');
1106 file.add(compareURL);
1107 file.add(compare);
1108 file.addSeparator();
1109 file.add(saveAs);
1110 file.add(saveAnimated);
1111 file.addSeparator();
1112 file.add(exit);
1113
1114 JMenu view = new JMenu("View");
1115 view.setMnemonic('V');
1116 view.add(zoomIn);
1117 view.add(zoomOut);
1118 view.add(zoomNormal);
1119 view.addSeparator();
1120 view.add(gridLinesItem);
1121
1122 JMenu help = new JMenu("Help");
1123 help.setMnemonic('H');
1124 help.add(about);
1125
1126 JMenuBar bar = new JMenuBar();
1127 bar.add(file);
1128 bar.add(view);
1129 bar.add(help);
1130 frame.setJMenuBar(bar);
1131 }
1132
1133 private boolean shouldDiff() {
1134 return hasProperty(DIFF_PROPERTY);
1135 }
1136
1137 private boolean shouldSave() {
1138 return hasProperty(SAVE_PROPERTY);
1139 }
1140
1141 // show dialog box with given choices; return index chosen (-1 == cancel)
1142 private int showOptionDialog(Frame parent, String title,
1143 String message, final String[] names) {
1144 final JDialog dialog = new JDialog(parent, title, true);
1145 JPanel center = new JPanel(new GridLayout(0, 1));
1146
1147 // just a hack to make the return value a mutable reference to an int
1148 final int[] hack = {-1};
1149
1150 for (int i = 0; i < names.length; i++) {
1151 if (names[i].endsWith(":")) {
1152 center.add(new JLabel("<html><b>" + names[i] + "</b></html>"));
1153 } else {
1154 final JButton button = new JButton(names[i]);
1155 button.setActionCommand(String.valueOf(i));
1156 button.addActionListener(new ActionListener() {
1157 public void actionPerformed(ActionEvent e) {
1158 hack[0] = Integer.parseInt(button.getActionCommand());
1159 dialog.setVisible(false);
1160 }
1161 });
1162 center.add(button);
1163 }
1164 }
1165
1166 JPanel south = new JPanel();
1167 JButton cancel = new JButton("Cancel");
1168 cancel.setMnemonic('C');
1169 cancel.requestFocus();
1170 cancel.addActionListener(new ActionListener() {
1171 public void actionPerformed(ActionEvent e) {
1172 dialog.setVisible(false);
1173 }
1174 });
1175 south.add(cancel);
1176
1177 dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
1178 dialog.getContentPane().setLayout(new BorderLayout(10, 5));
1179 // ((JComponent) dialog.getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
1180
1181 if (message != null) {
1182 JLabel messageLabel = new JLabel(message);
1183 dialog.add(messageLabel, BorderLayout.NORTH);
1184 }
1185 dialog.add(center);
1186 dialog.add(south, BorderLayout.SOUTH);
1187 dialog.pack();
1188 dialog.setResizable(false);
1189 center(dialog);
1190 cancel.requestFocus();
1191 dialog.setVisible(true);
1192 cancel.requestFocus();
1193
1194 return hack[0];
1195 }
1196
1197 // brings the given window to the front of the z-ordering
1198 private void toFront(final Window window) {
1199 EventQueue.invokeLater(new Runnable() {
1200 public void run() {
1201 if (window != null) {
1202 window.toFront();
1203 window.repaint();
1204 }
1205 }
1206 });
1207 }
1208
1209
1210
1211 // Reports the differences between two images.
1212 private class DiffImage extends JPanel implements ActionListener,
1213 ChangeListener {
1214 private static final long serialVersionUID = 0;
1215
1216 private BufferedImage image1;
1217 private BufferedImage image2;
1218 private String image1name;
1219 private int numDiffPixels;
1220 private int opacity = 50;
1221 private String label1Text = "Expected";
1222 private String label2Text = "Actual";
1223 private boolean highlightDiffs = false;
1224
1225 private Color highlightColor = new Color(224, 0, 224);
1226 private JLabel image1Label;
1227 private JLabel image2Label;
1228 private JLabel diffPixelsLabel;
1229 private JSlider slider;
1230 private JCheckBox box;
1231 private JMenuItem saveAsItem;
1232 private JMenuItem setImage1Item;
1233 private JMenuItem setImage2Item;
1234 private JFrame frame;
1235 private JButton colorButton;
1236
1237 public DiffImage(String file1, String file2) throws IOException {
1238 setImage1(file1);
1239 setImage2(file2);
1240 display();
1241 }
1242
1243 public void actionPerformed(ActionEvent e) {
1244 Object source = e.getSource();
1245 if (source == box) {
1246 highlightDiffs = box.isSelected();
1247 repaint();
1248 } else if (source == colorButton) {
1249 Color color = JColorChooser.showDialog(frame,
1250 "Choose highlight color", highlightColor);
1251 if (color != null) {
1252 highlightColor = color;
1253 colorButton.setBackground(color);
1254 colorButton.setForeground(color);
1255 repaint();
1256 }
1257 } else if (source == saveAsItem) {
1258 saveAs();
1259 } else if (source == setImage1Item) {
1260 setImage1();
1261 } else if (source == setImage2Item) {
1262 setImage2();
1263 }
1264 }
1265
1266 // Counts number of pixels that differ between the two images.
1267 public void countDiffPixels() {
1268 if (image1 == null || image2 == null) {
1269 return;
1270 }
1271
1272 int w1 = image1.getWidth();
1273 int h1 = image1.getHeight();
1274 int w2 = image2.getWidth();
1275 int h2 = image2.getHeight();
1276 int wmax = Math.max(w1, w2);
1277 int hmax = Math.max(h1, h2);
1278
1279 // check each pair of pixels
1280 numDiffPixels = 0;
1281 for (int y = 0; y < hmax; y++) {
1282 for (int x = 0; x < wmax; x++) {
1283 int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
1284 int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
1285 if (pixel1 != pixel2) {
1286 numDiffPixels++;
1287 }
1288 }
1289 }
1290 }
1291
1292 // initializes diffimage panel
1293 public void display() {
1294 countDiffPixels();
1295
1296 setupComponents();
1297 setupEvents();
1298 setupLayout();
1299
1300 frame.pack();
1301 center(frame);
1302
1303 frame.setVisible(true);
1304 toFront(frame);
1305 }
1306
1307 // draws the given image onto the given graphics context
1308 public void drawImageFull(Graphics2D g2, BufferedImage image) {
1309 int iw = image.getWidth();
1310 int ih = image.getHeight();
1311 int w = getWidth();
1312 int h = getHeight();
1313 int dw = w - iw;
1314 int dh = h - ih;
1315
1316 if (dw > 0) {
1317 g2.fillRect(iw, 0, dw, ih);
1318 }
1319 if (dh > 0) {
1320 g2.fillRect(0, ih, iw, dh);
1321 }
1322 if (dw > 0 && dh > 0) {
1323 g2.fillRect(iw, ih, dw, dh);
1324 }
1325 g2.drawImage(image, 0, 0, this);
1326 }
1327
1328 // paints the DiffImage panel
1329 public void paintComponent(Graphics g) {
1330 super.paintComponent(g);
1331 Graphics2D g2 = (Graphics2D) g;
1332
1333 // draw the expected output (image 1)
1334 if (image1 != null) {
1335 drawImageFull(g2, image1);
1336 }
1337
1338 // draw the actual output (image 2)
1339 if (image2 != null) {
1340 Composite oldComposite = g2.getComposite();
1341 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, ((float) opacity) / 100));
1342 drawImageFull(g2, image2);
1343 g2.setComposite(oldComposite);
1344 }
1345 g2.setColor(Color.BLACK);
1346
1347 // draw the highlighted diffs (if so desired)
1348 if (highlightDiffs && image1 != null && image2 != null) {
1349 int w1 = image1.getWidth();
1350 int h1 = image1.getHeight();
1351 int w2 = image2.getWidth();
1352 int h2 = image2.getHeight();
1353
1354 int wmax = Math.max(w1, w2);
1355 int hmax = Math.max(h1, h2);
1356
1357 // check each pair of pixels
1358 g2.setColor(highlightColor);
1359 for (int y = 0; y < hmax; y++) {
1360 for (int x = 0; x < wmax; x++) {
1361 int pixel1 = (x < w1 && y < h1) ? image1.getRGB(x, y) : 0;
1362 int pixel2 = (x < w2 && y < h2) ? image2.getRGB(x, y) : 0;
1363 if (pixel1 != pixel2) {
1364 g2.fillRect(x, y, 1, 1);
1365 }
1366 }
1367 }
1368 }
1369 }
1370
1371 public void save(File file) throws IOException {
1372 // String extension = filename.substring(filename.lastIndexOf(".") + 1);
1373 // ImageIO.write(diffImage, extension, new File(filename));
1374 String filename = file.getName();
1375 String extension = filename.substring(filename.lastIndexOf(".") + 1);
1376 BufferedImage img = new BufferedImage(getPreferredSize().width, getPreferredSize().height, BufferedImage.TYPE_INT_ARGB);
1377 img.getGraphics().setColor(getBackground());
1378 img.getGraphics().fillRect(0, 0, img.getWidth(), img.getHeight());
1379 paintComponent(img.getGraphics());
1380 ImageIO.write(img, extension, file);
1381 }
1382
1383 public void save(String filename) throws IOException {
1384 save(new File(filename));
1385 }
1386
1387 // Called when "Save As" menu item is clicked
1388 public void saveAs() {
1389 checkChooser();
1390 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1391 return;
1392 }
1393
1394 File selectedFile = chooser.getSelectedFile();
1395 try {
1396 save(selectedFile.toString());
1397 } catch (IOException ex) {
1398 JOptionPane.showMessageDialog(frame, "Unable to save image:\n" + ex);
1399 }
1400 }
1401
1402 // called when "Set Image 1" menu item is clicked
1403 public void setImage1() {
1404 checkChooser();
1405 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1406 return;
1407 }
1408
1409 File selectedFile = chooser.getSelectedFile();
1410 try {
1411 setImage1(selectedFile.toString());
1412 countDiffPixels();
1413 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
1414 image1Label.setText(selectedFile.getName());
1415 frame.pack();
1416 } catch (IOException ex) {
1417 JOptionPane.showMessageDialog(frame, "Unable to set image 1:\n" + ex);
1418 }
1419 }
1420
1421 // sets image 1 to be the given image
1422 public void setImage1(BufferedImage image) {
1423 if (image == null) {
1424 throw new NullPointerException();
1425 }
1426
1427 image1 = image;
1428 setPreferredSize(new Dimension(
1429 Math.max(getPreferredSize().width, image.getWidth()),
1430 Math.max(getPreferredSize().height, image.getHeight()))
1431 );
1432 if (frame != null) {
1433 frame.pack();
1434 }
1435 repaint();
1436 }
1437
1438 // loads image 1 from the given filename or URL
1439 public void setImage1(String filename) throws IOException {
1440 image1name = new File(filename).getName();
1441 if (filename.startsWith("http")) {
1442 setImage1(ImageIO.read(new URL(filename)));
1443 } else {
1444 setImage1(ImageIO.read(new File(filename)));
1445 }
1446 }
1447
1448 // called when "Set Image 2" menu item is clicked
1449 public void setImage2() {
1450 checkChooser();
1451 if (chooser.showSaveDialog(frame) != JFileChooser.APPROVE_OPTION) {
1452 return;
1453 }
1454
1455 File selectedFile = chooser.getSelectedFile();
1456 try {
1457 setImage2(selectedFile.toString());
1458 countDiffPixels();
1459 diffPixelsLabel.setText("(" + numDiffPixels + " pixels differ)");
1460 image2Label.setText(selectedFile.getName());
1461 frame.pack();
1462 } catch (IOException ex) {
1463 JOptionPane.showMessageDialog(frame, "Unable to set image 2:\n" + ex);
1464 }
1465 }
1466
1467 // sets image 2 to be the given image
1468 public void setImage2(BufferedImage image) {
1469 if (image == null) {
1470 throw new NullPointerException();
1471 }
1472
1473 image2 = image;
1474 setPreferredSize(new Dimension(
1475 Math.max(getPreferredSize().width, image.getWidth()),
1476 Math.max(getPreferredSize().height, image.getHeight()))
1477 );
1478 if (frame != null) {
1479 frame.pack();
1480 }
1481 repaint();
1482 }
1483
1484 // loads image 2 from the given filename
1485 public void setImage2(String filename) throws IOException {
1486 if (filename.startsWith("http")) {
1487 setImage2(ImageIO.read(new URL(filename)));
1488 } else {
1489 setImage2(ImageIO.read(new File(filename)));
1490 }
1491
1492 }
1493
1494 private void setupComponents() {
1495 String title = "DiffImage";
1496 if (image1name != null) {
1497 title = "Compare to " + image1name;
1498 }
1499 frame = new JFrame(title);
1500 frame.setResizable(false);
1501 // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
1502
1503 slider = new JSlider();
1504 slider.setPaintLabels(false);
1505 slider.setPaintTicks(true);
1506 slider.setSnapToTicks(true);
1507 slider.setMajorTickSpacing(25);
1508 slider.setMinorTickSpacing(5);
1509
1510 box = new JCheckBox("Highlight diffs in color: ", highlightDiffs);
1511
1512 colorButton = new JButton();
1513 colorButton.setBackground(highlightColor);
1514 colorButton.setForeground(highlightColor);
1515 colorButton.setPreferredSize(new Dimension(24, 24));
1516
1517 diffPixelsLabel = new JLabel("(" + numDiffPixels + " pixels differ)");
1518 diffPixelsLabel.setFont(diffPixelsLabel.getFont().deriveFont(Font.BOLD));
1519 image1Label = new JLabel(label1Text);
1520 image2Label = new JLabel(label2Text);
1521
1522 setupMenuBar();
1523 }
1524
1525 // initializes layout of components
1526 private void setupLayout() {
1527 JPanel southPanel1 = new JPanel();
1528 southPanel1.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
1529 southPanel1.add(image1Label);
1530 southPanel1.add(slider);
1531 southPanel1.add(image2Label);
1532 southPanel1.add(Box.createHorizontalStrut(20));
1533
1534 JPanel southPanel2 = new JPanel();
1535 southPanel2.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
1536 southPanel2.add(diffPixelsLabel);
1537 southPanel2.add(Box.createHorizontalStrut(20));
1538 southPanel2.add(box);
1539 southPanel2.add(colorButton);
1540
1541 Container southPanel = javax.swing.Box.createVerticalBox();
1542 southPanel.add(southPanel1);
1543 southPanel.add(southPanel2);
1544
1545 frame.add(this, BorderLayout.CENTER);
1546 frame.add(southPanel, BorderLayout.SOUTH);
1547 }
1548
1549 // initializes main menu bar
1550 private void setupMenuBar() {
1551 saveAsItem = new JMenuItem("Save As...", 'A');
1552 saveAsItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
1553 setImage1Item = new JMenuItem("Set Image 1...", '1');
1554 setImage1Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 1"));
1555 setImage2Item = new JMenuItem("Set Image 2...", '2');
1556 setImage2Item.setAccelerator(KeyStroke.getKeyStroke("ctrl 2"));
1557
1558 JMenu file = new JMenu("File");
1559 file.setMnemonic('F');
1560 file.add(setImage1Item);
1561 file.add(setImage2Item);
1562 file.addSeparator();
1563 file.add(saveAsItem);
1564
1565 JMenuBar bar = new JMenuBar();
1566 bar.add(file);
1567
1568 // disabling menu bar to simplify code
1569 // frame.setJMenuBar(bar);
1570 }
1571
1572 // method of ChangeListener interface
1573 public void stateChanged(ChangeEvent e) {
1574 opacity = slider.getValue();
1575 repaint();
1576 }
1577
1578 // adds event listeners to various components
1579 private void setupEvents() {
1580 slider.addChangeListener(this);
1581 box.addActionListener(this);
1582 colorButton.addActionListener(this);
1583 saveAsItem.addActionListener(this);
1584 this.setImage1Item.addActionListener(this);
1585 this.setImage2Item.addActionListener(this);
1586 }
1587 }
1588
1589
1590
1591 //******************************************************************************
1592 // DirectGif89Frame.java
1593 //******************************************************************************
1594
1595 //==============================================================================
1596 /** Instances of this Gif89Frame subclass are constructed from RGB image info,
1597 * either in the form of an Image object or a pixel array.
1598 * <p>
1599 * There is an important restriction to note. It is only permissible to add
1600 * DirectGif89Frame objects to a Gif89Encoder constructed without an explicit
1601 * color map. The GIF color table will be automatically generated from pixel
1602 * information.
1603 *
1604 * @version 0.90 beta (15-Jul-2000)
1605 * @author J. M. G. Elliott (tep@jmge.net)
1606 * @see Gif89Encoder
1607 * @see Gif89Frame
1608 * @see IndexGif89Frame
1609 */
1610 class DirectGif89Frame extends Gif89Frame {
1611
1612 private int[] argbPixels;
1613
1614 //----------------------------------------------------------------------------
1615 /** Construct an DirectGif89Frame from a Java image.
1616 *
1617 * @param img
1618 * A java.awt.Image object that supports pixel-grabbing.
1619 * @exception IOException
1620 * If the image is unencodable due to failure of pixel-grabbing.
1621 */
1622 public DirectGif89Frame(Image img) throws IOException
1623 {
1624 PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
1625
1626 String errmsg = null;
1627 try {
1628 if (!pg.grabPixels())
1629 errmsg = "can't grab pixels from image";
1630 } catch (InterruptedException e) {
1631 errmsg = "interrupted grabbing pixels from image";
1632 }
1633
1634 if (errmsg != null)
1635 throw new IOException(errmsg + " (" + getClass().getName() + ")");
1636
1637 theWidth = pg.getWidth();
1638 theHeight = pg.getHeight();
1639 argbPixels = (int[]) pg.getPixels();
1640 ciPixels = new byte[argbPixels.length];
1641
1642 // flush to conserve resources
1643 img.flush();
1644 }
1645
1646 //----------------------------------------------------------------------------
1647 /** Construct an DirectGif89Frame from ARGB pixel data.
1648 *
1649 * @param width
1650 * Width of the bitmap.
1651 * @param height
1652 * Height of the bitmap.
1653 * @param argb_pixels
1654 * Array containing at least width*height pixels in the format returned by
1655 * java.awt.Color.getRGB().
1656 */
1657 public DirectGif89Frame(int width, int height, int argb_pixels[])
1658 {
1659 theWidth = width;
1660 theHeight = height;
1661 argbPixels = new int[theWidth * theHeight];
1662 System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
1663 ciPixels = new byte[argbPixels.length];
1664 }
1665
1666 //----------------------------------------------------------------------------
1667 Object getPixelSource() { return argbPixels; }
1668 }
1669
1670
1671
1672 //******************************************************************************
1673 // Gif89Encoder.java
1674 //******************************************************************************
1675
1676 //==============================================================================
1677 /** This is the central class of a JDK 1.1 compatible GIF encoder that, AFAIK,
1678 * supports more features of the extended GIF spec than any other Java open
1679 * source encoder. Some sections of the source are lifted or adapted from Jef
1680 * Poskanzer's <cite>Acme GifEncoder</cite> (so please see the
1681 * <a href="../readme.txt">readme</a> containing his notice), but much of it,
1682 * including nearly all of the present class, is original code. My main
1683 * motivation for writing a new encoder was to support animated GIFs, but the
1684 * package also adds support for embedded textual comments.
1685 * <p>
1686 * There are still some limitations. For instance, animations are limited to
1687 * a single global color table. But that is usually what you want anyway, so
1688 * as to avoid irregularities on some displays. (So this is not really a
1689 * limitation, but a "disciplinary feature" :) Another rather more serious
1690 * restriction is that the total number of RGB colors in a given input-batch
1691 * mustn't exceed 256. Obviously, there is an opening here for someone who
1692 * would like to add a color-reducing preprocessor.
1693 * <p>
1694 * The encoder, though very usable in its present form, is at bottom only a
1695 * partial implementation skewed toward my own particular needs. Hence a
1696 * couple of caveats are in order. (1) During development it was in the back
1697 * of my mind that an encoder object should be reusable - i.e., you should be
1698 * able to make multiple calls to encode() on the same object, with or without
1699 * intervening frame additions or changes to options. But I haven't reviewed
1700 * the code with such usage in mind, much less tested it, so it's likely I
1701 * overlooked something. (2) The encoder classes aren't thread safe, so use
1702 * caution in a context where access is shared by multiple threads. (Better
1703 * yet, finish the library and re-release it :)
1704 * <p>
1705 * There follow a couple of simple examples illustrating the most common way to
1706 * use the encoder, i.e., to encode AWT Image objects created elsewhere in the
1707 * program. Use of some of the most popular format options is also shown,
1708 * though you will want to peruse the API for additional features.
1709 *
1710 * <p>
1711 * <strong>Animated GIF Example</strong>
1712 * <pre>
1713 * import net.jmge.gif.Gif89Encoder;
1714 * // ...
1715 * void writeAnimatedGIF(Image[] still_images,
1716 * String annotation,
1717 * boolean looped,
1718 * double frames_per_second,
1719 * OutputStream out) throws IOException
1720 * {
1721 * Gif89Encoder gifenc = new Gif89Encoder();
1722 * for (int i = 0; i < still_images.length; ++i)
1723 * gifenc.addFrame(still_images[i]);
1724 * gifenc.setComments(annotation);
1725 * gifenc.setLoopCount(looped ? 0 : 1);
1726 * gifenc.setUniformDelay((int) Math.round(100 / frames_per_second));
1727 * gifenc.encode(out);
1728 * }
1729 * </pre>
1730 *
1731 * <strong>Static GIF Example</strong>
1732 * <pre>
1733 * import net.jmge.gif.Gif89Encoder;
1734 * // ...
1735 * void writeNormalGIF(Image img,
1736 * String annotation,
1737 * int transparent_index, // pass -1 for none
1738 * boolean interlaced,
1739 * OutputStream out) throws IOException
1740 * {
1741 * Gif89Encoder gifenc = new Gif89Encoder(img);
1742 * gifenc.setComments(annotation);
1743 * gifenc.setTransparentIndex(transparent_index);
1744 * gifenc.getFrameAt(0).setInterlaced(interlaced);
1745 * gifenc.encode(out);
1746 * }
1747 * </pre>
1748 *
1749 * @version 0.90 beta (15-Jul-2000)
1750 * @author J. M. G. Elliott (tep@jmge.net)
1751 * @see Gif89Frame
1752 * @see DirectGif89Frame
1753 * @see IndexGif89Frame
1754 */
1755 class Gif89Encoder {
1756 private static final boolean DEBUG = false;
1757 private Dimension dispDim = new Dimension(0, 0);
1758 private GifColorTable colorTable;
1759 private int bgIndex = 0;
1760 private int loopCount = 1;
1761 private String theComments;
1762 private Vector<Gif89Frame> vFrames = new Vector<Gif89Frame>();
1763
1764 //----------------------------------------------------------------------------
1765 /** Use this default constructor if you'll be adding multiple frames
1766 * constructed from RGB data (i.e., AWT Image objects or ARGB-pixel arrays).
1767 */
1768 public Gif89Encoder()
1769 {
1770 // empty color table puts us into "palette autodetect" mode
1771 colorTable = new GifColorTable();
1772 }
1773
1774 //----------------------------------------------------------------------------
1775 /** Like the default except that it also adds a single frame, for conveniently
1776 * encoding a static GIF from an image.
1777 *
1778 * @param static_image
1779 * Any Image object that supports pixel-grabbing.
1780 * @exception IOException
1781 * See the addFrame() methods.
1782 */
1783 public Gif89Encoder(Image static_image) throws IOException
1784 {
1785 this();
1786 addFrame(static_image);
1787 }
1788
1789 //----------------------------------------------------------------------------
1790 /** This constructor installs a user color table, overriding the detection of
1791 * of a palette from ARBG pixels.
1792 *
1793 * Use of this constructor imposes a couple of restrictions:
1794 * (1) Frame objects can't be of type DirectGif89Frame
1795 * (2) Transparency, if desired, must be set explicitly.
1796 *
1797 * @param colors
1798 * Array of color values; no more than 256 colors will be read, since that's
1799 * the limit for a GIF.
1800 */
1801 public Gif89Encoder(Color[] colors)
1802 {
1803 colorTable = new GifColorTable(colors);
1804 }
1805
1806 //----------------------------------------------------------------------------
1807 /** Convenience constructor for encoding a static GIF from index-model data.
1808 * Adds a single frame as specified.
1809 *
1810 * @param colors
1811 * Array of color values; no more than 256 colors will be read, since
1812 * that's the limit for a GIF.
1813 * @param width
1814 * Width of the GIF bitmap.
1815 * @param height
1816 * Height of same.
1817 * @param ci_pixels
1818 * Array of color-index pixels no less than width * height in length.
1819 * @exception IOException
1820 * See the addFrame() methods.
1821 */
1822 public Gif89Encoder(Color[] colors, int width, int height, byte ci_pixels[])
1823 throws IOException
1824 {
1825 this(colors);
1826 addFrame(width, height, ci_pixels);
1827 }
1828
1829 //----------------------------------------------------------------------------
1830 /** Get the number of frames that have been added so far.
1831 *
1832 * @return
1833 * Number of frame items.
1834 */
1835 public int getFrameCount() { return vFrames.size(); }
1836
1837 //----------------------------------------------------------------------------
1838 /** Get a reference back to a Gif89Frame object by position.
1839 *
1840 * @param index
1841 * Zero-based index of the frame in the sequence.
1842 * @return
1843 * Gif89Frame object at the specified position (or null if no such frame).
1844 */
1845 public Gif89Frame getFrameAt(int index)
1846 {
1847 return isOk(index) ? vFrames.elementAt(index) : null;
1848 }
1849
1850 //----------------------------------------------------------------------------
1851 /** Add a Gif89Frame frame to the end of the internal sequence. Note that
1852 * there are restrictions on the Gif89Frame type: if the encoder object was
1853 * constructed with an explicit color table, an attempt to add a
1854 * DirectGif89Frame will throw an exception.
1855 *
1856 * @param gf
1857 * An externally constructed Gif89Frame.
1858 * @exception IOException
1859 * If Gif89Frame can't be accommodated. This could happen if either (1) the
1860 * aggregate cross-frame RGB color count exceeds 256, or (2) the Gif89Frame
1861 * subclass is incompatible with the present encoder object.
1862 */
1863 public void addFrame(Gif89Frame gf) throws IOException
1864 {
1865 accommodateFrame(gf);
1866 vFrames.addElement(gf);
1867 }
1868
1869 //----------------------------------------------------------------------------
1870 /** Convenience version of addFrame() that takes a Java Image, internally
1871 * constructing the requisite DirectGif89Frame.
1872 *
1873 * @param image
1874 * Any Image object that supports pixel-grabbing.
1875 * @exception IOException
1876 * If either (1) pixel-grabbing fails, (2) the aggregate cross-frame RGB
1877 * color count exceeds 256, or (3) this encoder object was constructed with
1878 * an explicit color table.
1879 */
1880 public void addFrame(Image image) throws IOException
1881 {
1882 DirectGif89Frame frame = new DirectGif89Frame(image);
1883 addFrame(frame);
1884 }
1885
1886 //----------------------------------------------------------------------------
1887 /** The index-model convenience version of addFrame().
1888 *
1889 * @param width
1890 * Width of the GIF bitmap.
1891 * @param height
1892 * Height of same.
1893 * @param ci_pixels
1894 * Array of color-index pixels no less than width * height in length.
1895 * @exception IOException
1896 * Actually, in the present implementation, there aren't any unchecked
1897 * exceptions that can be thrown when adding an IndexGif89Frame
1898 * <i>per se</i>. But I might add some pedantic check later, to justify the
1899 * generality :)
1900 */
1901 public void addFrame(int width, int height, byte ci_pixels[])
1902 throws IOException
1903 {
1904 addFrame(new IndexGif89Frame(width, height, ci_pixels));
1905 }
1906
1907 //----------------------------------------------------------------------------
1908 /** Like addFrame() except that the frame is inserted at a specific point in
1909 * the sequence rather than appended.
1910 *
1911 * @param index
1912 * Zero-based index at which to insert frame.
1913 * @param gf
1914 * An externally constructed Gif89Frame.
1915 * @exception IOException
1916 * If Gif89Frame can't be accommodated. This could happen if either (1)
1917 * the aggregate cross-frame RGB color count exceeds 256, or (2) the
1918 * Gif89Frame subclass is incompatible with the present encoder object.
1919 */
1920 public void insertFrame(int index, Gif89Frame gf) throws IOException
1921 {
1922 accommodateFrame(gf);
1923 vFrames.insertElementAt(gf, index);
1924 }
1925
1926 //----------------------------------------------------------------------------
1927 /** Set the color table index for the transparent color, if any.
1928 *
1929 * @param index
1930 * Index of the color that should be rendered as transparent, if any.
1931 * A value of -1 turns off transparency. (Default: -1)
1932 */
1933 public void setTransparentIndex(int index)
1934 {
1935 colorTable.setTransparent(index);
1936 }
1937
1938 //----------------------------------------------------------------------------
1939 /** Sets attributes of the multi-image display area, if applicable.
1940 *
1941 * @param dim
1942 * Width/height of display. (Default: largest detected frame size)
1943 * @param background
1944 * Color table index of background color. (Default: 0)
1945 * @see Gif89Frame#setPosition
1946 */
1947 public void setLogicalDisplay(Dimension dim, int background)
1948 {
1949 dispDim = new Dimension(dim);
1950 bgIndex = background;
1951 }
1952
1953 //----------------------------------------------------------------------------
1954 /** Set animation looping parameter, if applicable.
1955 *
1956 * @param count
1957 * Number of times to play sequence. Special value of 0 specifies
1958 * indefinite looping. (Default: 1)
1959 */
1960 public void setLoopCount(int count)
1961 {
1962 loopCount = count;
1963 }
1964
1965 //----------------------------------------------------------------------------
1966 /** Specify some textual comments to be embedded in GIF.
1967 *
1968 * @param comments
1969 * String containing ASCII comments.
1970 */
1971 public void setComments(String comments)
1972 {
1973 theComments = comments;
1974 }
1975
1976 //----------------------------------------------------------------------------
1977 /** A convenience method for setting the "animation speed". It simply sets
1978 * the delay parameter for each frame in the sequence to the supplied value.
1979 * Since this is actually frame-level rather than animation-level data, take
1980 * care to add your frames before calling this method.
1981 *
1982 * @param interval
1983 * Interframe interval in centiseconds.
1984 */
1985 public void setUniformDelay(int interval)
1986 {
1987 for (int i = 0; i < vFrames.size(); ++i)
1988 vFrames.elementAt(i).setDelay(interval);
1989 }
1990
1991 //----------------------------------------------------------------------------
1992 /** After adding your frame(s) and setting your options, simply call this
1993 * method to write the GIF to the passed stream. Multiple calls are
1994 * permissible if for some reason that is useful to your application. (The
1995 * method simply encodes the current state of the object with no thought
1996 * to previous calls.)
1997 *
1998 * @param out
1999 * The stream you want the GIF written to.
2000 * @exception IOException
2001 * If a write error is encountered.
2002 */
2003 public void encode(OutputStream out) throws IOException
2004 {
2005 int nframes = getFrameCount();
2006 boolean is_sequence = nframes > 1;
2007
2008 // N.B. must be called before writing screen descriptor
2009 colorTable.closePixelProcessing();
2010
2011 // write GIF HEADER
2012 putAscii("GIF89a", out);
2013
2014 // write global blocks
2015 writeLogicalScreenDescriptor(out);
2016 colorTable.encode(out);
2017 if (is_sequence && loopCount != 1)
2018 writeNetscapeExtension(out);
2019 if (theComments != null && theComments.length() > 0)
2020 writeCommentExtension(out);
2021
2022 // write out the control and rendering data for each frame
2023 for (int i = 0; i < nframes; ++i) {
2024 DirectGif89Frame frame = (DirectGif89Frame) vFrames.elementAt(i);
2025 frame.encode(out, is_sequence, colorTable.getDepth(), colorTable.getTransparent());
2026 vFrames.set(i, null); // for GC's sake
2027 System.gc();
2028 }
2029
2030 // write GIF TRAILER
2031 out.write((int) ';');
2032
2033 out.flush();
2034 }
2035
2036 public boolean hasStarted = false;
2037
2038 //----------------------------------------------------------------------------
2039 /** After adding your frame(s) and setting your options, simply call this
2040 * method to write the GIF to the passed stream. Multiple calls are
2041 * permissible if for some reason that is useful to your application. (The
2042 * method simply encodes the current state of the object with no thought
2043 * to previous calls.)
2044 *
2045 * @param out
2046 * The stream you want the GIF written to.
2047 * @exception IOException
2048 * If a write error is encountered.
2049 */
2050 public void startEncoding(OutputStream out, Image image, int delay) throws IOException
2051 {
2052 hasStarted = true;
2053 boolean is_sequence = true;
2054 Gif89Frame gf = new DirectGif89Frame(image);
2055 accommodateFrame(gf);
2056
2057 // N.B. must be called before writing screen descriptor
2058 colorTable.closePixelProcessing();
2059
2060 // write GIF HEADER
2061 putAscii("GIF89a", out);
2062
2063 // write global blocks
2064 writeLogicalScreenDescriptor(out);
2065 colorTable.encode(out);
2066 if (is_sequence && loopCount != 1)
2067 writeNetscapeExtension(out);
2068 if (theComments != null && theComments.length() > 0)
2069 writeCommentExtension(out);
2070 }
2071
2072 public void continueEncoding(OutputStream out, Image image, int delay) throws IOException {
2073 // write out the control and rendering data for each frame
2074 Gif89Frame gf = new DirectGif89Frame(image);
2075 accommodateFrame(gf);
2076 gf.encode(out, true, colorTable.getDepth(), colorTable.getTransparent());
2077 out.flush();
2078 image.flush();
2079 }
2080
2081 public void endEncoding(OutputStream out) throws IOException {
2082 // write GIF TRAILER
2083 out.write((int) ';');
2084
2085 out.flush();
2086 }
2087
2088 public void setBackground(Color color) {
2089 bgIndex = colorTable.indexOf(color);
2090 if (bgIndex < 0) {
2091 try {
2092 BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED);
2093 Graphics g = img.getGraphics();
2094 g.setColor(color);
2095 g.fillRect(0, 0, 2, 2);
2096 DirectGif89Frame frame = new DirectGif89Frame(img);
2097 accommodateFrame(frame);
2098 bgIndex = colorTable.indexOf(color);
2099 } catch (IOException e) {
2100 if (DEBUG) System.out.println("Error while setting background color: " + e);
2101 }
2102 }
2103 if (DEBUG) System.out.println("Setting bg index to " + bgIndex);
2104 }
2105
2106 //----------------------------------------------------------------------------
2107 private void accommodateFrame(Gif89Frame gf) throws IOException
2108 {
2109 dispDim.width = Math.max(dispDim.width, gf.getWidth());
2110 dispDim.height = Math.max(dispDim.height, gf.getHeight());
2111 colorTable.processPixels(gf);
2112 }
2113
2114 //----------------------------------------------------------------------------
2115 private void writeLogicalScreenDescriptor(OutputStream os) throws IOException
2116 {
2117 putShort(dispDim.width, os);
2118 putShort(dispDim.height, os);
2119
2120 // write 4 fields, packed into a byte (bitfieldsize:value)
2121 // global color map present? (1:1)
2122 // bits per primary color less 1 (3:7)
2123 // sorted color table? (1:0)
2124 // bits per pixel less 1 (3:varies)
2125 os.write(0xf0 | colorTable.getDepth() - 1);
2126
2127 // write background color index
2128 os.write(bgIndex);
2129
2130 // Jef Poskanzer's notes on the next field, for our possible edification:
2131 // Pixel aspect ratio - 1:1.
2132 //Putbyte( (byte) 49, outs );
2133 // Java's GIF reader currently has a bug, if the aspect ratio byte is
2134 // not zero it throws an ImageFormatException. It doesn't know that
2135 // 49 means a 1:1 aspect ratio. Well, whatever, zero works with all
2136 // the other decoders I've tried so it probably doesn't hurt.
2137
2138 // OK, if it's good enough for Jef, it's definitely good enough for us:
2139 os.write(0);
2140 }
2141
2142 //----------------------------------------------------------------------------
2143 private void writeNetscapeExtension(OutputStream os) throws IOException
2144 {
2145 // n.b. most software seems to interpret the count as a repeat count
2146 // (i.e., interations beyond 1) rather than as an iteration count
2147 // (thus, to avoid repeating we have to omit the whole extension)
2148
2149 os.write((int) '!'); // GIF Extension Introducer
2150 os.write(0xff); // Application Extension Label
2151
2152 os.write(11); // application ID block size
2153 putAscii("NETSCAPE2.0", os); // application ID data
2154
2155 os.write(3); // data sub-block size
2156 os.write(1); // a looping flag? dunno
2157
2158 // we finally write the relevent data
2159 putShort(loopCount > 1 ? loopCount - 1 : 0, os);
2160
2161 os.write(0); // block terminator
2162 }
2163
2164 //----------------------------------------------------------------------------
2165 private void writeCommentExtension(OutputStream os) throws IOException
2166 {
2167 os.write((int) '!'); // GIF Extension Introducer
2168 os.write(0xfe); // Comment Extension Label
2169
2170 int remainder = theComments.length() % 255;
2171 int nsubblocks_full = theComments.length() / 255;
2172 int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
2173 int ibyte = 0;
2174 for (int isb = 0; isb < nsubblocks; ++isb)
2175 {
2176 int size = isb < nsubblocks_full ? 255 : remainder;
2177
2178 os.write(size);
2179 putAscii(theComments.substring(ibyte, ibyte + size), os);
2180 ibyte += size;
2181 }
2182
2183 os.write(0); // block terminator
2184 }
2185
2186 //----------------------------------------------------------------------------
2187 private boolean isOk(int frame_index)
2188 {
2189 return frame_index >= 0 && frame_index < vFrames.size();
2190 }
2191 }
2192
2193 //==============================================================================
2194 class GifColorTable {
2195
2196 // the palette of ARGB colors, packed as returned by Color.getRGB()
2197 private int[] theColors = new int[256];
2198
2199 // other basic attributes
2200 private int colorDepth;
2201 private int transparentIndex = -1;
2202
2203 // these fields track color-index info across frames
2204 private int ciCount = 0; // count of distinct color indices
2205 private ReverseColorMap ciLookup; // cumulative rgb-to-ci lookup table
2206
2207 //----------------------------------------------------------------------------
2208 GifColorTable()
2209 {
2210 ciLookup = new ReverseColorMap(); // puts us into "auto-detect mode"
2211 }
2212
2213 //----------------------------------------------------------------------------
2214 GifColorTable(Color[] colors)
2215 {
2216 int n2copy = Math.min(theColors.length, colors.length);
2217 for (int i = 0; i < n2copy; ++i)
2218 theColors[i] = colors[i].getRGB();
2219 }
2220
2221 int indexOf(Color color) {
2222 int rgb = color.getRGB();
2223 for (int i = 0; i < theColors.length; i++) {
2224 if (rgb == theColors[i]) {
2225 return i;
2226 }
2227 }
2228 return -1;
2229 }
2230
2231 //----------------------------------------------------------------------------
2232 int getDepth() { return colorDepth; }
2233
2234 //----------------------------------------------------------------------------
2235 int getTransparent() { return transparentIndex; }
2236
2237 //----------------------------------------------------------------------------
2238 // default: -1 (no transparency)
2239 void setTransparent(int color_index)
2240 {
2241 transparentIndex = color_index;
2242 }
2243
2244 //----------------------------------------------------------------------------
2245 void processPixels(Gif89Frame gf) throws IOException
2246 {
2247 if (gf instanceof DirectGif89Frame)
2248 filterPixels((DirectGif89Frame) gf);
2249 else
2250 trackPixelUsage((IndexGif89Frame) gf);
2251 }
2252
2253 //----------------------------------------------------------------------------
2254 void closePixelProcessing() // must be called before encode()
2255 {
2256 colorDepth = computeColorDepth(ciCount);
2257 }
2258
2259 //----------------------------------------------------------------------------
2260 void encode(OutputStream os) throws IOException
2261 {
2262 // size of palette written is the smallest power of 2 that can accomdate
2263 // the number of RGB colors detected (or largest color index, in case of
2264 // index pixels)
2265 int palette_size = 1 << colorDepth;
2266 for (int i = 0; i < palette_size; ++i)
2267 {
2268 os.write(theColors[i] >> 16 & 0xff);
2269 os.write(theColors[i] >> 8 & 0xff);
2270 os.write(theColors[i] & 0xff);
2271 }
2272 }
2273
2274 //----------------------------------------------------------------------------
2275 // This method accomplishes three things:
2276 // (1) converts the passed rgb pixels to indexes into our rgb lookup table
2277 // (2) fills the rgb table as new colors are encountered
2278 // (3) looks for transparent pixels so as to set the transparent index
2279 // The information is cumulative across multiple calls.
2280 //
2281 // (Note: some of the logic is borrowed from Jef Poskanzer's code.)
2282 //----------------------------------------------------------------------------
2283 private void filterPixels(DirectGif89Frame dgf) throws IOException
2284 {
2285 if (ciLookup == null)
2286 throw new IOException("RGB frames require palette autodetection");
2287
2288 int[] argb_pixels = (int[]) dgf.getPixelSource();
2289 byte[] ci_pixels = dgf.getPixelSink();
2290 int npixels = argb_pixels.length;
2291 for (int i = 0; i < npixels; ++i)
2292 {
2293 int argb = argb_pixels[i];
2294
2295 // handle transparency
2296 if ((argb >>> 24) < 0x80) // transparent pixel?
2297 if (transparentIndex == -1) // first transparent color encountered?
2298 transparentIndex = ciCount; // record its index
2299 else if (argb != theColors[transparentIndex]) // different pixel value?
2300 {
2301 // collapse all transparent pixels into one color index
2302 ci_pixels[i] = (byte) transparentIndex;
2303 continue; // CONTINUE - index already in table
2304 }
2305
2306 // try to look up the index in our "reverse" color table
2307 int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
2308
2309 if (color_index == -1) // if it isn't in there yet
2310 {
2311 if (ciCount == 256)
2312 throw new IOException("can't encode as GIF (> 256 colors)");
2313
2314 // store color in our accumulating palette
2315 theColors[ciCount] = argb;
2316
2317 // store index in reverse color table
2318 ciLookup.put(argb & 0xffffff, ciCount);
2319
2320 // send color index to our output array
2321 ci_pixels[i] = (byte) ciCount;
2322
2323 // increment count of distinct color indices
2324 ++ciCount;
2325 }
2326 else // we've already snagged color into our palette
2327 ci_pixels[i] = (byte) color_index; // just send filtered pixel
2328 }
2329 }
2330
2331 //----------------------------------------------------------------------------
2332 private void trackPixelUsage(IndexGif89Frame igf) throws IOException
2333 {
2334 byte[] ci_pixels = (byte[]) igf.getPixelSource();
2335 int npixels = ci_pixels.length;
2336 for (int i = 0; i < npixels; ++i)
2337 if (ci_pixels[i] >= ciCount)
2338 ciCount = ci_pixels[i] + 1;
2339 }
2340
2341 //----------------------------------------------------------------------------
2342 private int computeColorDepth(int colorcount)
2343 {
2344 // color depth = log-base-2 of maximum number of simultaneous colors, i.e.
2345 // bits per color-index pixel
2346 if (colorcount <= 2)
2347 return 1;
2348 if (colorcount <= 4)
2349 return 2;
2350 if (colorcount <= 16)
2351 return 4;
2352 return 8;
2353 }
2354 }
2355
2356 //==============================================================================
2357 // We're doing a very simple linear hashing thing here, which seems sufficient
2358 // for our needs. I make no claims for this approach other than that it seems
2359 // an improvement over doing a brute linear search for each pixel on the one
2360 // hand, and creating a Java object for each pixel (if we were to use a Java
2361 // Hashtable) on the other. Doubtless my little hash could be improved by
2362 // tuning the capacity (at the very least). Suggestions are welcome.
2363 //==============================================================================
2364 class ReverseColorMap {
2365
2366 private class ColorRecord {
2367 int rgb;
2368 int ipalette;
2369 ColorRecord(int rgb, int ipalette)
2370 {
2371 this.rgb = rgb;
2372 this.ipalette = ipalette;
2373 }
2374 }
2375
2376 // I wouldn't really know what a good hashing capacity is, having missed out
2377 // on data structures and algorithms class :) Alls I know is, we've got a lot
2378 // more space than we have time. So let's try a sparse table with a maximum
2379 // load of about 1/8 capacity.
2380 private static final int HCAPACITY = 2053; // a nice prime number
2381
2382 // our hash table proper
2383 private ColorRecord[] hTable = new ColorRecord[HCAPACITY];
2384
2385 //----------------------------------------------------------------------------
2386 // Assert: rgb is not negative (which is the same as saying, be sure the
2387 // alpha transparency byte - i.e., the high byte - has been masked out).
2388 //----------------------------------------------------------------------------
2389 int getPaletteIndex(int rgb)
2390 {
2391 ColorRecord rec;
2392
2393 for ( int itable = rgb % hTable.length;
2394 (rec = hTable[itable]) != null && rec.rgb != rgb;
2395 itable = ++itable % hTable.length
2396 )
2397 ;
2398
2399 if (rec != null)
2400 return rec.ipalette;
2401
2402 return -1;
2403 }
2404
2405 //----------------------------------------------------------------------------
2406 // Assert: (1) same as above; (2) rgb key not already present
2407 //----------------------------------------------------------------------------
2408 void put(int rgb, int ipalette)
2409 {
2410 int itable;
2411
2412 for ( itable = rgb % hTable.length;
2413 hTable[itable] != null;
2414 itable = ++itable % hTable.length
2415 )
2416 ;
2417
2418 hTable[itable] = new ColorRecord(rgb, ipalette);
2419 }
2420 }
2421
2422
2423
2424 //******************************************************************************
2425 // Gif89Frame.java
2426 //******************************************************************************
2427
2428 //==============================================================================
2429 /** First off, just to dispel any doubt, this class and its subclasses have
2430 * nothing to do with GUI "frames" such as java.awt.Frame. We merely use the
2431 * term in its very common sense of a still picture in an animation sequence.
2432 * It's hoped that the restricted context will prevent any confusion.
2433 * <p>
2434 * An instance of this class is used in conjunction with a Gif89Encoder object
2435 * to represent and encode a single static image and its associated "control"
2436 * data. A Gif89Frame doesn't know or care whether it is encoding one of the
2437 * many animation frames in a GIF movie, or the single bitmap in a "normal"
2438 * GIF. (FYI, this design mirrors the encoded GIF structure.)
2439 * <p>
2440 * Since Gif89Frame is an abstract class we don't instantiate it directly, but
2441 * instead create instances of its concrete subclasses, IndexGif89Frame and
2442 * DirectGif89Frame. From the API standpoint, these subclasses differ only
2443 * in the sort of data their instances are constructed from. Most folks will
2444 * probably work with DirectGif89Frame, since it can be constructed from a
2445 * java.awt.Image object, but the lower-level IndexGif89Frame class offers
2446 * advantages in specialized circumstances. (Of course, in routine situations
2447 * you might not explicitly instantiate any frames at all, instead letting
2448 * Gif89Encoder's convenience methods do the honors.)
2449 * <p>
2450 * As far as the public API is concerned, objects in the Gif89Frame hierarchy
2451 * interact with a Gif89Encoder only via the latter's methods for adding and
2452 * querying frames. (As a side note, you should know that while Gif89Encoder
2453 * objects are permanently modified by the addition of Gif89Frames, the reverse
2454 * is NOT true. That is, even though the ultimate encoding of a Gif89Frame may
2455 * be affected by the context its parent encoder object provides, it retains
2456 * its original condition and can be reused in a different context.)
2457 * <p>
2458 * The core pixel-encoding code in this class was essentially lifted from
2459 * Jef Poskanzer's well-known <cite>Acme GifEncoder</cite>, so please see the
2460 * <a href="../readme.txt">readme</a> containing his notice.
2461 *
2462 * @version 0.90 beta (15-Jul-2000)
2463 * @author J. M. G. Elliott (tep@jmge.net)
2464 * @see Gif89Encoder
2465 * @see DirectGif89Frame
2466 * @see IndexGif89Frame
2467 */
2468 abstract class Gif89Frame {
2469
2470 //// Public "Disposal Mode" constants ////
2471
2472 /** The animated GIF renderer shall decide how to dispose of this Gif89Frame's
2473 * display area.
2474 * @see Gif89Frame#setDisposalMode
2475 */
2476 public static final int DM_UNDEFINED = 0;
2477
2478 /** The animated GIF renderer shall take no display-disposal action.
2479 * @see Gif89Frame#setDisposalMode
2480 */
2481 public static final int DM_LEAVE = 1;
2482
2483 /** The animated GIF renderer shall replace this Gif89Frame's area with the
2484 * background color.
2485 * @see Gif89Frame#setDisposalMode
2486 */
2487 public static final int DM_BGCOLOR = 2;
2488
2489 /** The animated GIF renderer shall replace this Gif89Frame's area with the
2490 * previous frame's bitmap.
2491 * @see Gif89Frame#setDisposalMode
2492 */
2493 public static final int DM_REVERT = 3;
2494
2495 //// Bitmap variables set in package subclass constructors ////
2496 int theWidth = -1;
2497 int theHeight = -1;
2498 byte[] ciPixels;
2499
2500 //// GIF graphic frame control options ////
2501 private Point thePosition = new Point(0, 0);
2502 private boolean isInterlaced;
2503 private int csecsDelay;
2504 private int disposalCode = DM_LEAVE;
2505
2506 //----------------------------------------------------------------------------
2507 /** Set the position of this frame within a larger animation display space.
2508 *
2509 * @param p
2510 * Coordinates of the frame's upper left corner in the display space.
2511 * (Default: The logical display's origin [0, 0])
2512 * @see Gif89Encoder#setLogicalDisplay
2513 */
2514 public void setPosition(Point p)
2515 {
2516 thePosition = new Point(p);
2517 }
2518
2519 //----------------------------------------------------------------------------
2520 /** Set or clear the interlace flag.
2521 *
2522 * @param b
2523 * true if you want interlacing. (Default: false)
2524 */
2525 public void setInterlaced(boolean b)
2526 {
2527 isInterlaced = b;
2528 }
2529
2530 //----------------------------------------------------------------------------
2531 /** Set the between-frame interval.
2532 *
2533 * @param interval
2534 * Centiseconds to wait before displaying the subsequent frame.
2535 * (Default: 0)
2536 */
2537 public void setDelay(int interval)
2538 {
2539 csecsDelay = interval;
2540 }
2541
2542 //----------------------------------------------------------------------------
2543 /** Setting this option determines (in a cooperative GIF-viewer) what will be
2544 * done with this frame's display area before the subsequent frame is
2545 * displayed. For instance, a setting of DM_BGCOLOR can be used for erasure
2546 * when redrawing with displacement.
2547 *
2548 * @param code
2549 * One of the four int constants of the Gif89Frame.DM_* series.
2550 * (Default: DM_LEAVE)
2551 */
2552 public void setDisposalMode(int code)
2553 {
2554 disposalCode = code;
2555 }
2556
2557 //----------------------------------------------------------------------------
2558 Gif89Frame() {} // package-visible default constructor
2559
2560 //----------------------------------------------------------------------------
2561 abstract Object getPixelSource();
2562
2563 //----------------------------------------------------------------------------
2564 int getWidth() { return theWidth; }
2565
2566 //----------------------------------------------------------------------------
2567 int getHeight() { return theHeight; }
2568
2569 //----------------------------------------------------------------------------
2570 byte[] getPixelSink() { return ciPixels; }
2571
2572 //----------------------------------------------------------------------------
2573 void encode(OutputStream os, boolean epluribus, int color_depth,
2574 int transparent_index) throws IOException
2575 {
2576 writeGraphicControlExtension(os, epluribus, transparent_index);
2577 writeImageDescriptor(os);
2578 new GifPixelsEncoder(
2579 theWidth, theHeight, ciPixels, isInterlaced, color_depth
2580 ).encode(os);
2581 }
2582
2583 //----------------------------------------------------------------------------
2584 private void writeGraphicControlExtension(OutputStream os, boolean epluribus,
2585 int itransparent) throws IOException
2586 {
2587 int transflag = itransparent == -1 ? 0 : 1;
2588 if (transflag == 1 || epluribus) // using transparency or animating ?
2589 {
2590 os.write((int) '!'); // GIF Extension Introducer
2591 os.write(0xf9); // Graphic Control Label
2592 os.write(4); // subsequent data block size
2593 os.write((disposalCode << 2) | transflag); // packed fields (1 byte)
2594 putShort(csecsDelay, os); // delay field (2 bytes)
2595 os.write(itransparent); // transparent index field
2596 os.write(0); // block terminator
2597 }
2598 }
2599
2600 //----------------------------------------------------------------------------
2601 private void writeImageDescriptor(OutputStream os) throws IOException
2602 {
2603 os.write((int) ','); // Image Separator
2604 putShort(thePosition.x, os);
2605 putShort(thePosition.y, os);
2606 putShort(theWidth, os);
2607 putShort(theHeight, os);
2608 os.write(isInterlaced ? 0x40 : 0); // packed fields (1 byte)
2609 }
2610 }
2611
2612 //==============================================================================
2613 class GifPixelsEncoder {
2614
2615 private static final int EOF = -1;
2616
2617 private int imgW, imgH;
2618 private byte[] pixAry;
2619 private boolean wantInterlaced;
2620 private int initCodeSize;
2621
2622 // raster data navigators
2623 private int countDown;
2624 private int xCur, yCur;
2625 private int curPass;
2626
2627 //----------------------------------------------------------------------------
2628 GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced,
2629 int color_depth)
2630 {
2631 imgW = width;
2632 imgH = height;
2633 pixAry = pixels;
2634 wantInterlaced = interlaced;
2635 initCodeSize = Math.max(2, color_depth);
2636 }
2637
2638 //----------------------------------------------------------------------------
2639 void encode(OutputStream os) throws IOException
2640 {
2641 os.write(initCodeSize); // write "initial code size" byte
2642
2643 countDown = imgW * imgH; // reset navigation variables
2644 xCur = yCur = curPass = 0;
2645
2646 compress(initCodeSize + 1, os); // compress and write the pixel data
2647
2648 os.write(0); // write block terminator
2649 }
2650
2651 //****************************************************************************
2652 // (J.E.) The logic of the next two methods is largely intact from
2653 // Jef Poskanzer. Some stylistic changes were made for consistency sake,
2654 // plus the second method accesses the pixel value from a prefiltered linear
2655 // array. That's about it.
2656 //****************************************************************************
2657
2658 //----------------------------------------------------------------------------
2659 // Bump the 'xCur' and 'yCur' to point to the next pixel.
2660 //----------------------------------------------------------------------------
2661 private void bumpPosition()
2662 {
2663 // Bump the current X position
2664 ++xCur;
2665
2666 // If we are at the end of a scan line, set xCur back to the beginning
2667 // If we are interlaced, bump the yCur to the appropriate spot,
2668 // otherwise, just increment it.
2669 if (xCur == imgW)
2670 {
2671 xCur = 0;
2672
2673 if (!wantInterlaced)
2674 ++yCur;
2675 else
2676 switch (curPass)
2677 {
2678 case 0:
2679 yCur += 8;
2680 if (yCur >= imgH)
2681 {
2682 ++curPass;
2683 yCur = 4;
2684 }
2685 break;
2686 case 1:
2687 yCur += 8;
2688 if (yCur >= imgH)
2689 {
2690 ++curPass;
2691 yCur = 2;
2692 }
2693 break;
2694 case 2:
2695 yCur += 4;
2696 if (yCur >= imgH)
2697 {
2698 ++curPass;
2699 yCur = 1;
2700 }
2701 break;
2702 case 3:
2703 yCur += 2;
2704 break;
2705 }
2706 }
2707 }
2708
2709 //----------------------------------------------------------------------------
2710 // Return the next pixel from the image
2711 //----------------------------------------------------------------------------
2712 private int nextPixel()
2713 {
2714 if (countDown == 0)
2715 return EOF;
2716
2717 --countDown;
2718
2719 byte pix = pixAry[yCur * imgW + xCur];
2720
2721 bumpPosition();
2722
2723 return pix & 0xff;
2724 }
2725
2726 //****************************************************************************
2727 // (J.E.) I didn't touch Jef Poskanzer's code from this point on. (Well, OK,
2728 // I changed the name of the sole outside method it accesses.) I figure
2729 // if I have no idea how something works, I shouldn't play with it :)
2730 //
2731 // Despite its unencapsulated structure, this section is actually highly
2732 // self-contained. The calling code merely calls compress(), and the present
2733 // code calls nextPixel() in the caller. That's the sum total of their
2734 // communication. I could have dumped it in a separate class with a callback
2735 // via an interface, but it didn't seem worth messing with.
2736 //****************************************************************************
2737
2738 // GIFCOMPR.C - GIF Image compression routines
2739 //
2740 // Lempel-Ziv compression based on 'compress'. GIF modifications by
2741 // David Rowley (mgardi@watdcsu.waterloo.edu)
2742
2743 // General DEFINEs
2744
2745 static final int BITS = 12;
2746
2747 static final int HSIZE = 5003; // 80% occupancy
2748
2749 // GIF Image compression - modified 'compress'
2750 //
2751 // Based on: compress.c - File compression ala IEEE Computer, June 1984.
2752 //
2753 // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
2754 // Jim McKie (decvax!mcvax!jim)
2755 // Steve Davies (decvax!vax135!petsd!peora!srd)
2756 // Ken Turkowski (decvax!decwrl!turtlevax!ken)
2757 // James A. Woods (decvax!ihnp4!ames!jaw)
2758 // Joe Orost (decvax!vax135!petsd!joe)
2759
2760 int n_bits; // number of bits/code
2761 int maxbits = BITS; // user settable max # bits/code
2762 int maxcode; // maximum code, given n_bits
2763 int maxmaxcode = 1 << BITS; // should NEVER generate this code
2764
2765 final int MAXCODE( int n_bits )
2766 {
2767 return ( 1 << n_bits ) - 1;
2768 }
2769
2770 int[] htab = new int[HSIZE];
2771 int[] codetab = new int[HSIZE];
2772
2773 int hsize = HSIZE; // for dynamic table sizing
2774
2775 int free_ent = 0; // first unused entry
2776
2777 // block compression parameters -- after all codes are used up,
2778 // and compression rate changes, start over.
2779 boolean clear_flg = false;
2780
2781 // Algorithm: use open addressing double hashing (no chaining) on the
2782 // prefix code / next character combination. We do a variant of Knuth's
2783 // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
2784 // secondary probe. Here, the modular division first probe is gives way
2785 // to a faster exclusive-or manipulation. Also do block compression with
2786 // an adaptive reset, whereby the code table is cleared when the compression
2787 // ratio decreases, but after the table fills. The variable-length output
2788 // codes are re-sized at this point, and a special CLEAR code is generated
2789 // for the decompressor. Late addition: construct the table according to
2790 // file size for noticeable speed improvement on small files. Please direct
2791 // questions about this implementation to ames!jaw.
2792
2793 int g_init_bits;
2794
2795 int ClearCode;
2796 int EOFCode;
2797
2798 void compress( int init_bits, OutputStream outs ) throws IOException
2799 {
2800 int fcode;
2801 int i /* = 0 */;
2802 int c;
2803 int ent;
2804 int disp;
2805 int hsize_reg;
2806 int hshift;
2807
2808 // Set up the globals: g_init_bits - initial number of bits
2809 g_init_bits = init_bits;
2810
2811 // Set up the necessary values
2812 clear_flg = false;
2813 n_bits = g_init_bits;
2814 maxcode = MAXCODE( n_bits );
2815
2816 ClearCode = 1 << ( init_bits - 1 );
2817 EOFCode = ClearCode + 1;
2818 free_ent = ClearCode + 2;
2819
2820 char_init();
2821
2822 ent = nextPixel();
2823
2824 hshift = 0;
2825 for ( fcode = hsize; fcode < 65536; fcode *= 2 )
2826 ++hshift;
2827 hshift = 8 - hshift; // set hash code range bound
2828
2829 hsize_reg = hsize;
2830 cl_hash( hsize_reg ); // clear hash table
2831
2832 output( ClearCode, outs );
2833
2834 outer_loop:
2835 while ( (c = nextPixel()) != EOF )
2836 {
2837 fcode = ( c << maxbits ) + ent;
2838 i = ( c << hshift ) ^ ent; // xor hashing
2839
2840 if ( htab[i] == fcode )
2841 {
2842 ent = codetab[i];
2843 continue;
2844 }
2845 else if ( htab[i] >= 0 ) // non-empty slot
2846 {
2847 disp = hsize_reg - i; // secondary hash (after G. Knott)
2848 if ( i == 0 )
2849 disp = 1;
2850 do
2851 {
2852 if ( (i -= disp) < 0 )
2853 i += hsize_reg;
2854
2855 if ( htab[i] == fcode )
2856 {
2857 ent = codetab[i];
2858 continue outer_loop;
2859 }
2860 }
2861 while ( htab[i] >= 0 );
2862 }
2863 output( ent, outs );
2864 ent = c;
2865 if ( free_ent < maxmaxcode )
2866 {
2867 codetab[i] = free_ent++; // code -> hashtable
2868 htab[i] = fcode;
2869 }
2870 else
2871 cl_block( outs );
2872 }
2873 // Put out the final code.
2874 output( ent, outs );
2875 output( EOFCode, outs );
2876 }
2877
2878 // output
2879 //
2880 // Output the given code.
2881 // Inputs:
2882 // code: A n_bits-bit integer. If == -1, then EOF. This assumes
2883 // that n_bits =< wordsize - 1.
2884 // Outputs:
2885 // Outputs code to the file.
2886 // Assumptions:
2887 // Chars are 8 bits long.
2888 // Algorithm:
2889 // Maintain a BITS character long buffer (so that 8 codes will
2890 // fit in it exactly). Use the VAX insv instruction to insert each
2891 // code in turn. When the buffer fills up empty it and start over.
2892
2893 int cur_accum = 0;
2894 int cur_bits = 0;
2895
2896 int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
2897 0x001F, 0x003F, 0x007F, 0x00FF,
2898 0x01FF, 0x03FF, 0x07FF, 0x0FFF,
2899 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
2900
2901 void output( int code, OutputStream outs ) throws IOException
2902 {
2903 cur_accum &= masks[cur_bits];
2904
2905 if ( cur_bits > 0 )
2906 cur_accum |= ( code << cur_bits );
2907 else
2908 cur_accum = code;
2909
2910 cur_bits += n_bits;
2911
2912 while ( cur_bits >= 8 )
2913 {
2914 char_out( (byte) ( cur_accum & 0xff ), outs );
2915 cur_accum >>= 8;
2916 cur_bits -= 8;
2917 }
2918
2919 // If the next entry is going to be too big for the code size,
2920 // then increase it, if possible.
2921 if ( free_ent > maxcode || clear_flg )
2922 {
2923 if ( clear_flg )
2924 {
2925 maxcode = MAXCODE(n_bits = g_init_bits);
2926 clear_flg = false;
2927 }
2928 else
2929 {
2930 ++n_bits;
2931 if ( n_bits == maxbits )
2932 maxcode = maxmaxcode;
2933 else
2934 maxcode = MAXCODE(n_bits);
2935 }
2936 }
2937
2938 if ( code == EOFCode )
2939 {
2940 // At EOF, write the rest of the buffer.
2941 while ( cur_bits > 0 )
2942 {
2943 char_out( (byte) ( cur_accum & 0xff ), outs );
2944 cur_accum >>= 8;
2945 cur_bits -= 8;
2946 }
2947
2948 flush_char( outs );
2949 }
2950 }
2951
2952 // Clear out the hash table
2953
2954 // table clear for block compress
2955 void cl_block( OutputStream outs ) throws IOException
2956 {
2957 cl_hash( hsize );
2958 free_ent = ClearCode + 2;
2959 clear_flg = true;
2960
2961 output( ClearCode, outs );
2962 }
2963
2964 // reset code table
2965 void cl_hash( int hsize )
2966 {
2967 for ( int i = 0; i < hsize; ++i )
2968 htab[i] = -1;
2969 }
2970
2971 // GIF Specific routines
2972
2973 // Number of characters so far in this 'packet'
2974 int a_count;
2975
2976 // Set up the 'byte output' routine
2977 void char_init()
2978 {
2979 a_count = 0;
2980 }
2981
2982 // Define the storage for the packet accumulator
2983 byte[] accum = new byte[256];
2984
2985 // Add a character to the end of the current packet, and if it is 254
2986 // characters, flush the packet to disk.
2987 void char_out( byte c, OutputStream outs ) throws IOException
2988 {
2989 accum[a_count++] = c;
2990 if ( a_count >= 254 )
2991 flush_char( outs );
2992 }
2993
2994 // Flush the packet to disk, and reset the accumulator
2995 void flush_char( OutputStream outs ) throws IOException
2996 {
2997 if ( a_count > 0 )
2998 {
2999 outs.write( a_count );
3000 outs.write( accum, 0, a_count );
3001 a_count = 0;
3002 }
3003 }
3004 }
3005
3006
3007
3008 //******************************************************************************
3009 // IndexGif89Frame.java
3010 //******************************************************************************
3011
3012 //==============================================================================
3013 /** Instances of this Gif89Frame subclass are constructed from bitmaps in the
3014 * form of color-index pixels, which accords with a GIF's native palettized
3015 * color model. The class is useful when complete control over a GIF's color
3016 * palette is desired. It is also much more efficient when one is using an
3017 * algorithmic frame generator that isn't interested in RGB values (such
3018 * as a cellular automaton).
3019 * <p>
3020 * Objects of this class are normally added to a Gif89Encoder object that has
3021 * been provided with an explicit color table at construction. While you may
3022 * also add them to "auto-map" encoders without an exception being thrown,
3023 * there obviously must be at least one DirectGif89Frame object in the sequence
3024 * so that a color table may be detected.
3025 *
3026 * @version 0.90 beta (15-Jul-2000)
3027 * @author J. M. G. Elliott (tep@jmge.net)
3028 * @see Gif89Encoder
3029 * @see Gif89Frame
3030 * @see DirectGif89Frame
3031 */
3032 class IndexGif89Frame extends Gif89Frame {
3033
3034 //----------------------------------------------------------------------------
3035 /** Construct a IndexGif89Frame from color-index pixel data.
3036 *
3037 * @param width
3038 * Width of the bitmap.
3039 * @param height
3040 * Height of the bitmap.
3041 * @param ci_pixels
3042 * Array containing at least width*height color-index pixels.
3043 */
3044 public IndexGif89Frame(int width, int height, byte ci_pixels[])
3045 {
3046 theWidth = width;
3047 theHeight = height;
3048 ciPixels = new byte[theWidth * theHeight];
3049 System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
3050 }
3051
3052 //----------------------------------------------------------------------------
3053 Object getPixelSource() { return ciPixels; }
3054 }
3055
3056
3057
3058 //----------------------------------------------------------------------------
3059 /** Write just the low bytes of a String. (This sucks, but the concept of an
3060 * encoding seems inapplicable to a binary file ID string. I would think
3061 * flexibility is just what we don't want - but then again, maybe I'm slow.)
3062 */
3063 public static void putAscii(String s, OutputStream os) throws IOException
3064 {
3065 byte[] bytes = new byte[s.length()];
3066 for (int i = 0; i < bytes.length; ++i) {
3067 bytes[i] = (byte) s.charAt(i); // discard the high byte
3068 }
3069 os.write(bytes);
3070 }
3071
3072 //----------------------------------------------------------------------------
3073 /** Write a 16-bit integer in little endian byte order.
3074 */
3075 public static void putShort(int i16, OutputStream os) throws IOException
3076 {
3077 os.write(i16 & 0xff);
3078 os.write(i16 >> 8 & 0xff);
3079 }
3080}