OSDN Git Service

zipとpsd形式での保存に対応
authorseraphy <seraphy@users.osdn.me>
Tue, 15 Jan 2019 17:34:24 +0000 (02:34 +0900)
committerseraphy <seraphy@users.osdn.me>
Tue, 15 Jan 2019 17:34:24 +0000 (02:34 +0900)
ただし、psd形式はGimp、FireAlpaca、およびLebreOffice Drawなどでは正常に表示されるが、
本家であるAdobe系のアプリケーションではレイヤーがずれた状態で表示される。

src/main/java/charactermanaj/graphics/AsyncImageBuilder.java
src/main/java/charactermanaj/graphics/ImageBuildJobAbstractAdaptor.java
src/main/java/charactermanaj/graphics/ImageBuilder.java
src/main/java/charactermanaj/graphics/io/ImageSaveHelper.java
src/main/java/charactermanaj/graphics/io/PSDCreator.java [new file with mode: 0644]
src/main/java/charactermanaj/ui/MainFrame.java

index ca157e6..19cb92c 100644 (file)
@@ -8,7 +8,7 @@ import java.util.logging.Logger;
  * 各パーツ情報をもとに非同期にイメージを合成する
  * @author seraphy
  */
-public class AsyncImageBuilder extends ImageBuilder implements Runnable {
+public class AsyncImageBuilder implements ImageBuilder, Runnable {
 
        /**
         * ロガー
@@ -27,24 +27,24 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
                 * @param ticket このイメージビルダでリクエストを受け付けた通し番号
                 */
                void onQueueing(long ticket);
-               
+
                /**
                 * リクエストを処理するまえに破棄された場合に呼び出される.<br>
                 */
                void onAbandoned();
        }
-       
+
        /**
         * 同期オブジェクト
         */
        private final Object lock = new Object();
-       
+
        /**
         * チケットのシリアルナンバー.<br>
         * リクエストがあるごとにインクリメントされる.<br>
         */
        private long ticketSerialNum = 0;
-       
+
        /**
         * リクエストされているジョブ、なければnull
         */
@@ -54,22 +54,35 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
         * 停止フラグ(volatile)
         */
        private volatile boolean stopFlag;
-       
+
        /**
         * スレッド
         */
        private Thread thread;
-       
+
+       /**
+        * イメージビルダ
+        */
+       private ImageBuilder imageBuilder;
+
        /**
         * イメージローダを指定して構築する.
         * @param imageLoader イメージローダー
         */
        public AsyncImageBuilder(ColorConvertedImageCachedLoader imageLoader) {
-               super(imageLoader);
+               imageBuilder = new ImageBuilderImpl(imageLoader);
                thread = new Thread(this);
                thread.setDaemon(true);
        }
-       
+
+       /**
+        * 同期イメージビルダを取得する。
+        * @return
+        */
+       public ImageBuilder getImageBuilder() {
+               return imageBuilder;
+       }
+
        /**
         * スレッドの実行部.
         */
@@ -95,12 +108,12 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
                                        lock.notifyAll();
                                }
                                // リクエストを処理する.
-                               AsyncImageBuilder.super.requestJob(job);
-                               
+                               imageBuilder.requestJob(job);
+
                        } catch (InterruptedException ex) {
                                logger.log(Level.FINE, "AsyncImageBuilder thead interrupted.");
                                // 割り込みされた場合、単にループを再開する.
-                               
+
                        } catch (Exception ex) {
                                logger.log(Level.SEVERE, "AsyncImageBuilder failed.", ex);
                                // ジョブ合成中の予期せぬ例外はログに記録するのみで
@@ -110,7 +123,7 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
                }
                logger.log(Level.FINE, "AsyncImageBuilder thread stopped.");
        }
-       
+
        /**
         * イメージ作成ジョブをリクエストする.<br>
         * イメージ作成ジョブは非同期に実行される.<br>
@@ -123,7 +136,7 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
                        if (this.requestJob != null && this.requestJob instanceof AsyncImageBuildJob) {
                                ((AsyncImageBuildJob) this.requestJob).onAbandoned();
                        }
-                       
+
                        // リクエストをセットして待機中のスレッドに通知を出す.
                        this.requestJob = imageSource;
                        if (imageSource != null && imageSource instanceof AsyncImageBuildJob) {
@@ -142,7 +155,7 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
        public boolean isAlive() {
                return thread.isAlive();
        }
-       
+
        /**
         * スレッドを開始する.
         */
@@ -168,5 +181,4 @@ public class AsyncImageBuilder extends ImageBuilder implements Runnable {
                        }
                }
        }
-       
 }
index 5d82593..ca0a7e6 100644 (file)
@@ -52,7 +52,8 @@ public abstract class ImageBuildJobAbstractAdaptor implements AsyncImageBuilder.
                                if (param == null) {
                                        param = new ColorConvertParameter();
                                }
-                               collector.setImageSource(layer, layerOrder, imageResource, param);
+                               String partsName = partsIdentifier.getPartsName();
+                               collector.setImageSource(partsName, layer, layerOrder, imageResource, param);
                        }
                });
                collector.setComplite();
index a16787d..8d1864c 100644 (file)
@@ -29,12 +29,7 @@ import charactermanaj.model.Layer;
  *
  * @author seraphy
  */
-public class ImageBuilder {
-
-       /**
-        * 各パーツ情報の読み取りタイムアウト
-        */
-       private static final int MAX_TIMEOUT = 20; // Secs
+public interface ImageBuilder {
 
        /**
         * 各パーツ情報を設定するためのインターフェイス.<br>
@@ -75,6 +70,8 @@ public class ImageBuilder {
                 * 複数パーツある場合は、これを繰り返し呼び出す.<br>
                 * すべて呼び出したらsetCompliteを呼び出す.<br>
                 *
+                * @param partsName
+                *             パーツ名
                 * @param layer
                 *            レイヤー
                 * @param layerOrder
@@ -84,7 +81,7 @@ public class ImageBuilder {
                 * @param param
                 *            色変換情報
                 */
-               void setImageSource(Layer layer, float layerOrder, ImageResource imageResource, ColorConvertParameter param);
+               void setImageSource(String partsName, Layer layer, float layerOrder, ImageResource imageResource, ColorConvertParameter param);
 
                /**
                 * パーツの登録が完了したことを通知する。
@@ -148,6 +145,21 @@ public class ImageBuilder {
                void handleException(Throwable ex);
        }
 
+       public interface ImageBuildJob2 extends ImageBuildJob {
+
+               void onCreateLayerImage(String partsName, Layer layer, BufferedImage img);
+       }
+
+       boolean requestJob(final ImageBuildJob imageBuildJob);
+}
+
+class ImageBuilderImpl implements ImageBuilder {
+
+       /**
+        * 各パーツ情報の読み取りタイムアウト
+        */
+       private static final int MAX_TIMEOUT = 20; // Secs
+
        /**
         * イメージ構築に使用したパーツ情報
         *
@@ -157,19 +169,23 @@ public class ImageBuilder {
 
                private final ImageBuildPartsInfo partsInfo;
 
-               private final long lastModified;
+               private final LoadedImage loadedImage;
 
                public BuildedPartsInfo(ImageBuildPartsInfo partsInfo, LoadedImage loadedImage) {
                        this.partsInfo = partsInfo;
-                       this.lastModified = loadedImage.getLastModified();
+                       this.loadedImage = loadedImage;
                }
 
                public ImageBuildPartsInfo getPartsInfo() {
                        return partsInfo;
                }
 
+               public LoadedImage getLoadedImage() {
+                       return loadedImage;
+               }
+
                public long getLastModified() {
-                       return lastModified;
+                       return loadedImage.getLastModified();
                }
        }
 
@@ -279,6 +295,15 @@ public class ImageBuilder {
                }
 
                /**
+                * イメージに構築したパーツ情報を取得する。
+                * 構築順序で返される。
+                * @return パーツ情報と構築されたレイヤーイメージ
+                */
+               public List<BuildedPartsInfo> getBuildPartsInfos() {
+                       return buildPartsInfos;
+               }
+
+               /**
                 * イメージ構築結果を取得する.
                 *
                 * @return イメージ構築結果
@@ -364,7 +389,7 @@ public class ImageBuilder {
         * @param imageLoader
         *            イメージローダー
         */
-       public ImageBuilder(ColorConvertedImageCachedLoader imageLoader) {
+       public ImageBuilderImpl(ColorConvertedImageCachedLoader imageLoader) {
                if (imageLoader == null) {
                        throw new IllegalArgumentException();
                }
@@ -391,16 +416,19 @@ public class ImageBuilder {
                // loadPartsが非同期に行われる場合、すぐに制御を戻す.
                imageBuildJob.loadParts(new ImageSourceCollector() {
                        // ジョブリクエスト側よりイメージサイズの設定として呼び出される
+                       @Override
                        public void setSize(Dimension size) {
                                synchronized (imageBuildInfo) {
                                        imageBuildInfo.setRect(size.width, size.height);
                                }
                        }
+                       @Override
                        public void setImageBgColor(Color color) {
                                synchronized (imageBuildInfo) {
                                        imageBuildInfo.setImageBgColor(color);
                                }
                        }
+                       @Override
                        public void setAffineTramsform(double[] param) {
                                if (param != null && !(param.length == 4 || param.length == 6)) {
                                        throw new IllegalArgumentException("affineTransformParameter invalid length.");
@@ -410,14 +438,16 @@ public class ImageBuilder {
                                }
                        }
                        // ジョブリクエスト側よりパーツの登録として呼び出される
-                       public void setImageSource(Layer layer, float layerOrder, ImageResource imageResource,
+                       @Override
+                       public void setImageSource(String partsName, Layer layer, float layerOrder, ImageResource imageResource,
                                        ColorConvertParameter param) {
                                synchronized (imageBuildInfo) {
                                        imageBuildInfo.add(new ImageBuildPartsInfo(
-                                                       imageBuildInfo.getPartsCount(), layer, layerOrder, imageResource, param));
+                                                       partsName, imageBuildInfo.getPartsCount(), layer, layerOrder, imageResource, param));
                                }
                        }
                        // ジョブリクエスト側よりイメージサイズとパーツの登録が完了したことを通知される.
+                       @Override
                        public void setComplite() {
                                compliteLock.release();
                        }
@@ -512,6 +542,7 @@ public class ImageBuilder {
         *            イメージを構築するジョブ
         * @return 画像がただちに得られた場合はtrue、そうでなければfalse
         */
+       @Override
        public boolean requestJob(final ImageBuildJob imageBuildJob) {
                if (imageBuildJob == null) {
                        throw new IllegalArgumentException();
@@ -544,6 +575,19 @@ public class ImageBuilder {
                                        lastUsedImageBuildInfo = imageBuildInfo;
                                }
 
+                               // 構築したレイヤーごとの画像を通知する
+                               if (imageBuildJob instanceof ImageBuildJob2) {
+                                       ImageBuildJob2 callback = (ImageBuildJob2) imageBuildJob;
+                                       for (BuildedPartsInfo bpInfo : lastUsedImageBuildInfo.getBuildPartsInfos()) {
+                                               ImageBuildPartsInfo partsInfo = bpInfo.getPartsInfo();
+                                               String partsName = partsInfo.getPartsName();
+                                               Layer layer = partsInfo.getLayer();
+                                               LoadedImage loadedImage = bpInfo.getLoadedImage();
+                                               BufferedImage img = loadedImage.getImage();
+                                               callback.onCreateLayerImage(partsName, layer, img);
+                                       }
+                               }
+
                                // 完成したカンバスを合成結果として通知する.
                                imageBuildJob.buildImage(new ImageOutput() {
                                        public BufferedImage getImageOutput() {
@@ -575,6 +619,11 @@ public class ImageBuilder {
 final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {
 
        /**
+        * パーツ名
+        */
+       private String partsName;
+
+       /**
         * 定義順
         */
        private int order;
@@ -590,8 +639,9 @@ final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {
 
        private ColorConvertParameter colorParam;
 
-       public ImageBuildPartsInfo(int order, Layer layer, float layerOrder, ImageResource imageResource,
+       public ImageBuildPartsInfo(String partsName, int order, Layer layer, float layerOrder, ImageResource imageResource,
                        ColorConvertParameter colorParam) {
+               this.partsName = partsName;
                this.order = order;
                this.layer = layer;
                this.layerOrder = layerOrder;
@@ -601,7 +651,7 @@ final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {
 
        @Override
        public int hashCode() {
-               return order ^ layer.hashCode() ^ imageResource.hashCode();
+               return partsName.hashCode() ^  order ^ layer.hashCode() ^ imageResource.hashCode();
        }
 
        @Override
@@ -611,7 +661,7 @@ final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {
                }
                if (obj != null && obj instanceof ImageBuildPartsInfo) {
                        ImageBuildPartsInfo o = (ImageBuildPartsInfo) obj;
-                       return order == o.order && layer.equals(o.layer)
+                       return order == o.order && partsName.equals(o.partsName) && layer.equals(o.layer)
                                        && imageResource.equals(o.imageResource) && colorParam.equals(o.colorParam);
                }
                return false;
@@ -635,6 +685,10 @@ final class ImageBuildPartsInfo implements Comparable<ImageBuildPartsInfo> {
                return ret;
        }
 
+       public String getPartsName() {
+               return partsName;
+       }
+
        public int getOrder() {
                return order;
        }
index b27d909..200e493 100644 (file)
@@ -11,11 +11,18 @@ import java.awt.Insets;
 import java.awt.RenderingHints;
 import java.awt.image.BufferedImage;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -38,8 +45,12 @@ import javax.swing.JSpinner;
 import javax.swing.SpinnerNumberModel;
 import javax.swing.filechooser.FileFilter;
 
+import org.apache.tools.zip.ZipEntry;
+import org.apache.tools.zip.ZipOutputStream;
+
 import charactermanaj.graphics.io.OutputOption.PictureMode;
 import charactermanaj.graphics.io.OutputOption.ZoomRenderingType;
+import charactermanaj.model.Layer;
 import charactermanaj.util.LocalizedMessageComboBoxRender;
 import charactermanaj.util.LocalizedResourcePropertyLoader;
 
@@ -48,7 +59,7 @@ import charactermanaj.util.LocalizedResourcePropertyLoader;
  * イメージを保存するためのヘルパークラス.<br>
  */
 public class ImageSaveHelper {
-       
+
        /**
         * ロガー
         */
@@ -74,7 +85,7 @@ public class ImageSaveHelper {
 
                        return isSupported(f);
                }
-               
+
                protected boolean isSupported(File f) {
                        // サポートしている拡張子のいずれかにマッチするか?
                        // (大文字・小文字は区別しない.)
@@ -89,7 +100,7 @@ public class ImageSaveHelper {
 
                /**
                 * 現在の選択されたファイル名を取得し、そのファイル名がデフォルトの拡張子で終端していなければ
-                * デフォルトの拡張子を設定してファイルチューザに設定し直す.<Br> 
+                * デフォルトの拡張子を設定してファイルチューザに設定し直す.<Br>
                 * @param fileChooser ファイルチューザ
                 * @return デフォルトの拡張子で終端されたファイル
                 */
@@ -98,7 +109,7 @@ public class ImageSaveHelper {
                        if (outFile == null) {
                                return null;
                        }
-                       
+
                        if ( !isSupported(outFile)) {
                                String extName = "." + getSupprotedExtension()[0];
                                outFile = new File(outFile.getPath() + extName);
@@ -106,7 +117,7 @@ public class ImageSaveHelper {
                        }
                        return outFile;
                }
-               
+
                /**
                 * サポートするファイルの拡張子を取得する.<br>
                 * 最初のものがデフォルトの拡張子として用いられる.<br>
@@ -114,7 +125,7 @@ public class ImageSaveHelper {
                 */
                protected abstract String[] getSupprotedExtension();
        }
-       
+
        /**
         * PNGファイルフィルタ
         */
@@ -156,13 +167,41 @@ public class ImageSaveHelper {
                        return new String[] {"bmp"};
                }
        };
-       
+
+       /**
+        * ZIPファイルフィルタ
+        */
+       protected static final FileFilter zipFilter = new ImageSaveHelperFilter() {
+               @Override
+               public String getDescription() {
+                       return "Zip Archive(*.zip)";
+               }
+               @Override
+               protected String[] getSupprotedExtension() {
+                       return new String[] {"zip"};
+               }
+       };
+
+       /**
+        * PSDファイルフィルタ
+        */
+       protected static final FileFilter psdFilter = new ImageSaveHelperFilter() {
+               @Override
+               public String getDescription() {
+                       return "Adobe Photoshop(*.psd)";
+               }
+               @Override
+               protected String[] getSupprotedExtension() {
+                       return new String[] {"psd"};
+               }
+       };
+
        /**
         * このヘルパクラスで定義されているファイルフィルタのリスト
         */
        protected static final List<FileFilter> fileFilters = Arrays.asList(
-                       pngFilter, jpegFilter, bmpFilter);
-       
+                       pngFilter, jpegFilter, bmpFilter, psdFilter, zipFilter);
+
        /**
         * イメージビルダファクトリ
         */
@@ -173,17 +212,17 @@ public class ImageSaveHelper {
         * 未使用であれば規定値.<br>
         */
        protected OutputOption outputOption;
-       
+
        /**
         * 最後に使用したディレクトリ
         */
        protected File lastUseSaveDir;
-       
+
        /**
         * 最後に使用したフィルタ
         */
        protected FileFilter lastUseFilter = pngFilter;
-       
+
        /**
         * 最後に使用したディレクトリを設定する
         * @param lastUseSaveDir 最後に使用したディレクトリ、設定しない場合はnull
@@ -191,7 +230,7 @@ public class ImageSaveHelper {
        public void setLastUseSaveDir(File lastUseSaveDir) {
                this.lastUseSaveDir = lastUseSaveDir;
        }
-       
+
        /**
         * 最後に使用したディレクトリを取得する
         * @return 最後に使用したディレクトリ、なければnull
@@ -199,7 +238,7 @@ public class ImageSaveHelper {
        public File getLastUsedSaveDir() {
                return lastUseSaveDir;
        }
-       
+
 
        /**
         * コンストラクタ
@@ -235,7 +274,7 @@ public class ImageSaveHelper {
                                if (selfilter instanceof ImageSaveHelperFilter) {
                                        outFile = ((ImageSaveHelperFilter) selfilter).supplyDefaultExtension(this);
                                }
-                               
+
                                // ファイルが存在すれば上書き確認する.
                                if (outFile.exists()) {
                                        Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
@@ -249,15 +288,15 @@ public class ImageSaveHelper {
                                                return;
                                        }
                                }
-                               
+
                                super.approveSelection();
                        }
                };
-               
+
 //             // アクセサリパネルの追加˙
 //             final OutputOptionPanel accessoryPanel = new OutputOptionPanel(this.outputOption);
 //             fileChooser.setAccessory(accessoryPanel);
-               
+
                // ファイルフィルタ設定
                fileChooser.setAcceptAllFileFilterUsed(false);
                for (FileFilter fileFilter : fileFilters) {
@@ -277,7 +316,7 @@ public class ImageSaveHelper {
                if (ret != JFileChooser.APPROVE_OPTION) {
                        return null;
                }
-               
+
 //             // 出力オプションの保存
 //             OutputOption outputOption = accessoryPanel.getOutputOption();
 //             this.outputOption = outputOption;
@@ -286,15 +325,15 @@ public class ImageSaveHelper {
                File outFile = fileChooser.getSelectedFile();
                lastUseSaveDir = outFile.getParentFile();
                lastUseFilter = fileChooser.getFileFilter();
-               
+
                // 選択したファイルを返す.
-               return outFile; 
+               return outFile;
        }
-       
+
        public OutputOption getOutputOption() {
                return outputOption.clone();
        }
-       
+
        public void setOutputOption(OutputOption outputOption) {
                if (outputOption == null) {
                        throw new IllegalArgumentException();
@@ -303,6 +342,22 @@ public class ImageSaveHelper {
        }
 
        /**
+        * ファイルから拡張子を取得する。
+        * @param outFile ファイル名
+        * @return 拡張子
+        * @throws IOException 拡張子がない場合
+        */
+       public String getFileExtension(File outFile) throws IOException {
+               // ファイル名から拡張子を取り出します.
+               String fname = outFile.getName();
+               int extpos = fname.lastIndexOf(".");
+               if (extpos < 0) {
+                       throw new IOException("missing file extension.");
+               }
+               return fname.substring(extpos + 1).toLowerCase();
+       }
+
+       /**
         * ファイル名を指定してイメージをファイルに出力します.<br>
         * 出力形式は拡張子より判定します.<br>
         * サポートされていない拡張子の場合はIOException例外が発生します.<br>
@@ -317,27 +372,21 @@ public class ImageSaveHelper {
                if (img == null || outFile == null) {
                        throw new IllegalArgumentException();
                }
-               
-               // ファイル名から拡張子を取り出します.
-               String fname = outFile.getName();
-               int extpos = fname.lastIndexOf(".");
-               if (extpos < 0) {
-                       throw new IOException("missing file extension.");
-               }
-               String ext = fname.substring(extpos + 1).toLowerCase();
+
+               String ext = getFileExtension(outFile);
 
                // 拡張子に対するImageIOのライタを取得します.
                Iterator<ImageWriter> ite = ImageIO.getImageWritersBySuffix(ext);
                if (!ite.hasNext()) {
                        throw new IOException("unsupported file extension: " + ext);
                }
-               
+
                ImageWriter iw = ite.next();
-               
+
                // ライタを使いイメージを書き込みます.
                savePicture(img, imgBgColor, iw, outFile, warnings);
        }
-       
+
        /**
         * イメージをMIMEで指定された形式で出力します.
         * @param img イメージ
@@ -370,7 +419,7 @@ public class ImageSaveHelper {
                savePicture(img, imgBgColor, iw, outstm, warnings);
                outstm.flush();
        }
-       
+
        protected void savePicture(BufferedImage img, Color imgBgColor, ImageWriter iw,
                        Object output, final StringBuilder warnings)
                        throws IOException {
@@ -440,7 +489,7 @@ public class ImageSaveHelper {
                        iw.dispose();
                }
        }
-       
+
        /**
         * ARGB形式から、アルファチャンネルを削除し、かわりに背景色を設定したBGR形式画像を返します.<br>
         * JPEG画像として用いることを想定しています.<br>
@@ -514,6 +563,121 @@ public class ImageSaveHelper {
                }
                return tmpImg;
        }
+
+       public static class LayerImage {
+
+               private String partsName;
+
+               private Layer layer;
+
+               private BufferedImage image;
+
+               public LayerImage(String partsName, Layer layer, BufferedImage image) {
+                       super();
+                       this.partsName = partsName;
+                       this.layer = layer;
+                       this.image = image;
+               }
+
+               public String getPartsName() {
+                       return partsName;
+               }
+
+               public Layer getLayer() {
+                       return layer;
+               }
+
+               public BufferedImage getImage() {
+                       return image;
+               }
+
+               @Override
+               public String toString() {
+                       return "(partsName=" + partsName + ", layer=" + layer + ")";
+               }
+       }
+
+       /**
+        * zipにレイヤーごとの画像とプレビュー画像をまとめて保存する
+        * @param outFile
+        * @param layerImages
+        * @param compositeImg
+        * @throws IOException
+        */
+       public void saveToZip(File outFile, Collection<LayerImage> layerImages, BufferedImage compositeImg)
+                       throws IOException {
+               ZipOutputStream zos = new ZipOutputStream(outFile);
+               try {
+                       if (layerImages != null) {
+                               for (LayerImage layerImage : layerImages) {
+                                       String partsName = layerImage.getPartsName();
+                                       Layer layer = layerImage.getLayer();
+                                       String dir = layer.getDir();
+                                       String fname = dir + "/" + partsName + ".png";
+
+                                       ZipEntry zipEntry = new ZipEntry(fname);
+                                       zos.putNextEntry(zipEntry);
+                                       ImageIO.write(layerImage.getImage(), "png", zos);
+                                       zos.closeEntry();
+                               }
+                       }
+                       if (compositeImg != null) {
+                               ZipEntry zipEntry = new ZipEntry("preview.png");
+                               zos.putNextEntry(zipEntry);
+                               ImageIO.write(compositeImg, "png", zos);
+                               zos.closeEntry();
+                       }
+
+               } finally {
+                       zos.close();
+               }
+       }
+
+       /**
+        * PSD形式で保存する
+        * @param outFile
+        * @param layerImages
+        * @param compositeImg
+        * @throws IOException
+        */
+       public void saveToPSD(File outFile, Collection<LayerImage> layerImages, BufferedImage compositeImg)
+                       throws IOException {
+               List<PSDCreator.LayerData> layerDatas = new ArrayList<PSDCreator.LayerData>();
+               Map<String, Integer> dupchk = new HashMap<String, Integer>();
+               for (LayerImage layerImage : layerImages) {
+                       Layer layer = layerImage.getLayer();
+                       String layerName = layer.getDir();
+                       Integer cnt = dupchk.get(layerName);
+                       if (cnt == null) {
+                               cnt = 1;
+                       } else {
+                               cnt = cnt + 1;
+                               layerName = layerName + "(" + cnt + ")";
+                       }
+                       dupchk.put(layerName, cnt);
+
+                       BufferedImage img = layerImage.getImage();
+                       PSDCreator.LayerData layerData = new PSDCreator.LayerData(layerName, img);
+                       layerDatas.add(layerData);
+               }
+               byte[] psdContents = PSDCreator.createPSD(layerDatas);
+
+               FileOutputStream fos = new FileOutputStream(outFile);
+               try {
+                       FileChannel fc = fos.getChannel();
+                       try {
+                               ByteBuffer buf = ByteBuffer.wrap(psdContents);
+                               while (buf.hasRemaining()) {
+                                       fc.write(buf);
+                               }
+
+                       } finally {
+                               fc.close();
+                       }
+               } finally {
+                       fos.close();
+               }
+       }
 }
 
 /**
@@ -524,18 +688,18 @@ class OutputOptionPanel extends JPanel {
        private static final long serialVersionUID = 1L;
 
        private JSpinner jpegQualitySpinner;
-       
+
        private JCheckBox lblZoom;
-       
+
        private JSpinner zoomSpinner;
-       
+
        private JComboBox zoomAlgoCombo;
-       
+
        private JComboBox pictureModeCombo;
-       
+
        private JCheckBox checkForceBgColor;
 
-       
+
        public OutputOptionPanel() {
                this(new OutputOption());
        }
@@ -566,7 +730,7 @@ class OutputOptionPanel extends JPanel {
                gbc.gridwidth = 1;
                gbc.weightx = 0.;
                add(Box.createHorizontalStrut(6), gbc);
-               
+
                // JPEG
                gbc.gridx = 0;
                gbc.gridy = 0;
@@ -576,7 +740,7 @@ class OutputOptionPanel extends JPanel {
                                strings.getProperty("outputOption.jpeg.caption"));
                lblJpeg.setFont(lblJpeg.getFont().deriveFont(Font.BOLD));
                add(lblJpeg, gbc);
-               
+
                gbc.gridx = 1;
                gbc.gridy = 1;
                gbc.gridwidth = 1;
@@ -584,10 +748,10 @@ class OutputOptionPanel extends JPanel {
                add(new JLabel(
                                strings.getProperty("outputOption.jpeg.quality"),
                                JLabel.RIGHT), gbc);
-               
+
                SpinnerNumberModel spmodel = new SpinnerNumberModel(100, 10, 100, 1);
                this.jpegQualitySpinner = new JSpinner(spmodel);
-               
+
                gbc.gridx = 2;
                gbc.gridy = 1;
                gbc.gridwidth = 1;
@@ -607,7 +771,7 @@ class OutputOptionPanel extends JPanel {
                lblZoom = new JCheckBox(strings.getProperty("outputOption.zoom.caption"));
                lblZoom.setFont(lblJpeg.getFont().deriveFont(Font.BOLD));
                add(lblZoom, gbc);
-               
+
                gbc.gridx = 1;
                gbc.gridy = 3;
                gbc.gridwidth = 1;
@@ -615,15 +779,15 @@ class OutputOptionPanel extends JPanel {
                gbc.insets = new Insets(3, 3, 3, 3);
                add(new JLabel(
                                strings.getProperty("outputOption.zoom.factor"), JLabel.RIGHT), gbc);
-               
+
                SpinnerNumberModel zoomSpModel = new SpinnerNumberModel(100, 20, 800, 1);
                this.zoomSpinner = new JSpinner(zoomSpModel);
-               
+
                gbc.gridx = 2;
                gbc.gridy = 3;
                gbc.gridwidth = 1;
                add(zoomSpinner, gbc);
-               
+
                gbc.gridx = 3;
                gbc.gridy = 3;
                gbc.gridwidth = 1;
@@ -636,7 +800,7 @@ class OutputOptionPanel extends JPanel {
                add(new JLabel(
                                strings.getProperty("outputOption.zoom.renderingMode"),
                                JLabel.RIGHT), gbc);
-               
+
                this.zoomAlgoCombo = new JComboBox(ZoomRenderingType.values());
                this.zoomAlgoCombo.setRenderer(new LocalizedMessageComboBoxRender(strings));
                gbc.gridx = 2;
@@ -644,7 +808,7 @@ class OutputOptionPanel extends JPanel {
                gbc.gridwidth = 2;
                gbc.weightx = 0.;
                add(zoomAlgoCombo, gbc);
-               
+
                // 画像モード
                gbc.gridx = 0;
                gbc.gridy = 5;
@@ -654,7 +818,7 @@ class OutputOptionPanel extends JPanel {
                                strings.getProperty("outputOption.picture"));
                lblPictureMode.setFont(lblJpeg.getFont().deriveFont(Font.BOLD));
                add(lblPictureMode, gbc);
-               
+
                gbc.gridx = 1;
                gbc.gridy = 6;
                gbc.gridwidth = 1;
@@ -662,7 +826,7 @@ class OutputOptionPanel extends JPanel {
                add(new JLabel(
                                strings.getProperty("outputOption.picture.type"),
                                JLabel.RIGHT), gbc);
-               
+
                this.pictureModeCombo = new JComboBox(PictureMode.values());
                this.pictureModeCombo.setRenderer(
                                new LocalizedMessageComboBoxRender(strings));
@@ -670,24 +834,24 @@ class OutputOptionPanel extends JPanel {
                gbc.gridy = 6;
                gbc.gridwidth = 2;
                gbc.weightx = 1.;
-               add(pictureModeCombo, gbc);             
-               
+               add(pictureModeCombo, gbc);
+
                gbc.gridx = 1;
                gbc.gridy = 7;
                gbc.gridwidth = 3;
                gbc.weightx = 0.;
                checkForceBgColor = new JCheckBox(
-                               strings.getProperty("outputOption.picture.forceBgColor"));  
+                               strings.getProperty("outputOption.picture.forceBgColor"));
                add(checkForceBgColor, gbc);
-               
+
                JPanel pnlBgAlpha = new JPanel(new BorderLayout(3, 3));
                pnlBgAlpha.add(new JLabel("背景アルファ"), BorderLayout.WEST);
-               
+
                SpinnerNumberModel bgAlphaModel = new SpinnerNumberModel(255, 0, 255, 1);
                JSpinner bgAlphaSpinner = new JSpinner(bgAlphaModel);
 
                pnlBgAlpha.add(bgAlphaSpinner, BorderLayout.CENTER);
-               
+
                gbc.gridx = 1;
                gbc.gridy = 8;
                gbc.gridwidth = 3;
@@ -701,16 +865,16 @@ class OutputOptionPanel extends JPanel {
                gbc.weightx = 1.;
                gbc.weighty = 1.;
                add(Box.createGlue(), gbc);
-               
+
                // update
                setOutputOption(outputOption);
        }
-       
+
        public void setOutputOption(OutputOption outputOption) {
                if (outputOption == null) {
                        outputOption = new OutputOption();
                }
-               
+
                jpegQualitySpinner.setValue((int) (outputOption.getJpegQuality() * 100));
                lblZoom.setSelected(outputOption.isEnableZoom());
                zoomSpinner.setValue((int) (outputOption.getZoomFactor() * 100));
@@ -728,7 +892,7 @@ class OutputOptionPanel extends JPanel {
                outputOption.setZoomRenderingType((ZoomRenderingType) zoomAlgoCombo.getSelectedItem());
                outputOption.setPictureMode((PictureMode) pictureModeCombo.getSelectedItem());
                outputOption.setForceBgColor(checkForceBgColor.isSelected());
-               
+
                return outputOption;
        }
 }
diff --git a/src/main/java/charactermanaj/graphics/io/PSDCreator.java b/src/main/java/charactermanaj/graphics/io/PSDCreator.java
new file mode 100644 (file)
index 0000000..a50e06b
--- /dev/null
@@ -0,0 +1,308 @@
+package charactermanaj.graphics.io;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * 複数レイヤー画像をPSD形式のデータとして作成する。
+ * https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
+ *
+ * @author seraphy
+ */
+public final class PSDCreator {
+
+       /**
+        * レイヤーデータ
+        */
+       public static class LayerData {
+
+               /**
+                * レイヤー名
+                */
+               private String layerName;
+
+               /**
+                * レイヤーの画像(TYPE_INT_ARGB限定)
+                */
+               private BufferedImage image;
+
+               public LayerData(String layerName, BufferedImage image) {
+                       this.layerName = layerName;
+                       this.image = image;
+               }
+
+               public String getLayerName() {
+                       return layerName;
+               }
+
+               public BufferedImage getImage() {
+                       return image;
+               }
+       }
+
+       /**
+        * プライベートコンストラクタ
+        */
+       private PSDCreator() {
+               super();
+       }
+
+       /**
+        * レイヤーを指定してPSDデータを作成する
+        * @param layerDatas レイヤーのコレクション、順番に重ねられる
+        * @return PSDデータ
+        * @throws IOException
+        */
+       public static byte[] createPSD(Collection<LayerData> layerDatas) throws IOException {
+               if (layerDatas == null) {
+                       throw new NullPointerException("layerDatas is required.");
+               }
+               if (layerDatas.isEmpty()) {
+                       throw new IllegalArgumentException("layerDatas must not be empty.");
+               }
+
+               BufferedImage cimg = createCompositeImage(layerDatas);
+               int width = cimg.getWidth();
+               int height = cimg.getHeight();
+
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+
+               dos.write("8BPS".getBytes());
+               dos.writeShort(1);
+               dos.write(new byte[6]); // reserved 6bytes
+
+               dos.writeShort(4); // argb
+
+               dos.writeInt(height);
+               dos.writeInt(width);
+
+               int depth = 8;
+               dos.writeShort(depth);
+
+               dos.writeShort(3); // ColorMode=RGB(3)
+
+               dos.writeInt(0); // カラーモードセクションなし
+               dos.writeInt(0); // リソースセクションなし
+
+               // レイヤーマスクセクション
+               byte[] layerMaskSection = createLayerMaskSection(layerDatas);
+               dos.writeInt(layerMaskSection.length);
+               dos.write(layerMaskSection);
+
+               // 画像セクション
+               dos.writeShort(0); // RAW
+               byte[][] channelDatas = createChannels(cimg);
+               int[] channelMap = { 1, 2, 3, 0 }; // R, G, B, Aにマップ
+               for (int channel = 0; channel < channelMap.length; channel++) {
+                       byte[] channelData = channelDatas[channelMap[channel]];
+                       dos.write(channelData);
+               }
+
+               return bos.toByteArray();
+       }
+
+       /**
+        * レイヤーマスクセクションを作成する
+        * @param layerDatas
+        * @return
+        * @throws IOException
+        */
+       private static byte[] createLayerMaskSection(Collection<LayerData> layerDatas) throws IOException {
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+
+               byte[] layerData = createLayerData(layerDatas);
+               dos.writeInt(layerData.length);
+               dos.write(layerData);
+
+               return bos.toByteArray();
+       }
+
+       /**
+        * レイヤーデータの作成
+        * @param layerDatas
+        * @return
+        * @throws IOException
+        */
+       private static byte[] createLayerData(Collection<LayerData> layerDatas) throws IOException {
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+
+               int numOfLayers = layerDatas.size();
+               dos.writeShort(numOfLayers); // non pre-multiplied
+
+               short[] channels = { -1, 0, 1, 2 }; // ALPHA, RED, GREEN, BLUE
+
+               for (LayerData layerData : layerDatas) {
+                       String layerName = layerData.getLayerName();
+                       BufferedImage image = layerData.getImage();
+                       int width = image.getWidth();
+                       int height = image.getHeight();
+
+                       dos.writeInt(0); // top
+                       dos.writeInt(0); // left
+                       dos.writeInt(height); // bottom
+                       dos.writeInt(width); // right
+
+                       dos.writeShort(channels.length);
+
+                       int rawSize = width * height;
+                       for (int channel = 0; channel < channels.length; channel++) {
+                               dos.writeShort(channels[channel]);
+                               dos.writeInt(rawSize);
+                       }
+
+                       dos.write("8BIM".getBytes());
+                       dos.write("norm".getBytes());
+
+                       dos.write((byte) 255); // opacity
+                       dos.write((byte) 0); // clipping
+                       dos.write((byte) 0); // protection
+                       dos.write((byte) 0); // filler
+
+                       byte[] layerMaskData = createLayerMaskData();
+                       byte[] layerBlendingData = createLayerBlendingData();
+                       byte[] layerNameData = createLayerName(layerName);
+                       int lenOfAdditional = layerMaskData.length + layerBlendingData.length + layerNameData.length;
+
+                       dos.writeInt(lenOfAdditional);
+                       dos.write(layerMaskData);
+                       dos.write(layerBlendingData);
+                       dos.write(layerNameData);
+               }
+
+               for (LayerData layerData : layerDatas) {
+                       BufferedImage image = layerData.getImage();
+
+                       byte[][] channelsData = createChannels(image);
+
+                       for (int channel = 0; channel < channels.length; channel++) {
+                               dos.writeShort(0); // RAW
+
+                               byte[] channelData = channelsData[channel];
+                               dos.write(channelData);
+                       }
+               }
+
+               return bos.toByteArray();
+       }
+
+       /**
+        * 空のレイヤーマスクデータ作成
+        * @return
+        * @throws IOException
+        */
+       private static byte[] createLayerMaskData() throws IOException {
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+               dos.writeInt(0);
+               return bos.toByteArray();
+       }
+
+       /**
+        * 空のレイヤーブレンダーデータの作成
+        * @return
+        * @throws IOException
+        */
+       private static byte[] createLayerBlendingData() throws IOException {
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+               dos.writeInt(0);
+               return bos.toByteArray();
+       }
+
+       /**
+        * レイヤー名の作成
+        * @param layerName
+        * @return
+        * @throws IOException
+        */
+       private static byte[] createLayerName(String layerName) throws IOException {
+               byte[] nameBuf = layerName.getBytes("UTF-8");
+               int layerNameSize = 1 + nameBuf.length; // PASCAL文字列長 (16の倍数サイズ)
+               int blockSize = (layerNameSize / 4) * 4 + ((layerNameSize % 4 > 0) ? 4 : 0);
+               int paddingSize = blockSize - layerNameSize;
+
+               ByteArrayOutputStream bos = new ByteArrayOutputStream();
+               DataOutputStream dos = new DataOutputStream(bos);
+               dos.write((byte) nameBuf.length);
+               dos.write(nameBuf);
+               dos.write(new byte[paddingSize]);
+               return bos.toByteArray();
+       }
+
+       /**
+        * 32ビットARGB形式のBuffeedImageを受け取り、
+        * ARGBのbyte[][]配列に変換して返す。
+        * @param img イメージ
+        * @return チャネル別配列
+        */
+       private static byte[][] createChannels(BufferedImage img) {
+               WritableRaster raster = img.getRaster();
+               DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer();
+               int[] pixels = buffer.getData();
+
+               int width = img.getWidth();
+               int height = img.getHeight();
+               int mx = width * height;
+               byte[][] channels = new byte[4][mx];
+               for (int idx = 0; idx < mx; idx++) {
+                       int argb = pixels[idx];
+
+                       int alpha = (argb >> 24) & 0xff;
+                       int red = (argb >> 16) & 0xff;
+                       int green = (argb >> 8) & 0xff;
+                       int blue = argb & 0xff;
+
+                       channels[0][idx] = (byte) alpha;
+                       channels[1][idx] = (byte) red;
+                       channels[2][idx] = (byte) green;
+                       channels[3][idx] = (byte) blue;
+               }
+
+               return channels;
+       }
+
+       /**
+        * レイヤーコレクションを重ねて1つの画像にして返す
+        * @param layerDatas レイヤーコレクション
+        * @return 重ね合わせた画像
+        */
+       private static BufferedImage createCompositeImage(Collection<LayerData> layerDatas) {
+               int width = 0;
+               int height = 0;
+               for (LayerData layerData : layerDatas) {
+                       BufferedImage img = layerData.getImage();
+                       int w = img.getWidth();
+                       int h = img.getHeight();
+                       if (w > width) {
+                               width = w;
+                       }
+                       if (h > height) {
+                               height = h;
+                       }
+               }
+
+               BufferedImage cimg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+
+               Graphics2D g = cimg.createGraphics();
+               try {
+                       for (LayerData layerData : layerDatas) {
+                               BufferedImage img = layerData.getImage();
+                               int w = img.getWidth();
+                               int h = img.getHeight();
+                               g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null);
+                       }
+               } finally {
+                       g.dispose();
+               }
+               return cimg;
+       }
+}
index 87dcf17..89fb379 100644 (file)
@@ -69,8 +69,13 @@ import charactermanaj.clipboardSupport.ClipboardUtil;
 import charactermanaj.graphics.AsyncImageBuilder;
 import charactermanaj.graphics.ColorConvertedImageCachedLoader;
 import charactermanaj.graphics.ImageBuildJobAbstractAdaptor;
+import charactermanaj.graphics.ImageBuilder;
 import charactermanaj.graphics.ImageBuilder.ImageOutput;
+import charactermanaj.graphics.ImageBuilder.ImageSourceCollector;
+import charactermanaj.graphics.filters.ColorConvertParameter;
+import charactermanaj.graphics.io.ImageResource;
 import charactermanaj.graphics.io.ImageSaveHelper;
+import charactermanaj.graphics.io.ImageSaveHelper.LayerImage;
 import charactermanaj.graphics.io.OutputOption;
 import charactermanaj.graphics.io.UkagakaImageSaveHelper;
 import charactermanaj.model.AppConfig;
@@ -81,6 +86,7 @@ import charactermanaj.model.CharacterDataChangeObserver;
 import charactermanaj.model.ColorGroup;
 import charactermanaj.model.CustomLayerOrder;
 import charactermanaj.model.CustomLayerOrderKey;
+import charactermanaj.model.Layer;
 import charactermanaj.model.LayerOrderMapper;
 import charactermanaj.model.PartsCategory;
 import charactermanaj.model.PartsColorInfo;
@@ -92,6 +98,7 @@ import charactermanaj.model.WorkingSet;
 import charactermanaj.model.io.CharacterDataPersistent;
 import charactermanaj.model.io.CustomLayerOrderPersist;
 import charactermanaj.model.io.CustomLayerOrderPersist.CustomLayerOrderPersistListener;
+import charactermanaj.model.io.PartsImageCollectionParser;
 import charactermanaj.model.io.PartsImageDirectoryWatchAgent;
 import charactermanaj.model.io.PartsImageDirectoryWatchAgentFactory;
 import charactermanaj.model.io.PartsImageDirectoryWatchEvent;
@@ -1708,7 +1715,7 @@ public class MainFrame extends JFrame
                        imageSaveHelper.setOutputOption(outputOption);
 
                        // ファイルダイアログ表示
-                       File outFile = imageSaveHelper.showSaveFileDialog(this);
+                       final File outFile = imageSaveHelper.showSaveFileDialog(this);
                        if (outFile == null) {
                                return;
                        }
@@ -1720,7 +1727,26 @@ public class MainFrame extends JFrame
 
                        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                        try {
-                               imageSaveHelper.savePicture(img, imgBgColor, outFile, warnings);
+                               String ext = imageSaveHelper.getFileExtension(outFile);
+                               if ("zip".equals(ext)) {
+                                       saveLayers(outFile, new SaveLayerCallback() {
+                                               @Override
+                                               public void collect(List<LayerImage> layerImages, BufferedImage compositeImg)
+                                                               throws IOException {
+                                                       imageSaveHelper.saveToZip(outFile, layerImages, compositeImg);
+                                               }
+                                       });
+                               } else if ("psd".equals(ext)) {
+                                       saveLayers(outFile, new SaveLayerCallback() {
+                                               @Override
+                                               public void collect(List<LayerImage> layerImages, BufferedImage compositeImg)
+                                                               throws IOException {
+                                                       imageSaveHelper.saveToPSD(outFile, layerImages, compositeImg);
+                                               }
+                                       });
+                               } else {
+                                       imageSaveHelper.savePicture(img, imgBgColor, outFile, warnings);
+                               }
 
                        } finally {
                                setCursor(Cursor.getDefaultCursor());
@@ -1734,6 +1760,59 @@ public class MainFrame extends JFrame
                }
        }
 
+       private interface SaveLayerCallback {
+               void collect(List<ImageSaveHelper.LayerImage> layerImages, BufferedImage compositeImg) throws IOException;
+       }
+
+       private void saveLayers(final File outFile, SaveLayerCallback callback) throws IOException {
+               final PartsImageCollectionParser partsImageCollectorParser = new PartsImageCollectionParser(characterData);
+               final List<ImageSaveHelper.LayerImage> layerImages = new ArrayList<ImageSaveHelper.LayerImage>();
+               final BufferedImage[] result = new BufferedImage[1];
+               ImageBuilder syncImgBuilder = this.imageBuilder.getImageBuilder(); // 同期型のイメージビルダを取得する
+               syncImgBuilder.requestJob(new ImageBuilder.ImageBuildJob2() {
+                       @Override
+                       public void loadParts(final ImageSourceCollector collector) throws IOException {
+                               PartsSet partsSet = partsSelectionManager.createPartsSet();
+                               partsSet.getActiveCustomLayerPatternIds();
+                               LayerOrderMapper layerOrderMapper = customLayerPatternMgr.getLayerOrderMapper();
+                               collector.setSize(partsImageCollectorParser.getPartsSpecResolver().getImageSize());
+                               collector.setImageBgColor(partsSet.getBgColor());
+                               collector.setAffineTramsform(partsSet.getAffineTransformParameter());
+                               partsImageCollectorParser.parse(partsSet, layerOrderMapper,
+                                               new PartsImageCollectionParser.PartsImageCollectionHandler() {
+                                       public void detectImageSource(PartsIdentifier partsIdentifier,
+                                                       Layer layer, float layerOrder, ImageResource imageResource,
+                                                       ColorConvertParameter param) {
+                                               if (param == null) {
+                                                       param = new ColorConvertParameter();
+                                               }
+                                               String partsName = partsIdentifier.getPartsName();
+                                               collector.setImageSource(partsName, layer, layerOrder, imageResource, param);
+                                       }
+                               });
+                               collector.setComplite();
+                       }
+
+                       @Override
+                       public void handleException(Throwable ex) {
+                               ErrorMessageHelper.showErrorDialog(MainFrame.this, ex);
+                       }
+
+                       @Override
+                       public void buildImage(ImageOutput output) {
+                               result[0] = output.getImageOutput();
+                       }
+
+                       @Override
+                       public void onCreateLayerImage(String partsName, Layer layer, BufferedImage img) {
+                               layerImages.add(new ImageSaveHelper.LayerImage(partsName,layer, img));
+                       }
+               });
+
+               // 集まった各レイヤーの画像と、合成された画像を呼び出し元にコールバックする
+               callback.collect(layerImages, result[0]);
+       }
+
        /**
         * 伺か用PNG/PNAの出力.
         */