com.github.tommyettinger.anim8.FastPNG Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of anim8-gdx Show documentation
Show all versions of anim8-gdx Show documentation
Support for writing animated GIF and PNG files, plus PNG8, to libGDX.
/*
* Copyright (c) 2023 Tommy Ettinger
*
* 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.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.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.ByteBuffer;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* An almost-drop-in replacement for {@link com.badlogic.gdx.graphics.PixmapIO.PNG},
* optimized for speed at the expense of features.
* This type of 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
* {@link PNG8}, which have comparable APIs. An instance can be
* reused to encode multiple PNGs with minimal allocation.
*
* The PNG files this produces default to using a somewhat-low compression level, but
* you can change the compression level to write large files quickly or small files
* slowly. The {@link #setCompression(int)} method can be set to 0 for the former large
* files, or 6 or higher for small files. You are encouraged to use some kind of tool to
* optimize the file size of less-compressed PNGs that you want to host online;
* oxipng or
* PNGOUT are good choices.
*
* This class has been optimized at the expense of some features. It reads bytes from a
* Pixmap's {@link Pixmap#getPixels()} buffer directly, and if the Pixmap uses RGBA8888
* format, it can copy whole rows at a time into the output PNG. If the Pixmap doesn't
* use RGBA8888 format, this isn't as fast and still produces RGBA8888 PNGs. This class
* does not support {@link #setFlipY(boolean)}. The method can be called, but
* it doesn't do anything. This is a consequence of how bytes are read in.
*
*
* 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 FastPNG implements Disposable {
static private final byte[] SIGNATURE = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
static private final int IHDR = 0x49484452;
static private final int IDAT = 0x49444154;
static private final int 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;
private final ChunkBuffer buffer;
private final Deflater deflater;
private ByteArray curLineBytes;
/**
* Creates an FastPNG writer with an initial buffer size of 1024. The buffer can resize later if needed.
*/
public FastPNG() {
this(1024);
}
/**
* Creates an FastPNG 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 1024 is reasonable.
* @param initialBufferSize the initial size for the buffer that stores PNG chunks; 1024 is a reasonable default
*/
public FastPNG(int initialBufferSize) {
buffer = new ChunkBuffer(initialBufferSize);
deflater = new Deflater(2);
}
/**
* A no-op; this class never flips the image, regardless of the setting. This method
* is here for API compatibility with PixmapIO.PNG, and also for possible future changes
* if flipping becomes viable.
*/
public void setFlipY(boolean flipY) {
}
/**
* Sets the deflate compression level. Default is 2 here instead of the default in PixmapIO.PNG, which is 6. Using
* compression level 2 is faster, but doesn't compress quite as well. You can set the compression level as low as 0,
* which is extremely fast but does no compression and so produces large files. You can set the compression level as
* high as 9, which is extremely slow and typically not much smaller than compression level 6.
*/
public void setCompression(int level) {
deflater.setLevel(level);
}
/**
* Writes the given Pixmap to the requested FileHandle. This can use all 32-bit colors.
* @param file a FileHandle that must be writable, and will have the given Pixmap written as a PNG image
* @param pixmap a Pixmap to write to the given file
*/
public void write (FileHandle file, Pixmap pixmap) {
OutputStream output = file.write(false);
try {
write(output, pixmap);
} finally {
StreamUtils.closeQuietly(output);
}
}
/**
* Writes the given Pixmap as a PNG to the given {@code output} stream without
* closing the stream. This can use all 32-bit colors.
*
* This makes some decisions in order to optimize speed at the expense of file size, by
* default. You can adjust the compression ratio from the default 6 with {@link #setCompression(int)},
* either up to 9 (slightly better file size, but much slower to write), or down to as low as 2
* (not-much-worse file size, but much faster to write), or even 0 (with no compression, this writes
* drastically more quickly... but the files are huge unless recompressed). Using compression level 0
* can be a good idea if you know that the output files will go into a ZIP or JAR file, since those
* use the same DEFLATE algorithm that PNG does, and that can't be done twice for any gain. It can
* also be a good idea if you intend to optimize the output later using a much smarter tool, like
* oxipng or
* PNGOUT.
*
* @param output the stream to write to; the stream will not be closed
* @param pixmap the Pixmap to write
*/
public void write(OutputStream output, Pixmap pixmap){
DeflaterOutputStream deflaterOutput = new DeflaterOutputStream(buffer, deflater);
DataOutputStream dataOutput = new DataOutputStream(output);
boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888);
// This is GWT-incompatible, which is fine because DeflaterOutputStream is already.
ByteBuffer pixels = pixmap.getPixels();
try {
dataOutput.write(SIGNATURE);
buffer.writeInt(IHDR);
buffer.writeInt(pixmap.getWidth());
buffer.writeInt(pixmap.getHeight());
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(IDAT);
deflater.reset();
int lineLen = pixmap.getWidth() * 4;
byte[] curLine;
if (curLineBytes == null) {
curLine = (curLineBytes = new ByteArray(lineLen)).items;
} else {
curLine = curLineBytes.ensureCapacity(lineLen);
}
final int width = pixmap.getWidth(), height = pixmap.getHeight();
if(hasAlpha) {
for (int y = 0; y < height; y++) {
pixels.get(curLine, 0, lineLen);
////NONE
deflaterOutput.write(FILTER_NONE);
deflaterOutput.write(curLine, 0, lineLen);
//// end of filtering
}
}
else {
for (int y = 0; y < height; y++) {
for (int px = 0, x = 0; px < width; px++) {
curLine[x++] = pixels.get();
curLine[x++] = pixels.get();
curLine[x++] = pixels.get();
curLine[x++] = -1;
}
////NONE
deflaterOutput.write(FILTER_NONE);
deflaterOutput.write(curLine, 0, lineLen);
}
}
deflaterOutput.finish();
buffer.endChunk(dataOutput);
buffer.writeInt(IEND);
buffer.endChunk(dataOutput);
output.flush();
} catch (IOException e) {
Gdx.app.error("anim8", e.getMessage());
} finally {
pixels.rewind();
}
}
/**
* 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 FastPNG 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();
}
}