package org.expeditee.items;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import org.expeditee.gui.FrameGraphics;
import org.expeditee.gui.FrameMouseActions;
import org.expeditee.stats.Logger;
/**
* This class represents an Image loaded from a file which is shown on the
* screen. Loading of the Image from disk occurs in the constructor, and takes
* approximately one second per mb of the Image file size.
*
* Currently Supported (Tested) Image formats:
* BMP
* JPG
* GIF
* GIF (Animated)
*
* Currently only the default size of the Image is supported, but future
* versions may support scaling.
*
* @author jdm18
*
*/
public class Picture extends XRayable {
private static final int MINIMUM_WIDTH = 10;
public static final int WIDTH = 0;
public static final int RATIO = 1;
protected Image _image = null;
private int _scaleType = RATIO;
private float _scale = 1.0f;
// Start of the crop relative to START
private Point _cropStart = null;
// Start of the crop relative to END
private Point _cropEnd = null;
private Point _start = new Point(0, 0);
private Point _end = new Point(0, 0);
private double _rotate = 0;
private boolean _flipX = false;
private boolean _flipY = false;
private boolean _showCropping = false;
private String _path = "";
private String _size = "";
private String _fileName = null;
// used to repaint animated GIF images, among other things.
protected ImageObserver _imageObserver = null;
protected Picture(Text source, ImageObserver observer, Image image) {
super(source);
_imageObserver = observer;
_image = image;
refresh();
if (_image != null)
parseSize();
}
/**
* Creates a new Picture from the given path. The ImageObserver is optional
* and can be set to NULL.
* Note: It is assumed that the file described in path has already been
* checked to exist.
*
* @param source
* The Text Item that was used to create this Picture
* @param fileName
* the name of the file as it should be displayed in the source
* text
* @param path
* The Path of the Image to load from disk.
* @param observer
* The ImageObserver to assign when painting the Image on the
* screen.
*/
public Picture(Text source, String fileName, String path, String size,
ImageObserver observer) {
super(source);
_imageObserver = observer;
_fileName = fileName;
_path = path;
_size = size;
refresh();
parseSize();
}
protected String getImageSize() {
return _size;
}
protected void parseSize() {
String size = getImageSize();
if (_end.x != 0 || _end.y != 0)
return;
// set the default values for start and end
_start.setLocation(0, 0);
if (_image == null)
_end.setLocation(0, 0);
else
_end.setLocation(_image.getWidth(null), _image.getHeight(null));
size = size.trim();
String sizeLower = size.toLowerCase();
String[] values = size.split("\\s+");
// Now get the cropping values if there are any
try {
if (values.length > 2) {
int startX = Integer.parseInt(values[1]);
int startY = Integer.parseInt(values[2]);
_start = new Point(startX, startY);
if (values.length > 4) {
int endX = Integer.parseInt(values[3]);
int endY = Integer.parseInt(values[4]);
_end = new Point(endX, endY);
}
scaleCrop();
}
} catch (Exception e) {
}
if(sizeLower.contains("flipx")) {
_flipX = true;
}
if(sizeLower.contains("flipy")) {
_flipY = true;
}
int index = sizeLower.indexOf("rotation=");
if(index != -1) {
int tmp = sizeLower.indexOf(" ", index);
String rotation;
if(tmp == -1) {
rotation = sizeLower.substring(index + "rotation=".length());
} else {
rotation = sizeLower.substring(index + "rotation=".length(), index + tmp);
}
_rotate = Double.parseDouble(rotation);
}
try {
if (size.length() == 0) {
size = "" + _image.getWidth(null);
_source.setText(getTagText() + size);
return;
}
size = values[0];
// parse width or ratio from text
if (size.contains(".")) {
// this is a ratio
_scale = Float.parseFloat(size);
_scaleType = RATIO;
} else if (size.length() > 0) {
// this is an absolute width
int width = Integer.parseInt(size);
_scaleType = WIDTH;
setWidth(width);
}
} catch (Exception e) {
_scale = 1F;
}
}
public void setStartCrop(int x, int y) {
invalidateCroppedArea();
_cropStart = new Point(x - getX(), y - getY());
invalidateCroppedArea();
}
public void setEndCrop(int x, int y) {
invalidateCroppedArea();
_cropEnd = new Point(x - getX(), y - getY());
invalidateCroppedArea();
}
private void invalidateCroppedArea() {
if (_cropStart != null && _cropEnd != null) {
Point topLeft = getTopLeftCrop();
Point bottomRight = getBottomRightCrop();
int startX = getX() + topLeft.x - _highlightThickness;
int startY = getY() + topLeft.y - _highlightThickness;
int border = 2 * _highlightThickness;
invalidate(new Rectangle(startX, startY, bottomRight.x - topLeft.x
+ 2 * border, bottomRight.y - topLeft.y + 2 * border));
invalidateAll();
} else {
invalidateAll();
}
}
public Point getTopLeftCrop() {
return new Point(Math.min(_cropStart.x, _cropEnd.x), Math.min(
_cropStart.y, _cropEnd.y));
}
public Point getBottomRightCrop() {
return new Point(Math.max(_cropStart.x, _cropEnd.x), Math.max(
_cropStart.y, _cropEnd.y));
}
public void setShowCrop(boolean value) {
// invalidateCroppedArea();
_showCropping = value;
invalidateCroppedArea();
}
public boolean isCropTooSmall() {
if (_cropStart == null || _cropEnd == null)
return true;
int cropWidth = Math.abs(_cropEnd.x - _cropStart.x);
int cropHeight = Math.abs(_cropEnd.y - _cropStart.y);
return cropWidth < MINIMUM_WIDTH || cropHeight < MINIMUM_WIDTH;
}
public void clearCropping() {
invalidateCroppedArea();
_cropStart = null;
_cropEnd = null;
setShowCrop(false);
}
public void updatePolygon() {
if (_image == null) {
refresh();
parseSize();
}
Point[] ori = new Point[4];
Point2D[] rot = new Point2D[4];
Point centre = new Point();
if (_cropStart == null || _cropEnd == null) {
int width = getWidth();
int height = getHeight();
centre.x = _source.getX() + width / 2;
centre.y = _source.getY() + height / 2;
int xdiff = -MARGIN_RIGHT; // -getLeftMargin();
// extra pixel around the image so the highlighting is visible
// _poly.addPoint(_source.getX() + 1 + xdiff, _source.getY() - 1);
// _poly.addPoint(_source.getX() + width, _source.getY() - 1);
// _poly.addPoint(_source.getX() + width, _source.getY() + height);
// _poly.addPoint(_source.getX() + 1 + xdiff, _source.getY() + height);
ori[0] = new Point(_source.getX() + 1 + xdiff, _source.getY() - 1);
ori[1] = new Point(_source.getX() + width, _source.getY() - 1);
ori[2] = new Point(_source.getX() + width, _source.getY() + height);
ori[3] = new Point(_source.getX() + 1 + xdiff, _source.getY() + height);
} else {
Point topLeft = getTopLeftCrop();
Point bottomRight = getBottomRightCrop();
centre.x = _source.getX() + (bottomRight.x - topLeft.x) / 2;
centre.y = _source.getY() + (bottomRight.y - topLeft.y) / 2;
Rectangle clip = new Rectangle(topLeft.x + _source.getX(),
topLeft.y + _source.getY(), bottomRight.x - topLeft.x,
bottomRight.y - topLeft.y).getBounds();
// _poly.addPoint((int) clip.getMinX() - 1, (int) clip.getMinY() - 1);
// _poly.addPoint((int) clip.getMinX() - 1, (int) clip.getMaxY());
// _poly.addPoint((int) clip.getMaxX(), (int) clip.getMaxY());
// _poly.addPoint((int) clip.getMaxX(), (int) clip.getMinY() - 1);
ori[0] = new Point((int) clip.getMinX() - 1, (int) clip.getMinY() - 1);
ori[1] = new Point((int) clip.getMinX() - 1, (int) clip.getMaxY());
ori[2] = new Point((int) clip.getMaxX(), (int) clip.getMaxY());
ori[3] = new Point((int) clip.getMaxX(), (int) clip.getMinY() - 1);
}
AffineTransform.getRotateInstance(Math.PI * _rotate / 180, centre.x, centre.y).transform(ori, 0, rot, 0, 4);
_poly = new Polygon();
for(Point2D p : rot) {
_poly.addPoint((int)p.getX(), (int)p.getY());
}
}
@Override
public double getEnclosedArea() {
return getWidth() * getHeight();
}
@Override
public void setWidth(Integer width) {
_scale = width * 1F / (_end.x - _start.x);
}
public Point getStart() {
return _start;
}
public Point getEnd() {
return _end;
}
/**
* Gets the width with which the picture is displayed on the screen.
*/
@Override
public Integer getWidth() {
return Math.round(getUnscaledWidth() * _scale);
}
/**
* Gets the height with which the picture is displayed on the screen.
*/
@Override
public int getHeight() {
return Math.round(getUnscaledHeight() * _scale);
}
/**
* Dont paint links in audience mode for images.
*/
@Override
protected void paintLink(Graphics2D g) {
if (FrameGraphics.isAudienceMode())
return;
super.paintLink(g);
}
public void paintImageTiling(Graphics2D g) {
if (_image == null) {
return;
}
int iw = _image.getWidth(null);
int ih = _image.getHeight(null);
if(iw <= 0 || ih <= 0) {
return;
}
int dX1 = _source.getX();
int dY1 = _source.getY();
int dX2 = _source.getX() + getWidth();
int dY2 = _source.getY() + getHeight();
BufferedImage tmp = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = tmp.createGraphics();
int offX = (tmp.getWidth() - getWidth()) / 2;
int offY = (tmp.getHeight() - getHeight()) / 2;
// g2d.rotate(rotate, tmp.getWidth() / 2, tmp.getHeight() / 2);
int cropStartX = _start.x;
int cropEndX = _end.x;
if(cropEndX > iw) {
cropEndX = iw;
}
for(int x = dX1; x < dX2; ) {
// end - start = (cropEnd - cropStart) * scale
// => cropEnd = cropStart + (end - start) / scale
int w = (int) ((cropEndX - cropStartX) * _scale);
int endX = x + w;
if(endX > dX2) {
endX = dX2;
cropEndX = cropStartX + (int) ((dX2 - x) / _scale);
}
int cropStartY = _start.y;
int cropEndY = _end.y;
if(cropEndY > ih) {
cropEndY = ih;
}
for(int y = dY1; y < dY2; ) {
int h = (int) ((cropEndY - cropStartY) * _scale);
int endY = y + h;
if(endY > dY2) {
endY = dY2;
cropEndY = cropStartY + (int) ((dY2 - y) / _scale);
}
int sx = _flipX ? cropEndX : cropStartX;
int ex = _flipX ? cropStartX : cropEndX;
int sy = _flipY ? cropEndY : cropStartY;
int ey = _flipY ? cropStartY : cropEndY;
g2d.drawImage(_image, x - dX1 + offX, y - dY1 + offY, endX - dX1 + offX, endY - dY1 + offY, sx, sy, ex, ey, null);
cropStartY = 0;
cropEndY = ih;
y = endY;
}
cropStartX = 0;
cropEndX = iw;
x = endX;
}
AffineTransform at = new AffineTransform();
at.translate(dX1, dY1);
at.rotate(Math.PI * _rotate / 180, tmp.getWidth() / 2, tmp.getHeight() / 2);
g.drawImage(tmp, at, _imageObserver);
// g.drawImage(tmp, dX1, dY1, dX2, dY2, 0, 0, tmp.getWidth(), tmp.getHeight(), _imageObserver);
}
@Override
public void paint(Graphics2D g) {
if (_image == null)
return;
paintLink(g);
// if we are showing the cropping, then show the original as transparent
if (_showCropping && !isCropTooSmall()) {
// show the full image as transparent
float alpha = .5f;
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha));
paintImageTiling(g);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1.0f));
// show the cropped area normally
Point topLeft = getTopLeftCrop();
Point bottomRight = getBottomRightCrop();
Shape clip = new Rectangle(_source.getX() + topLeft.x, _source.getY() + topLeft.y,
bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
g.setColor(getPaintHighlightColor());
g.draw(clip);
g.setClip(clip);
paintImageTiling(g);
g.draw(clip);
// if the image is cropped, but we are not showing the cropping
// otherwise, paint normally
} else {
paintImageTiling(g);
}
if (hasVisibleBorder()) {
g.setColor(getPaintBorderColor());
Stroke borderStroke = new BasicStroke(getThickness(), CAP, JOIN);
g.setStroke(borderStroke);
g.drawPolygon(getPolygon());
}
if (isHighlighted()) {
Stroke borderStroke = new BasicStroke(1, CAP, JOIN);
g.setStroke(borderStroke);
g.setColor(getHighlightColor());
g.drawPolygon(getPolygon());
}
//System.out.print("p_");
}
@Override
public Color getHighlightColor() {
if (_highlightColor.equals(getBorderColor()))
return ALTERNATE_HIGHLIGHT;
return _highlightColor;
}
@Override
public int setHighlightColor() {
super.setHighlightColor();
return Item.DEFAULT_CURSOR;
}
protected Picture createPicture() {
return ItemUtils.CreatePicture((Text) _source.copy(), _imageObserver);
}
@Override
public Picture copy() {
Picture p = createPicture();
p._image = _image;
p._mode = _mode;
// Doing Duplicate item duplicates link mark which we dont want to do
// when in audience mode because the linkMark will be copied incorrectly
// Get all properties from the source
if (!isCropTooSmall() && _cropStart != null && _cropEnd != null) {
assert (_cropEnd != null);
// make the start be the top left
// make the end be the bottom right
Point topLeft = getTopLeftCrop();
Point bottomRight = getBottomRightCrop();
int startX = Math.round(topLeft.x / _scale) + _start.x;
int startY = Math.round(topLeft.y / _scale) + _start.y;
int endX = Math.round(bottomRight.x / _scale + _start.x);
int endY = Math.round(bottomRight.y / _scale + _start.y);
int width = _image.getWidth(null);
int height = _image.getHeight(null);
// adjust our start and end if the user has dragged outside of the
// shape
if (endX > width) {
endX = width;
}
if (endY > height) {
endY = height;
}
if (startX < 0) {
startX = 0;
}
if (startY < 0) {
startY = 0;
}
p._start = new Point(startX, startY);
p._end = new Point(endX, endY);
p._source.setPosition(topLeft.x + _source.getX(), topLeft.y
+ _source.getY());
} else {
p._start = new Point(_start);
p._end = new Point(_end);
}
p._scale = _scale;
p._scaleType = _scaleType;
p._path = _path;
p._fileName = _fileName;
p.updateSource();
p.updatePolygon();
return p;
}
public float getScale() {
return _scale;
}
public void setScale(float scale) {
_scale = scale;
}
public void scaleCrop() {
// scale crop values to within image bounds
int iw = _image.getWidth(null);
int ih = _image.getHeight(null);
if(iw > 0 || ih > 0) {
while(_start.x >= iw) {
_start.x -= iw;
_end.x -= iw;
}
while(_start.y >= ih) {
_start.y -= ih;
_end.y -= ih;
}
while(_start.x < 0) {
_start.x += iw;
_end.x += iw;
}
while(_start.y < 0) {
_start.y += ih;
_end.y += ih;
}
}
}
public void setCrop(int startX, int startY, int endX, int endY) {
_start = new Point(startX, startY);
_end = new Point(endX, endY);
updateSource();
}
@Override
public float getSize() {
return _source.getSize();
}
@Override
public void setSize(float size) {
float diff = size - _source.getSize();
float oldScale = _scale;
float multiplier = (1000F + diff * 40F) / 1000F;
_scale = _scale * multiplier;
// picture must still be at least XX pixels wide
if (getWidth() < MINIMUM_WIDTH) {
_scale = oldScale;
} else {
_source.translate(new Point2D.Float(FrameMouseActions.MouseX,
FrameMouseActions.MouseY), multiplier);
}
updateSource();
updatePolygon();
// Make sure items that are resized display the border
invalidateAll();
}
@Override
public void setAnnotation(boolean val) {
}
/**
* Returns the Image that this Picture object is painting on the screen.
* This is used by Frame to repaint animated GIFs.
*
* @return The Image that this Picture object represents.
*/
public Image getImage() {
return _image;
}
public Image getCroppedImage() {
if (_image == null)
return null;
if (!isCropped()) {
return _image;
}
return Toolkit.getDefaultToolkit().createImage(
new FilteredImageSource(_image.getSource(),
new CropImageFilter(_start.x, _start.y,
getUnscaledWidth(), getUnscaledHeight())));
}
public int getUnscaledWidth() {
return _end.x - _start.x;
}
public int getUnscaledHeight() {
return _end.y - _start.y;
}
/**
* @return true if this is a cropped image.
*/
public boolean isCropped() {
return (_end.x != 0 && _end.x != _image.getWidth(null)) || (_end.y != 0 && _end.y != _image.getHeight(null)) || _start.y != 0 || _start.x != 0;
}
@Override
public boolean refresh() {
// ImageIcon is faster, but cannot handle some formats
// (notably.bmp) hence, we try this first, then if it fails we try
// ImageIO
try {
_image = new ImageIcon(_path).getImage();
} catch (Exception e) {
}
// if ImageIcon failed to read the image
if (_image == null || _image.getWidth(null) <= 0) {
try {
_image = ImageIO.read(new File(_path));
} catch (IOException e) {
// e.printStackTrace();
Logger.Log(e);
_image = null;
return false;
}
}
return true;
}
@Override
protected int getLinkYOffset() {
return getBoundsHeight() / 2;
}
@Override
public void setLinkMark(boolean state) {
// TODO use the more efficient invalidiate method
// The commented code below is not quite working
// if(!state)
// invalidateCommonTrait(ItemAppearence.LinkChanged);
_source.setLinkMark(state);
// if(state)
// invalidateCommonTrait(ItemAppearence.LinkChanged);
invalidateAll();
}
@Override
public void setActionMark(boolean state) {
// if (!state)
// invalidateCommonTrait(ItemAppearence.LinkChanged);
_source.setActionMark(state);
// if (state)
// invalidateCommonTrait(ItemAppearence.LinkChanged);
invalidateAll();
}
@Override
public boolean getLinkMark() {
return !FrameGraphics.isAudienceMode() && _source.getLinkMark();
}
@Override
public boolean getActionMark() {
return _source.getActionMark();
}
@Override
public String getName() {
return _fileName;
}
protected String getTagText() {
return "@i: " + _fileName + " ";
}
/**
* Updates the source text for this item to match the current size of the
* image.
*
*/
private void updateSource() {
StringBuffer newText = new StringBuffer(getTagText());
switch (_scaleType) {
case (RATIO):
DecimalFormat format = new DecimalFormat("0.00");
newText.append(format.format(_scale));
break;
case (WIDTH):
newText.append(getWidth());
break;
}
scaleCrop();
// If the image is cropped add the position for the start and finish of
// the crop to the soure text
if (_start.x > 0 || _start.y > 0 || _end.x != _image.getWidth(null)
|| _end.y != _image.getHeight(null)) {
newText.append(" ").append(_start.x).append(" ").append(_start.y);
newText.append(" ").append(_end.x).append(" ").append(_end.y);
}
if(_flipX) {
newText.append(" flipX");
}
if(_flipY) {
newText.append(" flipY");
}
if(Double.compare(_rotate, 0) != 0) {
newText.append(" rotation=" + _rotate);
}
_source.setText(newText.toString());
}
@Override
public void translate(Point2D origin, double ratio) {
_scale *= ratio;
updateSource();
super.translate(origin, ratio);
}
@Override
public Rectangle[] getDrawingArea() {
Rectangle[] da = super.getDrawingArea();
if (getLink() != null || hasAction()) {
Rectangle[] da2 = new Rectangle[da.length + 1];
System.arraycopy(da, 0, da2, 0, da.length);
da2[da.length] = getLinkPoly().getBounds();
da2[da.length].translate(getX() - LEFT_MARGIN, getY()
+ getLinkYOffset());
da2[da.length].width += 2;
da2[da.length].height += 2;
da = da2;
}
return da;
}
@Override
public void scale(Float scale, int originX, int originY) {
setScale(getScale() * scale);
super.scale(scale, originX, originY);
}
public void setFlipX(boolean flip) {
_flipX = flip;
}
public void setFlipY(boolean flip) {
_flipY = flip;
}
public boolean getFlipX() {
return _flipX;
}
public boolean getFlipY() {
return _flipY;
}
public void setRotate(double rotate) {
_rotate = rotate;
updateSource();
updatePolygon();
}
public double getRotate() {
return _rotate;
}
}