//
// $Id$
//
// jupload - A file upload applet.
//
// Copyright 2008 The JUpload Team
//
// Created: 12 fevr. 08
// Creator: etienne_sf
// Last modified: $Date$
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
package wjhk.jupload2.filedata.helper;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import wjhk.jupload2.exception.JUploadException;
import wjhk.jupload2.exception.JUploadIOException;
import wjhk.jupload2.filedata.DefaultFileData;
import wjhk.jupload2.filedata.PictureFileData;
import wjhk.jupload2.policies.PictureUploadPolicy;
/**
* Class that contains various utilities about picture, mainly about picture
* transformation.
*
* @author etienne_sf
*
*/
public class ImageHelper implements ImageObserver {
/**
* hasToTransformPicture indicates whether the picture should be
* transformed. Null if unknown. This can happen (for instance) if no calcul
* where done (during initialization), or after rotating the picture back to
* the original orientation.
* Note: this attribute is from the class Boolean (and not a simple
* boolean), to allow null value, meaning unknown.
*/
private Boolean hasToTransformPicture = null;
/**
* The {@link PictureFileData} that this helper will have to help.
*/
private PictureFileData pictureFileData;
/**
* Current rotation of the picture: 0 to 3.
*
* @see PictureFileData
*/
private int quarterRotation;
/**
* Maximum width for the current transformation
*/
private int maxWidth;
/**
* Maximum height for the current transformation
*/
private int maxHeight;
/**
* Defines the number of pixel for the current picture. Used to update the
* progress bar.
*
* @see #getBufferedImage(boolean, BufferedImage)
* @see #imageUpdate(Image, int, int, int, int, int)
*/
private int nbPixelsTotal = -1;
/**
* Indicates the number of pixels that have been read.
*
* @see #nbPixelsTotal
* @see #imageUpdate(Image, int, int, int, int, int)
*/
private int nbPixelsRead = 0;
/**
* Width of picture, after rescaling but without rotation. It should be
* scale*originalWidth, but, due to rounding number, it can be transformed
* to scale*originalWidth-1.
*
* @see #initScale()
*/
private int scaledNonRotatedWidth = -1;
/**
* Same as {@link #scaledNonRotatedWidth}
*/
private int scaledNonRotatedHeight = -1;
/**
* The value that has the progress bar when starting to load the picture.
* The {@link #imageUpdate(Image, int, int, int, int, int)} method will add
* from 0 to 100, to indicate progress with a percentage value of picture
* loading.
*/
private int progressBarBaseValue = 0;
/**
* Current scaling factor. If less than 1, means a picture reduction.
*
* @see #initScale()
*/
private double scale = 1;
/**
* Width of picture, after re-scaling and rotation. It should be
* scale*originalWidth or scale*originalHeight (depending on the rotation).
* But, due to rounding number, it can be transformed to
* scale*originalWidth-1 or scale*originalHeight-1.
*
* @see #initScale()
*/
private int scaledRotatedWidth = -1;
/**
* Same as {@link #scaledRotatedWidth}, for the height.
*/
private int scaledRotatedHeight = -1;
/**
* The current upload policy must be a {@link PictureUploadPolicy}
*/
PictureUploadPolicy uploadPolicy;
/**
* Standard constructor.
*
* @param uploadPolicy The current upload policy
* @param pictureFileData The picture file data to help
* @param targetMaxWidth
* @param targetMaxHeight
* @param quarterRotation Current quarter rotation (from 0 to 3)
* @throws JUploadIOException
*/
public ImageHelper(PictureUploadPolicy uploadPolicy,
PictureFileData pictureFileData, int targetMaxWidth,
int targetMaxHeight, int quarterRotation) throws JUploadIOException {
this.uploadPolicy = uploadPolicy;
this.pictureFileData = pictureFileData;
this.maxWidth = targetMaxWidth;
this.maxHeight = targetMaxHeight;
this.quarterRotation = quarterRotation;
// Pre-calculation: should the current picture be rescaled, to match the
// given target size ?
initScale();
}
/**
* Intialization of scale factor, for the current picture state. The scale
* is based on the maximum width and height, the current rotation, and the
* picture size.
*/
private void initScale() throws JUploadIOException {
double theta = Math.toRadians(90 * this.quarterRotation);
// The width and height depend on the current rotation :
// calculation of the width and height of picture after
// rotation.
int nonScaledRotatedWidth = this.pictureFileData.getOriginalWidth();
int nonScaledRotatedHeight = this.pictureFileData.getOriginalHeight();
if (this.quarterRotation % 2 != 0) {
// 90 degrees or 270 degrees rotation: width and height are
// switched.
nonScaledRotatedWidth = this.pictureFileData.getOriginalHeight();
nonScaledRotatedHeight = this.pictureFileData.getOriginalWidth();
}
// Now, we can compare these width and height to the maximum
// width and height
double scaleWidth = ((this.maxWidth < 0) ? 1 : ((double) this.maxWidth)
/ nonScaledRotatedWidth);
double scaleHeight = ((this.maxHeight < 0) ? 1
: ((double) this.maxHeight) / nonScaledRotatedHeight);
this.scale = Math.min(scaleWidth, scaleHeight);
if (this.scale < 1) {
// With number rounding, it can happen that width or size
// became one pixel too big. Let's correct it.
if ((this.maxWidth > 0 && this.maxWidth < (int) (this.scale
* Math.cos(theta) * nonScaledRotatedWidth))
|| (this.maxHeight > 0 && this.maxHeight < (int) (this.scale
* Math.cos(theta) * nonScaledRotatedHeight))) {
scaleWidth = ((this.maxWidth < 0) ? 1
: ((double) this.maxWidth - 1)
/ (nonScaledRotatedWidth));
scaleHeight = ((this.maxHeight < 0) ? 1
: ((double) this.maxHeight - 1)
/ (nonScaledRotatedHeight));
this.scale = Math.min(scaleWidth, scaleHeight);
}
}
// These variables contain the actual width and height after
// rescaling, and before rotation.
this.scaledRotatedWidth = nonScaledRotatedWidth;
this.scaledRotatedHeight = nonScaledRotatedHeight;
// Is there any rescaling to do ?
// Patch for the first bug, tracked in the sourceforge bug
// tracker ! ;-)
if (this.scale < 1) {
this.scaledRotatedWidth *= this.scale;
this.scaledRotatedHeight *= this.scale;
this.uploadPolicy.displayDebug("Resizing factor (scale): "
+ this.scale, 30);
} else {
this.uploadPolicy.displayDebug(
"Resizing factor (scale): no resizing (calculated scale was "
+ this.scale + ")", 30);
}
// Due to rounded numbers, the resulting targetWidth or
// targetHeight
// may be one pixel too big. Let's check that.
if (this.scaledRotatedWidth > this.maxWidth) {
this.uploadPolicy.displayDebug("Correcting rounded width: "
+ this.scaledRotatedWidth + " to " + this.maxWidth, 50);
this.scaledRotatedWidth = this.maxWidth;
}
if (this.scaledRotatedHeight > this.maxHeight) {
this.uploadPolicy.displayDebug("Correcting rounded height: "
+ this.scaledRotatedHeight + " to " + this.maxHeight, 50);
this.scaledRotatedHeight = this.maxHeight;
}
// getBufferedImage will need the two following value:
if (this.quarterRotation % 2 == 0) {
this.scaledNonRotatedWidth = this.scaledRotatedWidth;
this.scaledNonRotatedHeight = this.scaledRotatedHeight;
} else {
this.scaledNonRotatedWidth = this.scaledRotatedHeight;
this.scaledNonRotatedHeight = this.scaledRotatedWidth;
}
}
/**
* This function indicate if the picture has to be modified. For instance :
* a maximum width, height, a target format...
*
* @return true if the picture must be transformed. false if the file can be
* directly transmitted.
* @throws JUploadException Contains any exception that could be thrown in
* this method
*/
public boolean hasToTransformPicture() throws JUploadException {
// Animated gif must be transmit as is, as I can't find a way to
// recreate them.
if (DefaultFileData.getExtension(this.pictureFileData.getFile())
.equalsIgnoreCase("gif")) {
// If this is an animated gif, no transformation... I can't succeed
// to create a transformed picture file for them.
ImageReaderWriterHelper irwh = new ImageReaderWriterHelper(
this.uploadPolicy, this.pictureFileData);
int nbImages = irwh.getNumImages(true);
irwh.dispose();
irwh = null;
if (nbImages > 1) {
// Too bad. We can not transform it.
this.hasToTransformPicture = Boolean.FALSE;
this.uploadPolicy
.displayWarn("No transformation for gif picture file, that contain several pictures. (see JUpload documentation for details)");
}
}
// Did we already estimate if transformation is needed ?
if (this.hasToTransformPicture == null) {
// First : the easiest test. Should we block metadata ?
if (this.hasToTransformPicture == null
&& !(this.uploadPolicy).getPictureTransmitMetadata()) {
this.hasToTransformPicture = Boolean.TRUE;
this.uploadPolicy
.displayDebug(
this.pictureFileData.getFileName()
+ " : hasToTransformPicture=true (pictureTransmitMetadata is false)",
80);
}
// Second : another easy test. A rotation is needed ?
if (this.hasToTransformPicture == null && this.quarterRotation != 0) {
this.uploadPolicy
.displayDebug(
this.pictureFileData.getFileName()
+ " : hasToTransformPicture = true (quarterRotation != 0)",
10);
this.hasToTransformPicture = Boolean.TRUE;
}
// Third : the picture format is the same ?
String targetFormat = this.uploadPolicy
.getImageFileConversionInfo().getTargetFormatOrNull(
this.pictureFileData.getFileExtension());
if (this.hasToTransformPicture == null && targetFormat != null) {
this.uploadPolicy
.displayDebug(
this.pictureFileData.getFileName()
+ " : hasToTransformPicture = true (targetPictureFormat)",
10);
this.hasToTransformPicture = Boolean.TRUE;
}
// Fourth : should we resize the picture ?
if (this.hasToTransformPicture == null && this.scale < 1) {
this.uploadPolicy.displayDebug(this.pictureFileData
.getFileName()
+ " : hasToTransformPicture = true (scale < 1)", 10);
this.hasToTransformPicture = Boolean.TRUE;
}
// If we find no reason to transform the picture, then let's let the
// picture unmodified.
if (this.hasToTransformPicture == null) {
this.uploadPolicy.displayDebug(this.pictureFileData
.getFileName()
+ " : hasToTransformPicture = false", 10);
this.hasToTransformPicture = Boolean.FALSE;
this.uploadPolicy.displayDebug(this.pictureFileData
.getFileName()
+ " : hasToTransformPicture = false", 10);
}
}
return this.hasToTransformPicture.booleanValue();
}// end of hasToTransformPicture
/**
* This function resizes the picture, if necessary, according to the
* maxWidth and maxHeight, given to the ImageHelper constructor.
* This function should only be called if isPicture is true. Otherwise, an
* exception is raised.
* Note (Update given by David Gnedt): the highquality will condition the
* call of getScaledInstance, instead of a basic scale Transformation. The
* generated picture is of better quality, but this is longer, especially on
* 'small' CPU. Time samples, with one picture from my canon EOS20D, on a
* PII 500M:
* ~3s for the full screen preview with highquality to false, and a quarter
* rotation. 12s to 20s with highquality to true.
* ~5s for the first (small) preview of the picture, with both highquality
* to false or true.
*
* @param highquality (added by David Gnedt): if set to true, the
* BufferedImage.getScaledInstance() is called. This generates
* better image, but consumes more CPU.
* @param sourceBufferedImage The image to resize or rotate or both or no
* tranformation...
* @return A BufferedImage which contains the picture according to current
* parameters (resizing, rotation...), or null if this is not a
* picture.
* @throws JUploadException Contains any exception thrown from within this
* method.
*/
public BufferedImage getBufferedImage(boolean highquality,
BufferedImage sourceBufferedImage) throws JUploadException {
long msGetBufferedImage = System.currentTimeMillis();
double theta = Math.toRadians(90 * this.quarterRotation);
BufferedImage returnedBufferedImage = null;
this.uploadPolicy.displayDebug("getBufferedImage: start", 10);
try {
AffineTransform transform = new AffineTransform();
if (this.quarterRotation != 0) {
double translationX = 0, translationY = 0;
this.uploadPolicy.displayDebug("getBufferedImage: quarter: "
+ this.quarterRotation, 50);
// quarterRotation is one of 0, 1, 2, 3 : see addRotation.
// If we're here : it's not 0, so it's one of 1, 2 or 3.
switch (this.quarterRotation) {
case 1:
translationX = 0;
translationY = -this.scaledRotatedWidth;
break;
case 2:
translationX = -this.scaledRotatedWidth;
translationY = -this.scaledRotatedHeight;
break;
case 3:
translationX = -this.scaledRotatedHeight;
translationY = 0;
break;
default:
this.uploadPolicy
.displayWarn("Invalid quarterRotation : "
+ this.quarterRotation);
this.quarterRotation = 0;
theta = 0;
}
transform.rotate(theta);
transform.translate(translationX, translationY);
}
// If we have to rescale the picture, we first do it:
if (this.scale < 1) {
if (highquality) {
this.uploadPolicy
.displayDebug(
"getBufferedImage: Resizing picture(using high quality picture)",
30);
// SCALE_AREA_AVERAGING forces the picture calculation
// algorithm.
// Other parameters give bad picture quality.
Image img = sourceBufferedImage.getScaledInstance(
this.scaledNonRotatedWidth,
this.scaledNonRotatedHeight,
Image.SCALE_AREA_AVERAGING);
// the localBufferedImage may be 'unknown'.
int localImageType = sourceBufferedImage.getType();
if (localImageType == BufferedImage.TYPE_CUSTOM) {
localImageType = BufferedImage.TYPE_INT_BGR;
}
BufferedImage tempBufferedImage = new BufferedImage(
this.scaledNonRotatedWidth,
this.scaledNonRotatedHeight, localImageType);
// drawImage can be long. Let's follow its progress,
// with the applet progress bar.
this.nbPixelsTotal = this.scaledNonRotatedWidth
* this.scaledNonRotatedHeight;
this.nbPixelsRead = 0;
// Let's draw the picture: this code do the rescaling.
this.uploadPolicy.displayDebug(
"getBufferedImage: Before drawImage", 50);
tempBufferedImage.getGraphics().drawImage(img, 0, 0, this);
this.uploadPolicy.displayDebug(
"getBufferedImage: After drawImage", 50);
tempBufferedImage.flush();
img.flush();
img = null;
PictureFileData
.freeMemory("ImageHelper.getBufferedImage()",
this.uploadPolicy);
// tempBufferedImage contains the rescaled picture. It's
// the source image for the next step (rotation).
sourceBufferedImage = tempBufferedImage;
tempBufferedImage = null;
} else {
// 'low' quality
//
// The scale method adds scaling before current
// transformation.
this.uploadPolicy
.displayDebug(
"getBufferedImage: Resizing picture(using standard quality picture)",
50);
transform.scale(this.scale, this.scale);
}
}
if (transform.isIdentity()) {
returnedBufferedImage = sourceBufferedImage;
} else {
AffineTransformOp affineTransformOp = null;
// Pictures are Ok.
affineTransformOp = new AffineTransformOp(transform,
AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
returnedBufferedImage = affineTransformOp
.createCompatibleDestImage(sourceBufferedImage, null);
// Checks, after the fact the pictures produces by the Canon
// EOS 30D are not properly resized: colors are 'strange'
// after resizing.
this.uploadPolicy.displayDebug(
"getBufferedImage: returnedBufferedImage.getColorModel(): "
+ sourceBufferedImage.getColorModel()
.toString(), 50);
this.uploadPolicy.displayDebug(
"getBufferedImage: returnedBufferedImage.getColorModel(): "
+ sourceBufferedImage.getColorModel()
.toString(), 50);
affineTransformOp.filter(sourceBufferedImage,
returnedBufferedImage);
affineTransformOp = null;
returnedBufferedImage.flush();
}
} catch (Exception e) {
throw new JUploadException(e.getClass().getName() + " ("
+ this.getClass().getName() + ".getBufferedImage()) : "
+ e.getMessage());
}
if (returnedBufferedImage != null
&& this.uploadPolicy.getDebugLevel() >= 50) {
this.uploadPolicy.displayDebug("getBufferedImage: "
+ returnedBufferedImage, 50);
this.uploadPolicy.displayDebug("getBufferedImage: MinX="
+ returnedBufferedImage.getMinX(), 50);
this.uploadPolicy.displayDebug("getBufferedImage: MinY="
+ returnedBufferedImage.getMinY(), 50);
}
this.uploadPolicy.displayDebug("getBufferedImage: was "
+ (System.currentTimeMillis() - msGetBufferedImage)
+ " ms long", 50);
PictureFileData.freeMemory("ImageHelper.getBufferedImage()",
this.uploadPolicy);
return returnedBufferedImage;
}
/**
* This method is a work in progress
*
* @param highquality
* @param sourceBufferedImage
* @return The calculated BufferedImage, resized and rotated according to
* the current configuration.
* @throws JUploadException
*/
BufferedImage getBufferedImage2(boolean highquality,
BufferedImage sourceBufferedImage) throws JUploadException {
long msGetBufferedImage = System.currentTimeMillis();
BufferedImage dest = null;
// Scale factor calculation
this.uploadPolicy.displayDebug("getBufferedImage: quarter: "
+ this.quarterRotation, 50);
// quarterRotation is one of 0, 1, 2, 3 : see addRotation.
@SuppressWarnings("unused")
int maxWidthBeforeRotation, maxHeigthBeforeRotation, widthBeforeRotation, heigthBeforeRotation, widthAfterRotation, heigthAfterRotation;
@SuppressWarnings("unused")
double theta = Math.toRadians(90 * this.quarterRotation);
switch (this.quarterRotation) {
case 0:
case 2:
maxWidthBeforeRotation = this.uploadPolicy.getMaxWidth();
maxHeigthBeforeRotation = this.uploadPolicy.getMaxHeight();
widthBeforeRotation = sourceBufferedImage.getWidth();
heigthBeforeRotation = sourceBufferedImage.getHeight();
widthAfterRotation = sourceBufferedImage.getWidth();
heigthAfterRotation = sourceBufferedImage.getHeight();
break;
case 1:
case 3:
maxWidthBeforeRotation = this.uploadPolicy.getMaxHeight();
maxHeigthBeforeRotation = this.uploadPolicy.getMaxWidth();
widthBeforeRotation = sourceBufferedImage.getHeight();
heigthBeforeRotation = sourceBufferedImage.getWidth();
widthAfterRotation = sourceBufferedImage.getHeight();
heigthAfterRotation = sourceBufferedImage.getWidth();
break;
default:
throw new JUploadException("Invalid quarter rotation: <"
+ this.quarterRotation + ">");
}
double scaleWidthBeforeRotation = widthBeforeRotation
/ maxWidthBeforeRotation;
double scaleHeigthBeforeRotation = heigthBeforeRotation
/ maxHeigthBeforeRotation;
double scale = Math.min(scaleWidthBeforeRotation,
scaleHeigthBeforeRotation);
// First: we scale the picture... if necessary.
@SuppressWarnings("unused")
Image scaledPicture = sourceBufferedImage;
if (scale < 1) {
int targetWidthBeforeRotation, targetHeigthBeforeRotation;
if (scaleWidthBeforeRotation < scaleHeigthBeforeRotation) {
// The constraint is on the width.
targetWidthBeforeRotation = maxWidthBeforeRotation;
targetHeigthBeforeRotation = (int) (heigthBeforeRotation * scale);
} else {
// The constraint is on the heigth
targetHeigthBeforeRotation = maxHeigthBeforeRotation;
targetWidthBeforeRotation = (int) (widthBeforeRotation * scale);
}
int scale_xxx = highquality ? Image.SCALE_SMOOTH : Image.SCALE_FAST;
scaledPicture = sourceBufferedImage.getScaledInstance(
targetWidthBeforeRotation, targetHeigthBeforeRotation,
scale_xxx);
}// if (scale < 1)
// Then, rotation of the scaled picture.
if (this.quarterRotation != 0) {
@SuppressWarnings("unused")
AffineTransform rotationTransform;
switch (this.quarterRotation) {
case 0:
case 2:
maxWidthBeforeRotation = this.uploadPolicy.getMaxWidth();
maxHeigthBeforeRotation = this.uploadPolicy.getMaxHeight();
widthBeforeRotation = sourceBufferedImage.getWidth();
heigthBeforeRotation = sourceBufferedImage.getHeight();
widthAfterRotation = sourceBufferedImage.getWidth();
heigthAfterRotation = sourceBufferedImage.getHeight();
break;
case 1:
case 3:
maxWidthBeforeRotation = this.uploadPolicy.getMaxHeight();
maxHeigthBeforeRotation = this.uploadPolicy.getMaxWidth();
widthBeforeRotation = sourceBufferedImage.getHeight();
heigthBeforeRotation = sourceBufferedImage.getWidth();
widthAfterRotation = sourceBufferedImage.getHeight();
heigthAfterRotation = sourceBufferedImage.getWidth();
break;
default:
throw new JUploadException("Invalid quarter rotation: <"
+ this.quarterRotation + ">");
}
// TODO finish this new version
// dest = new BufferedImage(widthAfterRotation, heigthAfterRotation,
// BufferedImage.TYPE_BYTE_INDEXED,sourceBufferedImage.getColorModel())
// It's finished !
this.uploadPolicy.displayDebug("getBufferedImage: was "
+ (System.currentTimeMillis() - msGetBufferedImage)
+ " ms long", 50);
}
return dest;
}
/**
* Implementation of the ImageObserver interface. Used to follow the
* drawImage progression, and update the applet progress bar.
*
* @param img
* @param infoflags
* @param x
* @param y
* @param width
* @param height
* @return Whether or not the work must go on.
*
*/
public boolean imageUpdate(Image img, int infoflags, int x, int y,
int width, int height) {
if ((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) {
this.progressBarBaseValue = this.uploadPolicy.getContext()
.getUploadPanel().getPreparationProgressBar().getValue();
this.uploadPolicy.displayDebug(
" imageUpdate (start of), progressBar geValue: "
+ this.progressBarBaseValue, 50);
int max = this.uploadPolicy.getContext().getUploadPanel()
.getPreparationProgressBar().getMaximum();
this.uploadPolicy
.displayDebug(
" imageUpdate (start of), progressBar maximum: "
+ max, 50);
} else if ((infoflags & ImageObserver.SOMEBITS) == ImageObserver.SOMEBITS) {
this.nbPixelsRead += width * height;
int percentage = (int) ((long) this.nbPixelsRead * 100 / this.nbPixelsTotal);
this.uploadPolicy.getContext().getUploadPanel()
.getPreparationProgressBar().setValue(
this.progressBarBaseValue + percentage);
// TODO: drawImage in another thread, to allow repaint of the
// progress bar ?
// Current status: the progress bar is only updated ... when
// draImage returns, that is: when everything is finished. NO
// interest.
this.uploadPolicy.getContext().getUploadPanel()
.getPreparationProgressBar().repaint();
} else if ((infoflags & ImageObserver.ALLBITS) == ImageObserver.ALLBITS) {
this.uploadPolicy.displayDebug(
" imageUpdate, total number of pixels: "
+ this.nbPixelsRead + " read", 50);
}
// We want to go on, after these bits
return true;
}
}