package org.hipi.image.io; import org.hipi.image.HipiImageHeader; import org.hipi.image.HipiImageHeader.HipiImageFormat; import org.hipi.image.HipiImageHeader.HipiColorSpace; import org.hipi.image.RasterImage; import org.hipi.image.HipiImage; import org.hipi.image.HipiImage.HipiImageType; import org.hipi.image.HipiImageFactory; import org.hipi.image.PixelArray; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import java.util.HashMap; import javax.imageio.metadata.IIOMetadata; import javax.imageio.ImageIO; /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU General Public License * Version 2 only ("GPL") or the Common Development and Distribution License("CDDL") (collectively, * the "License"). You may not use this file except in compliance with the License. You can obtain a * copy of the License at http://www.netbeans.org/cddl-gplv2.html or nbbuild/licenses/CDDL-GPL-2-CP. * See the License for the specific language governing permissions and limitations under the * License. When distributing the software, include this License Header Notice in each file and * include the License file at nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this particular file * as subject to the "Classpath" exception as provided by Sun in the GPL Version 2 section of the * License file that accompanied this code. If applicable, add the following below the License * Header, with the fields enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): Alexandre Iline. * * The Original Software is the Jemmy library. The Initial Developer of the Original Software is * Alexandre Iline. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL or only the GPL Version 2, * indicate your decision by adding "[Contributor] elects to include this software in this * distribution under the [CDDL or GPL Version 2] license." If you do not indicate a single choice * of license, a recipient has the option to distribute your version of this file under either the * CDDL, the GPL Version 2 or to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL Version 2 license, then the * option applies only if the new code is made subject to such option by the copyright holder. * * Heavy modifications were made to the original library by Chris Sweeney. */ /** * Extends {@link ImageCodec} and serves as both an {@link ImageDecoder} and * {@link ImageEncoder} for the PNG image storage format. Currently only supports RGB encodings. */ public class PngCodec extends ImageCodec { //implements ImageDecoder, ImageEncoder { private static final PngCodec staticObject = new PngCodec(); /** black and white image mode. */ private static final byte BW_MODE = 0; /** grey scale image mode. */ private static final byte GREYSCALE_MODE = 1; /** full color image mode. */ private static final byte COLOR_MODE = 2; private CRC32 crc; public static PngCodec getInstance() { return staticObject; } /** * Decodes the image header from an input stream that contains the PNG image. PNG images are * broken up into "chunks" (see PNG documentation), and the PNG header could be located anywhere * in the image * * @param inputStream the {@link InputStream} that contains the PNG image * @return {@link HipiImageHeader} found in the input stream */ public HipiImageHeader decodeHeader(InputStream inputStream, boolean includeExifData) throws IOException { DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream)); dis.mark(Integer.MAX_VALUE); readSignature(dis); int width = -1; int height = -1; boolean trucking = true; while (trucking) { try { // Read the length. int length = dis.readInt(); if (length <= 0) throw new IOException("PNG file is too long to proceed. (Found length <= 0)."); // Read the type. byte[] typeBytes = new byte[4]; dis.readFully(typeBytes); String typeString = new String(typeBytes, "UTF8"); if (typeString.equals("IHDR")) { // Read the data. byte[] data = new byte[length]; dis.readFully(data); // Read the CRC. long crc = dis.readInt() & 0x00000000ffffffffL; // Make it unsigned. if (verifyCRC(typeBytes, data, crc) == false) { throw new IOException("PNG file appears to be corrupted (unverifiable CRC)."); } PNGChunk chunk = staticObject.new PNGChunk(typeBytes, data); width = (int)chunk.getUnsignedInt(0); height = (int)chunk.getUnsignedInt(4); break; } else { // Skip data + CRC signature. dis.skipBytes(length + 4); } } catch (EOFException eofe) { trucking = false; } } if (width <= 0 || height <= 0) { throw new IOException("Failed to decode PNG image header. (Found invalid dimensions width <= 0 or height <= 0.)"); } HashMap<String,String> exifData = null; if (includeExifData) { dis.reset(); exifData = ExifDataReader.extractAndFlatten(dis); } return new HipiImageHeader(HipiImageFormat.PNG, HipiColorSpace.RGB, width, height, 3, null, exifData); } protected static void readSignature(DataInputStream in) throws IOException { long signature = in.readLong(); if (signature != 0x89504e470d0a1a0aL) throw new IOException("PNG signature not found!"); } protected static PNGData readChunks(DataInputStream in) throws IOException { PNGData chunks = staticObject.new PNGData(); boolean trucking = true; while (trucking) { try { // Read the length. int length = in.readInt(); if (length <= 0) throw new IOException("Found invalid length in PNG segment (length <= 0)."); // Read the type. byte[] typeBytes = new byte[4]; in.readFully(typeBytes); // Read the data. byte[] data = new byte[length]; in.readFully(data); // Read the CRC. long crc = in.readInt() & 0x00000000ffffffffL; // Make it // unsigned. if (verifyCRC(typeBytes, data, crc) == false) throw new IOException("That file appears to be corrupted."); PNGChunk chunk = staticObject.new PNGChunk(typeBytes, data); chunks.add(chunk); } catch (EOFException eofe) { trucking = false; } } return chunks; } protected static boolean verifyCRC(byte[] typeBytes, byte[] data, long crc) { CRC32 crc32 = new CRC32(); crc32.update(typeBytes); crc32.update(data); long calculated = crc32.getValue(); return (calculated == crc); } class PNGData { private int mNumberOfChunks; private PNGChunk[] mChunks; public PNGData() { mNumberOfChunks = 0; mChunks = new PNGChunk[10]; } public void printAll() { System.out.println("number of chunks: " + mNumberOfChunks); for (int i = 0; i < mChunks.length; i++) System.out.println("(" + mChunks[i].getTypeString() + ", " + ")"); } public void add(PNGChunk chunk) { mChunks[mNumberOfChunks++] = chunk; if (mNumberOfChunks >= mChunks.length) { PNGChunk[] largerArray = new PNGChunk[mChunks.length + 10]; System.arraycopy(mChunks, 0, largerArray, 0, mChunks.length); mChunks = largerArray; } } public long getWidth() { return getChunk("IHDR").getUnsignedInt(0); } public long getHeight() { return getChunk("IHDR").getUnsignedInt(4); } public short getBitsPerPixel() { return getChunk("IHDR").getUnsignedByte(8); } public short getColorType() { return getChunk("IHDR").getUnsignedByte(9); } public short getCompression() { return getChunk("IHDR").getUnsignedByte(10); } public short getFilter() { return getChunk("IHDR").getUnsignedByte(11); } public short getInterlace() { return getChunk("IHDR").getUnsignedByte(12); } public byte[] getImageData() { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); // Write all the IDAT data into the array. for (int i = 0; i < mNumberOfChunks; i++) { PNGChunk chunk = mChunks[i]; if (chunk.getTypeString().equals("IDAT")) { out.write(chunk.getData()); } } out.flush(); // Now deflate the data. InflaterInputStream in = new InflaterInputStream(new ByteArrayInputStream(out.toByteArray())); ByteArrayOutputStream inflatedOut = new ByteArrayOutputStream(); int readLength; byte[] block = new byte[8192]; while ((readLength = in.read(block)) != -1) inflatedOut.write(block, 0, readLength); inflatedOut.flush(); byte[] imageData = inflatedOut.toByteArray(); // Compute the real length. int width = (int) getWidth(); int height = (int) getHeight(); int bitsPerPixel = getBitsPerPixel(); int length = width * height * bitsPerPixel / 8 * 3; // hard code the 3 for RGB for now byte[] prunedData = new byte[length]; // We can only deal with non-interlaced images. if (getInterlace() == 0) { int index = 0; for (int i = 0; i < length; i++) { if (i % (width * bitsPerPixel / 8 * 3) == 0) { // again, hard code the 3 for RGB index++; // Skip the filter byte. } prunedData[i] = imageData[index++]; } } else System.out.println("Couldn't undo interlacing."); return prunedData; } catch (IOException ioe) { } return null; } public PNGChunk getChunk(String type) { for (int i = 0; i < mNumberOfChunks; i++) if (mChunks[i].getTypeString().equals(type)) return mChunks[i]; return null; } } class PNGChunk { private byte[] mType; private byte[] mData; public PNGChunk(byte[] type, byte[] data) { mType = type; mData = data; } public String getTypeString() { try { return new String(mType, "UTF8"); } catch (UnsupportedEncodingException uee) { return ""; } } public String getDataString() { try { return new String(mData, "UTF8"); } catch (UnsupportedEncodingException uee) { return ""; } } public byte[] getData() { return mData; } public long getUnsignedInt(int offset) { long value = 0; for (int i = 0; i < 4; i++) value += (mData[offset + i] & 0xff) << ((3 - i) * 8); return value; } public short getUnsignedByte(int offset) { return (short) (mData[offset] & 0x00ff); } } /** * Encodes an image in the PNG format. * * @param image the input {@link HipiImage} to be encoded * @param os the {@link OutputStream} that the encoded image will be written to */ public void encodeImage(HipiImage image, OutputStream os) throws IllegalArgumentException, IOException { if (!(RasterImage.class.isAssignableFrom(image.getClass()))) { throw new IllegalArgumentException("PNG encoder supports only RasterImage input types."); } if (image.getWidth() <= 0 || image.getHeight() <= 0) { throw new IllegalArgumentException("Invalid image dimensions."); } if (image.getColorSpace() != HipiColorSpace.RGB) { throw new IllegalArgumentException("PNG encoder supports only linear RGB color space."); } if (image.getNumBands() != 3) { throw new IllegalArgumentException("PNG encoder supports only three band images."); } crc = new CRC32(); int width = image.getWidth(); int height = image.getHeight(); final byte id[] = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13}; write(os, id); crc.reset(); write(os, "IHDR".getBytes()); write(os, width); write(os, height); byte head[] = null; int mode = COLOR_MODE; switch (mode) { case BW_MODE: head = new byte[] {1, 0, 0, 0, 0}; break; case GREYSCALE_MODE: head = new byte[] {8, 0, 0, 0, 0}; break; case COLOR_MODE: head = new byte[] {8, 2, 0, 0, 0}; break; } write(os, head); write(os, (int) crc.getValue()); ByteArrayOutputStream compressed = new ByteArrayOutputStream(65536); BufferedOutputStream bos = new BufferedOutputStream(new DeflaterOutputStream(compressed, new Deflater(9))); PixelArray pa = ((RasterImage)image).getPixelArray(); switch (mode) { case COLOR_MODE: for (int y = 0; y < height; y++) { bos.write(0); for (int x = 0; x < width; x++) { /* int r = Math.min(Math.max((int) (image.getPixel(x, y, 0) * 255), 0), 255); int g = Math.min(Math.max((int) (image.getPixel(x, y, 1) * 255), 0), 255); int b = Math.min(Math.max((int) (image.getPixel(x, y, 2) * 255), 0), 255); */ int r = pa.getElem((y*width+x)*3+0); int g = pa.getElem((y*width+x)*3+1); int b = pa.getElem((y*width+x)*3+2); bos.write((byte)r); bos.write((byte)g); bos.write((byte)b); } } break; } bos.close(); write(os, compressed.size()); crc.reset(); write(os, "IDAT".getBytes()); write(os, compressed.toByteArray()); write(os, (int) crc.getValue()); write(os, 0); crc.reset(); write(os, "IEND".getBytes()); write(os, (int) crc.getValue()); os.close(); } private void write(OutputStream os, int i) throws IOException { byte b[] = {(byte) ((i >> 24) & 0xff), (byte) ((i >> 16) & 0xff), (byte) ((i >> 8) & 0xff), (byte) (i & 0xff)}; write(os, b); } private void write(OutputStream os, byte b[]) throws IOException { os.write(b); crc.update(b); } }