/* * 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.AppLogic; import pixelitor.Canvas; import pixelitor.Composition; import pixelitor.gui.HistogramsPanel; import pixelitor.gui.ImageComponent; import pixelitor.history.AddLayerMaskEdit; import pixelitor.history.DeleteLayerMaskEdit; import pixelitor.history.DeselectEdit; import pixelitor.history.EnableLayerMaskEdit; import pixelitor.history.History; import pixelitor.history.LayerBlendingEdit; import pixelitor.history.LayerOpacityEdit; import pixelitor.history.LayerRenameEdit; import pixelitor.history.LayerVisibilityChangeEdit; import pixelitor.history.LinkedEdit; import pixelitor.history.PixelitorEdit; import pixelitor.selection.Selection; import pixelitor.utils.Messages; import java.awt.AlphaComposite; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import static java.awt.AlphaComposite.DstIn; import static java.awt.AlphaComposite.SRC_OVER; import static java.awt.image.BufferedImage.TYPE_INT_ARGB; import static pixelitor.Composition.ImageChangeActions.FULL; /** * The abstract superclass of all layer classes */ public abstract class Layer implements Serializable { private static final long serialVersionUID = 2L; protected Canvas canvas; protected String name; // the real layer for layer masks, // null for real layers protected final Layer parent; private boolean visible = true; protected Composition comp; protected LayerMask mask; private boolean maskEnabled = true; float opacity = 1.0f; BlendingMode blendingMode = BlendingMode.NORMAL; // transient variables from here private transient LayerGUI ui; protected transient boolean isAdjustment = false; private transient List<LayerChangeListener> layerChangeObservers; /** * Whether the edited image is the layer image or * the layer mask image. * This flag is logically independent from the showLayerMask * flag in the image component. */ private transient boolean maskEditing = false; Layer(Composition comp, String name, Layer parent) { this.comp = comp; this.name = name; this.parent = parent; this.opacity = 1.0f; canvas = comp.getCanvas(); if (parent != null) { // this is a layer mask ui = parent.getUI(); } else { // normal layer ui = new LayerGUI(this); } layerChangeObservers = new ArrayList<>(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); layerChangeObservers = new ArrayList<>(); // We create a layer button only for real layers. // For layer masks, we share the button of the real layer. if (parent == null) { // not mask ui = new LayerGUI(this); if (mask != null) { mask.setUI(ui); } } } public boolean isVisible() { return visible; } public void setVisible(boolean newVisibility, boolean addToHistory) { if (this.visible == newVisibility) { return; } this.visible = newVisibility; comp.imageChanged(FULL); ui.setOpenEye(newVisibility); History.addEdit(addToHistory, () -> new LayerVisibilityChangeEdit(comp, this, newVisibility)); } public LayerGUI getUI() { return ui; } public void setUI(LayerGUI ui) { this.ui = ui; } /** * If sameName is true, then the duplicate layer will * have the same name, otherwise a new "copy name" is generated */ public abstract Layer duplicate(boolean sameName); public float getOpacity() { return opacity; } public BlendingMode getBlendingMode() { return blendingMode; } private void updateAfterBMorOpacityChange() { comp.imageChanged(FULL); HistogramsPanel hp = HistogramsPanel.INSTANCE; if (hp.areHistogramsShown()) { hp.updateFromCompIfShown(comp); } } public void setOpacity(float newOpacity, boolean updateGUI, boolean addToHistory, boolean updateImage) { assert newOpacity <= 1.0f : "newOpacity = " + newOpacity; assert newOpacity >= 0.0f : "newOpacity = " + newOpacity; if (opacity == newOpacity) { return; } History.addEdit(addToHistory, () -> new LayerOpacityEdit(this, opacity)); this.opacity = newOpacity; if (updateGUI) { ui.setOpacityFromModel(newOpacity); } if(updateImage) { updateAfterBMorOpacityChange(); } } public void setBlendingMode(BlendingMode mode, boolean updateGUI, boolean addToHistory, boolean updateImage) { History.addEdit(addToHistory, () -> new LayerBlendingEdit(this, blendingMode)); this.blendingMode = mode; if (updateGUI) { LayerBlendingModePanel.INSTANCE.setBlendingModeFromModel(mode); } if(updateImage) { updateAfterBMorOpacityChange(); } } public void setName(String newName, boolean addToHistory) { String previousName = name; this.name = newName; if (name.equals(previousName)) { // important because this might be called twice for a single rename return; } ui.changeNameProgrammatically(newName); History.addEdit(addToHistory, () -> new LayerRenameEdit(this, previousName, name)); } public String getName() { return name; } public Composition getComp() { return comp; } public void setComp(Composition comp) { this.comp = comp; } public void mergeDownOn(ImageLayer bellow) { // TODO what about translations of the bellow layer BufferedImage bellowImage = bellow.getImage(); Graphics2D g = bellowImage.createGraphics(); BufferedImage result = applyLayer(g, false, bellowImage); if(result != null) { // this was an adjustment bellow.setImage(result); } g.dispose(); } public void makeActive(boolean addToHistory) { comp.setActiveLayer(this, addToHistory); } boolean isActive() { return comp.isActiveLayer(this); } public boolean hasMask() { return mask != null; } public void addMask(LayerMaskAddType addType) { if (mask != null) { Messages.showInfo("Has layer mask", String.format("The layer \"%s\" already has a layer mask.", getName())); return; } Selection selection = comp.getSelection(); if (addType.missingSelection(selection)) { Messages.showInfo("No selection", String.format("The composition \"%s\" has no selection.", comp.getName())); return; } int canvasWidth = canvas.getWidth(); int canvasHeight = canvas.getHeight(); BufferedImage bwMask = addType.getBWImage(canvasWidth, canvasHeight, selection); String editName = "Add Layer Mask"; boolean deselect = addType.needsSelection(); if (deselect) { editName = "Layer Mask from Selection"; } addImageAsMask(bwMask, deselect, editName, false); } public void addImageAsMask(BufferedImage bwMask, boolean deselect, String editName, boolean inheritTranslation) { assert mask == null; mask = new LayerMask(comp, bwMask, this, inheritTranslation); maskEnabled = true; // needs to be added first, because the inherited layer // mask constructor already will try to update the image ui.addMaskIconLabel(); comp.imageChanged(FULL); AppLogic.maskChanged(this); PixelitorEdit edit = new AddLayerMaskEdit(comp, this, editName); if (deselect) { Shape backupShape = comp.getSelectionShape(); comp.deselect(false); if (backupShape != null) { // TODO on Mac Random GUI test we can get null here DeselectEdit deselectEdit = new DeselectEdit(comp, backupShape, "nested deselect"); edit = new LinkedEdit(comp, editName, edit, deselectEdit); } } History.addEdit(edit); MaskViewMode.EDIT_MASK.activate(comp, this); } public void addOrReplaceMaskImage(BufferedImage bwMask, String editName) { if (hasMask()) { mask.replaceImage(bwMask, editName); } else { addImageAsMask(bwMask, false, editName, true); } } /** * Adds a mask that is already configured to be used * with this layer */ public void addMask(LayerMask mask) { assert mask != null; assert mask.getParent() == this; this.mask = mask; comp.imageChanged(FULL); ui.addMaskIconLabel(); AppLogic.maskChanged(this); mask.updateIconImage(); } public void deleteMask(boolean addToHistory) { LayerMask oldMask = mask; ImageComponent ic = comp.getIC(); MaskViewMode oldMode = ic.getMaskViewMode(); mask = null; maskEditing = false; comp.imageChanged(FULL); History.addEdit(addToHistory, () -> new DeleteLayerMaskEdit(comp, this, oldMask, oldMode)); AppLogic.maskChanged(this); ui.deleteMaskIconLabel(); MaskViewMode.NORMAL.activate(ic, this); } /** * Applies the effect of this layer on the given Graphics2D or on the given BufferedImage. * Adjustment layers and watermarked text layers change a BufferedImage, while other layers * just paint on the graphics. * If the BufferedImage is changed, the method returns the new image and null otherwise. */ public BufferedImage applyLayer(Graphics2D g, boolean firstVisibleLayer, BufferedImage imageSoFar) { if (isAdjustment) { // adjustment layer or watermarked text layers return adjustImageWithMasksAndBlending(imageSoFar, firstVisibleLayer); } else { if (!useMask()) { setupDrawingComposite(g, firstVisibleLayer); paintLayerOnGraphics(g, firstVisibleLayer); } else { paintLayerOnGraphicsWithMask(firstVisibleLayer, g); } } return null; } // used by the non-adjustment stuff // This method assumes that the composite of the graphics is already // set up according to the transparency and blending mode public abstract void paintLayerOnGraphics(Graphics2D g, boolean firstVisibleLayer); /** * Returns the masked image for the non-adjustment case. * The returned image is canvas-sized, and the masks and the * translations are taken into account */ private void paintLayerOnGraphicsWithMask(boolean firstVisibleLayer, Graphics2D g) { // Canvas canvas = comp.getCanvas(); // 1. create the masked image // TODO the masked image should be cached BufferedImage maskedImage = new BufferedImage(canvas.getWidth(), canvas.getHeight(), TYPE_INT_ARGB); Graphics2D mig = maskedImage.createGraphics(); paintLayerOnGraphics(mig, firstVisibleLayer); mig.setComposite(DstIn); mig.drawImage(mask.getTransparencyImage(), mask.getTX(), mask.getTY(), null); mig.dispose(); // 2. paint the masked image onto the graphics // g.drawImage(maskedImage, getTX(), getTY(), null); setupDrawingComposite(g, firstVisibleLayer); g.drawImage(maskedImage, 0, 0, null); } /** * Used by adjustment layers and watermarked text layers */ private BufferedImage adjustImageWithMasksAndBlending(BufferedImage imgSoFar, boolean isFirstVisibleLayer) { if (isFirstVisibleLayer) { return imgSoFar; // there's nothing we can do } BufferedImage transformed = adjustImage(imgSoFar); if (useMask()) { mask.applyToImage(transformed); } if (!useMask() && isNormalAndOpaque()) { return transformed; } else { Graphics2D g = imgSoFar.createGraphics(); setupDrawingComposite(g, isFirstVisibleLayer); g.drawImage(transformed, 0, 0, null); g.dispose(); return imgSoFar; } } /** * Used by adjustment layers and watermarked text layers */ protected abstract BufferedImage adjustImage(BufferedImage src); public abstract void resize(int targetWidth, int targetHeight, boolean progressiveBilinear); public abstract void crop(Rectangle2D cropRect); public LayerMask getMask() { return mask; } public void setCanvas(Canvas canvas) { this.canvas = canvas; } public Object getVisibilityAsORAString() { return isVisible() ? "visible" : "hidden"; } public void dragFinished(int newIndex) { comp.dragFinished(this, newIndex); } public void setMaskEditing(boolean b) { assert b ? hasMask() : true; this.maskEditing = b; ui.setMaskEditing(b); // sets the border around the icon } public boolean isMaskEditing() { assert maskEditing ? hasMask() : true; return maskEditing; } /** * Returns true if the layer is in normal mode and the opacity is 100% */ protected boolean isNormalAndOpaque() { return blendingMode == BlendingMode.NORMAL && opacity > 0.999f; } /** * Configures the composite of the given Graphics, * according to the blending mode and opacity of the layer */ public void setupDrawingComposite(Graphics2D g, boolean isFirstVisibleLayer) { if (isFirstVisibleLayer) { // the first visible layer is always painted with normal mode g.setComposite(AlphaComposite.getInstance(SRC_OVER, opacity)); } else { Composite composite = blendingMode.getComposite(opacity); g.setComposite(composite); } } // On this level startMovement, moveWhileDragging and // endMovement only care about the movement of the // mask or parent. Our own movement is handled in // ContentLayer. public void startMovement() { Layer linked = getLinked(); if (linked != null) { linked.startMovement(); } } public void moveWhileDragging(double x, double y) { Layer linked = getLinked(); if (linked != null) { linked.moveWhileDragging(x, y); } } public PixelitorEdit endMovement() { // Returns the edit of the linked layer. // Handles the case when we are in an adjustment // layer and the layer mask needs to be moved. // Otherwise the ContentLayer will override this, // and call super for the linked edit. Layer linked = getLinked(); if (linked != null) { return linked.endMovement(); } return null; } /** * Returns the layer that should move together with the current one, * (Assuming that we are in the edited layer) * or null if this layer should move alone */ private Layer getLinked() { if (mask != null) { if (!maskEditing) { // we are in the edited layer if (mask.isLinked()) { return mask; } } } if (parent != null) { // we are in a mask if (parent.isMaskEditing()) { // we are in the edited layer if (((LayerMask) this).isLinked()) { return parent; } } } return null; } public boolean isMaskEnabled() { return maskEnabled; } public void setMaskEnabled(boolean maskEnabled, boolean addToHistory) { assert mask != null; this.maskEnabled = maskEnabled; comp.imageChanged(FULL); mask.updateIconImage(); notifyLayerChangeObservers(); History.addEdit(addToHistory, () -> new EnableLayerMaskEdit(comp, this)); } private boolean useMask() { return mask != null && maskEnabled; } public Layer getParent() { return parent; } public void activateUI() { ui.setSelected(true); } public void addLayerChangeObserver(LayerChangeListener listener) { layerChangeObservers.add(listener); } protected void notifyLayerChangeObservers() { for (LayerChangeListener observer : layerChangeObservers) { observer.layerStateChanged(); } } @Override public String toString() { return "{name='" + name + '\'' + ", visible=" + visible + ", mask=" + mask + ", maskEditing=" + maskEditing + ", maskEnabled=" + maskEnabled + ", isAdjustment=" + isAdjustment + '}'; } }