// // APNGWriter.java // /* OME Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package loci.formats.out; import java.awt.image.IndexColorModel; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.CRC32; import java.util.zip.DeflaterOutputStream; import loci.common.DataTools; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.FormatWriter; import loci.formats.meta.MetadataRetrieve; /** * APNGWriter is the file format writer for PNG and APNG files. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/out/APNGWriter.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/out/APNGWriter.java;hb=HEAD">Gitweb</a></dd></dl> */ public class APNGWriter extends FormatWriter { // -- Constants -- private static final byte[] PNG_SIG = new byte[] { (byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; // -- Fields -- private int numFrames = 0; private long numFramesPointer = 0; private int nextSequenceNumber; private boolean littleEndian; // -- Constructor -- public APNGWriter() { super("Animated PNG", "png"); } // -- IFormatWriter API methods -- /** * @see loci.formats.IFormatWriter#saveBytes(int, byte[], int, int, int, int) */ public void saveBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { checkParams(no, buf, x, y, w, h); if (!isFullPlane(x, y, w, h)) { throw new FormatException( "APNGWriter does not yet support saving image tiles."); } MetadataRetrieve meta = getMetadataRetrieve(); int width = meta.getPixelsSizeX(series).getValue().intValue(); int height = meta.getPixelsSizeY(series).getValue().intValue(); if (!initialized[series][no]) { writeFCTL(width, height); if (numFrames == 0) writePLTE(); initialized[series][no] = true; } writePixels(numFrames == 0 ? "IDAT" : "fdAT", buf, x, y, w, h); numFrames++; } /* @see loci.formats.IFormatWriter#canDoStacks() */ public boolean canDoStacks() { return true; } /* @see loci.formats.IFormatWriter#getPixelTypes(String) */ public int[] getPixelTypes(String codec) { return new int[] {FormatTools.INT8, FormatTools.UINT8, FormatTools.INT16, FormatTools.UINT16}; } // -- IFormatHandler API methods -- /* @see loci.formats.IFormatHandler#setId(String) */ public void setId(String id) throws FormatException, IOException { super.setId(id); if (out.length() == 0) { MetadataRetrieve r = getMetadataRetrieve(); int width = r.getPixelsSizeX(series).getValue().intValue(); int height = r.getPixelsSizeY(series).getValue().intValue(); int bytesPerPixel = FormatTools.getBytesPerPixel(r.getPixelsType(series).toString()); int nChannels = getSamplesPerPixel(); boolean indexed = getColorModel() != null && (getColorModel() instanceof IndexColorModel); littleEndian = !r.getPixelsBinDataBigEndian(series, 0); // write 8-byte PNG signature out.write(PNG_SIG); // write IHDR chunk out.writeInt(13); byte[] b = new byte[17]; b[0] = 'I'; b[1] = 'H'; b[2] = 'D'; b[3] = 'R'; DataTools.unpackBytes(width, b, 4, 4, false); DataTools.unpackBytes(height, b, 8, 4, false); b[12] = (byte) (bytesPerPixel * 8); if (indexed) b[13] = (byte) 3; else if (nChannels == 1) b[13] = (byte) 0; else if (nChannels == 2) b[13] = (byte) 4; else if (nChannels == 3) b[13] = (byte) 2; else if (nChannels == 4) b[13] = (byte) 6; b[14] = (byte) 0; b[15] = (byte) 0; b[16] = (byte) 0; out.write(b); out.writeInt(crc(b)); // write acTL chunk out.writeInt(8); out.writeBytes("acTL"); numFramesPointer = out.getFilePointer(); out.writeInt(0); out.writeInt(0); out.writeInt(0); // save a place for the CRC } } /* @see loci.formats.IFormatHandler#close() */ public void close() throws IOException { if (out != null) { writeFooter(); } super.close(); numFrames = 0; numFramesPointer = 0; nextSequenceNumber = 0; littleEndian = false; } // -- Helper methods -- private int crc(byte[] buf) { return crc(buf, 0, buf.length); } private int crc(byte[] buf, int off, int len) { CRC32 crc = new CRC32(); crc.update(buf, off, len); return (int) crc.getValue(); } private void writeFCTL(int width, int height) throws IOException { out.writeInt(26); byte[] b = new byte[30]; b[0] = 'f'; b[1] = 'c'; b[2] = 'T'; b[3] = 'L'; DataTools.unpackBytes(nextSequenceNumber++, b, 4, 4, false); DataTools.unpackBytes(width, b, 8, 4, false); DataTools.unpackBytes(height, b, 12, 4, false); DataTools.unpackBytes(0, b, 16, 4, false); DataTools.unpackBytes(0, b, 20, 4, false); DataTools.unpackBytes(1, b, 24, 2, false); DataTools.unpackBytes(fps, b, 26, 2, false); b[28] = (byte) 1; b[29] = (byte) 0; out.write(b); out.writeInt(crc(b)); } private void writePLTE() throws IOException { if (!(getColorModel() instanceof IndexColorModel)) return; IndexColorModel model = (IndexColorModel) getColorModel(); byte[][] lut = new byte[3][256]; model.getReds(lut[0]); model.getGreens(lut[1]); model.getBlues(lut[2]); out.writeInt(768); byte[] b = new byte[772]; b[0] = 'P'; b[1] = 'L'; b[2] = 'T'; b[3] = 'E'; for (int i=0; i<lut[0].length; i++) { for (int j=0; j<lut.length; j++) { b[i*lut.length + j + 4] = lut[j][i]; } } out.write(b); out.writeInt(crc(b)); } private void writePixels(String chunk, byte[] stream, int x, int y, int width, int height) throws FormatException, IOException { MetadataRetrieve r = getMetadataRetrieve(); int sizeC = getSamplesPerPixel(); String type = r.getPixelsType(series).toString(); int pixelType = FormatTools.pixelTypeFromString(type); boolean signed = FormatTools.isSigned(pixelType); if (!isFullPlane(x, y, width, height)) { throw new FormatException("APNGWriter does not support writing tiles."); } ByteArrayOutputStream s = new ByteArrayOutputStream(); s.write(chunk.getBytes()); if (chunk.equals("fdAT")) { s.write(DataTools.intToBytes(nextSequenceNumber++, false)); } DeflaterOutputStream deflater = new DeflaterOutputStream(s); int planeSize = stream.length / sizeC; int rowLen = stream.length / height; int bytesPerPixel = stream.length / (width * height * sizeC); byte[] rowBuf = new byte[rowLen]; for (int i=0; i<height; i++) { deflater.write(0); if (interleaved) { if (littleEndian) { for (int col=0; col<width*sizeC; col++) { int offset = (i * sizeC * width + col) * bytesPerPixel; int pixel = DataTools.bytesToInt(stream, offset, bytesPerPixel, littleEndian); DataTools.unpackBytes(pixel, rowBuf, col * bytesPerPixel, bytesPerPixel, false); } } else System.arraycopy(stream, i * rowLen, rowBuf, 0, rowLen); } else { int max = (int) Math.pow(2, bytesPerPixel * 8 - 1); for (int col=0; col<width; col++) { for (int c=0; c<sizeC; c++) { int offset = c * planeSize + (i * width + col) * bytesPerPixel; int pixel = DataTools.bytesToInt(stream, offset, bytesPerPixel, littleEndian); if (signed) { if (pixel < max) pixel += max; else pixel -= max; } int output = (col * sizeC + c) * bytesPerPixel; DataTools.unpackBytes(pixel, rowBuf, output, bytesPerPixel, false); } } } deflater.write(rowBuf); } deflater.finish(); byte[] b = s.toByteArray(); // write chunk length out.writeInt(b.length - 4); out.write(b); // write checksum out.writeInt(crc(b)); } private void writeFooter() throws IOException { // write IEND chunk out.writeInt(0); out.writeBytes("IEND"); out.writeInt(crc("IEND".getBytes())); // update frame count out.seek(numFramesPointer); out.writeInt(numFrames); out.skipBytes(4); byte[] b = new byte[12]; b[0] = 'a'; b[1] = 'c'; b[2] = 'T'; b[3] = 'L'; DataTools.unpackBytes(numFrames, b, 4, 4, false); DataTools.unpackBytes(0, b, 8, 4, false); out.writeInt(crc(b)); } }