/** * Picture.java * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org * * 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 3 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, see . */ package org.expeditee.items; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.expeditee.core.Clip; import org.expeditee.core.Colour; import org.expeditee.core.Dimension; import org.expeditee.core.EnforcedClipStack.EnforcedClipKey; import org.expeditee.core.Image; import org.expeditee.core.Point; import org.expeditee.core.Stroke; import org.expeditee.core.bounds.AxisAlignedBoxBounds; import org.expeditee.core.bounds.CombinationBoxBounds; import org.expeditee.core.bounds.PolygonBounds; import org.expeditee.gio.EcosystemManager; import org.expeditee.gio.GraphicsManager; import org.expeditee.gui.DisplayController; import org.expeditee.gui.FrameGraphics; import org.expeditee.gui.FrameIO; import org.expeditee.gui.FrameUtils; /** * 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 float CROPPING_COMPOSITE_ALPHA = 0.5f; 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; protected Integer _anchorLeft = null; protected Integer _anchorTop = null; private String _path = ""; private String _size = ""; private String _fileName = null; protected Picture(Text source, Image image) { super(source); _image = image; refresh(); if (_image != null) { // Should parsing for minus options also be done? // To be honest, looking at the code, can't really see how _size can be anything but // the empty string at this stage of calling the constructor, although it does have // the 'side' effect of setting other things (such as _start, _end, and _scale) 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) { super(source); _fileName = fileName; _path = path; String size_without_options = parseMinusOptions(size); _size = size_without_options; refresh(); parseSize(); } protected String getImageSize() { return _size; } protected String parseMinusOptions(String cmd_line) { String[] tokens = Text.parseArgsApache(cmd_line); // make anything starting with a '-' lowercase for (int i=0; i 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(); _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(Point p) { if (p != null) setStartCrop(p.getX(), p.getY()); } public void setStartCrop(int x, int y) { invalidateCroppedArea(); _cropStart = new Point(x - getX(), y - getY()); invalidateCroppedArea(); } public void setEndCrop(Point p) { if (p != null) setEndCrop(p.getX(), p.getY()); } 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.getX() - _highlightThickness; int startY = getY() + topLeft.getY() - _highlightThickness; int border = 2 * _highlightThickness; // TODO: Why invalidate specific area just before invalidateAll? cts16 invalidate(new AxisAlignedBoxBounds(startX, startY, bottomRight.getX() - topLeft.getX() + 2 * border, bottomRight.getY() - topLeft.getY() + 2 * border)); invalidateAll(); } else { invalidateAll(); } } public Point getTopLeftCrop() { return new Point(Math.min(_cropStart.getX(), _cropEnd.getX()), Math.min( _cropStart.getY(), _cropEnd.getY())); } public Point getBottomRightCrop() { return new Point(Math.max(_cropStart.getX(), _cropEnd.getX()), Math.max(_cropStart.getY(), _cropEnd.getY())); } public void setShowCrop(boolean value) { // invalidateCroppedArea(); _showCropping = value; invalidateCroppedArea(); } public boolean isBeingCropped() { return (_cropStart != null && _cropEnd != null); } public boolean isCropTooSmall() { if (!isBeingCropped()) return true; int cropWidth = Math.abs(_cropEnd.getX() - _cropStart.getX()); int cropHeight = Math.abs(_cropEnd.getY() - _cropStart.getY()); return cropWidth < MINIMUM_WIDTH || cropHeight < MINIMUM_WIDTH; } public void clearCropping() { invalidateCroppedArea(); _cropStart = null; _cropEnd = null; setShowCrop(false); } public PolygonBounds updateBounds() { if (_image == null) { refresh(); parseSize(); } Point[] ori = new Point[4]; Point centre = new Point(); int base_x = (_anchorLeft!=null) ? _anchorLeft : _source.getX(); int base_y = (_anchorTop!=null) ? _anchorTop : _source.getY(); if (_cropStart == null || _cropEnd == null) { int width = getWidth(); int height = getHeight(); centre.setX(base_x + width / 2); centre.setY(base_y + 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(base_x + 1 + xdiff, base_y - 1); ori[1] = new Point(base_x + width, base_y - 1); ori[2] = new Point(base_x + width, base_y + height); ori[3] = new Point(base_x + 1 + xdiff, base_y + height); } else { Point topLeft = getTopLeftCrop(); Point bottomRight = getBottomRightCrop(); centre.setX(base_x + (bottomRight.getX() - topLeft.getX()) / 2); centre.setY(base_y + (bottomRight.getY() - topLeft.getY()) / 2); AxisAlignedBoxBounds clip = new AxisAlignedBoxBounds(topLeft.getX() + base_x, topLeft.getY() + base_y, bottomRight.getX() - topLeft.getX(), bottomRight.getY() - topLeft.getY()); // _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); } PolygonBounds poly = new PolygonBounds(); for (Point p : ori) { poly.addPoint(p); } poly.rotate(Math.PI * _rotate / 180, centre); return poly.close(); } @Override public double getEnclosedArea() { return getWidth() * getHeight(); } @Override public void setWidth(Integer width) { _scale = width * 1F / (_end.getX() - _start.getX()); } 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() { if (DisplayController.isAudienceMode()) return; super.paintLink(); } /** * Paint the image repeatedly tiled over the drawing area. */ public void paintImageTiling() { if (_image == null) return; int iw = _image.getWidth(); int ih = _image.getHeight(); if(iw <= 0 || ih <= 0) return; int base_x = (_anchorLeft != null) ? _anchorLeft : _source.getX(); int base_y = (_anchorTop != null) ? _anchorTop : _source.getY(); int dX1 = base_x; int dY1 = base_y; int dX2 = base_x + getWidth(); int dY2 = base_y + getHeight(); Image tmp = Image.createImage(getWidth(), getHeight()); EcosystemManager.getGraphicsManager().pushDrawingSurface(tmp); int offX = (tmp.getWidth() - getWidth()) / 2; int offY = (tmp.getHeight() - getHeight()) / 2; int cropStartX = _start.getX(); int cropEndX = _end.getX(); 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.getY(); int cropEndY = _end.getY(); 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; Point topLeft = new Point(x - dX1 + offX, y - dY1 + offY); Dimension size = new Dimension(endX - x, endY - y); Point cropTopLeft = new Point(sx, sy); Dimension cropSize = new Dimension(ex - sx, ey - sy); if (cropSize.width > 0 && cropSize.height > 0) { EcosystemManager.getGraphicsManager().drawImage(_image, topLeft, size, 0.0, cropTopLeft, cropSize); } cropStartY = 0; cropEndY = ih; y = endY; } cropStartX = 0; cropEndX = iw; x = endX; } EcosystemManager.getGraphicsManager().popDrawingSurface(); EcosystemManager.getGraphicsManager().drawImage(tmp, new Point(dX1, dY1), null, Math.PI * _rotate / 180); tmp.releaseImage(); } @Override public void paint() { if (_image == null) return; paintLink(); GraphicsManager g = EcosystemManager.getGraphicsManager(); // if we are showing the cropping if (_showCropping && !isCropTooSmall()) { // show the uncropped area as transparent g.setCompositeAlpha(CROPPING_COMPOSITE_ALPHA); paintImageTiling(); g.setCompositeAlpha(1.0f); // show the cropped area normally Point topLeft = getTopLeftCrop(); Point bottomRight = getBottomRightCrop(); int base_x = (_anchorLeft != null) ? _anchorLeft : _source.getX(); int base_y = (_anchorTop != null) ? _anchorTop : _source.getY(); Clip clip = new Clip(new AxisAlignedBoxBounds( base_x + topLeft.getX(), base_y + topLeft.getY(), bottomRight.getX() - topLeft.getX(), bottomRight.getY() - topLeft.getY())); EnforcedClipKey key = g.pushClip(clip); paintImageTiling(); g.popClip(key); // Draw an outline for the crop selection box g.drawRectangle(clip.getBounds(), 0.0, null, getPaintHighlightColor(), HIGHLIGHT_STROKE, null); // otherwise, paint normally } else { paintImageTiling(); } PolygonBounds poly = (PolygonBounds) getBounds(); if (hasVisibleBorder()) { Stroke borderStroke = new Stroke(getThickness(), DEFAULT_CAP, DEFAULT_JOIN); g.drawPolygon(poly, null, null, 0.0, null, getPaintBorderColor(), borderStroke); } if (isHighlighted()) { Stroke borderStroke = new Stroke(1, DEFAULT_CAP, DEFAULT_JOIN); g.drawPolygon(poly, null, null, 0.0, null, getHighlightColor(), borderStroke); } } @Override public Colour getHighlightColor() { if (_highlightColour.equals(getBorderColor())) return ALTERNATE_HIGHLIGHT; return _highlightColour; } protected Picture createPicture() { return ItemUtils.CreatePicture((Text) _source.copy()); } @Override public Picture copy() { Picture p = createPicture(); p._image = _image; p._highlightMode = _highlightMode; // 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.getX() / _scale) + _start.getX(); int startY = Math.round(topLeft.getY() / _scale) + _start.getY(); int endX = Math.round(bottomRight.getX() / _scale + _start.getX()); int endY = Math.round(bottomRight.getY() / _scale + _start.getY()); int width = _image.getWidth(); int height = _image.getHeight(); // 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); int base_x = (_anchorLeft!=null) ? _anchorLeft : _source.getX(); int base_y = (_anchorTop!=null) ? _anchorTop : _source.getY(); p._source.setPosition(topLeft.getX() + base_x, topLeft.getY() + base_y); } 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.invalidateBounds(); 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(); int ih = _image.getHeight(); if(iw > 0 || ih > 0) { while(_start.getX() >= iw) { _start.setX(_start.getX() - iw); _end.setX(_end.getX() - iw); } while(_start.getY() >= ih) { _start.setY(_start.getY() - ih); _end.setY(_end.getY() - ih); } while(_start.getX() < 0) { _start.setX(_start.getX() + iw); _end.setX(_end.getX() + iw); } while(_start.getY() < 0) { _start.setY(_start.getY() + ih); _end.setY(_end.getY() + 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(EcosystemManager.getInputManager().getCursorPosition(), multiplier); } updateSource(); invalidateBounds(); // 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 Image.createImageAsCroppedCopy(_image, _start.getX(), _start.getY(), getUnscaledWidth(), getUnscaledHeight()); } public int getUnscaledWidth() { return _end.getX() - _start.getX(); } public int getUnscaledHeight() { return _end.getY() - _start.getY(); } /** * @return true if this is a cropped image. */ public boolean isCropped() { return (_end.getX() != 0 && _end.getX() != _image.getWidth()) || (_end.getY() != 0 && _end.getY() != _image.getHeight()) || _start.getY() != 0 || _start.getX() != 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() <= 0) { try { _image = ImageIO.read(new File(_path)); } catch (IOException e) { // e.printStackTrace(); Logger.Log(e); _image = null; return false; } } */ _image = Image.getImage(_path); 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 !DisplayController.isAudienceMode() && _source.getLinkMark(); } @Override public boolean getActionMark() { return _source.getActionMark(); } @Override public String getName() { return _fileName; } public String getPath() { return _path; } /** * Copies the image to the default images folder and updates the reference to it in Expeditee * Used for correcting image references for FrameShare */ public void moveToImagesFolder() { File f = new File(getPath()); // if the file is not in the default images folder, copy it there if(! f.getParentFile().equals(new File(FrameIO.IMAGES_PATH))) { try { File f2 = new File(FrameIO.IMAGES_PATH + f.getName()); FrameUtils.copyFile(f, f2, false); f = f2; } catch (IOException e) { e.printStackTrace(); f = null; } } _path = f.getPath(); _fileName = f.getName(); updateSource(); } 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.getX() > 0 || _start.getY() > 0 || _end.getX() != _image.getWidth() || _end.getY() != _image.getHeight()) { newText.append(" ").append(_start.getX()).append(" ").append(_start.getY()); newText.append(" ").append(_end.getX()).append(" ").append(_end.getY()); } 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(Point origin, double ratio) { _scale *= ratio; updateSource(); super.translate(origin, ratio); } @Override public AxisAlignedBoxBounds getDrawingArea() { AxisAlignedBoxBounds da = super.getDrawingArea(); if (getLink() != null || hasAction()) { AxisAlignedBoxBounds linkBounds = AxisAlignedBoxBounds.getEnclosing(getLinkBounds()); linkBounds.getTopLeft().add(getX() - LEFT_MARGIN, getY() + getLinkYOffset()); linkBounds.getSize().width += 2; linkBounds.getSize().height += 2; da.combineWith(linkBounds); } 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(); invalidateBounds(); } public double getRotate() { return _rotate; } public boolean MouseOverBackgroundPixel(int mouseX, int mouseY, Colour bg_col) { int base_x = (_anchorLeft!=null) ? _anchorLeft : _source.getX(); int base_y = (_anchorTop!=null) ? _anchorTop : _source.getY(); int x = mouseX - base_x; int y = mouseY - base_y; Colour c = _image.getPixel(x, y); int c_red = c.getRed255(); int c_green = c.getGreen255(); int c_blue = c.getBlue255(); int bg_red = (bg_col!=null) ? bg_col.getRed255() : 0xff; int bg_green = (bg_col!=null) ? bg_col.getGreen255() : 0xff; int bg_blue = (bg_col!=null) ? bg_col.getBlue255() : 0xff; int red_diff = Math.abs(c_red - bg_red); int green_diff = Math.abs(c_green - bg_green); int blue_diff = Math.abs(c_blue - bg_blue); return ((red_diff<=2) && (green_diff<=2) && (blue_diff<=2)); } }