/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. ******************************************************************************/ package com.badlogic.gdx.graphics; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.utils.GdxRuntimeException; /** * Writes Pixmaps to various formats. * * @author mzechner * @author Nathan Sweet */ public class PixmapIO { /** * Writes the {@link Pixmap} to the given file using a custom compression scheme. First three integers define the * width, height and format, remaining bytes are zlib compressed pixels. To be able to load the Pixmap to a Texture, * use ".cim" as the file suffix! Throws a GdxRuntimeException in case the Pixmap couldn't be written to the file. * * @param file * the file to write the Pixmap to */ static public void writeCIM(FileHandle file, Pixmap pixmap) { CIM.write(file, pixmap); } /** * Reads the {@link Pixmap} from the given file, assuming the Pixmap was written with the * {@link PixmapIO#writeCIM(FileHandle, Pixmap)} method. Throws a GdxRuntimeException in case the file couldn't be * read. * * @param file * the file to read the Pixmap from */ static public Pixmap readCIM(FileHandle file) { return CIM.read(file); } /** * Writes the pixmap as a PNG. Note this method uses quite a bit of working memory. * {@link #writeCIM(FileHandle, Pixmap)} is faster if the file does not need to be read outside of libgdx. */ static public void writePNG(FileHandle file, Pixmap pixmap) { try { file.writeBytes(PNG.write(pixmap), false); } catch (IOException ex) { throw new GdxRuntimeException("Error writing PNG: " + file, ex); } } /** @author mzechner */ static private class CIM { static private final int BUFFER_SIZE = 32000; static private final byte[] writeBuffer = new byte[BUFFER_SIZE]; static private final byte[] readBuffer = new byte[BUFFER_SIZE]; static public void write(FileHandle file, Pixmap pixmap) { DataOutputStream out = null; try { // long start = System.nanoTime(); DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(file.write(false)); out = new DataOutputStream(deflaterOutputStream); out.writeInt(pixmap.getWidth()); out.writeInt(pixmap.getHeight()); out.writeInt(Format.toGdx2DPixmapFormat(pixmap.getFormat())); ByteBuffer pixelBuf = pixmap.getPixels(); pixelBuf.position(0); pixelBuf.limit(pixelBuf.capacity()); int remainingBytes = pixelBuf.capacity() % BUFFER_SIZE; int iterations = pixelBuf.capacity() / BUFFER_SIZE; synchronized (writeBuffer) { for (int i = 0; i < iterations; i++) { pixelBuf.get(writeBuffer); out.write(writeBuffer); } pixelBuf.get(writeBuffer, 0, remainingBytes); out.write(writeBuffer, 0, remainingBytes); } pixelBuf.position(0); pixelBuf.limit(pixelBuf.capacity()); // Gdx.app.log("PixmapIO", "write (" + file.name() + "):" + (System.nanoTime() - start) / 1000000000.0f + ", " + // Thread.currentThread().getName()); } catch (Exception e) { throw new GdxRuntimeException("Couldn't write Pixmap to file '" + file + "'", e); } finally { if (out != null) try { out.close(); } catch (Exception e) { } } } static public Pixmap read(FileHandle file) { DataInputStream in = null; try { // long start = System.nanoTime(); in = new DataInputStream(new InflaterInputStream(new BufferedInputStream(file.read()))); int width = in.readInt(); int height = in.readInt(); Format format = Format.fromGdx2DPixmapFormat(in.readInt()); Pixmap pixmap = new Pixmap(width, height, format); ByteBuffer pixelBuf = pixmap.getPixels(); pixelBuf.position(0); pixelBuf.limit(pixelBuf.capacity()); synchronized (readBuffer) { int readBytes = 0; while ((readBytes = in.read(readBuffer)) > 0) { pixelBuf.put(readBuffer, 0, readBytes); } } pixelBuf.position(0); pixelBuf.limit(pixelBuf.capacity()); // Gdx.app.log("PixmapIO", "read:" + (System.nanoTime() - start) / 1000000000.0f); return pixmap; } catch (Exception e) { throw new GdxRuntimeException("Couldn't read Pixmap from file '" + file + "'", e); } finally { if (in != null) try { in.close(); } catch (Exception e) { } } } } /** * Minimal PNG encoder to create PNG streams (and MIDP images) from RGBA arrays.<br> * Copyright 2006-2009 Christian Fröschlin www.chrfr.de<br> * Terms of Use: You may use the PNG encoder free of charge for any purpose you desire, as long as you do not claim * credit for the original sources and agree not to hold me responsible for any damage arising out of its use.<br> * If you have a suitable location in GUI or documentation for giving credit, I'd appreciate a non-mandatory mention * of:<br> * PNG encoder (C) 2006-2009 by Christian Fröschlin, www.chrfr.de */ static private class PNG { static int[] crcTable; static final int ZLIB_BLOCK_SIZE = 32000; static byte[] write(Pixmap pixmap) throws IOException { byte[] signature = new byte[] { (byte) 137, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10, (byte) 26, (byte) 10 }; byte[] header = PNG.createHeaderChunk(pixmap.getWidth(), pixmap.getHeight()); byte[] data = PNG.createDataChunk(pixmap); byte[] trailer = PNG.createTrailerChunk(); ByteArrayOutputStream png = new ByteArrayOutputStream(signature.length + header.length + data.length + trailer.length); png.write(signature); png.write(header); png.write(data); png.write(trailer); return png.toByteArray(); } static private byte[] createHeaderChunk(int width, int height) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(13); DataOutputStream chunk = new DataOutputStream(baos); chunk.writeInt(width); chunk.writeInt(height); chunk.writeByte(8); // Bitdepth chunk.writeByte(6); // Colortype ARGB chunk.writeByte(0); // Compression chunk.writeByte(0); // Filter chunk.writeByte(0); // Interlace return toChunk("IHDR", baos.toByteArray()); } static private byte[] createDataChunk(Pixmap pixmap) throws IOException { int width = pixmap.getWidth(); int height = pixmap.getHeight(); int dest = 0; byte[] raw = new byte[4 * width * height + height]; for (int y = 0; y < height; y++) { raw[dest++] = 0; // No filter for (int x = 0; x < width; x++) { // 32-bit RGBA8888 int pixel = pixmap.getPixel(x, y); int mask = pixel & 0xFFFFFFFF; int rr = mask >> 24 & 0xff; int gg = mask >> 16 & 0xff; int bb = mask >> 8 & 0xff; int aa = mask & 0xff; raw[dest++] = (byte) rr; raw[dest++] = (byte) gg; raw[dest++] = (byte) bb; raw[dest++] = (byte) aa; } } return toChunk("IDAT", toZLIB(raw)); } static private byte[] createTrailerChunk() throws IOException { return toChunk("IEND", new byte[] {}); } static private byte[] toChunk(String id, byte[] raw) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 12); DataOutputStream chunk = new DataOutputStream(baos); chunk.writeInt(raw.length); byte[] bid = new byte[4]; for (int i = 0; i < 4; i++) { bid[i] = (byte) id.charAt(i); } chunk.write(bid); chunk.write(raw); int crc = 0xFFFFFFFF; crc = updateCRC(crc, bid); crc = updateCRC(crc, raw); chunk.writeInt(~crc); return baos.toByteArray(); } static private void createCRCTable() { crcTable = new int[256]; for (int i = 0; i < 256; i++) { int c = i; for (int k = 0; k < 8; k++) c = (c & 1) > 0 ? 0xedb88320 ^ c >>> 1 : c >>> 1; crcTable[i] = c; } } static private int updateCRC(int crc, byte[] raw) { if (crcTable == null) createCRCTable(); for (byte element : raw) crc = crcTable[(crc ^ element) & 0xFF] ^ crc >>> 8; return crc; } /* * This method is called to encode the image data as a zlib block as required by the PNG specification. This file comes with * a minimal ZLIB encoder which uses uncompressed deflate blocks (fast, short, easy, but no compression). If you want * compression, call another encoder (such as JZLib?) here. */ static private byte[] toZLIB(byte[] raw) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 6 + raw.length / ZLIB_BLOCK_SIZE * 5); DataOutputStream zlib = new DataOutputStream(baos); byte tmp = (byte) 8; zlib.writeByte(tmp); // CM = 8, CMINFO = 0 zlib.writeByte((31 - (tmp << 8) % 31) % 31); // FCHECK // (FDICT/FLEVEL=0) int pos = 0; while (raw.length - pos > ZLIB_BLOCK_SIZE) { writeUncompressedDeflateBlock(zlib, false, raw, pos, (char) ZLIB_BLOCK_SIZE); pos += ZLIB_BLOCK_SIZE; } writeUncompressedDeflateBlock(zlib, true, raw, pos, (char) (raw.length - pos)); // zlib check sum of uncompressed data zlib.writeInt(calcADLER32(raw)); return baos.toByteArray(); } static private void writeUncompressedDeflateBlock(DataOutputStream zlib, boolean last, byte[] raw, int off, char len) throws IOException { zlib.writeByte((byte) (last ? 1 : 0)); // Final flag, Compression type 0 zlib.writeByte((byte) (len & 0xFF)); // Length LSB zlib.writeByte((byte) ((len & 0xFF00) >> 8)); // Length MSB zlib.writeByte((byte) (~len & 0xFF)); // Length 1st complement LSB zlib.writeByte((byte) ((~len & 0xFF00) >> 8)); // Length 1st complement // MSB zlib.write(raw, off, len); // Data } private static int calcADLER32(final byte[] raw) { int s1 = 1; int s2 = 0; for (int i = 0; i < raw.length; i++) { final int abs = raw[i] >= 0 ? raw[i] : (raw[i] + 256); s1 = (s1 + abs) % 65521; s2 = (s2 + s1) % 65521; } return (s2 << 16) + s1; } } }