/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2003 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.util.image;

import java.awt.RenderingHints;
import java.awt.image.*;
import java.awt.image.renderable.ParameterBlock;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.media.jai.*;

/**
 * A class that performs "normalization" on an image to fit within a bounding box of a specified pixel
 * size.  "Normalized" images are scaled down proportionally to fit the bounding box, then centered over
 * a black backdrop (so the image will be "letterboxed" if it has a larger H:V aspect ratio than the bounding
 * box, or "pillarboxed" if it has a smaller H:V aspect ratio than the bounding box).
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class ImageNormalizer
{
  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * ImageNormalizer instances should NOT be constructed in standard programming.
   * Instead, the class should be used as <code>ImageNormalizer.normalizeImage(...);</code>.
   * This constructor is public to permit tools that require a JavaBean instance
   * to operate.
   */
  public ImageNormalizer()
  {
    super();

  } // end constructor

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * "Normalizes" an image to fit within a bounding box of a specified pixel size.  "Normalized" images are
   * scaled down proportionally to fit the bounding box, then centered over a black backdrop (so the image
   * will be "letterboxed" if it has a larger H:V aspect ratio than the bounding box, or "pillarboxed" if it
   * has a smaller H:V aspect ratio than the bounding box).
   *
   * @param image_data Raw image data from the input, in any ImageIO-recognized format.
   * @param width The width of the bounding box to use to normalize the image.
   * @param height The height of the bounding box to use to normalize the image.
   * @param output_type The desired MIME type for output (such as "image/jpeg").
   * @return The normalized image data as a byte array.
   * @exception com.silverwrist.util.image.ImageNormalizerException An error occurred in the image data or
   *            the image transformation process.
   * @see ImageNormalizerException
   */
  public static byte[] normalizeImage(InputStream image_data, int width, int height, String output_type)
      throws ImageNormalizerException
  {
    PlanarImage img1;  // initial image
    try
    { // get an image input stream for the image
      ImageInputStream iistm = ImageIO.createImageInputStream(image_data);

      // create the "ImageRead" operation to load the image; it will autodetect the
      // image format and create an appropriate ImageReader
      ParameterBlock pb = new ParameterBlock();
      pb.add(iistm);
      pb.add(new Integer(0));
      pb.add(Boolean.FALSE);
      pb.add(Boolean.FALSE);
      RenderedOp rop = JAI.create("ImageRead",pb);
      img1 = rop.getRendering();

    } // end try
    catch (IOException e)
    { // try to create the ImageInputStream
      throw new ImageNormalizerException("Unable to load image",e);

    } // end catch
    catch (RuntimeException re)
    { // unable to get the rendering here!
      throw new ImageNormalizerException("Image data not a valid image format",re);

    } // end catch

    try
    { // Compute the scaling factors required to get the image down to the appropriate size, then choose
      // the smaller of the two to use as the final scaling factor.  Note that we always scale DOWN,
      // not UP.
      Float scale_width = null, scale_height = null;
      if (img1.getWidth()>width)
	scale_width = new Float((float)width / (float)(img1.getWidth()));
      if (img1.getHeight()>height)
	scale_height = new Float((float)height / (float)(img1.getHeight()));
      Float scale = null;
      if (scale_width!=null)
      { // we can scale by width, how about height?
	if (scale_height!=null)
	{ // yes, height too...pick the smaller of the two
	  if (scale_width.floatValue()<scale_height.floatValue())
	    scale = scale_width;
	  else
	    scale = scale_height;

	} // end if
	else  // no, just width
	  scale = scale_width;

      } // end if
      else if (scale_height!=null)  // can only scale by height
	scale = scale_height;

      // If we need to scale the image now, do so.
      PlanarImage img2;   // image post-scaling
      if (scale!=null)
      { // scale the image down!
	ParameterBlock pb1 = new ParameterBlock();
	pb1.addSource(img1);
	pb1.add(scale.floatValue());
	pb1.add(scale.floatValue());
	pb1.add(0.0F);
	pb1.add(0.0F);
	pb1.add(Interpolation.getInstance(Interpolation.INTERP_BILINEAR));
	img2 = JAI.create("scale",pb1);

      } // end if
      else  // just use this as the next image
	img2 = img1;

      // Figure out the offsets required to center the new image within the "frame."
      int offset_x = (width - img2.getWidth()) / 2;
      int offset_y = (height - img2.getHeight()) / 2;

      // If we need to translate the image now, do so.
      PlanarImage img3;    // image after scaling and translation
      if ((offset_x!=0) || (offset_y!=0))
      { // set up a translation to move the image to the right location
	ParameterBlock pb2 = new ParameterBlock();
	pb2.addSource(img2);
	pb2.add((float)offset_x);
	pb2.add((float)offset_y);
	pb2.add(Interpolation.getInstance(Interpolation.INTERP_NEAREST));
	img3 = JAI.create("translate",pb2);

      } // end if
      else  // just take the image as it is
	img3 = img2;

      // To set up the backdrop, first we need to create an image of the right size but with the same
      // sample model and color model as our transformed image.
      TiledImage back1 = new TiledImage(0,0,width,height,0,0,img3.getSampleModel(),img3.getColorModel());

      // Now we need to make that image black.  The easiest way to do that is multiply all pixel
      // values in the image by 0.
      ParameterBlock pb = new ParameterBlock();
      pb.addSource(back1);
      double[] parms = new double[1];
      parms[0] = 0.0;
      pb.add(parms);
      PlanarImage back2 = JAI.create("multiplyconst",pb);

      // Now overlay the scaled/translated image on top of the background to get the final image.
      PlanarImage final_img = JAI.create("overlay",back2,img3);

      // Now we need to output this new image in the right format.
      ByteArrayOutputStream stm = new ByteArrayOutputStream();
      ImageOutputStream iostm = ImageIO.createImageOutputStream(stm);

      // Get an ImageWriter for the MIME type.
      Iterator it = ImageIO.getImageWritersByMIMEType(output_type);
      ImageWriter iwr = (ImageWriter)(it.next());

      // Create the ParameterBlock and the operation.  Most of the parameters are null, but we
      // do need to specify the ImageWriter right at the end.
      pb = new ParameterBlock();
      pb.addSource(final_img);
      pb.add(iostm);
      pb.add(null);
      pb.add(Boolean.FALSE);
      pb.add(Boolean.FALSE);
      pb.add(Boolean.TRUE);
      pb.add(Boolean.FALSE);
      pb.add(null);
      pb.add(null);
      pb.add(null);
      pb.add(null);
      pb.add(null);
      pb.add(null);
      pb.add(null);
      pb.add(iwr);
      RenderedOp rop = JAI.create("ImageWrite",pb);
      rop.getSourceImage(0).removeSink(rop);  // disconnect the image modification mechanism

      // Return the new image data.
      iostm.flush();
      return stm.toByteArray();

    } // end try
    catch (Exception e)
    { // catchall exception
      throw new ImageNormalizerException("error in scaling or translation: " + e.getMessage(),e);

    } // end catch

  } // end normalizeImage

  /*--------------------------------------------------------------------------------
   * Test code
   *--------------------------------------------------------------------------------
   */

  /**
   * A simple piece of test code for the <CODE>ImageNormalizer</CODE>, which reads an image from one file,
   * normalizes it, and writes it to a second file.
   *
   * @param args Command-line arguments.  The first of these should be the name of the input file, and the
   *             second should be the name of the output file.
   * @return (Exit code) 0 on success, 1 on error.
   */
  public static void main(String[] args)
  {
    if (args.length<2)
    { // make sure we have 2 arguments
      System.out.println("Usage: ImageNormalizer input-file output-file");
      System.exit(1);

    } // end if

    try
    { // very simple, very easy
      FileInputStream instm = new FileInputStream(args[0]);
      byte[] outdata = normalizeImage(instm,100,100,"image/jpeg");
      instm.close();
      FileOutputStream outstm = new FileOutputStream(args[1]);
      outstm.write(outdata);
      outstm.close();
      System.out.println(args[0] + " => " + args[1]);

    } // end try
    catch (Exception e)
    { // dump exception on error
      e.printStackTrace();
      System.exit(1);

    } // end catch

    System.exit(0);

  } // end main

} // end class ImageNormalizer
