/**
* 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.PolygonBounds;
import org.expeditee.encryption.core.EncryptedImage;
import org.expeditee.encryption.items.surrogates.Label;
import org.expeditee.encryption.items.surrogates.Label.LabelResult;
import org.expeditee.gio.EcosystemManager;
import org.expeditee.gio.GraphicsManager;
import org.expeditee.gui.DisplayController;
import org.expeditee.gui.FrameIO;
import org.expeditee.gui.FrameUtils;
import org.expeditee.gui.MessageBay;
/**
* 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 {
public static final String REDACTED_IMAGE_NAME = "expeditee_noise.encrypted";
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();
}
public boolean isNoise() {
return _fileName.equals(REDACTED_IMAGE_NAME);
}
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() {
if (isNoise()) {
_image = EncryptedImage.getNoise();
} else {
String encryptionLabel = _source.getEncryptionLabel();
if (encryptionLabel == null || encryptionLabel.isEmpty()) {
_image = Image.getImage(_path);
} else {
LabelResult result = Label.getLabel(encryptionLabel);
if (result == LabelResult.SuccessResolveLabelToKey) {
_image = EncryptedImage.getImage(_path, result.key);
} else {
MessageBay.displayMessage(result.toString());
_image = EncryptedImage.getNoise();
}
}
}
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));
}
}