1 package charactermanaj.graphics.io;
3 import java.awt.Graphics2D;
4 import java.awt.image.BufferedImage;
5 import java.awt.image.DataBufferInt;
6 import java.awt.image.WritableRaster;
7 import java.io.ByteArrayOutputStream;
8 import java.io.DataOutputStream;
9 import java.io.IOException;
10 import java.nio.ByteBuffer;
11 import java.nio.ByteOrder;
12 import java.util.ArrayList;
13 import java.util.Collection;
14 import java.util.HashMap;
15 import java.util.List;
19 * 複数レイヤー画像をPSD形式のデータとして作成する。
20 * https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
24 public final class PSDCreator {
29 public static class LayerData {
34 private String layerName;
37 * レイヤーの画像(TYPE_INT_ARGB限定)
39 private BufferedImage image;
41 public LayerData(String layerName, BufferedImage image) {
42 this.layerName = layerName;
46 public String getLayerName() {
50 public BufferedImage getImage() {
58 static final class LayerChannelPair {
60 private final LayerData layerData;
62 private final int channel;
64 public LayerChannelPair(LayerData layerData, int channel) {
65 this.layerData = layerData;
66 this.channel = channel;
69 public LayerData getLayerData() {
73 public int getChannel() {
78 public int hashCode() {
81 result = prime * result + channel;
82 result = prime * result + ((layerData == null) ? 0 : layerData.hashCode());
87 public boolean equals(Object obj) {
92 if (getClass() != obj.getClass())
94 LayerChannelPair other = (LayerChannelPair) obj;
95 if (channel != other.channel)
97 if (layerData == null) {
98 if (other.layerData != null)
100 } else if (!layerData.equals(other.layerData))
109 private static boolean useRLECompression = true;
111 public static boolean isUseRLECompression() {
112 return useRLECompression;
115 public static void setUseRLECompression(boolean useRLECompression) {
116 PSDCreator.useRLECompression = useRLECompression;
120 * レイヤーを指定してPSDデータを作成する
121 * @param layerDatas レイヤーのコレクション、順番に重ねられる
123 * @throws IOException
125 public static byte[] createPSD(Collection<LayerData> layerDatas) throws IOException {
126 if (layerDatas == null) {
127 throw new NullPointerException("layerDatas is required.");
129 if (layerDatas.isEmpty()) {
130 throw new IllegalArgumentException("layerDatas must not be empty.");
133 BufferedImage cimg = createCompositeImage(layerDatas);
134 int width = cimg.getWidth();
135 int height = cimg.getHeight();
137 ByteArrayOutputStream bos = new ByteArrayOutputStream();
138 DataOutputStream dos = new DataOutputStream(bos);
140 dos.write("8BPS".getBytes());
142 dos.write(new byte[6]); // reserved 6bytes
144 dos.writeShort(4); // argb
146 dos.writeInt(height);
150 dos.writeShort(depth);
152 dos.writeShort(3); // ColorMode=RGB(3)
154 dos.writeInt(0); // カラーモードセクションなし
155 dos.writeInt(0); // リソースセクションなし
158 byte[] layerMaskSection = createLayerMaskSection(layerDatas);
159 dos.writeInt(layerMaskSection.length);
160 dos.write(layerMaskSection);
163 byte[] pictureDatas = createPictureSection(cimg, width, height);
164 dos.write(pictureDatas);
166 return bos.toByteArray();
173 * @throws IOException
175 private static byte[] createLayerMaskSection(Collection<LayerData> layerDatas) throws IOException {
176 ByteArrayOutputStream bos = new ByteArrayOutputStream();
177 DataOutputStream dos = new DataOutputStream(bos);
179 byte[] layerData = createLayerData(layerDatas);
180 dos.writeInt(layerData.length);
181 dos.write(layerData);
183 return bos.toByteArray();
190 * @throws IOException
192 private static byte[] createLayerData(Collection<LayerData> layerDatas) throws IOException {
193 ByteArrayOutputStream bos = new ByteArrayOutputStream();
194 DataOutputStream dos = new DataOutputStream(bos);
196 int numOfLayers = layerDatas.size();
197 dos.writeShort(numOfLayers); // non pre-multiplied
199 short[] channels = { -1, 0, 1, 2 }; // ALPHA, RED, GREEN, BLUE
201 Map<LayerChannelPair, byte[]> channelDataMap = new HashMap<LayerChannelPair, byte[]>();
202 for (LayerData layerData : layerDatas) {
203 String layerName = layerData.getLayerName();
204 BufferedImage image = layerData.getImage();
205 int width = image.getWidth();
206 int height = image.getHeight();
208 dos.writeInt(0); // top
209 dos.writeInt(0); // left
210 dos.writeInt(height); // bottom
211 dos.writeInt(width); // right
213 dos.writeShort(channels.length);
215 byte[][] channelsData = createChannels(image);
217 for (int channel = 0; channel < channels.length; channel++) {
218 byte[] channelData = channelsData[channel];
219 byte[] outChannelData;
221 if (useRLECompression) {
225 List<byte[]> rleRows = new ArrayList<byte[]>();
226 for (int y = 0; y < height; y++) {
227 byte[] rleRow = compressRLE(channelData, y * width, width);
229 bufsiz += 2 + rleRow.length;
232 ByteBuffer outbuf = ByteBuffer.allocate(bufsiz);
235 for (byte[] rleRow : rleRows) {
236 outbuf.putShort((short) rleRow.length);
239 for (byte[] rleRow : rleRows) {
243 outChannelData = outbuf.array();
247 outChannelData = channelData;
250 // チャネルID (-1: alpha, 0: red, 1:green, 2:blue)
251 dos.writeShort(channels[channel]);
254 dos.writeInt(2 + outChannelData.length);
256 channelDataMap.put(new LayerChannelPair(layerData, channel), outChannelData);
259 dos.write("8BIM".getBytes());
260 dos.write("norm".getBytes());
262 dos.write((byte) 255); // opacity
263 dos.write((byte) 0); // clipping
264 dos.write((byte) 0); // protection
265 dos.write((byte) 0); // filler
267 byte[] layerMaskData = createLayerMaskData();
268 byte[] layerBlendingData = createLayerBlendingData();
269 byte[] layerNameData = createLayerName(layerName);
270 int lenOfAdditional = layerMaskData.length + layerBlendingData.length + layerNameData.length;
272 dos.writeInt(lenOfAdditional);
273 dos.write(layerMaskData);
274 dos.write(layerBlendingData);
275 dos.write(layerNameData);
278 for (LayerData layerData : layerDatas) {
279 for (int channel = 0; channel < channels.length; channel++) {
280 byte[] outChannelData = channelDataMap.get(new LayerChannelPair(layerData, channel));
281 assert outChannelData != null;
283 dos.writeShort(useRLECompression ? 1 : 0); // 0:RAW 1:RLE 2..zip
284 dos.write(outChannelData);
288 return bos.toByteArray();
294 * @throws IOException
296 private static byte[] createLayerMaskData() throws IOException {
297 ByteArrayOutputStream bos = new ByteArrayOutputStream();
298 DataOutputStream dos = new DataOutputStream(bos);
300 return bos.toByteArray();
306 * @throws IOException
308 private static byte[] createLayerBlendingData() throws IOException {
309 ByteArrayOutputStream bos = new ByteArrayOutputStream();
310 DataOutputStream dos = new DataOutputStream(bos);
312 return bos.toByteArray();
319 * @throws IOException
321 private static byte[] createLayerName(String layerName) throws IOException {
322 byte[] nameBuf = layerName.getBytes("UTF-8");
323 int layerNameSize = 1 + nameBuf.length; // PASCAL文字列長 (16の倍数サイズ)
324 int blockSize = (layerNameSize / 4) * 4 + ((layerNameSize % 4 > 0) ? 4 : 0);
325 int paddingSize = blockSize - layerNameSize;
327 ByteArrayOutputStream bos = new ByteArrayOutputStream();
328 DataOutputStream dos = new DataOutputStream(bos);
329 dos.write((byte) nameBuf.length);
331 dos.write(new byte[paddingSize]);
332 return bos.toByteArray();
336 * 32ビットARGB形式のBuffeedImageを受け取り、
337 * ARGBのbyte[][]配列に変換して返す。
341 private static byte[][] createChannels(BufferedImage img) {
342 WritableRaster raster = img.getRaster();
343 DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer();
344 int[] pixels = buffer.getData();
346 int width = img.getWidth();
347 int height = img.getHeight();
348 int mx = width * height;
349 byte[][] channels = new byte[4][mx];
350 for (int idx = 0; idx < mx; idx++) {
351 int argb = pixels[idx];
353 int alpha = (argb >> 24) & 0xff;
354 int red = (argb >> 16) & 0xff;
355 int green = (argb >> 8) & 0xff;
356 int blue = argb & 0xff;
358 channels[0][idx] = (byte) alpha;
359 channels[1][idx] = (byte) red;
360 channels[2][idx] = (byte) green;
361 channels[3][idx] = (byte) blue;
368 * レイヤーコレクションを重ねて1つの画像にして返す
369 * @param layerDatas レイヤーコレクション
372 private static BufferedImage createCompositeImage(Collection<LayerData> layerDatas) {
375 for (LayerData layerData : layerDatas) {
376 BufferedImage img = layerData.getImage();
377 int w = img.getWidth();
378 int h = img.getHeight();
387 BufferedImage cimg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
389 Graphics2D g = cimg.createGraphics();
391 for (LayerData layerData : layerDatas) {
392 BufferedImage img = layerData.getImage();
393 int w = img.getWidth();
394 int h = img.getHeight();
395 g.drawImage(img, 0, 0, w, h, 0, 0, w, h, null);
404 * ARGB画像をRLE圧縮されたピクチャーセクションデータに変換する
406 * @param width 幅、画像とPSDヘッダと一致していること
407 * @param height 高さ、画像とPSDヘッダと一致していること
408 * @return RLE圧縮されたRGBA順チャンネルをつなげたデータ
410 private static byte[] createPictureSection(BufferedImage img, int width, int height) {
411 byte[][] channels = createChannels(img);
413 assert width == img.getWidth();
414 assert height == img.getHeight();
416 int[] channelMap = { 1, 2, 3, 0 }; // R, G, B, Aにマップ
418 ByteBuffer channelData;
419 if (useRLECompression) {
422 List<byte[]> rows = new ArrayList<byte[]>();
423 for (int channel = 0; channel < channels.length; channel++) {
424 byte[] pixels = channels[channelMap[channel]];
425 for (int y = 0; y < height; y++) {
426 byte[] row = compressRLE(pixels, y * width, width);
428 bufsiz += 2 + row.length; // ラインごとのバイト数保存(16bit)とラインデータ分を加算
433 channelData = ByteBuffer.allocate(bufsiz);
434 channelData.order(ByteOrder.BIG_ENDIAN);
436 channelData.putShort((short) 1); // RLE圧縮
439 for (byte[] row : rows) {
440 channelData.putShort((short) row.length);
442 for (byte[] row : rows) {
443 channelData.put(row);
449 for (int channel = 0; channel < channels.length; channel++) {
450 byte[] pixels = channels[channelMap[channel]];
451 bufsiz += pixels.length;
455 channelData = ByteBuffer.allocate(bufsiz);
456 channelData.order(ByteOrder.BIG_ENDIAN);
458 channelData.putShort((short) 0); // RAW
460 for (int channel = 0; channel < channels.length; channel++) {
461 byte[] pixels = channels[channelMap[channel]];
462 channelData.put(pixels);
466 return channelData.array();
471 * http://www.snap-tck.com/room03/c02/comp/comp02.html
472 * @param data 圧縮するバイト配列
477 public static byte[] compressRLE(byte[] data, int offset, int length) {
478 ByteBuffer outbuf = ByteBuffer.allocate(length * 2); // ワーストケース
479 ByteBuffer buf = ByteBuffer.wrap(data, offset, length);
480 while (buf.hasRemaining()) {
486 while (buf.hasRemaining() && count < 128) {
493 if (!buf.hasRemaining() && count < 128) {
494 // 終端に達した場合は終端も不連続数と数える
503 outbuf.put((byte) (count - 1));
504 outbuf.put((byte) ch);
505 while (--count > 0) {
507 outbuf.put((byte) ch);
514 while (buf.hasRemaining() && count < 128) {
523 outbuf.put((byte) (-count + 1));
524 outbuf.put((byte) prev);
529 int limit = outbuf.limit();
530 byte[] array = outbuf.array();
531 byte[] result = new byte[limit];
532 System.arraycopy(array, 0, result, 0, limit);