/* @(#)JPTImageIO.java   25 March 2007 */

/* Useful imports */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.codec.*;
import edu.neu.ccs.console.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.jpf.*;
import edu.neu.ccs.parser.*;
import edu.neu.ccs.pedagogy.*;
import edu.neu.ccs.quick.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import java.io.*;
import java.util.*;
import java.math.*;
import java.beans.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.regex.*;
import java.text.ParseException;

/**
 * <p><code>JPTImageIO</code> builds on the methods introduced
 * in Java 5.0 in the class <code>ImageIO</code>.
 * See the javadocs for that class for more details.</p>
 *
 * <p>The class provides a static method to write images based
 * on the following entities:</p>
 * 
 * <ul>
 *   <li><code>JComponent</code></li>
 *   <li><code>Paintable</code></li>
 * </ul>
 * 
 * <p>The entity <code>RenderedImage</code> that is used in
 * the class <code>ImageIO</code> is also supported.</p>
 * 
 * <p>The class has useful helper methods that are public.</p>
 * 
 * <p>In Java 5.0, the following write format names are
 * supported:</p>
 * 
 * <ul>
 *   <li><code>BMP JPEG JPG PNG WBMP</code></li>
 *   <li><code>bmp jpeg jpg png wbmp</code></li>
 * </ul>
 * 
 * <p>Experiments show that writing images using <code>bmp</code>
 * or <code>png</code> formats produces high quality images but
 * that writing images with <code>jpg</code> produces images of
 * low quality with many artifact.</p>
 */
public class JPTImageIO
    implements JPTConstants
{
    
    /**
     * <p>Returns a new <code>BufferedImage</code> of type
     * <code>BufferedImage.TYPE_INT_RGB</code> whose
     * dimensions are width by height.</p>
     * 
     * <p>Initializes the RGB value of each pixel in the
     * image to -1 which corresponds to opaque white.</p>
     * 
     * <p>Returns <code>null</code> if either width or
     * height is less than or equal to zero.</p>
     */
    public static BufferedImage make_INT_RGB(int width, int height) {
        if ((width <= 0) || (height <= 0))
            return null;
        
        BufferedImage image =
            new BufferedImage
                (width, height, BufferedImage.TYPE_INT_RGB);
        
        for (int x = 0; x < width; x++)
            for (int y = 0; y < height; y++)
                image.setRGB(x, y, -1);
        
        return image;
    }
    
    
    /**
     * <p>Returns a new <code>BufferedImage</code> of type
     * <code>BufferedImage.TYPE_INT_RGB</code> whose
     * size and data are determined by the given component.<.p>
     * 
     * <p>The returned image is first initialized using the
     * preferred size of the component plus its insets.
     * Next, a graphics context for the image is created
     * and this context is translated by the top-left inset
     * values.  Then, the paint method of the component is
     * called to paint the component contents onto the image.</p>
     * 
     * <p>Returns <code>null</code> if the given component
     * is <code>null</code> or has zero width or height.</p>
     * 
     * <p><b>Multi-thread warning:</b></p>
     * 
     * <p>If the application is multi-threaded and if a lock
     * must be obtained before painting the given component,
     * then the same lock must be obtained when calling this
     * method.</p>
     */
    public static BufferedImage make_INT_RGB(JComponent component) {
        if (component == null)
            return null;
        
        Dimension dimension = component.getPreferredSize();
        
        if (dimension == null)
            return null;
        
        int W = dimension.width;
        int H = dimension.height;
        
        if ((W <= 0) || (H <= 0))
            return null;
        
        int L = 0;
        int R = 0;
        int T = 0;
        int B = 0;
        
        Insets insets = component.getInsets();
        
        if (insets != null) {
            L = insets.left;
            R = insets.right;
            T = insets.top;
            B = insets.bottom;
        }
        
        W += L + R;
        H += T + B;
        
        BufferedImage image = make_INT_RGB(W, H);
        
        if (image == null)
            return null;
        
        Graphics2D g = image.createGraphics();
        
        g.translate(L, T);
        
        component.paint(g);
        
        return image;
    }
    
    
    /**
     * <p>Returns a new <code>BufferedImage</code> of type
     * <code>BufferedImage.TYPE_INT_RGB</code> whose
     * size and data are determined by the given paintable.<.p>
     * 
     * <p>The returned image is first initialized with the
     * size of the paintable as determined by getBounds2D().
     * Then the paintable is painted onto the image with a
     * suitable translation so that the top-left corner of
     * the paintable maps to (0,0) in the image.</p>
     * 
     * <p>Returns <code>null</code> if the given paintable
     * is <code>null</code> or has zero width or height.</p>
     * 
     * <p><b>Multi-thread warning:</b></p>
     * 
     * <p>If the application is multi-threaded and if a lock
     * must be obtained before painting the given paintable,
     * then the same lock must be obtained when calling this
     * method.</p>
     */
    public static BufferedImage make_INT_RGB(Paintable paintable) {
        if (paintable == null)
            return null;
        
        XRect bounds = paintable.getBounds2D();
        
        int W = (int) bounds.width;
        int H = (int) bounds.height;
        
        if ((W <= 0) || (H <= 0))
            return null;
        
        BufferedImage image = make_INT_RGB(W, H);
        
        int X = (int) bounds.x;
        int Y = (int) bounds.y;
        
        Graphics2D g = image.createGraphics();
        
        g.translate(-X, -Y);
        
        paintable.paint(g);
        
        return image;
    }
    
    
    /**
     * <p>Returns <code>ImageIO.getWriterFormatNames()</code>
     * but sorts the strings prior to the return.</p>
     * 
     * <p>See the notes on image quality in the introduction
     * to this class.</p>
     */
    public static String[] getWriterFormatNames() {
        String[] formats = ImageIO.getWriterFormatNames();
        
        Arrays.sort(formats);
        
        return formats;
    }
    
    
    /**
     * <p>Returns a comma-separated list of the strings
     * returned by <code>getWriterFormatNames()</code>.</p>
     * 
     * <p>See the notes on image quality in the introduction
     * to this class.</p>
     */
    public static String getWriterFormatNamesList() {
        String[] formats = getWriterFormatNames();
        
        int length = formats.length;
        
        int size = 2 * (length - 1);
        
        for (int i = 0; i < length; i++)
            size += formats[i].length();
        
        StringBuffer buffer = new StringBuffer(size);
        
        for (int i = 0; i < length; i++) {
            buffer.append(formats[i]);
            
            if (i < (length - 1))
                buffer.append(", ");
        }
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Returns true if the given format string is a valid
     * format for writing images under the current
     * implementation in Java's class <code>ImageIO</code>.</p>
     * 
     * <p>See the notes on image quality in the introduction
     * to this class.</p>
     */
    public static boolean isWriteFormat(String format) {
        if (format == null)
            return false;
        
        String[] formats = getWriterFormatNames();
        
        int length = formats.length;
        
        for (int i = 0; i < length; i++)
            if (format.equals(formats[i]))
                return true;
        
        return false;
    }
    
    
    /**
     * <p>Returns a <code>RadioPanel</code> with the format
     * strings available for image IO.</p>
     * 
     * <p>The layout uses 2 rows</p>.
     */
    public static RadioPanel makeWriteFormatRadioPanel() {
        String[] formats = getWriterFormatNames();
        
        int length = formats.length;
        
        int rows = 2;
        int cols = length / 2;
        
        int gap = 5;
        
        TableLayout layout =
            new TableLayout
                (rows, cols, gap, gap, CENTER, VERTICAL);
        
        return new RadioPanel(formats, layout);
    }
    
    
    /**
     * <p>Writes the given image source
     * using the given format
     * to the given output target.</p>
     * 
     * <p>The type of source must be one of:</p>
     * 
     * <ul>
     *   <li><code>RenderedImage</code></li>
     *   <li><code>JComponent</code></li>
     *   <li><code>Paintable</code></li>
     * </ul>
     * 
     * <p>The format string must be one of the strings
     * returned by <code>getWriterFormatNames()</code>.
     * See the notes below.</p>
     * 
     * <p>The type of target must be one of:</p>
     * 
     * <ul>
     *   <li><code>File</code></li>
     *   <li><code>OutputStream</code></li>
     *   <li><code>ImageOutputStream</code></li>
     * </ul>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if the above pre-conditions fail.</p>
     * 
     * <p>Throws <code>RuntimeException</code>
     * if the IO operation fails.</p>
     * 
     * <p>In Java 5.0, the following write format strings
     * are supported:</p>
     * 
     * <ul>
     *   <li><code>BMP JPEG JPG PNG WBMP</code></li>
     *   <li><code>bmp jpeg jpg png wbmp</code></li>
     * </ul>
     * 
     * <p>Before choosing a format, see the notes on image
     * quality in the introduction to this class.</p>
     * 
     * <p><b>Multi-thread warning:</b></p>
     * 
     * <p>If the application is multi-threaded and if a lock
     * must be obtained before painting the given component
     * or paintable, then the same lock must be obtained
     * when calling this method.</p>
     */
    public static void write
        (Object source, String format, Object target)
    {
        RenderedImage image = null;
        String message = null;
        
        if (source instanceof RenderedImage)
            image = (RenderedImage) source;
        
        else if (source instanceof JComponent)
            image = make_INT_RGB((JComponent) source);
        
        else if (source instanceof Paintable)
            image = make_INT_RGB((Paintable) source);
        
        if (image == null) {
            message =
                "In JPTImageIO.write, the source must be one of "
                + "RenderedImage, JComponent, Paintable";
            
            throw new IllegalArgumentException(message);
        }
        
        if (! isWriteFormat(format)) {
            message =
                "In JPTImageIO.write, the format must be one of "
                + getWriterFormatNamesList();
            
            throw new IllegalArgumentException(message);
        }
        
        try {
            if (target instanceof File)
                ImageIO.write
                    (image, format, (File) target);
            
            else if (target instanceof OutputStream)
                ImageIO.write
                    (image, format, (OutputStream) target);
            
            else if (target instanceof ImageOutputStream)
                ImageIO.write
                    (image, format, (ImageOutputStream) target);
            
            else {
                message =
                    "In JPTImageIO.write, the target must be one of "
                    + "File, OutputStream, ImageOutputStream";
                
                throw new IllegalArgumentException(message);
            }
        }
        catch(IOException ex) {
            throw new RuntimeException(ex.getMessage());
        }
    }
}

