All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.tommyettinger.anim8.AnimatedPNG Maven / Gradle / Ivy

package com.github.tommyettinger.anim8;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ByteArray;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.StreamUtils;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

/**
 * Full-color animated PNG encoder with compression.
 * This type of animated PNG supports both full color and a full alpha channel; it
 * does not reduce the colors to match a palette. If your image does not have a full
 * alpha channel and has 256 or fewer colors, you can use {@link AnimatedGif} or the
 * animated mode of {@link PNG8}, which have comparable APIs. An instance can be 
 * reused to encode multiple animated PNGs with minimal allocation.
 * 
*
 * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de
 * Copyright (c) 2014 Nathan Sweet
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * 
* * @author Matthias Mann * @author Nathan Sweet * @author Tommy Ettinger */ public class AnimatedPNG implements AnimationWriter, Disposable { static private final byte[] SIGNATURE = {(byte) 137, 80, 78, 71, 13, 10, 26, 10}; static private final int IHDR = 0x49484452, acTL = 0x6163544C, fcTL = 0x6663544C, IDAT = 0x49444154, fdAT = 0x66644154, IEND = 0x49454E44; static private final byte COLOR_ARGB = 6; static private final byte COMPRESSION_DEFLATE = 0; static private final byte FILTER_NONE = 0; static private final byte INTERLACE_NONE = 0; static private final byte PAETH = 4; private final ChunkBuffer buffer; private final Deflater deflater; private ByteArray lineOutBytes, curLineBytes, prevLineBytes; private boolean flipY = true; private int lastLineLen; /** * Creates an AnimatedPNG writer with an initial buffer size of 16384. The buffer can resize later if needed. */ public AnimatedPNG () { this(16384); } /** * Creates an AnimatedPNG writer with the given initial buffer size. The buffer can resize if needed, so using a * small size is only a problem if it slows down writing by forcing a resize for several parts of a PNG. A default * of 16384 is reasonable. * @param initialBufferSize the initial size for the buffer that stores PNG chunks; 16384 is a reasonable default */ public AnimatedPNG (int initialBufferSize) { buffer = new ChunkBuffer(initialBufferSize); deflater = new Deflater(); } /** * If true, the resulting AnimatedPNG is flipped vertically. Default is true. */ public void setFlipY(boolean flipY) { this.flipY = flipY; } /** * Sets the deflate compression level. Default is {@link Deflater#DEFAULT_COMPRESSION}, which is currently 6 on all * Java versions in the 8 to 14 range, but is permitted to change. */ public void setCompression(int level) { deflater.setLevel(level); } /** * Writes an animated PNG file consisting of the given {@code frames} to the given {@code file}, at 60 frames per * second. This doesn't guarantee that the animated PNG will be played back at a steady 60 frames per second, just * that the duration of each frame is 1/60 of a second if playback is optimal. * @param file the file location to write to; any existing file with this name will be overwritten * @param frames an Array of Pixmap frames to write in order to the animated PNG */ @Override public void write(FileHandle file, Array frames) { OutputStream output = file.write(false); try { write(output, frames, 60); } finally { StreamUtils.closeQuietly(output); } } /** * Writes an animated PNG file consisting of the given {@code frames} to the given {@code file}, * at {@code fps} frames per second. * @param file the file location to write to; any existing file with this name will be overwritten * @param frames an Array of Pixmap frames to write in order to the animated PNG * @param fps how many frames per second the animated PNG should display */ @Override public void write(FileHandle file, Array frames, int fps) { OutputStream output = file.write(false); try { write(output, frames, fps); } finally { StreamUtils.closeQuietly(output); } } /** * Writes animated PNG data consisting of the given {@code frames} to the given {@code output} stream without * closing the stream, at {@code fps} frames per second. * @param output the stream to write to; the stream will not be closed * @param frames an Array of Pixmap frames to write in order to the animated PNG * @param fps how many frames per second the animated PNG should display */ @SuppressWarnings("RedundantCast") @Override public void write(OutputStream output, Array frames, int fps) { Pixmap pixmap = frames.first(); DeflaterOutputStream deflaterOutput = new DeflaterOutputStream(buffer, deflater); DataOutputStream dataOutput = new DataOutputStream(output); try { dataOutput.write(SIGNATURE); final int width = pixmap.getWidth(); final int height = pixmap.getHeight(); buffer.writeInt(IHDR); buffer.writeInt(width); buffer.writeInt(height); buffer.writeByte(8); // 8 bits per component. buffer.writeByte(COLOR_ARGB); buffer.writeByte(COMPRESSION_DEFLATE); buffer.writeByte(FILTER_NONE); buffer.writeByte(INTERLACE_NONE); buffer.endChunk(dataOutput); buffer.writeInt(acTL); buffer.writeInt(frames.size); buffer.writeInt(0); buffer.endChunk(dataOutput); int lineLen = width * 4; byte[] lineOut, curLine, prevLine; ByteBuffer pixels; int oldPosition; boolean rgba8888 = pixmap.getFormat() == Pixmap.Format.RGBA8888; int seq = 0; for (int i = 0; i < frames.size; i++) { buffer.writeInt(fcTL); buffer.writeInt(seq++); buffer.writeInt(width); buffer.writeInt(height); buffer.writeInt(0); buffer.writeInt(0); buffer.writeShort(1); buffer.writeShort(fps); buffer.writeByte(0); buffer.writeByte(0); buffer.endChunk(dataOutput); if (i == 0) { buffer.writeInt(IDAT); } else { pixmap = frames.get(i); buffer.writeInt(fdAT); buffer.writeInt(seq++); } deflater.reset(); if (lineOutBytes == null) { lineOut = (lineOutBytes = new ByteArray(lineLen)).items; curLine = (curLineBytes = new ByteArray(lineLen)).items; prevLine = (prevLineBytes = new ByteArray(lineLen)).items; } else { lineOut = lineOutBytes.ensureCapacity(lineLen); curLine = curLineBytes.ensureCapacity(lineLen); prevLine = prevLineBytes.ensureCapacity(lineLen); for (int ln = 0, n = lastLineLen; ln < n; ln++) prevLine[ln] = 0; } lastLineLen = lineLen; pixels = pixmap.getPixels(); oldPosition = ((Buffer)pixels).position(); for (int y = 0; y < height; y++) { int py = flipY ? (height - y - 1) : y; if (rgba8888) { ((Buffer)pixels).position(py * lineLen); pixels.get(curLine, 0, lineLen); } else { for (int px = 0, x = 0; px < width; px++) { int pixel = pixmap.getPixel(px, py); curLine[x++] = (byte) ((pixel >>> 24) & 0xff); curLine[x++] = (byte) ((pixel >>> 16) & 0xff); curLine[x++] = (byte) ((pixel >>> 8) & 0xff); curLine[x++] = (byte) (pixel & 0xff); } } lineOut[0] = (byte) (curLine[0] - prevLine[0]); lineOut[1] = (byte) (curLine[1] - prevLine[1]); lineOut[2] = (byte) (curLine[2] - prevLine[2]); lineOut[3] = (byte) (curLine[3] - prevLine[3]); for (int x = 4; x < lineLen; x++) { int a = curLine[x - 4] & 0xff; int b = prevLine[x] & 0xff; int c = prevLine[x - 4] & 0xff; int p = a + b - c; int pa = p - a; if (pa < 0) pa = -pa; int pb = p - b; if (pb < 0) pb = -pb; int pc = p - c; if (pc < 0) pc = -pc; if (pa <= pb && pa <= pc) c = a; else if (pb <= pc) // c = b; lineOut[x] = (byte) (curLine[x] - c); } deflaterOutput.write(PAETH); deflaterOutput.write(lineOut, 0, lineLen); byte[] temp = curLine; curLine = prevLine; prevLine = temp; } ((Buffer)pixels).position(oldPosition); deflaterOutput.finish(); buffer.endChunk(dataOutput); } buffer.writeInt(IEND); buffer.endChunk(dataOutput); output.flush(); } catch (IOException e) { Gdx.app.error("anim8", e.getMessage()); } } /** * Disposal should probably be done explicitly, especially if using JRE versions after 8. * In Java 8 and earlier, you could rely on finalize() doing what this does, but that isn't * a safe assumption in Java 9 and later. Note, don't use the same AnimatedPNG object after you call * this method; you'll need to make a new one if you need to write again after disposing. */ @Override public void dispose() { deflater.end(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy