/* * 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.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import pixelitor.Composition; import pixelitor.layers.BlendingMode; import pixelitor.layers.ImageLayer; import pixelitor.layers.Layer; import pixelitor.utils.ImageUtils; import pixelitor.utils.Utils; import javax.imageio.ImageIO; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.Enumeration; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Scanner; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class OpenRaster { private OpenRaster() { } public static void writeOpenRaster(Composition comp, File outFile, boolean addMergedImage) throws IOException { FileOutputStream fos = new FileOutputStream(outFile); ZipOutputStream zos = new ZipOutputStream(fos); String stackXML = String.format("<?xml version='1.0' encoding='UTF-8'?>\n" + "<image w=\"%d\" h=\"%d\">\n" + "<stack>\n", comp.getCanvasWidth(), comp.getCanvasHeight()); int numLayers = comp.getNumLayers(); // Reverse iteration: in stack.xml the first element in a stack is the uppermost. for (int i = numLayers - 1; i >= 0; i--) { Layer layer = comp.getLayer(i); stackXML += writeLayer(zos, i, layer); } if(addMergedImage) { zos.putNextEntry(new ZipEntry("mergedimage.png")); ImageIO.write(comp.getCompositeImage(), "PNG", zos); zos.closeEntry(); } stackXML += "</stack>\n</image>"; // write the stack.xml file zos.putNextEntry(new ZipEntry("stack.xml")); zos.write(stackXML.getBytes("UTF-8")); zos.closeEntry(); // write the mimetype zos.putNextEntry(new ZipEntry("mimetype")); zos.write("image/openraster".getBytes("UTF-8")); zos.closeEntry(); zos.close(); } private static String writeLayer(ZipOutputStream zos, int layerIndex, Layer layer) throws IOException { if (!(layer instanceof ImageLayer)) { return ""; // currently only image layers are supported } ImageLayer imageLayer = (ImageLayer) layer; String stackXML = String.format(Locale.ENGLISH, "<layer name=\"%s\" visibility=\"%s\" composite-op=\"%s\" opacity=\"%f\" src=\"data/%d.png\" x=\"%d\" y=\"%d\"/>\n", layer.getName(), layer.getVisibilityAsORAString(), layer.getBlendingMode().toSVGName(), layer.getOpacity(), layerIndex, imageLayer.getTX(), imageLayer.getTY()); ZipEntry entry = new ZipEntry(String.format("data/%d.png", layerIndex)); zos.putNextEntry(entry); BufferedImage image = imageLayer.getImage(); ImageIO.write(image, "PNG", zos); zos.closeEntry(); return stackXML; } public static Composition readOpenRaster(File file) throws IOException, ParserConfigurationException, SAXException { boolean DEBUG = System.getProperty("openraster.debug", "false").equals("true"); String stackXML = null; Map<String, BufferedImage> images = new HashMap<>(); try (ZipFile zipFile = new ZipFile(file)) { Enumeration<? extends ZipEntry> fileEntries = zipFile.entries(); while (fileEntries.hasMoreElements()) { ZipEntry entry = fileEntries.nextElement(); String name = entry.getName(); if (name.equalsIgnoreCase("stack.xml")) { stackXML = extractString(zipFile.getInputStream(entry)); } else if (name.equalsIgnoreCase("mergedimage.png")) { // no need for that } else { String extension = FileExtensionUtils.getExt(name); if ("png".equalsIgnoreCase(extension)) { BufferedImage image = ImageIO.read(zipFile.getInputStream(entry)); images.put(name, image); if (DEBUG) { System.out.println(String.format("OpenRaster::readOpenRaster: found png image in zip file at the path '%s'", name)); } } } } } if(stackXML == null) { throw new IllegalStateException("No stack.xml found."); } if(DEBUG) { System.out.println(String.format("OpenRaster::readOpenRaster: stackXML = '%s'", stackXML)); } Document doc = loadXMLFromString(stackXML); Element docElement = doc.getDocumentElement(); docElement.normalize(); String documentElementNodeName = docElement.getNodeName(); if(!documentElementNodeName.equals("image")) { throw new IllegalStateException(String.format("stack.xml root element is '%s', expected: 'image'", documentElementNodeName)); } String w = docElement.getAttribute("w"); int compWidth = Integer.parseInt(w); String h = docElement.getAttribute("h"); int compHeight = Integer.parseInt(h); if(DEBUG) { System.out.println(String.format("OpenRaster::readOpenRaster: w = '%s', h = '%s', compWidth = %d, compHeight = %d", w, h, compWidth, compHeight)); } Composition comp = Composition.createEmpty(compWidth, compHeight); comp.setFile(file); NodeList layers = docElement.getElementsByTagName("layer"); for (int i = layers.getLength() - 1; i >= 0; i--) { // stack.xml contains layers in reverse order Node node = layers.item(i); Element element = (Element) node; String layerName = element.getAttribute("name"); String layerVisibility = element.getAttribute("visibility"); String layerVisible = element.getAttribute("visible"); String layerBlendingMode = element.getAttribute("composite-op"); String layerOpacity = element.getAttribute("opacity"); String layerImageSource = element.getAttribute("src"); String layerX = element.getAttribute("x"); String layerY = element.getAttribute("y"); BufferedImage image = images.get(layerImageSource); image = ImageUtils.toSysCompatibleImage(image); if(DEBUG) { int imgWidth = image.getWidth(); int imgHeight = image.getHeight(); System.out.println("OpenRaster::readOpenRaster: imgWidth = " + imgWidth + ", imgHeight = " + imgHeight); // Utils.debugImage(image, layerImageSource); } if(layerVisibility == null || layerVisibility.isEmpty()) { //workaround: paint.net exported files use "visible" attribute instead of "visibility" layerVisibility = layerVisible; } boolean visibility = layerVisibility == null ? true : layerVisibility.equals("visible"); ImageLayer layer = new ImageLayer(comp, image, layerName, null); layer.setVisible(visibility, false); BlendingMode blendingMode = BlendingMode.fromSVGName(layerBlendingMode); if(DEBUG) { System.out.println("OpenRaster::readOpenRaster: blendingMode = " + blendingMode); } layer.setBlendingMode(blendingMode, false, false, false); float opacity = Utils.parseFloat(layerOpacity, 1.0f); layer.setOpacity(opacity, false, false, false); int tX = Utils.parseInt(layerX, 0); int tY = Utils.parseInt(layerY, 0); // TODO assuming that there is no layer mask layer.setTranslation(tX, tY); if(DEBUG) { System.out.println(String.format("OpenRaster::readOpenRaster: opacity = %.2f, tX = %d, tY = %d", opacity, tX, tY)); } comp.addLayerNoGUI(layer); } comp.setActiveLayer(comp.getLayer(0), false); return comp; } private static Document loadXMLFromString(String xml) throws ParserConfigurationException, IOException, SAXException { if (xml.startsWith("\uFEFF")) { // starts with UTF BOM character // paint.net exported xml files start with this // see http://www.rgagnon.com/javadetails/java-handle-utf8-file-with-bom.html xml = xml.substring(1); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); InputSource is = new InputSource(new StringReader(xml)); return builder.parse(is); } private static String extractString(InputStream is) { Scanner s = new Scanner(is).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } }