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形式のデータとして作成する。
}
/**
- * ã\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;
}
/**
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();
}
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();
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());
}
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);
}
}
}
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;
+ }
}