• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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