OSDN Git Service

PSD出力時のRLE圧縮に対応。
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / graphics / io / PSDCreator.java
index f73d42b..0ff9346 100644 (file)
@@ -7,7 +7,13 @@ import java.awt.image.WritableRaster;
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 複数レイヤー画像をPSD形式のデータとして作成する。
@@ -47,10 +53,67 @@ public final class PSDCreator {
        }
 
        /**
-        * ã\83\97ã\83©ã\82¤ã\83\99ã\83¼ã\83\88ã\82³ã\83³ã\82¹ã\83\88ã\83©ã\82¯ã\82¿
+        * ã\83¬ã\82¤ã\83¤ã\83¼ã\81¨ã\83\81ã\83£ã\83\8dã\83«ã\81®ã\83\9aã\82¢
         */
-       private PSDCreator() {
-               super();
+       static final class LayerChannelPair {
+
+               private final LayerData layerData;
+
+               private final int channel;
+
+               public LayerChannelPair(LayerData layerData, int channel) {
+                       this.layerData = layerData;
+                       this.channel = channel;
+               }
+
+               public LayerData getLayerData() {
+                       return layerData;
+               }
+
+               public int getChannel() {
+                       return channel;
+               }
+
+               @Override
+               public int hashCode() {
+                       final int prime = 31;
+                       int result = 1;
+                       result = prime * result + channel;
+                       result = prime * result + ((layerData == null) ? 0 : layerData.hashCode());
+                       return result;
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (this == obj)
+                               return true;
+                       if (obj == null)
+                               return false;
+                       if (getClass() != obj.getClass())
+                               return false;
+                       LayerChannelPair other = (LayerChannelPair) obj;
+                       if (channel != other.channel)
+                               return false;
+                       if (layerData == null) {
+                               if (other.layerData != null)
+                                       return false;
+                       } else if (!layerData.equals(other.layerData))
+                               return false;
+                       return true;
+               }
+       }
+
+       /**
+        * RLEで圧縮するか?
+        */
+       private static boolean useRLECompression = true;
+
+       public static boolean isUseRLECompression() {
+               return useRLECompression;
+       }
+
+       public static void setUseRLECompression(boolean useRLECompression) {
+               PSDCreator.useRLECompression = useRLECompression;
        }
 
        /**
@@ -97,13 +160,8 @@ public final class PSDCreator {
                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);
-               }
+               byte[] pictureDatas = createPictureSection(cimg, width, height);
+               dos.write(pictureDatas);
 
                return bos.toByteArray();
        }
@@ -140,6 +198,7 @@ public final class PSDCreator {
 
                short[] channels = { -1, 0, 1, 2 }; // ALPHA, RED, GREEN, BLUE
 
+               Map<LayerChannelPair, byte[]> channelDataMap = new HashMap<LayerChannelPair, byte[]>();
                for (LayerData layerData : layerDatas) {
                        String layerName = layerData.getLayerName();
                        BufferedImage image = layerData.getImage();
@@ -153,10 +212,48 @@ public final class PSDCreator {
 
                        dos.writeShort(channels.length);
 
-                       int rawSize = width * height;
+                       byte[][] channelsData = createChannels(image);
+
                        for (int channel = 0; channel < channels.length; channel++) {
+                               byte[] channelData = channelsData[channel];
+                               byte[] outChannelData;
+
+                               if (useRLECompression) {
+                                       // RLE圧縮
+                                       // 行ごとにRLE圧縮する
+                                       int bufsiz = 0;
+                                       List<byte[]> rleRows = new ArrayList<byte[]>();
+                                       for (int y = 0; y < height; y++) {
+                                               byte[] rleRow = compressRLE(channelData, y * width, width);
+                                               rleRows.add(rleRow);
+                                               bufsiz += 2 + rleRow.length;
+                                       }
+
+                                       ByteBuffer outbuf = ByteBuffer.allocate(bufsiz);
+
+                                       // 行ごとの圧縮サイズを格納
+                                       for (byte[] rleRow : rleRows) {
+                                               outbuf.putShort((short) rleRow.length);
+                                       }
+                                       // 行ごとに圧縮後データの格納
+                                       for (byte[] rleRow : rleRows) {
+                                               outbuf.put(rleRow);
+                                       }
+
+                                       outChannelData = outbuf.array();
+
+                               } else {
+                                       // RAW (圧縮なし)
+                                       outChannelData = channelData;
+                               }
+
+                               // チャネルID (-1: alpha, 0: red, 1:green, 2:blue)
                                dos.writeShort(channels[channel]);
-                               dos.writeInt(2 + rawSize);
+
+                               // チャネルのデータサイズ
+                               dos.writeInt(2 + outChannelData.length);
+
+                               channelDataMap.put(new LayerChannelPair(layerData, channel), outChannelData);
                        }
 
                        dos.write("8BIM".getBytes());
@@ -179,15 +276,12 @@ public final class PSDCreator {
                }
 
                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[] outChannelData = channelDataMap.get(new LayerChannelPair(layerData, channel));
+                               assert outChannelData != null;
 
-                               byte[] channelData = channelsData[channel];
-                               dos.write(channelData);
+                               dos.writeShort(useRLECompression ? 1 : 0); // 0:RAW 1:RLE 2..zip
+                               dos.write(outChannelData);
                        }
                }
 
@@ -305,4 +399,137 @@ public final class PSDCreator {
                }
                return cimg;
        }
+
+       /**
+        * ARGB画像をRLE圧縮されたピクチャーセクションデータに変換する
+        * @param img 画像
+        * @param width 幅、画像とPSDヘッダと一致していること
+        * @param height 高さ、画像とPSDヘッダと一致していること
+        * @return RLE圧縮されたRGBA順チャンネルをつなげたデータ
+        */
+       private static byte[] createPictureSection(BufferedImage img, int width, int height) {
+               byte[][] channels = createChannels(img);
+
+               assert width == img.getWidth();
+               assert height == img.getHeight();
+
+               int[] channelMap = { 1, 2, 3, 0 }; // R, G, B, Aにマップ
+
+               ByteBuffer channelData;
+               if (useRLECompression) {
+                       // RLE圧縮とサイズの計算
+                       int bufsiz = 2;
+                       List<byte[]> rows = new ArrayList<byte[]>();
+                       for (int channel = 0; channel < channels.length; channel++) {
+                               byte[] pixels = channels[channelMap[channel]];
+                               for (int y = 0; y < height; y++) {
+                                       byte[] row = compressRLE(pixels, y * width, width);
+                                       rows.add(row);
+                                       bufsiz += 2 + row.length; // ラインごとのバイト数保存(16bit)とラインデータ分を加算
+                               }
+                       }
+
+                       // RLE圧縮済みバッファ作成
+                       channelData = ByteBuffer.allocate(bufsiz);
+                       channelData.order(ByteOrder.BIG_ENDIAN);
+
+                       channelData.putShort((short) 1); // RLE圧縮
+
+                       // 各チャネルの各行ごとのデータ
+                       for (byte[] row : rows) {
+                               channelData.putShort((short) row.length);
+                       }
+                       for (byte[] row : rows) {
+                               channelData.put(row);
+                       }
+
+               } else {
+                       // RAWサイズの計算
+                       int bufsiz = 2;
+                       for (int channel = 0; channel < channels.length; channel++) {
+                               byte[] pixels = channels[channelMap[channel]];
+                               bufsiz += pixels.length;
+                       }
+
+                       // RLE圧縮済みバッファ作成
+                       channelData = ByteBuffer.allocate(bufsiz);
+                       channelData.order(ByteOrder.BIG_ENDIAN);
+
+                       channelData.putShort((short) 0); // RAW
+
+                       for (int channel = 0; channel < channels.length; channel++) {
+                               byte[] pixels = channels[channelMap[channel]];
+                               channelData.put(pixels);
+                       }
+               }
+
+               return channelData.array();
+       }
+
+       /**
+        * バイト配列をRLE圧縮して返す
+        *  http://www.snap-tck.com/room03/c02/comp/comp02.html
+        * @param data 圧縮するバイト配列
+        * @param offset 開始位置
+        * @param length 長さ
+        * @return RLE圧縮結果
+        */
+       public static byte[] compressRLE(byte[] data, int offset, int length) {
+               ByteBuffer outbuf = ByteBuffer.allocate(length * 2); // ワーストケース
+               ByteBuffer buf = ByteBuffer.wrap(data, offset, length);
+               while (buf.hasRemaining()) {
+                       int ch = buf.get();
+                       // 不連続数を数える
+                       int count = 0;
+                       buf.mark();
+                       int prev = ch;
+                       while (buf.hasRemaining() && count < 128) {
+                               int ch2 = buf.get();
+                               if (prev == ch2) {
+                                       break;
+                               }
+                               count++;
+                               prev = ch2;
+                               if (!buf.hasRemaining() && count < 128) {
+                                       // 終端に達した場合は終端も不連続数と数える
+                                       count++;
+                                       break;
+                               }
+                       }
+                       buf.reset();
+
+                       if (count > 0) {
+                               // 不連続数がある場合
+                               outbuf.put((byte) (count - 1));
+                               outbuf.put((byte) ch);
+                               while (--count > 0) {
+                                       ch = buf.get();
+                                       outbuf.put((byte) ch);
+                               }
+
+                       } else {
+                               // 連続数を数える
+                               prev = ch;
+                               count = 1;
+                               while (buf.hasRemaining() && count < 128) {
+                                       ch = buf.get();
+                                       if (prev != ch) {
+                                               buf.reset();
+                                               break;
+                                       }
+                                       count++;
+                                       buf.mark();
+                               }
+                               outbuf.put((byte) (-count + 1));
+                               outbuf.put((byte) prev);
+                       }
+               }
+
+               outbuf.flip();
+               int limit = outbuf.limit();
+               byte[] array = outbuf.array();
+               byte[] result = new byte[limit];
+               System.arraycopy(array, 0, result, 0, limit);
+               return result;
+       }
 }