/*
* 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.io;
import org.xml.sax.SAXException;
import pixelitor.AppLogic;
import pixelitor.Composition;
import pixelitor.automate.SingleDirChooserPanel;
import pixelitor.gui.GlobalKeyboardWatch;
import pixelitor.gui.ImageComponent;
import pixelitor.gui.ImageComponents;
import pixelitor.gui.PixelitorWindow;
import pixelitor.layers.ImageLayer;
import pixelitor.layers.Layer;
import pixelitor.layers.TextLayer;
import pixelitor.menus.file.RecentFilesMenu;
import pixelitor.utils.Messages;
import pixelitor.utils.Utils;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class OpenSaveManager {
private static final int CURRENT_PXC_VERSION_NUMBER = 0x03;
private static final float DEFAULT_JPEG_QUALITY = 0.87f;
private static float jpegQuality = DEFAULT_JPEG_QUALITY;
/**
* Utility class with static methods
*/
private OpenSaveManager() {
}
public static void openFile(File file) {
assert SwingUtilities.isEventDispatchThread();
Runnable r = () -> {
Composition comp = createCompositionFromFile(file);
if(comp != null) { // there was no decoding problem
AppLogic.addCompAsNewImage(comp);
}
};
Utils.executeWithBusyCursor(r);
RecentFilesMenu.getInstance().addFile(file);
}
public static Composition createCompositionFromFile(File file) {
String ext = FileExtensionUtils.getExt(file.getName());
if ("pxc".equals(ext)) {
return openLayered(file, "pxc");
} else if ("ora".equals(ext)) {
return openLayered(file, "ora");
} else {
return openSimpleFile(file);
}
}
// opens an a file with a single-layer image format
private static Composition openSimpleFile(File file) {
BufferedImage img = null;
try {
img = ImageIO.read(file);
} catch (IOException ex) {
Messages.showException(ex);
}
if (img == null) {
String message = String.format("Could not load \"%s\" as an image file", file.getName());
Messages.showError("Error", message);
return null;
}
return Composition.fromImage(img, file, null);
}
private static Composition openLayered(File selectedFile, String type) {
Composition comp = null;
try {
switch (type) {
case "pxc":
comp = deserializeComposition(selectedFile);
break;
case "ora":
comp = OpenRaster.readOpenRaster(selectedFile);
break;
default:
throw new IllegalStateException("type = " + type);
}
} catch (NotPxcFormatException | ParserConfigurationException | IOException | SAXException e) {
Messages.showException(e);
}
return comp;
}
public static boolean save(boolean saveAs) {
Composition comp = ImageComponents.getActiveCompOrNull();
return save(comp, saveAs);
}
/**
* Returns true if the file was saved, false if the user cancels the saving
*/
private static boolean save(Composition comp, boolean saveAs) {
boolean needsFileChooser = saveAs || (comp.getFile() == null);
if (needsFileChooser) {
return FileChoosers.saveWithChooser(comp);
} else {
File file = comp.getFile();
OutputFormat outputFormat = OutputFormat.fromFile(file);
outputFormat.saveComp(comp, file, true);
return true;
}
}
public static void saveImageToFile(File selectedFile, BufferedImage image, String format) {
Objects.requireNonNull(selectedFile);
Objects.requireNonNull(image);
Objects.requireNonNull(format);
Runnable r = () -> {
try {
if ("jpg".equals(format)) {
JpegOutput.writeJPG(image, selectedFile, jpegQuality);
} else {
ImageIO.write(image, format, selectedFile);
}
} catch (IOException e) {
if (e.getMessage().contains("another process")) {
String msg = String.format("Cannot save to\n%s\nbecause this file is being used by another program.",
selectedFile.getAbsolutePath());
Messages.showError("Cannot save", msg);
} else {
Messages.showException(e);
}
}
};
Utils.executeWithBusyCursor(r);
}
public static void warnAndCloseImage(ImageComponent ic) {
try {
Composition comp = ic.getComp();
if (comp.isDirty()) {
Object[] options = {"Save",
"Don't Save",
"Cancel"};
String question = String.format("<html><b>Do you want to save the changes made to %s?</b>" +
"<br>Your changes will be lost if you don't save them.</html>", comp.getName());
GlobalKeyboardWatch.setDialogActive(true);
int answer = JOptionPane.showOptionDialog(PixelitorWindow.getInstance(), new JLabel(question),
"Unsaved changes", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
GlobalKeyboardWatch.setDialogActive(false);
if (answer == JOptionPane.YES_OPTION) { // save
boolean fileSaved = OpenSaveManager.save(comp, false);
if (fileSaved) {
ic.close();
}
} else if (answer == JOptionPane.NO_OPTION) { // don't save
ic.close();
} else if (answer == JOptionPane.CANCEL_OPTION) { // cancel
// do nothing
} else { // dialog closed by pressing X
// do nothing
}
} else {
ic.close();
}
} catch (Exception ex) {
Messages.showException(ex);
}
}
public static void warnAndCloseAllImages() {
List<ImageComponent> imageComponents = ImageComponents.getICList();
// make a copy because items will be removed from the original while iterating
Iterable<ImageComponent> tmpCopy = new ArrayList<>(imageComponents);
for (ImageComponent component : tmpCopy) {
warnAndCloseImage(component);
}
}
public static void serializePXC(Composition comp, File f) {
try (FileOutputStream fos = new FileOutputStream(f)) {
fos.write(new byte[]{(byte) 0xAB, (byte) 0xC4, CURRENT_PXC_VERSION_NUMBER});
try (GZIPOutputStream gz = new GZIPOutputStream(fos)) {
try (ObjectOutput oos = new ObjectOutputStream(gz)) {
oos.writeObject(comp);
oos.flush();
}
}
} catch (IOException e) {
Messages.showException(e);
}
}
private static Composition deserializeComposition(File file) throws NotPxcFormatException {
Composition comp = null;
try (FileInputStream fis = new FileInputStream(file)) {
int firstByte = fis.read();
int secondByte = fis.read();
if (firstByte == 0xAB && secondByte == 0xC4) {
// identification bytes OK
} else {
throw new NotPxcFormatException(file.getName() + " is not in the pxc format.");
}
int versionByte = fis.read();
if (versionByte == 0) {
throw new NotPxcFormatException(file.getName() + " is in an obsolete pxc format, it can only be opened in the old beta Pixelitor versions 0.9.2-0.9.7");
}
if (versionByte == 1) {
throw new NotPxcFormatException(file.getName() + " is in an obsolete pxc format, it can only be opened in the old beta Pixelitor version 0.9.8");
}
if (versionByte == 2) {
throw new NotPxcFormatException(file.getName() + " is in an obsolete pxc format, it can only be opened in the old Pixelitor versions 0.9.9-1.1.2");
}
if (versionByte > 3) {
throw new NotPxcFormatException(file.getName() + " has unknown version byte " + versionByte);
}
try (GZIPInputStream gs = new GZIPInputStream(fis)) {
try (ObjectInput ois = new ObjectInputStream(gs)) {
comp = (Composition) ois.readObject();
// file is transient in Composition because the pxc file can be renamed
comp.setFile(file);
}
}
} catch (IOException | ClassNotFoundException e) {
Messages.showException(e);
}
return comp;
}
public static void openAllImagesInDir(File dir) {
File[] files = FileExtensionUtils.getAllSupportedInputFilesInDir(dir);
if (files != null) {
for (File file : files) {
openFile(file);
}
}
}
public static void exportLayersToPNG() {
boolean okPressed = SingleDirChooserPanel.selectOutputDir(false);
if (!okPressed) {
return;
}
Composition comp = ImageComponents.getActiveCompOrNull();
for (int layerIndex = 0; layerIndex < comp.getNumLayers(); layerIndex++) {
Layer layer = comp.getLayer(layerIndex);
if (layer instanceof ImageLayer) {
ImageLayer imageLayer = (ImageLayer) layer;
BufferedImage image = imageLayer.getImage();
saveImage(layerIndex, layer, image);
} else if (layer instanceof TextLayer) {
TextLayer textLayer = (TextLayer) layer;
BufferedImage image = textLayer.createRasterizedImage();
saveImage(layerIndex, layer, image);
}
// TODO what about masks? Either they should be applied
// or they should be saved as images
}
}
private static void saveImage(int layerIndex, Layer layer, BufferedImage image) {
File outputDir = Directories.getLastSaveDir();
String fileName = String.format("%03d_%s.%s", layerIndex, Utils.toFileName(layer.getName()), "png");
File file = new File(outputDir, fileName);
saveImageToFile(file, image, "png");
}
public static void saveCurrentImageInAllFormats() {
Composition comp = ImageComponents.getActiveCompOrNull();
boolean canceled = !SingleDirChooserPanel.selectOutputDir(false);
if (canceled) {
return;
}
File saveDir = Directories.getLastSaveDir();
if (saveDir != null) {
OutputFormat[] outputFormats = OutputFormat.values();
for (OutputFormat outputFormat : outputFormats) {
File f = new File(saveDir, "all_formats." + outputFormat.toString());
outputFormat.saveComp(comp, f, false);
}
}
}
// called by the "Save All Images to Folder..." menu
public static void saveAllImagesToDir() {
boolean cancelled = !SingleDirChooserPanel.selectOutputDir(true);
if (cancelled) {
return;
}
OutputFormat outputFormat = OutputFormat.getLastUsed();
File saveDir = Directories.getLastSaveDir();
List<ImageComponent> imageComponents = ImageComponents.getICList();
ProgressMonitor progressMonitor = Utils.createPercentageProgressMonitor("Saving All Images to Folder");
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
@Override
public Void doInBackground() {
for (int i = 0; i < imageComponents.size(); i++) {
progressMonitor.setProgress((int) ((float) i * 100 / imageComponents.size()));
if (progressMonitor.isCanceled()) {
break;
}
ImageComponent ic = imageComponents.get(i);
Composition comp = ic.getComp();
String fileName = String.format("%04d_%s.%s", i, Utils.toFileName(comp.getName()), outputFormat.toString());
File f = new File(saveDir, fileName);
progressMonitor.setNote("Saving " + fileName);
outputFormat.saveComp(comp, f, false);
}
progressMonitor.close();
return null;
} // end of doInBackground()
};
worker.execute();
}
public static void saveJpegWithQuality(float quality) {
try {
FileChoosers.initSaveChooser();
FileChoosers.setOnlyOneSaveExtension(FileChoosers.jpegFilter);
jpegQuality = quality;
FileChoosers.showSaveChooserAndSaveComp(ImageComponents.getActiveCompOrNull());
} finally {
FileChoosers.setDefaultSaveExtensions();
jpegQuality = DEFAULT_JPEG_QUALITY;
}
}
public static void afterSaveActions(Composition comp, File file, boolean addToRecentMenus) {
// TODO for a multilayered image this should be set only if it was saved in a layered format?
comp.setDirty(false);
comp.setFile(file);
if(addToRecentMenus) {
RecentFilesMenu.getInstance().addFile(file);
}
Messages.showFileSavedMessage(file);
}
}