/* * Syncany, www.syncany.org * Copyright (C) 2011-2014 Philipp C. Heckel <philipp.heckel@gmail.com> * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package org.syncany.plugins.flickr; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; import java.util.Arrays; import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterOutputStream; /** * Encodes any byte array, stream or file into a PNG image. * * The <code>encodeToPng()</code>-methods can be used to transform any binary data * into a rectangular image and stored in the widely supported PNG * format. The method does not hide, encrypt or compress the source data in any * way. It merely prepends a PNG header and transforms the payload as * specified by the PNG file format. * * <p>The <code>decodeFromPng()</code>-methods retrieve the orginal payload from a * previously encoded PNG image. A PNG not encoded with this class cannot be read! * * <p><b>Example:</b> * * <pre> * // Encode file "/etc/hosts" into file "/tmp/hosts.png", and restore it to "/tmp/hosts-restored" * PngEncoder.encodeToPng(new File("/etc/hosts"), new File("/tmp/hosts.png")); * PngEncoder.decodeFromPng(new File("/tmp/hosts.png"), new File("/tmp/hosts-restored")); * * // Encode 3 bytes into a PNG file * PngEncoder.encodeToPng(new byte[] { 0x01, 0x02, 0x03 }, new File("/tmp/3-bytes.png")); * byte[] threebytes = PngEncoder.decodeFromPng(new File("/tmp/3-bytes.png")); * </pre> * * @author Philipp C. Heckel <philipp.heckel@gmail.com> * @see http://www.w3.org/TR/PNG/ */ public class PngEncoder { private static final int PNG_CHUNK_IHDR_OFFSET_DATA_TYPE = 4; private static final int PNG_CHUNK_IHDR_SIZE_DATA_AND_TYPE = 17; private static final int PNG_CHUNK_IHDR_OFFSET_IMAGE_WIDTH = 8; private static final int PNG_CHUNK_IHDR_OFFSET_IMAGE_HEIGHT = 12; private static final int PNG_CHUNK_IHDR_OFFSET_CRC_CHECKSUM = 21; private static final int PNG_CHUNK_IHDR_SIZE_IMAGE_WIDTH = 4; private static final int PNG_CHUNK_IHDR_SIZE_IMAGE_HEIGHT = 4; private static final int PNG_CHUNK_IHDR_SIZE_BIT_DEPTH = 1; private static final int PNG_CHUNK_IHDR_SIZE_COLOR_TYPE = 1; private static final int PNG_CHUNK_IHDR_SIZE_COMPRESSION_METHOD = 1; private static final int PNG_CHUNK_IHDR_SIZE_FILTER_METHOD = 1; private static final int PNG_CHUNK_IHDR_SIZE_INTERLACE_METHOD = 1; private static final int PNG_CHUNK_IHDR_SIZE_CRC_CHECKSUM = 4; private static final int PNG_CHUNK_TEXT_OFFSET_DATA_TYPE = 4; private static final int PNG_CHUNK_TEXT_OFFSET_MAGIC_IDENTIFIER = 8; private static final int PNG_CHUNK_TEXT_SIZE_DATA_AND_TYPE = 12; private static final int PNG_CHUNK_TEXT_SIZE_MAGIC_IDENTIFIER = 4; private static final int PNG_CHUNK_TEXT_SIZE_PAYLOAD = 4; private static final int PNG_CHUNK_TEXT_OFFSET_PAYLOAD_LENGTH = 12; private static final int PNG_CHUNK_TEXT_OFFSET_CRC_CHECKSUM = 16; private static final int PNG_CHUNK_TEXT_SIZE_CRC_CHECKSUM = 4; private static final byte[] PNG_CHUNK_IDAT_TYPE = new byte[] { 0x49, 0x44, 0x41, 0x54 }; private static final byte PNG_CHUNK_IDAT_BEGIN_FILTER_METHOD = 0x00; private static final int PNG_CHUNK_IDAT_BEGIN_OFFSET_DATA_SIZE = 0; private static final int PNG_CHUNK_IDAT_END_OFFSET_CRC_CHECKSUM = 0; private static final int PNG_CHUNK_IDAT_BEGIN_SIZE_FILTER_METHOD = 1; private static final byte UDEF = 0x00; // undefined value in PNG header, to be overwritten by methods private static final byte[] PNG_SIGNATURE = new byte[] { /* 00 */ (byte) 0x89, 0x50, 0x4e, 0x47, // PNG magic, signature /* 04 */ 0x0d, 0x0a, 0x1a, 0x0a, }; private static final byte[] PNG_CHUNK_IHDR = new byte[] { /* 00 */ 0x00, 0x00, 0x00, 0x0d, // Chunk size, always 0x0d = 13 /* 04 */ 0x49, 0x48, 0x44, 0x52, // Chunk type "IHDR" /* 08 */ UDEF, UDEF, UDEF, UDEF, // Image width /* 12 */ UDEF, UDEF, UDEF, UDEF, // Image height /* 16 */ 0x08, // Bit depth (here: 8 bits, for color type true color) /* 17 */ 0x02, // Color type (here: 2 / true color) /* 18 */ 0x00, // Compression method (here: 0, deflate/inflate) /* 19 */ 0x00, // Filter method (here: adaptive filtering with five basic filter type) /* 20 */ 0x00, // Interlace method (here: 0, no interlace) /* 21 */ UDEF, UDEF, UDEF, UDEF // CRC of IHDR chunk data }; private static final byte[] PNG_CHUNK_SRGB = new byte[] { /* 00 */ 0x00, 0x00, 0x00, 0x01, // Chunk size, always 01 /* 04 */ 0x73, 0x52, 0x47, 0x42, // Chunk type "sRGB" /* 08 */ 0x00, // RGB mode, here: 0 /* 09 */(byte) 0xae, (byte) 0xce, 0x1c, (byte) 0xe9 // CRC of sRGB chunk data }; private static final byte[] PNG_CHUNK_TEXT = new byte[] { /* 00 */ 0x00, 0x00, 0x00, 0x08, // Chunk size, here: 8 /* 04 */ 0x74, 0x45, 0x58, 0x74, // Chunk type "tEXt" /* 08 */ UDEF, UDEF, UDEF, UDEF, // Magic identifier (!) /* 12 */ UDEF, UDEF, UDEF, UDEF, // Payload length (!) /* 16 */ UDEF, UDEF, UDEF, UDEF // CRC of tEXt chunk data }; private static final byte[] PNG_CHUNK_TEXT_MAGIC_IDENTIFIER = new byte[] { /* 00 */ 0x4e, 0x33, 0x52, 0x44 // Magic identifier "N3RD" }; private static final byte[] PNG_CHUNK_IDAT_BEGIN = new byte[] { /* 00 */ UDEF, UDEF, UDEF, UDEF, // Chunk size, unsigned int /* 04 */ 0x49, 0x44, 0x41, 0x54, // Chunk type "IDAT" }; private static final byte[] PNG_CHUNK_IDAT_END = new byte[] { UDEF, UDEF, UDEF, UDEF, // CRC of IDAT chunk data }; private static final byte[] PNG_CHUNK_IEND = new byte[] { 0x00, 0x00, 0x00, 0x00, // Chunk size, always 0 0x49, 0x45, 0x4e, 0x44, // Chunk type "IEND" (byte) 0xae, 0x42, 0x60, (byte) 0x82 // CRC of IEND chunk data }; /** * Encodes any <code>File</code> into a PNG and outputs the image * to another <code>File</code>. * * <p>The method does not hide, encrypt or compress the source data in any * way. It merely prepends a PNG header and transforms the payload as * specified by the PNG file format. * * @param srcFile File to be read pointing to the input data (payload) * @param destFile File to write the PNG file to * @throws IOException Thrown if the input/output stream cannot be read/written */ public static void encodeToPng(File srcFile, File destFile) throws IOException { encodeToPng(new FileInputStream(srcFile), (int) srcFile.length(), new FileOutputStream(destFile)); } /** * Encodes any <code>File</code> into a PNG and outputs the image * to another <code>File</code>. * * <p>The method does not hide, encrypt or compress the source data in any * way. It merely prepends a PNG header and transforms the payload as * specified by the PNG file format. * * @param srcBytes Byte array of input data (payload) * @param destStream Stream to write the PNG file to * @throws IOException Thrown if the input/output stream cannot be read/written */ public static void encodeToPng(byte[] srcBytes, File destFile) throws IOException { encodeToPng(new ByteArrayInputStream(srcBytes), srcBytes.length, new FileOutputStream(destFile)); } /** * Encodes a byte array into a PNG and outputs the image * to an <code>OutputStream</code>. * * <p>The method does not hide, encrypt or compress the source data in any * way. It merely prepends a PNG header and transforms the payload as * specified by the PNG file format. * * @param srcBytes Byte array of input data (payload) * @param destStream Stream to write the PNG file to * @throws IOException Thrown if the input/output stream cannot be read/written */ public static void encodeToPng(byte[] srcBytes, OutputStream destStream) throws IOException { encodeToPng(new ByteArrayInputStream(srcBytes), srcBytes.length, destStream); } /** * Encodes an <code>InputStream</code> into a PNG and outputs the image * as byte array. * * <p>The method does not hide, encrypt or compress the source data in any * way. It merely prepends a PNG header and transforms the payload as * specified by the PNG file format. * * @param srcStream Stream of input data (payload) * @param srcStreamLength Length of the input data stream (in bytes) * @param destStream Stream to write the PNG file to * @throws IOException Thrown if the input/output stream cannot be read/written */ public static void encodeToPng(InputStream srcStream, int srcStreamLength, OutputStream destStream) throws IOException { if (srcStreamLength > Integer.MAX_VALUE) { throw new IOException("File too big; max. " + Integer.MAX_VALUE + " bytes supported."); } // Write PNG signature destStream.write(PNG_SIGNATURE, 0, PNG_SIGNATURE.length); // Write IHDR chunk int ihdrImageWidth = (int) Math.ceil(Math.sqrt((double) srcStreamLength / 3)); // Image width, sqrt(payload/3), divided by 3 because of RGB int ihdrImageHeight = (int) Math.ceil((double) srcStreamLength // Image height, payload / image width / 3 / (double) ihdrImageWidth / 3); byte[] ihdrChunk = PNG_CHUNK_IHDR.clone(); // Clone PNG header template, and overwrite with fields writeIntBE(ihdrChunk, PNG_CHUNK_IHDR_OFFSET_IMAGE_WIDTH, ihdrImageWidth); writeIntBE(ihdrChunk, PNG_CHUNK_IHDR_OFFSET_IMAGE_HEIGHT, ihdrImageHeight); int ihdrChecksum = calculateChunkChecksum(ihdrChunk, PNG_CHUNK_IHDR_OFFSET_DATA_TYPE, PNG_CHUNK_IHDR_SIZE_DATA_AND_TYPE); writeIntBE(ihdrChunk, PNG_CHUNK_IHDR_OFFSET_CRC_CHECKSUM, ihdrChecksum); destStream.write(ihdrChunk, 0, ihdrChunk.length); // Write sRGB chunk destStream.write(PNG_CHUNK_SRGB); // Write tEXt chunk (with magic ID and hidden data size) byte[] textChunk = PNG_CHUNK_TEXT.clone(); System.arraycopy(PNG_CHUNK_TEXT_MAGIC_IDENTIFIER, 0, textChunk, PNG_CHUNK_TEXT_OFFSET_MAGIC_IDENTIFIER, PNG_CHUNK_TEXT_SIZE_MAGIC_IDENTIFIER); writeIntBE(textChunk, PNG_CHUNK_TEXT_OFFSET_PAYLOAD_LENGTH, srcStreamLength); int textChecksum = calculateChunkChecksum(textChunk, PNG_CHUNK_TEXT_OFFSET_DATA_TYPE, PNG_CHUNK_TEXT_SIZE_DATA_AND_TYPE); writeIntBE(textChunk, PNG_CHUNK_TEXT_OFFSET_CRC_CHECKSUM, textChecksum); destStream.write(textChunk, 0, textChunk.length); // Create IDAT chunk // 1. Compress data Deflater deflater = new Deflater(); deflater.setStrategy(Deflater.FILTERED); deflater.setLevel(1); ByteArrayOutputStream compressedIdatOutputStream = new ByteArrayOutputStream(); DeflaterOutputStream deflaterDestStream = new DeflaterOutputStream(compressedIdatOutputStream, deflater); byte[] row = new byte[ihdrImageWidth * 3]; int numberOfLines = ihdrImageHeight; int numberOfBytesRead = -1; for (int i = 0; i < numberOfLines; i++) { numberOfBytesRead = srcStream.read(row); if (numberOfBytesRead == -1) { throw new IOException("Unable to read input stream data."); } deflaterDestStream.write(PNG_CHUNK_IDAT_BEGIN_FILTER_METHOD); deflaterDestStream.write(row, 0, numberOfBytesRead); } int numberOfPaddingBytesForLastRow = row.length - numberOfBytesRead; deflaterDestStream.write(new byte[numberOfPaddingBytesForLastRow]); deflaterDestStream.finish(); deflaterDestStream.flush(); byte[] compressedIdatOutput = compressedIdatOutputStream.toByteArray(); // 2. Write chunk size byte[] idatChunkBegin = PNG_CHUNK_IDAT_BEGIN.clone(); // Clone PNG header template, and overwrite with fields int idatDataSize = compressedIdatOutput.length;//(ihdrImageWidth+1)*ihdrImageWidth*3; writeIntBE(idatChunkBegin, PNG_CHUNK_IDAT_BEGIN_OFFSET_DATA_SIZE, idatDataSize); destStream.write(idatChunkBegin); // 3. Write chunk data destStream.write(compressedIdatOutput); // 4. Write chunk checksum CRC32 idatCRC32 = new CRC32(); idatCRC32.update(new byte[] { 0x49, 0x44, 0x41, 0x54 }); // TODO "IDAT" idatCRC32.update(compressedIdatOutput); byte[] idatChunkEnd = PNG_CHUNK_IDAT_END.clone(); int idatChecksum = (int) idatCRC32.getValue(); writeIntBE(idatChunkEnd, PNG_CHUNK_IDAT_END_OFFSET_CRC_CHECKSUM, idatChecksum); destStream.write(idatChunkEnd); // Create IEND chunk destStream.write(PNG_CHUNK_IEND); srcStream.close(); destStream.close(); } public static String toHex(byte[] bytes) { BigInteger bi = new BigInteger(1, bytes); return String.format("%0" + (bytes.length << 1) + "x", bi); } private static int calculateChunkChecksum(byte[] chunk, int offset, int length) { CRC32 crc32 = new CRC32(); crc32.update(chunk, offset, length); return (int) crc32.getValue(); } /** * Decodes a PNG image previously encoded by this class from * an <code>InputStream</code> to a byte array. * * <p>The method can only read PNGs that were encoded using one of the * <code>encodeToPng()</code> methods. It will throw an exception if * any other PNGs are read. * * @param srcStream Stream of input data (PNG) * @return The original data read from the PNG (payload) * @throws IOException Thrown if the input/output stream cannot be read/written */ public static byte[] decodeFromPng(InputStream srcStream) throws IOException { ByteArrayOutputStream destStream = new ByteArrayOutputStream(); decodeFromPng(srcStream, destStream); return destStream.toByteArray(); } /** * Decodes a PNG image previously encoded by this class from * a <code>File</code> to a byte array. * * <p>The method can only read PNGs that were encoded using one of the * <code>encodeToPng()</code> methods. It will throw an exception if * any other PNGs are read. * * @param srcFile File to be read pointing to the input data (PNG data) * @return The original data read from the PNG (payload) * @throws IOException Thrown if the input/output stream cannot be read/written */ public static byte[] decodeFromPng(File srcFile) throws IOException { ByteArrayOutputStream destStream = new ByteArrayOutputStream(); decodeFromPng(new FileInputStream(srcFile), destStream); return destStream.toByteArray(); } /** * Decodes a PNG previously encoded by this class from * a <code>File</code> to another <code>File</code>. * * <p>The method can only read PNGs that were encoded using one of the * <code>encodeToPng()</code> methods. It will throw an exception if * any other PNGs are read. * * @param srcFile File to be read pointing to the input data (24-bit PNG) * @param destFile File to be written the original data to (payload) * @throws IOException Thrown if the input/output stream cannot be read/written */ public static void decodeFromPng(File srcFile, File destFile) throws IOException { decodeFromPng(new FileInputStream(srcFile), new FileOutputStream(destFile)); } /** * Decodes a PNG previously encoded by this class from * an <code>InputStream</code> to an <code>OutputStream</code>. * * <p>The method can only read PNGs that were encoded using one of the * <code>encodeToPng()</code> methods. It will throw an exception if * any other PNGs are read. * * @param srcStream Stream to be read pointing to the input data (24-bit PNG) * @param destStream Stream to be written the original data to (payload) * @throws IOException Thrown if the input/output stream cannot be read/written */ @SuppressWarnings("unused") public static void decodeFromPng(InputStream srcStream, OutputStream destStream) throws IOException { // READ HEADER long bytesRead = 0; // Skip over PNG signature bytesRead += srcStream.skip(PNG_SIGNATURE.length); // Read/skip IHDR ('image width' field) bytesRead += srcStream.skip(PNG_CHUNK_IHDR_OFFSET_IMAGE_WIDTH); byte[] imageWidthBytes = new byte[PNG_CHUNK_IHDR_SIZE_IMAGE_WIDTH]; bytesRead += srcStream.read(imageWidthBytes); int imageWidth = toIntBE(imageWidthBytes); bytesRead += srcStream.skip( PNG_CHUNK_IHDR_SIZE_IMAGE_HEIGHT + PNG_CHUNK_IHDR_SIZE_BIT_DEPTH + PNG_CHUNK_IHDR_SIZE_COLOR_TYPE + PNG_CHUNK_IHDR_SIZE_COMPRESSION_METHOD + PNG_CHUNK_IHDR_SIZE_FILTER_METHOD + PNG_CHUNK_IHDR_SIZE_INTERLACE_METHOD + PNG_CHUNK_IHDR_SIZE_CRC_CHECKSUM); // Read/skip sRGB bytesRead += srcStream.skip(PNG_CHUNK_SRGB.length); // Read/skip tEXt ('payload length' field) bytesRead += srcStream.skip(PNG_CHUNK_TEXT_OFFSET_MAGIC_IDENTIFIER); byte[] magicIdentifierBytes = new byte[PNG_CHUNK_TEXT_SIZE_MAGIC_IDENTIFIER]; bytesRead += srcStream.read(magicIdentifierBytes); //System.out.println("magic id = "+toHex(magicIdentifierBytes)); if (!Arrays.equals(magicIdentifierBytes, PNG_CHUNK_TEXT_MAGIC_IDENTIFIER)) { throw new IOException("Magic identifier in tEXt chunk not valid. Maybe PNG file is not encoded with this class."); } byte[] payloadLengthBytes = new byte[PNG_CHUNK_TEXT_SIZE_PAYLOAD]; bytesRead += srcStream.read(payloadLengthBytes); int payloadLength = toIntBE(payloadLengthBytes); //System.out.println("payload = "+payloadLength); bytesRead += srcStream.skip(PNG_CHUNK_TEXT_SIZE_CRC_CHECKSUM); ByteArrayOutputStream compressedPayloadStream = new ByteArrayOutputStream(); Chunk nextChunk = readChunk(srcStream); while (Arrays.equals(PNG_CHUNK_IDAT_TYPE, nextChunk.type)) { compressedPayloadStream.write(nextChunk.data, 0, nextChunk.size); nextChunk = readChunk(srcStream); } compressedPayloadStream.close(); ByteArrayOutputStream filteredPayloadStream = new ByteArrayOutputStream(); InflaterOutputStream inflaterStream = new InflaterOutputStream(filteredPayloadStream); inflaterStream.write(compressedPayloadStream.toByteArray()); inflaterStream.close(); filteredPayloadStream.close(); byte[] filteredPayload = filteredPayloadStream.toByteArray(); int maxRowLength = imageWidth * 3; for (int filteredPayloadOffset = 0, payloadRead = 0; payloadRead < payloadLength;) { filteredPayloadOffset += PNG_CHUNK_IDAT_BEGIN_SIZE_FILTER_METHOD; int rowLength = (payloadRead + maxRowLength < payloadLength) ? maxRowLength : payloadLength - payloadRead; destStream.write(filteredPayload, filteredPayloadOffset, rowLength); payloadRead += rowLength; filteredPayloadOffset += rowLength; } srcStream.close(); destStream.close(); } private static Chunk readChunk(InputStream srcStream) throws IOException { byte[] chunkSizeBytes = new byte[4]; srcStream.read(chunkSizeBytes); int chunkSize = toIntBE(chunkSizeBytes); byte[] chunkType = new byte[4]; srcStream.read(chunkType); byte[] chunkData = new byte[chunkSize]; srcStream.read(chunkData); byte[] chunkChecksumBytes = new byte[4]; srcStream.read(chunkChecksumBytes); int chunkChecksum = toIntBE(chunkChecksumBytes); return new Chunk(chunkSize, chunkType, chunkData, chunkChecksum); } @SuppressWarnings("unused") private static class Chunk { private int size; private byte[] type; private byte[] data; private int checksum; public Chunk(int size, byte[] type, byte[] data, int checksum) { this.size = size; this.type = type; this.data = data; this.checksum = checksum; } } /** * Write an integer to a byte array (as little endian) at a * specific offset. */ private static void writeIntBE(byte[] bytes, int startoffset, int value) { bytes[startoffset] = (byte) (value >>> 24); bytes[startoffset + 1] = (byte) (value >>> 16); bytes[startoffset + 2] = (byte) (value >>> 8); bytes[startoffset + 3] = (byte) (value); } /** * Read an integer value from a 4-byte array (as little endian). */ private static int toIntBE(byte[] value) { return ((value[0] & 0xff) << 24) | ((value[1] & 0xff) << 16) | ((value[2] & 0xff) << 8) | (value[3] & 0xff); } /** * Encode/decode a PNG from the command line. * * <p><b>Syntax:</b><br /> * <tt>PngEncoder encode SRCFILE DESTOFILE.png<br /> * <tt>PngEncoder decode SRCFILE.png DESTOFILE */ public static void main(String[] args) throws IOException { if (args.length == 3 && "encode".equals(args[0])) { PngEncoder.encodeToPng(new File(args[1]), new File(args[2])); } else if (args.length == 3 && "decode".equals(args[0])) { PngEncoder.decodeFromPng(new File(args[1]), new File(args[2])); } else { System.out.println("Usage: PngEncoder encode SRCFILE DESTFILE.png"); System.out.println(" PngEncoder decode SRCFILE.png DESTFILE"); } } }