/*
* Copyright 2017 Laszlo Balazs-Csiki
*
* This file is part of Pixelitor. Pixelitor is free software: you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License, version 3 as published by the Free
* Software Foundation.
*
* Pixelitor 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 Pixelitor. If not, see <http://www.gnu.org/licenses/>.
*/
package pixelitor.layers;
import pixelitor.ChangeReason;
import pixelitor.Composition;
import pixelitor.ConsistencyChecks;
import pixelitor.filters.comp.Flip;
import pixelitor.filters.comp.Rotate;
import pixelitor.gui.utils.Dialogs;
import pixelitor.history.ApplyLayerMaskEdit;
import pixelitor.history.ContentLayerMoveEdit;
import pixelitor.history.History;
import pixelitor.history.ImageEdit;
import pixelitor.history.PixelitorEdit;
import pixelitor.selection.IgnoreSelection;
import pixelitor.selection.Selection;
import pixelitor.tools.Tools;
import pixelitor.utils.ImageUtils;
import pixelitor.utils.Messages;
import pixelitor.utils.Utils;
import pixelitor.utils.VisibleForTesting;
import javax.swing.*;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RasterFormatException;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import static java.awt.RenderingHints.KEY_INTERPOLATION;
import static java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC;
import static java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
import static java.util.Objects.requireNonNull;
import static pixelitor.ChangeReason.REPEAT_LAST;
import static pixelitor.Composition.ImageChangeActions.FULL;
import static pixelitor.Composition.ImageChangeActions.INVALIDATE_CACHE;
import static pixelitor.Composition.ImageChangeActions.REPAINT;
import static pixelitor.filters.comp.Flip.Direction.HORIZONTAL;
import static pixelitor.layers.ImageLayer.State.NORMAL;
import static pixelitor.layers.ImageLayer.State.PREVIEW;
import static pixelitor.layers.ImageLayer.State.SHOW_ORIGINAL;
/**
* An image layer.
* <p>
* Filters without dialog are executed as ChangeReason.OP_WITHOUT_DIALOG on the EDT.
* The filter asks getFilterSource() in the NORMAL state, and
* (if there is no selection) the image (not a copy!) is returned as the filter source.
* The filter transforms the image, and calls filterWithoutDialogFinished
* with the transformed image.
* <p>
* Filters with dialog are executed as ChangeReason.OP_PREVIEW on the EDT.
* startPreviewing() is called when a new dialog appears,
* right before creating the adjustment panel.
* Before executing a filter with dialog, startNewPreviewFromDialog() is called
* (does nothing in the current implementation), then the filter is executed,
* and each execution is followed by changePreviewImage().
* At the end, depending on the user action, dialogAccepted()
* or dialogCanceled() is called.
*/
public class ImageLayer extends ContentLayer implements Drawable {
enum State {
/**
* The layer is in normal state when no filter is running on it
*/
NORMAL {
},
/**
* The layer is in previewing mode when a filter with dialog is opened
* Filters that work normally without a dialog can still work with a
* dialog when invoked from places like the the "Random Filter"
*/
PREVIEW {
},
/**
* The layer is in previewing mode, but "Show Original" is pressed in the dialog
*/
SHOW_ORIGINAL {
}
}
/**
* Whether the preview image is different from the normal image
* It makes sense only in PREVIEW mode
*/
private transient boolean imageContentChanged = false;
private static final long serialVersionUID = 2L;
//
// transient variables from here!
//
private transient State state = NORMAL;
private transient TmpDrawingLayer tmpDrawingLayer;
/**
* The image content of this image layer
*/
protected transient BufferedImage image = null;
/**
* The image shown during previews
*/
private transient BufferedImage previewImage;
/**
* The source image passed to the filters.
* This is different than image if there is a selection.
*/
private transient BufferedImage filterSourceImage;
/**
* Creates a new layer with the given image
*/
public ImageLayer(Composition comp, BufferedImage image, String name, Layer parent) {
super(comp, name == null ? comp.generateNewLayerName() : name, parent);
requireNonNull(image);
setImage(image);
updateIconImage();
checkConstructorPostConditions();
}
/**
* Creates a new layer with the given image and size.
* Used when an image is pasted into a layer
*/
public ImageLayer(Composition comp, BufferedImage pastedImage, String name, int width, int height) {
super(comp, name, null);
requireNonNull(pastedImage);
int pastedWidth = pastedImage.getWidth();
int pastedHeight = pastedImage.getHeight();
boolean pastedImageTooSmall = pastedWidth < width || pastedHeight < height;
BufferedImage newImage = pastedImage;
if (pastedImageTooSmall) {
// a new image is created
int newWidth = Math.max(width, pastedWidth);
int newHeight = Math.max(height, pastedHeight);
newImage = createEmptyImageForLayer(newWidth, newHeight);
Graphics2D g = newImage.createGraphics();
int drawX = Math.max((width - pastedWidth) / 2, 0);
int drawY = Math.max((height - pastedHeight) / 2, 0);
g.drawImage(pastedImage, drawX, drawY, null);
g.dispose();
}
setImage(newImage);
boolean addXTranslation = pastedWidth > width;
boolean addYTranslation = pastedHeight > height;
int newTX = 0;
int newTY = 0;
if (addXTranslation) {
newTX = -(pastedWidth - width) / 2;
}
if (addYTranslation) {
newTY = -(pastedHeight - height) / 2;
}
setTranslation(newTX, newTY);
updateIconImage();
checkConstructorPostConditions();
}
/**
* Creates a new empty layer
*/
public ImageLayer(Composition comp, String name) {
super(comp, name == null ? comp.generateNewLayerName() : name, null);
BufferedImage emptyImage = createEmptyImageForLayer(canvas.getWidth(), canvas.getHeight());
setImage(emptyImage);
checkConstructorPostConditions();
}
private void checkConstructorPostConditions() {
assert canvas != null;
assert image != null;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
ImageUtils.serializeImage(out, image);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
state = NORMAL;
in.defaultReadObject();
setImage(ImageUtils.deserializeImage(in));
imageContentChanged = false;
}
@Override
public ImageLayer duplicate(boolean sameName) {
BufferedImage imageCopy = ImageUtils.copyImage(image);
String duplicateName = sameName ? name : Utils.createCopyName(name);
ImageLayer d = new ImageLayer(comp, imageCopy, duplicateName, null);
d.setOpacity(opacity, false, false, true);
d.setTranslation(translationX, translationY);
d.setBlendingMode(blendingMode, false, false, false);
if (hasMask()) {
d.addMask(mask.duplicate(d));
}
return d;
}
@Override
public BufferedImage getImage() {
return image;
}
private void setPreviewWithSelection(BufferedImage newImage) {
previewImage = replaceSelectedPart(previewImage, newImage);
}
private void setImageWithSelection(BufferedImage newImage) {
image = replaceSelectedPart(image, newImage);
imageRefChanged();
comp.imageChanged(INVALIDATE_CACHE);
}
/**
* If there is no selection, returns the newImage
* If there is a selection, copies newImage into src
* according to the selection, and returns src
*/
private BufferedImage replaceSelectedPart(BufferedImage src, BufferedImage newImg) {
assert src != null;
assert newImg != null;
assert Utils.checkRasterMinimum(newImg);
Shape selectionShape = comp.getSelectionShape();
if (selectionShape == null) {
return newImg;
} else {
// the argument image pixels will replace the old ones only where selected
Graphics2D g = src.createGraphics();
g.translate(-getTX(), -getTY());
g.setComposite(AlphaComposite.Src);
g.setClip(selectionShape);
Rectangle bounds = selectionShape.getBounds();
g.drawImage(newImg, bounds.x, bounds.y, null);
g.dispose();
return src;
}
}
/**
* Sets the image ignoring the selection
*/
@Override
public void setImage(BufferedImage newImage) {
BufferedImage oldRef = image;
image = requireNonNull(newImage);
imageRefChanged();
assert Utils.checkRasterMinimum(newImage);
comp.imageChanged(INVALIDATE_CACHE);
if (oldRef != null && oldRef != image) {
oldRef.flush();
}
}
/**
* Replaces the image with history and icon update
*/
@Override
public void replaceImage(BufferedImage newImage, String editName) {
BufferedImage oldImage = image;
setImage(newImage);
ImageEdit edit = new ImageEdit(comp, editName, this, oldImage, IgnoreSelection.YES, false);
History.addEdit(edit);
updateIconImage();
}
/**
* Initializes a preview session
*/
@Override
public void startPreviewing() {
assert state == NORMAL : "state was " + state;
if (comp.hasSelection()) {
// if we have a selection, then the preview image reference cannot be simply
// the image reference, because when we draw into the preview image, we would
// also draw on the real image, and after cancel we would still have the changed version.
previewImage = ImageUtils.copyImage(image);
} else {
// if there is no selection, then there is no problem, because
// the previewImage reference will be overwritten
previewImage = image;
}
setState(PREVIEW);
}
@Override
public void stopPreviewing() {
assert state == PREVIEW || state == SHOW_ORIGINAL;
assert previewImage != null;
setState(NORMAL);
// so that layer mask transparency image is regenerated
// from the real image after the previews
imageRefChanged();
previewImage = null;
comp.imageChanged(FULL);
}
@Override
public void onDialogAccepted(String filterName) {
assert (state == PREVIEW) || (state == SHOW_ORIGINAL);
assert previewImage != null;
if (imageContentChanged) {
ImageEdit edit = new ImageEdit(comp, filterName, this, getImageOrSubImageIfSelected(true, true),
IgnoreSelection.NO, true);
History.addEdit(edit);
}
image = previewImage;
imageRefChanged();
if (imageContentChanged) {
updateIconImage();
}
previewImage = null;
boolean wasShowOriginal = (state == SHOW_ORIGINAL);
setState(NORMAL);
if (wasShowOriginal) {
comp.imageChanged(FULL);
}
}
@Override
public void onDialogCanceled() {
stopPreviewing();
}
@Override
public void tweenCalculatingStarted() {
assert state == NORMAL;
startPreviewing();
}
@Override
public void tweenCalculatingEnded() {
assert state == PREVIEW;
stopPreviewing();
}
/**
* @return true if the image has to be repainted
*/
@Override
public void changePreviewImage(BufferedImage img, String filterName, ChangeReason changeReason) {
// typically we should be in PREVIEW mode
if (state == SHOW_ORIGINAL) {
// this is OK, something was adjusted while in show original mode
} else if (state == NORMAL) {
throw new IllegalStateException(String.format(
"change preview in normal state, filter = %s, changeReason = %s, class = %s)",
filterName, changeReason, this.getClass().getSimpleName()));
}
assert previewImage != null :
String.format("previewImage was null with %s, changeReason = %s, class = %s",
filterName, changeReason, this.getClass().getSimpleName());
assert img != null;
if (img == image) {
// this can happen if a filter with preview decides that no
// change is necessary and returns the src
imageContentChanged = false; // no history will be necessary
// it still can happen that the image needs to be repainted
// because the preview image can be different from the image
// (the user does something, but then resets the params to a do-nothing state)
boolean shouldRefresh = image != previewImage;
previewImage = image;
if (shouldRefresh) {
imageRefChanged();
comp.imageChanged(FULL);
}
} else {
imageContentChanged = true; // history will be necessary
setPreviewWithSelection(img);
setState(PREVIEW);
imageRefChanged();
comp.imageChanged(FULL);
}
}
@Override
public void filterWithoutDialogFinished(BufferedImage transformedImage, ChangeReason changeReason, String opName) {
requireNonNull(transformedImage);
comp.setDirty(true);
// A filter without dialog should never return the original image...
if (transformedImage == image) {
// ...unless Repeat Last starts a filter that normally has a dialog without one
if (changeReason != REPEAT_LAST) {
throw new IllegalStateException(opName + " returned the original image, changeReason = " + changeReason);
} else {
return;
}
}
// filters without dialog run in the normal state
assert state == NORMAL;
BufferedImage imageForUndo = getFilterSourceImage();
setImageWithSelection(transformedImage);
if (!changeReason.needsUndo()) {
return;
}
// at this point we are sure that the image changed,
// considering that a filter without dialog was running
if (imageForUndo == image) {
throw new IllegalStateException("imageForUndo == image");
}
assert imageForUndo != null;
ImageEdit edit = new ImageEdit(comp, opName, this,
imageForUndo, IgnoreSelection.NO, true);
History.addEdit(edit);
// otherwise the next filter run will take the old image source,
// not the actual one
filterSourceImage = null;
updateIconImage();
comp.imageChanged(FULL);
}
@Override
public void changeImageUndoRedo(BufferedImage img, IgnoreSelection ignoreSelection) {
requireNonNull(img);
assert img != image; // simple filters always change something
assert state == NORMAL;
if (ignoreSelection.isYes()) {
setImage(img);
} else {
setImageWithSelection(img);
}
}
// returns the image bounds relative to the canvas
@Override
public Rectangle getImageBounds() {
return new Rectangle(translationX, translationY, image.getWidth(), image.getHeight());
}
@Override
public boolean checkImageDoesNotCoverCanvas() {
Rectangle canvasBounds = comp.getCanvasBounds();
Rectangle imageBounds = getImageBounds();
boolean needsEnlarging = !(imageBounds.contains(canvasBounds));
return needsEnlarging;
}
/**
* Enlarges the image so that it covers the canvas completely.
*/
@Override
public void enlargeImage(Rectangle canvasBounds) {
try {
Rectangle current = getImageBounds();
Rectangle target = current.union(canvasBounds);
BufferedImage bi = createEmptyImageForLayer(target.width, target.height);
Graphics2D g = bi.createGraphics();
int drawX = current.x - target.x;
int drawY = current.y - target.y;
g.drawImage(image, drawX, drawY, null);
g.dispose();
translationX = target.x - canvasBounds.x;
translationY = target.y - canvasBounds.y;
setImage(bi);
} catch (OutOfMemoryError e) {
Dialogs.showOutOfMemoryDialog(e);
}
}
/**
* Returns the image shown in the image selector in filter dialogs.
* The canvas size is not considered, only the selection.
*/
@Override
public BufferedImage getImageForFilterDialogs() {
Selection selection = comp.getSelection();
if (selection == null) {
return image;
}
Rectangle selBounds = selection.getShapeBounds();
return image.getSubimage(selBounds.x, selBounds.y, selBounds.width, selBounds.height);
}
@Override
public void flip(Flip.Direction direction) {
AffineTransform imageTx = direction.getImageTX(this);
int tXAbs = -getTX();
int tYAbs = -getTY();
int newTXAbs;
int newTYAbs;
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
BufferedImage dest = ImageUtils.createImageWithSameColorModel(image);
Graphics2D g2 = dest.createGraphics();
if (direction == HORIZONTAL) {
newTXAbs = imageWidth - canvasWidth - tXAbs;
newTYAbs = tYAbs;
} else {
newTXAbs = tXAbs;
newTYAbs = imageHeight - canvasHeight - tYAbs;
}
g2.setTransform(imageTx);
g2.drawImage(image, 0, 0, imageWidth, imageHeight, null);
g2.dispose();
setTranslation(-newTXAbs, -newTYAbs);
setImage(dest);
}
@Override
public void rotate(Rotate.SpecialAngle angle) {
int tx = getTX();
int ty = getTY();
int tXAbs = -tx;
int tYAbs = -ty;
int newTXAbs = 0;
int newTYAbs = 0;
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
int angleDegree = angle.getAngleDegree();
if (angleDegree == 90) {
newTXAbs = imageHeight - tYAbs - canvasHeight;
newTYAbs = tXAbs;
} else if (angleDegree == 270) {
newTXAbs = tYAbs;
newTYAbs = imageWidth - tXAbs - canvasWidth;
} else if (angleDegree == 180) {
newTXAbs = imageWidth - canvasWidth - tXAbs;
newTYAbs = imageHeight - canvasHeight - tYAbs;
}
BufferedImage dest = angle.createDestImage(image);
Graphics2D g2 = dest.createGraphics();
// nearest neighbor should be ok for 90, 180, 270 degrees
g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2.setTransform(angle.getImageTX(this));
g2.drawImage(image, 0, 0, imageWidth, imageHeight, null);
g2.dispose();
setTranslation(-newTXAbs, -newTYAbs);
// setImageWithSelection(dest);
setImage(dest);
}
private BufferedImage getMaskedImage() {
if(mask == null || !isMaskEnabled()) {
return image;
} else {
BufferedImage copy = ImageUtils.copyImage(image);
mask.applyToImage(copy);
return copy;
}
}
@Override
public void mergeDownOn(ImageLayer bellowImageLayer) {
int aX = getTX();
int aY = getTY();
BufferedImage bellowImage = bellowImageLayer.getImage();
int bX = bellowImageLayer.getTX();
int bY = bellowImageLayer.getTY();
BufferedImage ourImage = getMaskedImage();
Graphics2D g = bellowImage.createGraphics();
int x = aX - bX;
int y = aY - bY;
Composite composite = blendingMode.getComposite(opacity);
g.setComposite(composite);
g.drawImage(ourImage, x, y, null);
g.dispose();
}
@Override
public TmpDrawingLayer createTmpDrawingLayer(Composite c) {
tmpDrawingLayer = new TmpDrawingLayer(this, c);
return tmpDrawingLayer;
}
@Override
public void mergeTmpDrawingLayerDown() {
if (tmpDrawingLayer == null) {
return;
}
Graphics2D g = image.createGraphics();
tmpDrawingLayer.paintLayer(g, -getTX(), -getTY());
g.dispose();
tmpDrawingLayer.dispose();
tmpDrawingLayer = null;
}
@Override
public BufferedImage createCompositionSizedTmpImage() {
int width = canvas.getWidth();
int height = canvas.getHeight();
// it is important that the tmp image has transparency
// even for layer masks, otherwise drawing is not possible
return ImageUtils.createSysCompatibleImage(width, height);
}
@Override
public BufferedImage getCanvasSizedSubImage() {
if (!isBigLayer()) {
return image;
}
int x = -getTX();
int y = -getTY();
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
assert ConsistencyChecks.imageCoversCanvasCheck(this);
BufferedImage subImage;
try {
subImage = image.getSubimage(x, y, canvasWidth, canvasHeight);
} catch (RasterFormatException e) {
System.out.printf("ImageLayer.getCanvasSizedSubImage x = %d, y = %d, " +
"canvasWidth = %d, canvasHeight = %d, " +
"imageWidth = %d, imageHeight = %d%n",
x, y, canvasWidth, canvasHeight, image.getWidth(), image.getHeight());
WritableRaster raster = image.getRaster();
System.out.printf("ImageLayer.getCanvasSizedSubImage " +
"minX = %d, minY = %d, width = %d, height=%d %n",
raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight());
throw e;
}
assert subImage.getWidth() == canvasWidth;
assert subImage.getHeight() == canvasHeight;
return subImage;
}
@Override
public BufferedImage getFilterSourceImage() {
if (filterSourceImage == null) {
filterSourceImage = getImageOrSubImageIfSelected(false, true);
}
return filterSourceImage;
}
/**
* If there is a selection, then the filters work on a subimage determined by the selection bounds.
*/
@Override
public BufferedImage getImageOrSubImageIfSelected(boolean copyIfNoSelection, boolean copyAndTranslateIfSelected) {
Selection selection = comp.getSelection();
if (selection == null) {
if (copyIfNoSelection) {
return ImageUtils.copyImage(image);
}
return image;
}
return getSelectionSizedPartFrom(image, selection, copyAndTranslateIfSelected);
}
@Override
public BufferedImage getSelectionSizedPartFrom(BufferedImage src, Selection selection, boolean copyAndTranslateIfSelected) {
assert selection != null;
Rectangle bounds = selection.getShapeBounds(); // relative to the composition
bounds.translate(-getTX(), -getTY()); // relative to the image
bounds = SwingUtilities.computeIntersection(
0, 0, src.getWidth(), src.getHeight(), // image bounds
bounds);
if (bounds.isEmpty()) { // TODO if the selection is outside the image?
if (copyAndTranslateIfSelected) {
return ImageUtils.copyImage(src);
} else {
return src;
}
}
if (copyAndTranslateIfSelected) {
return ImageUtils.getCopiedSubimage(src, bounds);
} else {
return src.getSubimage(bounds.x, bounds.y, bounds.width, bounds.height);
}
}
/**
* Returns true if something was changed
*/
@Override
public boolean cropToCanvasSize() {
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
if ((imageWidth > canvasWidth) || (imageHeight > canvasHeight)) {
BufferedImage newImage = ImageUtils.crop(image, -getTX(), -getTY(), canvasWidth, canvasHeight);
BufferedImage tmp = image;
setImage(newImage);
tmp.flush();
setTranslation(0, 0);
return true;
}
return false;
}
@Override
public void enlargeCanvas(int north, int east, int south, int west) {
// all coordinates in this method are
// relative to the previous state of the canvas
Rectangle imageBounds = getImageBounds();
Rectangle canvasBounds = comp.getCanvasBounds();
int newX = canvasBounds.x - west;
int newY = canvasBounds.y - north;
int newWidth = canvasBounds.width + west + east;
int newHeight = canvasBounds.height + north + south;
Rectangle newCanvasBounds = new Rectangle(newX, newY, newWidth, newHeight);
if (imageBounds.contains(newCanvasBounds)) {
// even after the canvas enlargement, the image does not need to be enlarged
translationX += west;
translationY += north;
} else {
enlargeImage(newCanvasBounds);
}
}
@Override
ContentLayerMoveEdit createMovementEdit(int oldTX, int oldTY) {
ContentLayerMoveEdit edit;
boolean needsEnlarging = checkImageDoesNotCoverCanvas();
if (needsEnlarging) {
BufferedImage backupImage = getImage();
enlargeImage(comp.getCanvasBounds());
edit = new ContentLayerMoveEdit(this, backupImage, oldTX, oldTY);
} else {
edit = new ContentLayerMoveEdit(this, null, oldTX, oldTY);
}
return edit;
}
@Override
public void resize(int canvasTargetWidth, int canvasTargetHeight, boolean progressiveBilinear) {
boolean bigLayer = isBigLayer();
int imgTargetWidth = canvasTargetWidth;
int imgTargetHeight = canvasTargetHeight;
double horizontalResizeRatio = 1.0;
double verticalResizeRatio = 1.0;
int newTx = 0, newTy = 0; // used only for big layers
if (bigLayer) {
horizontalResizeRatio = ((double) canvasTargetWidth) / canvas.getWidth();
verticalResizeRatio = ((double) canvasTargetHeight) / canvas.getHeight();
imgTargetWidth = (int) (image.getWidth() * horizontalResizeRatio);
imgTargetHeight = (int) (image.getHeight() * verticalResizeRatio);
newTx = (int) (getTX() * horizontalResizeRatio);
newTy = (int) (getTY() * verticalResizeRatio);
// correct rounding problems that can cause
// "image does dot cover canvas" errors
if (imgTargetWidth + newTx < canvasTargetWidth) {
imgTargetWidth++;
}
if (imgTargetHeight + newTy < canvasTargetHeight) {
imgTargetHeight++;
}
}
BufferedImage resizedImg = ImageUtils.getFasterScaledInstance(image, imgTargetWidth, imgTargetHeight, VALUE_INTERPOLATION_BICUBIC, progressiveBilinear);
setImage(resizedImg);
if (bigLayer) {
setTranslation(newTx, newTy);
}
}
@Override
public boolean isBigLayer() {
Rectangle canvasBounds = canvas.getBounds();
Rectangle layerBounds = getImageBounds();
return !canvasBounds.contains(layerBounds);
}
@Override
public void crop(Rectangle2D cropRect) {
int cropWidth = (int) cropRect.getWidth();
int cropHeight = (int) cropRect.getHeight();
BufferedImage img = getImage();
// the selectionBounds is in image space except for the translation
int transX = getTX();
int transY = getTY();
int cropX = (int) (cropRect.getX() - transX);
int cropY = (int) (cropRect.getY() - transY);
BufferedImage dest = ImageUtils.crop(img, cropX, cropY, cropWidth, cropHeight);
setImage(dest);
setTranslation(0, 0);
}
@Override
public void paintLayerOnGraphics(Graphics2D g, boolean firstVisibleLayer) {
BufferedImage visibleImage = getVisibleImage();
if (tmpDrawingLayer == null) {
paintLayerOnGraphicsWOTmpLayer(g, firstVisibleLayer, visibleImage);
} else { // we are in the middle of a brush draw
if (isNormalAndOpaque()) {
g.drawImage(visibleImage, getTX(), getTY(), null);
tmpDrawingLayer.paintLayer(g, 0, 0);
} else { // layer is not in normal mode
// first create a merged layer-brush image
BufferedImage mergedLayerBrushImg = ImageUtils.copyImage(visibleImage); // TODO a canvas-sized image is enough and then less translating is necessary
Graphics2D mergedLayerBrushG = mergedLayerBrushImg.createGraphics();
tmpDrawingLayer.paintLayer(mergedLayerBrushG, -getTX(), -getTY()); // draw the brush on the layer
mergedLayerBrushG.dispose();
// now draw the merged layer-brush on the target Graphics with the layer composite
g.drawImage(mergedLayerBrushImg, getTX(), getTY(), null);
}
}
}
protected void paintLayerOnGraphicsWOTmpLayer(Graphics2D g, boolean firstVisibleLayer, BufferedImage visibleImage) {
if (Tools.isShapesDrawing() && isActive() && !isMaskEditing()) {
paintDraggedShapesIntoActiveLayer(g, visibleImage, firstVisibleLayer);
} else { // the simple case
g.drawImage(visibleImage, getTX(), getTY(), null);
}
}
protected void paintDraggedShapesIntoActiveLayer(Graphics2D g, BufferedImage visibleImage, boolean firstVisibleLayer) {
if (firstVisibleLayer) {
// Create a copy of the graphics, because we don't want to
// mess with the clipping of the original
Graphics2D gCopy = (Graphics2D) g.create();
gCopy.drawImage(visibleImage, getTX(), getTY(), null);
comp.applySelectionClipping(gCopy, null);
Tools.SHAPES.paintOverLayer(gCopy, comp);
gCopy.dispose();
} else {
// We need to draw inside the layer, but only temporarily.
// When the mouse is released, then the shape will become part of
// the image pixels.
// But, until then, the image and the shape have to be mixed first
// and then the result must be composited into the main Graphics,
// otherwise we don't get the correct result if this layer not the
// first visible layer and has a blending mode different from normal
BufferedImage tmp = createCompositionSizedTmpImage();
Graphics2D tmpG = tmp.createGraphics();
tmpG.drawImage(visibleImage, getTX(), getTY(), null);
comp.applySelectionClipping(tmpG, null);
Tools.SHAPES.paintOverLayer(tmpG, comp);
tmpG.dispose();
g.drawImage(tmp, 0, 0, null);
tmp.flush();
}
}
/**
* Returns the image that should be shown by this layer.
*/
protected BufferedImage getVisibleImage() {
BufferedImage visibleImage;
switch (state) {
case NORMAL:
visibleImage = image;
break;
case PREVIEW:
assert previewImage != null : "no preview image in state " + state;
visibleImage = previewImage;
break;
case SHOW_ORIGINAL:
assert previewImage != null : "no preview image in state " + state;
visibleImage = image;
break;
default:
throw new IllegalStateException("state = " + state);
}
return visibleImage;
}
@Override
public void setShowOriginal(boolean b) {
if (b) {
if (state == SHOW_ORIGINAL) {
return;
}
setState(SHOW_ORIGINAL);
} else {
if (state == PREVIEW) {
return;
}
setState(PREVIEW);
}
imageRefChanged();
comp.imageChanged(REPAINT);
}
private void setState(State newState) {
state = newState;
if (newState == NORMAL) { // back to normal: cleanup
previewImage = null;
filterSourceImage = null;
}
}
@Override
public void debugImages() {
Utils.debugImage(image, "image");
if (previewImage != null) {
Utils.debugImage(previewImage, "previewImage");
} else {
Messages.showInfo("null", "previewImage is null");
}
}
State getState() {
return state;
}
// every image creation in this class should use this method
// which is overridden by the LayerMask subclass
// because normal image layers are enlarged with transparent pixels
// and layer masks are enlarged with white pixels
protected BufferedImage createEmptyImageForLayer(int width, int height) {
return ImageUtils.createSysCompatibleImage(width, height);
}
protected void imageRefChanged() {
// overridden in LayerMask
}
@Override
public void updateIconImage() {
// Thread.dumpStack();
getUI().updateLayerIconImage(this);
}
@Override
public BufferedImage applyLayerMask(boolean addToHistory) {
// the image reference will not be replaced
BufferedImage oldImage = ImageUtils.copyImage(image);
LayerMask oldMask = mask;
MaskViewMode oldMode = comp.getIC().getMaskViewMode();
mask.applyToImage(image);
deleteMask(false);
History.addEdit(addToHistory, () ->
new ApplyLayerMaskEdit(comp, this, oldMask, oldImage, oldMode));
updateIconImage();
return oldImage;
}
@Override
public BufferedImage adjustImage(BufferedImage src) {
throw new UnsupportedOperationException();
}
@Override
public PixelitorEdit endMovement() {
PixelitorEdit edit = super.endMovement();
updateIconImage();
return edit;
}
@Override
@VisibleForTesting
public BufferedImage getPreviewImage() {
return previewImage;
}
public String toDebugCanvasString() {
return "{canvasWidth=" + canvas.getWidth()
+ ", canvasHeight=" + canvas.getHeight()
+ ", tx=" + translationX
+ ", ty=" + translationY
+ ", imgWidth=" + image.getWidth()
+ ", imgHeight=" + image.getHeight()
+ '}';
}
@Override
public String toString() {
return getClass().getSimpleName()
+ "{" + "state=" + state
+ ", super=" + super.toString()
+ '}';
}
}