1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.graphics; 18 19 import java.io.BufferedInputStream; 20 import java.io.ByteArrayOutputStream; 21 import java.io.DataInputStream; 22 import java.io.DataOutputStream; 23 import java.io.IOException; 24 import java.io.OutputStream; 25 import java.nio.ByteBuffer; 26 import java.util.zip.CRC32; 27 import java.util.zip.CheckedOutputStream; 28 import java.util.zip.Deflater; 29 import java.util.zip.DeflaterOutputStream; 30 import java.util.zip.InflaterInputStream; 31 32 import com.badlogic.gdx.files.FileHandle; 33 import com.badlogic.gdx.graphics.Pixmap.Format; 34 import com.badlogic.gdx.utils.ByteArray; 35 import com.badlogic.gdx.utils.Disposable; 36 import com.badlogic.gdx.utils.GdxRuntimeException; 37 import com.badlogic.gdx.utils.StreamUtils; 38 39 /** Writes Pixmaps to various formats. 40 * @author mzechner 41 * @author Nathan Sweet */ 42 public class PixmapIO { 43 /** Writes the {@link Pixmap} to the given file using a custom compression scheme. First three integers define the width, height 44 * and format, remaining bytes are zlib compressed pixels. To be able to load the Pixmap to a Texture, use ".cim" as the file 45 * suffix. Throws a GdxRuntimeException in case the Pixmap couldn't be written to the file. 46 * @param file the file to write the Pixmap to */ writeCIM(FileHandle file, Pixmap pixmap)47 static public void writeCIM (FileHandle file, Pixmap pixmap) { 48 CIM.write(file, pixmap); 49 } 50 51 /** Reads the {@link Pixmap} from the given file, assuming the Pixmap was written with the 52 * {@link PixmapIO#writeCIM(FileHandle, Pixmap)} method. Throws a GdxRuntimeException in case the file couldn't be read. 53 * @param file the file to read the Pixmap from */ readCIM(FileHandle file)54 static public Pixmap readCIM (FileHandle file) { 55 return CIM.read(file); 56 } 57 58 /** Writes the pixmap as a PNG with compression. See {@link PNG} to configure the compression level, more efficiently flip the 59 * pixmap vertically, and to write out multiple PNGs with minimal allocation. */ writePNG(FileHandle file, Pixmap pixmap)60 static public void writePNG (FileHandle file, Pixmap pixmap) { 61 try { 62 PNG writer = new PNG((int)(pixmap.getWidth() * pixmap.getHeight() * 1.5f)); // Guess at deflated size. 63 try { 64 writer.setFlipY(false); 65 writer.write(file, pixmap); 66 } finally { 67 writer.dispose(); 68 } 69 } catch (IOException ex) { 70 throw new GdxRuntimeException("Error writing PNG: " + file, ex); 71 } 72 } 73 74 /** @author mzechner */ 75 static private class CIM { 76 static private final int BUFFER_SIZE = 32000; 77 static private final byte[] writeBuffer = new byte[BUFFER_SIZE]; 78 static private final byte[] readBuffer = new byte[BUFFER_SIZE]; 79 write(FileHandle file, Pixmap pixmap)80 static public void write (FileHandle file, Pixmap pixmap) { 81 DataOutputStream out = null; 82 83 try { 84 // long start = System.nanoTime(); 85 DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(file.write(false)); 86 out = new DataOutputStream(deflaterOutputStream); 87 out.writeInt(pixmap.getWidth()); 88 out.writeInt(pixmap.getHeight()); 89 out.writeInt(Format.toGdx2DPixmapFormat(pixmap.getFormat())); 90 91 ByteBuffer pixelBuf = pixmap.getPixels(); 92 pixelBuf.position(0); 93 pixelBuf.limit(pixelBuf.capacity()); 94 95 int remainingBytes = pixelBuf.capacity() % BUFFER_SIZE; 96 int iterations = pixelBuf.capacity() / BUFFER_SIZE; 97 98 synchronized (writeBuffer) { 99 for (int i = 0; i < iterations; i++) { 100 pixelBuf.get(writeBuffer); 101 out.write(writeBuffer); 102 } 103 104 pixelBuf.get(writeBuffer, 0, remainingBytes); 105 out.write(writeBuffer, 0, remainingBytes); 106 } 107 108 pixelBuf.position(0); 109 pixelBuf.limit(pixelBuf.capacity()); 110 // Gdx.app.log("PixmapIO", "write (" + file.name() + "):" + (System.nanoTime() - start) / 1000000000.0f + ", " + 111 // Thread.currentThread().getName()); 112 } catch (Exception e) { 113 throw new GdxRuntimeException("Couldn't write Pixmap to file '" + file + "'", e); 114 } finally { 115 StreamUtils.closeQuietly(out); 116 } 117 } 118 read(FileHandle file)119 static public Pixmap read (FileHandle file) { 120 DataInputStream in = null; 121 122 try { 123 // long start = System.nanoTime(); 124 in = new DataInputStream(new InflaterInputStream(new BufferedInputStream(file.read()))); 125 int width = in.readInt(); 126 int height = in.readInt(); 127 Format format = Format.fromGdx2DPixmapFormat(in.readInt()); 128 Pixmap pixmap = new Pixmap(width, height, format); 129 130 ByteBuffer pixelBuf = pixmap.getPixels(); 131 pixelBuf.position(0); 132 pixelBuf.limit(pixelBuf.capacity()); 133 134 synchronized (readBuffer) { 135 int readBytes = 0; 136 while ((readBytes = in.read(readBuffer)) > 0) { 137 pixelBuf.put(readBuffer, 0, readBytes); 138 } 139 } 140 141 pixelBuf.position(0); 142 pixelBuf.limit(pixelBuf.capacity()); 143 // Gdx.app.log("PixmapIO", "read:" + (System.nanoTime() - start) / 1000000000.0f); 144 return pixmap; 145 } catch (Exception e) { 146 throw new GdxRuntimeException("Couldn't read Pixmap from file '" + file + "'", e); 147 } finally { 148 StreamUtils.closeQuietly(in); 149 } 150 } 151 } 152 153 /** PNG encoder with compression. An instance can be reused to encode multiple PNGs with minimal allocation. 154 * 155 * <pre> 156 * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de 157 * Copyright (c) 2014 Nathan Sweet 158 * 159 * Permission is hereby granted, free of charge, to any person obtaining a copy 160 * of this software and associated documentation files (the "Software"), to deal 161 * in the Software without restriction, including without limitation the rights 162 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 163 * copies of the Software, and to permit persons to whom the Software is 164 * furnished to do so, subject to the following conditions: 165 * 166 * The above copyright notice and this permission notice shall be included in 167 * all copies or substantial portions of the Software. 168 * 169 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 170 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 171 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 172 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 173 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 174 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 175 * THE SOFTWARE. 176 * </pre> 177 * @author Matthias Mann 178 * @author Nathan Sweet */ 179 static public class PNG implements Disposable { 180 static private final byte[] SIGNATURE = {(byte)137, 80, 78, 71, 13, 10, 26, 10}; 181 static private final int IHDR = 0x49484452, IDAT = 0x49444154, IEND = 0x49454E44; 182 static private final byte COLOR_ARGB = 6; 183 static private final byte COMPRESSION_DEFLATE = 0; 184 static private final byte FILTER_NONE = 0; 185 static private final byte INTERLACE_NONE = 0; 186 static private final byte PAETH = 4; 187 188 private final ChunkBuffer buffer; 189 private final Deflater deflater; 190 private ByteArray lineOutBytes, curLineBytes, prevLineBytes; 191 private boolean flipY = true; 192 private int lastLineLen; 193 PNG()194 public PNG () { 195 this(128 * 128); 196 } 197 PNG(int initialBufferSize)198 public PNG (int initialBufferSize) { 199 buffer = new ChunkBuffer(initialBufferSize); 200 deflater = new Deflater(); 201 } 202 203 /** If true, the resulting PNG is flipped vertically. Default is true. */ setFlipY(boolean flipY)204 public void setFlipY (boolean flipY) { 205 this.flipY = flipY; 206 } 207 208 /** Sets the deflate compression level. Default is {@link Deflater#DEFAULT_COMPRESSION}. */ setCompression(int level)209 public void setCompression (int level) { 210 deflater.setLevel(level); 211 } 212 write(FileHandle file, Pixmap pixmap)213 public void write (FileHandle file, Pixmap pixmap) throws IOException { 214 OutputStream output = file.write(false); 215 try { 216 write(output, pixmap); 217 } finally { 218 StreamUtils.closeQuietly(output); 219 } 220 } 221 222 /** Writes the pixmap to the stream without closing the stream. */ write(OutputStream output, Pixmap pixmap)223 public void write (OutputStream output, Pixmap pixmap) throws IOException { 224 DeflaterOutputStream deflaterOutput = new DeflaterOutputStream(buffer, deflater); 225 DataOutputStream dataOutput = new DataOutputStream(output); 226 dataOutput.write(SIGNATURE); 227 228 buffer.writeInt(IHDR); 229 buffer.writeInt(pixmap.getWidth()); 230 buffer.writeInt(pixmap.getHeight()); 231 buffer.writeByte(8); // 8 bits per component. 232 buffer.writeByte(COLOR_ARGB); 233 buffer.writeByte(COMPRESSION_DEFLATE); 234 buffer.writeByte(FILTER_NONE); 235 buffer.writeByte(INTERLACE_NONE); 236 buffer.endChunk(dataOutput); 237 238 buffer.writeInt(IDAT); 239 deflater.reset(); 240 241 int lineLen = pixmap.getWidth() * 4; 242 byte[] lineOut, curLine, prevLine; 243 if (lineOutBytes == null) { 244 lineOut = (lineOutBytes = new ByteArray(lineLen)).items; 245 curLine = (curLineBytes = new ByteArray(lineLen)).items; 246 prevLine = (prevLineBytes = new ByteArray(lineLen)).items; 247 } else { 248 lineOut = lineOutBytes.ensureCapacity(lineLen); 249 curLine = curLineBytes.ensureCapacity(lineLen); 250 prevLine = prevLineBytes.ensureCapacity(lineLen); 251 for (int i = 0, n = lastLineLen; i < n; i++) 252 prevLine[i] = 0; 253 } 254 lastLineLen = lineLen; 255 256 ByteBuffer pixels = pixmap.getPixels(); 257 int oldPosition = pixels.position(); 258 boolean rgba8888 = pixmap.getFormat() == Format.RGBA8888; 259 for (int y = 0, h = pixmap.getHeight(); y < h; y++) { 260 int py = flipY ? (h - y - 1) : y; 261 if (rgba8888) { 262 pixels.position(py * lineLen); 263 pixels.get(curLine, 0, lineLen); 264 } else { 265 for (int px = 0, x = 0; px < pixmap.getWidth(); px++) { 266 int pixel = pixmap.getPixel(px, py); 267 curLine[x++] = (byte)((pixel >> 24) & 0xff); 268 curLine[x++] = (byte)((pixel >> 16) & 0xff); 269 curLine[x++] = (byte)((pixel >> 8) & 0xff); 270 curLine[x++] = (byte)(pixel & 0xff); 271 } 272 } 273 274 lineOut[0] = (byte)(curLine[0] - prevLine[0]); 275 lineOut[1] = (byte)(curLine[1] - prevLine[1]); 276 lineOut[2] = (byte)(curLine[2] - prevLine[2]); 277 lineOut[3] = (byte)(curLine[3] - prevLine[3]); 278 279 for (int x = 4; x < lineLen; x++) { 280 int a = curLine[x - 4] & 0xff; 281 int b = prevLine[x] & 0xff; 282 int c = prevLine[x - 4] & 0xff; 283 int p = a + b - c; 284 int pa = p - a; 285 if (pa < 0) pa = -pa; 286 int pb = p - b; 287 if (pb < 0) pb = -pb; 288 int pc = p - c; 289 if (pc < 0) pc = -pc; 290 if (pa <= pb && pa <= pc) 291 c = a; 292 else if (pb <= pc) // 293 c = b; 294 lineOut[x] = (byte)(curLine[x] - c); 295 } 296 297 deflaterOutput.write(PAETH); 298 deflaterOutput.write(lineOut, 0, lineLen); 299 300 byte[] temp = curLine; 301 curLine = prevLine; 302 prevLine = temp; 303 } 304 pixels.position(oldPosition); 305 deflaterOutput.finish(); 306 buffer.endChunk(dataOutput); 307 308 buffer.writeInt(IEND); 309 buffer.endChunk(dataOutput); 310 311 output.flush(); 312 } 313 314 /** Disposal will happen automatically in {@link #finalize()} but can be done explicitly if desired. */ dispose()315 public void dispose () { 316 deflater.end(); 317 } 318 319 static class ChunkBuffer extends DataOutputStream { 320 final ByteArrayOutputStream buffer; 321 final CRC32 crc; 322 ChunkBuffer(int initialSize)323 ChunkBuffer (int initialSize) { 324 this(new ByteArrayOutputStream(initialSize), new CRC32()); 325 } 326 ChunkBuffer(ByteArrayOutputStream buffer, CRC32 crc)327 private ChunkBuffer (ByteArrayOutputStream buffer, CRC32 crc) { 328 super(new CheckedOutputStream(buffer, crc)); 329 this.buffer = buffer; 330 this.crc = crc; 331 } 332 endChunk(DataOutputStream target)333 public void endChunk (DataOutputStream target) throws IOException { 334 flush(); 335 target.writeInt(buffer.size() - 4); 336 buffer.writeTo(target); 337 target.writeInt((int)crc.getValue()); 338 buffer.reset(); 339 crc.reset(); 340 } 341 } 342 } 343 } 344