/* * Copyright (C) 2011,2012 IsmAvatar <IsmAvatar@gmail.com> * Copyright (C) 2011 Josh Ventura <JoshV10@gmail.com> * * This file is part of Enigma Plugin. * Enigma Plugin is free software and comes with ABSOLUTELY NO WARRANTY. * See LICENSE for details. */ package org.lateralgm.file; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.zip.CRC32; import javax.imageio.ImageIO; import org.lateralgm.main.LGM; public final class ApngIO { public static final byte[] PNG_SIGNATURE = { (byte) 0x89,'P','N','G',0x0D,0x0A,0x1A,0x0A }; public static final String IO_FMT = "png"; //$NON-NLS-1$ //Convenience byte[]/int methods private static byte bite(int i, int p) { return (byte) (i >> p & 0xFF); } private static byte[] i2b(int i) { return new byte[] { bite(i,24),bite(i,16),bite(i,8),bite(i,0) }; } /** Convenience method to fully read a buffer, since in.read(buf) can fall short. */ private static void readFully(InputStream in, byte[] buffer) throws IOException { readFully(in,buffer,0,buffer.length); } private static void readFully(InputStream in, byte[] buffer, int off, int len) throws IOException { int total = 0; while (true) { int n = in.read(buffer,off + total,len - total); if (n <= 0) { if (total == 0) total = n; break; } total += n; if (total == len) break; } } //Chunk classes private static class ChunkType { private int val; private byte[] data; public ChunkType(byte[] bytes) { val = bytesToInt(data = bytes); } public byte[] getBytes() { return data; } public static int bytesToInt(byte[] b) { if (b.length != 4) throw new ArrayIndexOutOfBoundsException(); return b[0] << 24 | b[1] << 16 | b[2] << 8 | b[3]; } public boolean equals(ChunkType other) { return val == other.val; } } private static abstract class PNG_Chunk { protected int length = 0; protected ChunkType chunkType; protected byte[] data; protected byte[] crc; public PNG_Chunk(ChunkType type) { chunkType = type; } void updateCRC() { CRC32 crc32 = new CRC32(); crc32.update(chunkType.getBytes()); crc32.update(data); int tcrc = (int) crc32.getValue(); length = data.length; crc = i2b(tcrc); } abstract void repopulate(); byte[] getBytes() { // Get the bytes of this chunk for writing ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { baos.write(i2b(length)); baos.write(chunkType.getBytes()); baos.write(data); baos.write(crc); } catch (IOException e) { LGM.showDefaultExceptionHandler(e); } return baos.toByteArray(); } /* boolean isType2(String t) { if (chunkType == null || t.length() != 4) return false; return chunkType[0] == t.charAt(0) && chunkType[1] == t.charAt(1) && chunkType[2] == t.charAt(2) && chunkType[3] == t.charAt(3); }*/ boolean isType(ChunkType t) { return chunkType == null ? false : chunkType.equals(t); } boolean read(InputStream dis) throws IOException { int b1 = dis.read(); if (b1 == -1) return false; length = (b1 << 24) | (dis.read() << 16) | (dis.read() << 8) | dis.read(); byte[] chunkTypeBytes = new byte[4]; readFully(dis,chunkTypeBytes); chunkType = new ChunkType(chunkTypeBytes); data = new byte[length]; readFully(dis,data); crc = new byte[4]; readFully(dis,crc); return true; } } private static class Generic_Chunk extends PNG_Chunk { public Generic_Chunk() { super(null); } @Override void repopulate() { System.err.println("Repopulate called on generic chunk."); } } private static class IHDR_Dummy extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'I','H','D','R' }); public IHDR_Dummy(byte[] png) { super(type); data = new byte[13]; System.arraycopy(png,16,data,0,data.length); repopulate(); } @Override void repopulate() { updateCRC(); } } private static class acTL extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'a','c','T','L' }); int numFrames; int numPlays; public acTL(int nf, int np) { super(type); numFrames = nf; numPlays = np; repopulate(); } public void repopulate() { data = new byte[] { bite(numFrames,24),bite(numFrames,16),bite(numFrames,8), bite(numFrames,0),bite(numPlays,24),bite(numPlays,16),bite(numPlays,8),bite(numPlays,0) }; updateCRC(); } } private static class fcTL extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'f','c','T','L' }); /** Sequence number of the animation chunk, 0-based */ protected int sequenceNumber; /** Width of the following frame */ protected int width; /** Height of the following frame */ protected int height; /** X position at which to render the following frame */ protected int xOffset; /** Y position at which to render the following frame */ protected int yOffset; /** Frame delay fraction numerator */ protected short delayNum; /** Frame delay fraction denominator */ protected short delayDen; /** Type of frame area disposal to be done after rendering this frame */ protected Dispose disposeOp; /** Type of frame area rendering for this frame */ protected Blend blendOp; public fcTL(int sn, int w, int h, int xo, int yo, short dn, short dd, Dispose dop, Blend bop) { super(type); sequenceNumber = sn; width = w; height = h; xOffset = xo; yOffset = yo; delayNum = dn; delayDen = dd; disposeOp = dop; blendOp = bop; repopulate(); } public static enum Dispose { /** * No disposal is done on this frame before rendering the next; * the contents of the output buffer are left as-is. */ NONE(0), /** * The frame's region of the output buffer is to be cleared * to fully transparent black before rendering the next frame. */ BACKGROUND(1), /** * The frame's region of the output buffer is to be reverted * to the previous contents before rendering the next frame. */ PREVIOUS(2); private byte value; private Dispose(int v) { value = (byte) v; } public byte getValue() { return value; } } public static enum Blend { /** * All color components of the frame, including alpha, overwrite * the current contents of the frame's output buffer region. */ SOURCE(0), /** * The frame should be composited onto the output buffer * based on its alpha, using a simple OVER operation * as described in the "Alpha Channel Processing" section * of the PNG specification [PNG-1.2]. */ OVER(1); private byte value; private Blend(int v) { value = (byte) v; } public byte getValue() { return value; } } @Override void repopulate() { data = new byte[] { bite(sequenceNumber,24),bite(sequenceNumber,16),bite(sequenceNumber,8), bite(sequenceNumber,0),bite(width,24),bite(width,16),bite(width,8),bite(width,0), bite(height,24),bite(height,16),bite(height,8),bite(height,0),bite(xOffset,24), bite(xOffset,16),bite(xOffset,8),bite(xOffset,0),bite(yOffset,24),bite(yOffset,16), bite(yOffset,8),bite(yOffset,0),bite(delayNum,8),bite(delayNum,0),bite(delayDen,8), bite(delayDen,0),disposeOp.getValue(),blendOp.getValue() }; updateCRC(); } } private static class IDAT extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'I','D','A','T' }); /** Creates a new IDAT chunk with the given frame data. */ public IDAT(byte[] dat) { super(type); data = dat; repopulate(); } @Override public void repopulate() { updateCRC(); } } private static class fdAT extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'f','d','A','T' }); protected int sequenceNumber; // Sequence number, 0-based protected byte[] frameData; /** * Creates an fdAT from a PNG_Chunk that is already in fdAT format. * Namely, it extracts the sequence number from the frame data. */ public fdAT(PNG_Chunk pc) { super(pc.chunkType); length = pc.length; sequenceNumber = pc.data[0] << 24 | pc.data[1] << 16 | pc.data[2] << 8 | pc.data[3]; frameData = new byte[pc.data.length - 4]; System.arraycopy(pc.data,4,frameData,0,frameData.length); crc = pc.crc; } /** Creates an fdAT from a PNG_Chunk that is in another format (converts it) */ public fdAT(PNG_Chunk src, int sn) { super(type); length = src.length; sequenceNumber = sn; frameData = src.data; //will just be arraycopy'd anyways repopulate(); } @Override public void repopulate() { data = new byte[frameData.length + 4]; System.arraycopy(i2b(sequenceNumber),0,data,0,4); System.arraycopy(frameData,0,data,4,frameData.length); updateCRC(); } public IDAT toIDAT() { return new IDAT(frameData); } } private static class IEND extends PNG_Chunk { public static final ChunkType type = new ChunkType(new byte[] { 'I','E','N','D' }); public static final IEND instance = new IEND(); /** * IEND is a singleton class and should not be instantiated. * Use IEND.instance instead. */ private IEND() { super(type); repopulate(); } @Override public void repopulate() { data = new byte[] {}; updateCRC(); } } //Functionality private static void transferIDATs(InputStream dis, OutputStream os, int sn, boolean ignoreOthers) throws IOException { Generic_Chunk chunk = new Generic_Chunk(); while (chunk.read(dis) && !chunk.isType(IEND.type)) { if (chunk.isType(IDAT.type)) os.write((sn == -1 ? chunk : new fdAT(chunk,sn)).getBytes()); else if (!ignoreOthers) os.write(chunk.getBytes()); } } public static void imagesToApng(ArrayList<BufferedImage> imgs, OutputStream fullFile) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(imgs.get(0),IO_FMT,baos); byte buf[] = baos.toByteArray(); IHDR_Dummy myhdr = new IHDR_Dummy(buf); acTL myactl = new acTL(imgs.size(),0); fcTL fctl = new fcTL(0,imgs.get(0).getWidth(),imgs.get(0).getHeight(),0,0,(short) 1,(short) 30, fcTL.Dispose.BACKGROUND,fcTL.Blend.OVER); fullFile.write(PNG_SIGNATURE); fullFile.write(myhdr.getBytes()); fullFile.write(myactl.getBytes()); fullFile.write(fctl.getBytes()); //Transfer the IDAT chunks ByteArrayInputStream bais = new ByteArrayInputStream(buf); bais.skip(8); transferIDATs(bais,fullFile,-1,true); int indx = -1; for (BufferedImage bi : imgs) { if (indx++ == -1) // Continues once to skip the first image; continue; // index will be 1 on first iteration baos.reset(); // Clear our buffer, ImageIO.write(bi,IO_FMT,baos); // Fetch new image. // Create the frame control. IDs will be 2, 4, 6, 8... fctl = new fcTL(indx++,bi.getWidth(),bi.getHeight(),0,0,(short) 1,(short) 30, fcTL.Dispose.BACKGROUND,fcTL.Blend.OVER); fullFile.write(fctl.getBytes()); // Write it into the file // Now make our frame data. IDs will be 3, 5, 7, 9... buf= baos.toByteArray(); bais = new ByteArrayInputStream(buf); bais.skip(8); transferIDATs(bais,fullFile,indx,true); } fullFile.write(IEND.instance.getBytes()); } public static ArrayList<BufferedImage> apngToBufferedImages(InputStream is) { PNG_Chunk genChunk = new Generic_Chunk(); ByteArrayOutputStream png = new ByteArrayOutputStream(); ArrayList<BufferedImage> ret = new ArrayList<BufferedImage>(); byte[] imageHeader=null; try { png.write(PNG_SIGNATURE); byte pngBase[] = new byte[8]; is.read(pngBase); if (!Arrays.equals(pngBase,PNG_SIGNATURE)) throw new IOException("Not APNG"); boolean hasData = false; while (genChunk.read(is)) { if (genChunk.isType(acTL.type)) { //Look at all the fucks I give } else if (genChunk.isType(fcTL.type)) { if (hasData) { png.write(IEND.instance.getBytes()); ByteArrayInputStream bais = new ByteArrayInputStream(png.toByteArray()); ret.add(ImageIO.read(bais)); png.reset(); png.write(pngBase); png.write(imageHeader); hasData = false; bais.close(); } } else if (genChunk.isType(IDAT.type)) { png.write(genChunk.getBytes()); hasData = true; } else if (genChunk.isType(fdAT.type)) { byte[] a = new fdAT(genChunk).toIDAT().getBytes(); png.write(a); hasData = true; } else if (genChunk.isType(IEND.type)) { png.write(genChunk.getBytes()); ByteArrayInputStream bais = new ByteArrayInputStream(png.toByteArray()); ret.add(ImageIO.read(bais)); bais.close(); break; } else if (genChunk.isType(IHDR_Dummy.type)) { //save the image header as it is used for all subimages imageHeader=genChunk.getBytes(); png.write(imageHeader); } else { png.write(genChunk.getBytes()); //Uncomment this if we want to include these chunks with every png //Otherwise, keep commented to only include with current png // pngBase = png.toByteArray(); } } } catch (IOException e) { LGM.showDefaultExceptionHandler(e); } return ret; } }