9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-28 11:29:17 +00:00

修改png优化实现

This commit is contained in:
XiaoMoMi
2025-10-27 03:03:28 +08:00
parent b43bb00060
commit fbcb28e923
4 changed files with 526 additions and 342 deletions

View File

@@ -89,15 +89,16 @@ resource-pack:
# Fix images that are not within the texture atlas. It is unreasonable to always rely on plugins to fix your mistakes.
# You should strive to make your resource pack more standardized after gaining some experience with resource packs.
fix-atlas: true
# Optimize your resource pack by reducing its size without any quality loss.
# Due to potentially long processing time, use this only for production resource pack.
# Optimize your resource pack by reducing its size without any noticeable quality loss.
optimization:
enable: false
# .png
texture:
enable: true
color-quantization: false
# If your image is special, for example, containing color pixels that need to be specifically recognized by a shader, the optimization might break it. You can add exclusions here.
exclude:
- pack.png
- assets/minecraft/textures/block/do_not_optimize.png
# .json / .mcmeta
json:
enable: true

View File

@@ -36,7 +36,6 @@ import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.sound.AbstractSoundManager;
import net.momirealms.craftengine.core.sound.SoundEvent;
import net.momirealms.craftengine.core.util.*;
import org.apache.commons.imaging.palette.PaletteFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.LoaderOptions;
@@ -1121,9 +1120,8 @@ public abstract class AbstractPackManager implements PackManager {
private byte[] optimizeImage(byte[] previousImageBytes) throws IOException {
try (ByteArrayInputStream is = new ByteArrayInputStream(previousImageBytes)) {
BufferedImage src = ImageIO.read(is);
PaletteFactory factory = new PaletteFactory();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new PngWriter().write(src, baos, factory);
new PngOptimizer(src).write(baos);
return baos.toByteArray();
}
}

View File

@@ -0,0 +1,521 @@
package net.momirealms.craftengine.core.util;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
public class PngOptimizer {
private static final byte[] PNG_SIGNATURE = new byte[] { (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n' };
private static final byte[] IDAT = "IDAT".getBytes(StandardCharsets.UTF_8);
private static final byte[] IEND = "IEND".getBytes(StandardCharsets.UTF_8);
private static final byte[] tRNS = "tRNS".getBytes(StandardCharsets.UTF_8);
private static final byte[] PLTE = "PLTE".getBytes(StandardCharsets.UTF_8);
private static final byte[] IHDR = "IHDR".getBytes(StandardCharsets.UTF_8);
private final BufferedImage src;
public PngOptimizer(BufferedImage src) {
this.src = src;
}
private boolean isGrayscale(final BufferedImage src) {
final int width = src.getWidth();
final int height = src.getHeight();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final int argb = src.getRGB(x, y);
final int red = 0xff & argb >> 16;
final int green = 0xff & argb >> 8;
final int blue = 0xff & argb >> 0;
if (red != green || red != blue) {
return false;
}
}
}
return true;
}
public void write(OutputStream os) throws IOException {
BufferedImage src = convertTo8BitRGB(this.src);
final int width = src.getWidth();
final int height = src.getHeight();
ImageColorInfo info = createColorInfo(src);
ImageData bestChoice = findBestFileStructure(src, info);
{
os.write(PNG_SIGNATURE);
}
{
final byte compressionMethod = 0;
final byte filterMethod = 0;
final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
final ImageHeader imageHeader = new ImageHeader(width, height, bestChoice.bitDepth, bestChoice.colorType, compressionMethod, filterMethod, interlaceMethod);
writeChunkIHDR(os, imageHeader);
}
os.write(bestChoice.data);
writeChunkIEND(os);
os.close();
}
private ImageColorInfo createColorInfo(final BufferedImage src) {
final int width = src.getWidth();
final int height = src.getHeight();
boolean isGrayscale = isGrayscale(src);
Map<Integer, Integer> ope = new HashMap<>();
Map<Integer, Integer> tra = new HashMap<>();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int argb = src.getRGB(x, y);
int alpha = (argb >> 24) & 0xFF;
if (alpha == 255) {
ope.put(argb, ope.getOrDefault(argb, 0) + 1);
} else {
tra.put(argb, ope.getOrDefault(argb, 0) + 1);
}
}
}
return new ImageColorInfo(ope, tra, isGrayscale);
}
private BufferedImage convertTo8BitRGB(BufferedImage src) {
int type = src.getType();
if (type == BufferedImage.TYPE_INT_ARGB ||
type == BufferedImage.TYPE_INT_RGB ||
type == BufferedImage.TYPE_BYTE_INDEXED) {
return src;
}
BufferedImage eightBitImage = new BufferedImage(
src.getWidth(),
src.getHeight(),
src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB
);
Graphics2D g2d = eightBitImage.createGraphics();
g2d.drawImage(src, 0, 0, null);
g2d.dispose();
return eightBitImage;
}
private boolean hasAlpha(final int transparency) {
return transparency != 0;
}
private ImageData findBestFileStructure(BufferedImage src, ImageColorInfo info) throws IOException {
byte[] normalSize = tryNormal(src, info.hasAlpha(), info.isGrayscale());
// 可以考虑使用调色盘
if (info.uniqueColorCount() <= 256) {
Pair<Palette, byte[]> palettePair = tryPalette(src, info);
byte[] paletteSize = palettePair.right();
if (normalSize.length > paletteSize.length) {
return new ImageData(PngColorType.INDEXED_COLOR, (byte) palettePair.left().calculateBitDepth(), paletteSize);
}
}
if (info.isGrayscale()) {
return new ImageData(info.hasAlpha() ? PngColorType.GREYSCALE_WITH_ALPHA : PngColorType.GREYSCALE, (byte) 8, normalSize);
} else {
return new ImageData(info.hasAlpha() ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR, (byte) 8, normalSize);
}
}
private byte[] tryNormal(BufferedImage src, boolean hasAlpha, boolean isGrayscale) throws IOException {
byte[] bytes = generatePngData(src, hasAlpha, isGrayscale);
return compressImage(bytes);
}
private byte[] generatePngData(BufferedImage src, boolean hasAlpha, boolean isGrayscale) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
int width = src.getWidth();
int height = src.getHeight();
int[] row = new int[width];
for (int y = 0; y < height; y++) {
src.getRGB(0, y, width, 1, row, 0, width);
baos.write(FilterType.NONE.ordinal());
for (int x = 0; x < width; x++) {
final int argb = row[x];
final int alpha = 0xff & argb >> 24;
final int red = 0xff & argb >> 16;
final int green = 0xff & argb >> 8;
final int blue = 0xff & argb >> 0;
if (isGrayscale) {
final int gray = (red + green + blue) / 3;
baos.write(gray);
} else {
baos.write(red);
baos.write(green);
baos.write(blue);
}
if (hasAlpha) {
baos.write(alpha);
}
}
}
return baos.toByteArray();
}
private Pair<Palette, byte[]> tryPalette(BufferedImage src, ImageColorInfo info) throws IOException {
ByteArrayOutputStream paletteOs = new ByteArrayOutputStream();
Palette palette;
if (info.hasAlpha()) {
palette = new ExactTransparentPalette(info.opaque, info.transparent);
writeChunkPLTE(paletteOs, palette);
writeChunkTRNS(paletteOs, palette);
} else {
palette = new ExactOpaquePalette(info.opaque);
writeChunkPLTE(paletteOs, palette);
}
byte[] bytes = generatePaletteData(src, palette);
paletteOs.write(compressImage(bytes));
return Pair.of(palette, paletteOs.toByteArray());
}
private byte[] generatePaletteData(BufferedImage src, Palette palette) {
int width = src.getWidth();
int height = src.getHeight();
int bitsPerIndex = palette.calculateBitDepth();
final ByteArrayOutputStream dataOs = new ByteArrayOutputStream();
final int[] row = new int[width];
for (int y = 0; y < height; y++) {
src.getRGB(0, y, width, 1, row, 0, width);
dataOs.write(FilterType.NONE.ordinal());
// 根据位深度选择相应的处理方法
switch (bitsPerIndex) {
case 4 -> process4Bit(row, width, dataOs, palette);
case 2 -> process2Bit(row, width, dataOs, palette);
case 1 -> process1Bit(row, width, dataOs, palette);
default -> process8Bit(row, width, dataOs, palette);
}
}
return dataOs.toByteArray();
}
// 处理8位深度每个索引占1字节
private void process8Bit(int[] row, int width, ByteArrayOutputStream dataOs, Palette palette) {
for (int x = 0; x < width; x++) {
final int argb = row[x];
final int index = palette.getPaletteIndex(argb);
dataOs.write(0xff & index);
}
}
// 处理4位深度每2个索引打包到1字节中
private void process4Bit(int[] row, int width, ByteArrayOutputStream dataOs, Palette palette) {
for (int x = 0; x < width; x += 2) {
final int argb1 = row[x];
final int index1 = palette.getPaletteIndex(argb1);
if (x + 1 < width) {
final int argb2 = row[x + 1];
final int index2 = palette.getPaletteIndex(argb2);
// 将两个4位索引打包到一个字节中
byte packed = (byte) ((index1 << 4) | index2);
dataOs.write(packed);
} else {
// 如果是奇数宽度,最后一个像素单独处理
byte packed = (byte) (index1 << 4);
dataOs.write(packed);
}
}
}
// 处理2位深度每4个索引打包到1字节中
private void process2Bit(int[] row, int width, ByteArrayOutputStream dataOs, Palette palette) {
for (int x = 0; x < width; x += 4) {
int packed = 0;
for (int i = 0; i < 4; i++) {
if (x + i < width) {
final int argb = row[x + i];
final int index = palette.getPaletteIndex(argb);
packed |= (index << (6 - i * 2)) & 0xFF;
}
}
dataOs.write(packed);
}
}
// 处理1位深度每8个索引打包到1字节中
private void process1Bit(int[] row, int width, ByteArrayOutputStream dataOs, Palette palette) {
for (int x = 0; x < width; x += 8) {
int packed = 0;
for (int i = 0; i < 8; i++) {
if (x + i < width) {
final int argb = row[x + i];
final int index = palette.getPaletteIndex(argb);
packed |= (index << (7 - i));
}
}
dataOs.write(packed);
}
}
private byte[] compressImage(byte[] uncompressed) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final int chunkSize = 32 * 1024;
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
for (int index = 0; index < uncompressed.length; index += chunkSize) {
final int end = Math.min(uncompressed.length, index + chunkSize);
final int length = end - index;
dos.write(uncompressed, index, length);
dos.flush();
baos.flush();
final byte[] compressed = baos.toByteArray();
baos.reset();
if (compressed.length > 0) {
writeChunkIDAT(output, compressed);
}
}
{
dos.finish();
final byte[] compressed = baos.toByteArray();
if (compressed.length > 0) {
writeChunkIDAT(output, compressed);
}
}
return output.toByteArray();
}
private void writeChunkIDAT(final OutputStream os, final byte[] bytes) throws IOException {
writeChunk(os, IDAT, bytes);
}
private void writeChunkIEND(final OutputStream os) throws IOException {
writeChunk(os, IEND, null);
}
private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException {
List<Byte> alphaValues = new ArrayList<>();
boolean hasTransparency = false;
for (int i = 0; i < palette.length(); i++) {
int argb = palette.getEntry(i);
int alpha = (argb >> 24) & 0xFF;
if (alpha < 255) {
hasTransparency = true;
alphaValues.add((byte) alpha);
} else {
break;
}
}
if (!hasTransparency) {
return;
}
final byte[] bytes = new byte[alphaValues.size()];
for (int i = 0; i < alphaValues.size(); i++) {
bytes[i] = alphaValues.get(i);
}
writeChunk(os, tRNS, bytes);
}
private void writeChunkPLTE(final OutputStream os, final Palette palette) throws IOException {
final int length = palette.length();
final byte[] bytes = new byte[length * 3];
for (int i = 0; i < length; i++) {
final int rgb = palette.getEntry(i);
final int index = i * 3;
bytes[index + 0] = (byte) (0xff & rgb >> 16);
bytes[index + 1] = (byte) (0xff & rgb >> 8);
bytes[index + 2] = (byte) (0xff & rgb >> 0);
}
writeChunk(os, PLTE, bytes);
}
private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeInt(baos, value.width);
writeInt(baos, value.height);
baos.write(0xff & value.bitDepth);
baos.write(0xff & value.pngColorType.value);
baos.write(0xff & value.compressionMethod);
baos.write(0xff & value.filterMethod);
baos.write(0xff & value.interlaceMethod.ordinal());
writeChunk(os, IHDR, baos.toByteArray());
}
private void writeInt(final OutputStream os, final int value) throws IOException {
os.write(0xff & value >> 24);
os.write(0xff & value >> 16);
os.write(0xff & value >> 8);
os.write(0xff & value >> 0);
}
private void writeChunk(final OutputStream os, final byte[] chunkType, final byte[] data) throws IOException {
final int dataLength = data == null ? 0 : data.length;
writeInt(os, dataLength);
os.write(chunkType);
if (data != null) {
os.write(data);
}
writeInt(os, 0); // crc
}
enum PngColorType {
GREYSCALE(0), TRUE_COLOR(2),
INDEXED_COLOR(3), GREYSCALE_WITH_ALPHA(4),
TRUE_COLOR_WITH_ALPHA(6);
private final int value;
PngColorType(final int value) {
this.value = value;
}
public int value() {
return value;
}
}
enum FilterType {
NONE, SUB, UP, AVERAGE, PAETH
}
enum InterlaceMethod {
NONE, ADAM7
}
interface Palette {
int getEntry(int index);
int getPaletteIndex(int rgb);
int length();
default int calculateBitDepth() {
int colorCount = length();
if (colorCount <= 2) return 1;
if (colorCount <= 4) return 2;
if (colorCount <= 16) return 4;
return 8;
}
}
static class ExactOpaquePalette implements Palette {
private final int[] palette; // 频次排序的颜色数组
private final Map<Integer, Integer> colorToIndex; // 颜色到索引的映射
public ExactOpaquePalette(final Map<Integer, Integer> colorFrequency) {
this.palette = colorFrequency.entrySet().stream()
.sorted((a, b) -> Integer.compare(b.getValue(), a.getValue()))
.mapToInt(Map.Entry::getKey)
.toArray();
this.colorToIndex = new HashMap<>();
for (int i = 0; i < palette.length; i++) {
this.colorToIndex.put(palette[i], i);
}
}
@Override
public int getEntry(int index) {
if (index < 0 || index >= palette.length) {
throw new IllegalArgumentException("Index out of bounds: " + index);
}
return palette[index];
}
@Override
public int getPaletteIndex(int rgb) {
return colorToIndex.get(rgb);
}
@Override
public int length() {
return palette.length;
}
}
static class ExactTransparentPalette implements Palette {
private final int[] palette; // 透明色在前,不透明色在后
private final Map<Integer, Integer> colorToIndex; // 颜色到索引的映射
public ExactTransparentPalette(final Map<Integer, Integer> opaque, final Map<Integer, Integer> transparent) {
// 分别处理透明色和不透明色
List<Integer> transparentList = transparent.entrySet().stream()
.sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) // 按频次降序
.map(Map.Entry::getKey)
.toList();
List<Integer> opaqueList = opaque.entrySet().stream()
.sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) // 按频次降序
.map(Map.Entry::getKey)
.toList();
// 合并:透明色在前,不透明色在后
List<Integer> combinedList = new ArrayList<>();
combinedList.addAll(transparentList);
combinedList.addAll(opaqueList);
this.palette = combinedList.stream().mapToInt(Integer::intValue).toArray();
this.colorToIndex = new HashMap<>();
for (int i = 0; i < palette.length; i++) {
this.colorToIndex.put(palette[i], i);
}
}
@Override
public int getEntry(int index) {
if (index < 0 || index >= palette.length) {
throw new IllegalArgumentException("Index out of bounds: " + index);
}
return palette[index];
}
@Override
public int getPaletteIndex(int rgb) {
Integer index = colorToIndex.get(rgb);
if (index == null) {
throw new IllegalArgumentException("Color not found in palette: 0x" + Integer.toHexString(rgb));
}
return index;
}
@Override
public int length() {
return palette.length;
}
}
record ImageData(PngColorType colorType, byte bitDepth, byte[] data) {
}
record ImageHeader(int width, int height, byte bitDepth, PngColorType pngColorType, byte compressionMethod, byte filterMethod, InterlaceMethod interlaceMethod) {
}
record ImageColorInfo(Map<Integer, Integer> opaque, Map<Integer, Integer> transparent, boolean isGrayscale) {
public int uniqueColorCount() {
return this.opaque.size() + this.transparent.size();
}
public boolean hasAlpha() {
return !this.transparent.isEmpty();
}
}
}

View File

@@ -1,336 +0,0 @@
/**
* This file is based on the work from the Apache Commons Imaging project.
* <p>
* Original source: https://github.com/apache/commons-imaging/blob/master/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java
* <p>
* Modifications have been made to the original code.
* <p>
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 net.momirealms.craftengine.core.util;
import org.apache.commons.imaging.common.Allocator;
import org.apache.commons.imaging.formats.png.ChunkType;
import org.apache.commons.imaging.formats.png.InterlaceMethod;
import org.apache.commons.imaging.formats.png.PngConstants;
import org.apache.commons.imaging.palette.Palette;
import org.apache.commons.imaging.palette.PaletteFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
public class PngWriter {
private static final class ImageHeader {
public final int width;
public final int height;
public final byte bitDepth;
public final PngColorType pngColorType;
public final byte compressionMethod;
public final byte filterMethod;
public final InterlaceMethod interlaceMethod;
ImageHeader(final int width, final int height, final byte bitDepth, final PngColorType pngColorType, final byte compressionMethod,
final byte filterMethod, final InterlaceMethod interlaceMethod) {
this.width = width;
this.height = height;
this.bitDepth = bitDepth;
this.pngColorType = pngColorType;
this.compressionMethod = compressionMethod;
this.filterMethod = filterMethod;
this.interlaceMethod = interlaceMethod;
}
}
public void write(BufferedImage src, OutputStream os, PaletteFactory paletteFactory) throws IOException {
final int width = src.getWidth();
final int height = src.getHeight();
src = convertTo8BitRGB(src);
final boolean hasAlpha = paletteFactory.hasTransparency(src);
boolean isGrayscale = paletteFactory.isGrayscale(src);
Pair<PngColorType, byte[]> bestChoice = findBestCompressMethod(src, paletteFactory, hasAlpha, isGrayscale);
byte bitDepth = 8;
{
PngConstants.PNG_SIGNATURE.writeTo(os);
}
{
final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE;
final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE;
final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, bestChoice.left(), compressionMethod, filterMethod, interlaceMethod);
writeChunkIHDR(os, imageHeader);
}
os.write(bestChoice.right());
writeChunkIEND(os);
os.close();
}
public static BufferedImage convertTo8BitRGB(BufferedImage sourceImage) {
int type = sourceImage.getType();
if (type == BufferedImage.TYPE_INT_ARGB ||
type == BufferedImage.TYPE_INT_RGB ||
type == BufferedImage.TYPE_BYTE_INDEXED) {
return sourceImage;
}
BufferedImage eightBitImage = new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
sourceImage.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB
);
Graphics2D g2d = eightBitImage.createGraphics();
g2d.drawImage(sourceImage, 0, 0, null);
g2d.dispose();
return eightBitImage;
}
private Pair<PngColorType, byte[]> findBestCompressMethod(BufferedImage src, PaletteFactory paletteFactory, boolean hasAlpha, boolean isGrayscale) throws IOException {
byte[] paletteSize = tryPalette(src, paletteFactory, hasAlpha);
byte[] normalSize = tryNormal(src, hasAlpha, isGrayscale);
if (normalSize.length > paletteSize.length) {
return Pair.of(PngColorType.INDEXED_COLOR, paletteSize);
} else {
if (isGrayscale) {
return Pair.of(hasAlpha ? PngColorType.GREYSCALE_WITH_ALPHA : PngColorType.GREYSCALE, normalSize);
} else {
return Pair.of(hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR, normalSize);
}
}
}
private byte[] tryNormal(BufferedImage src, boolean hasAlpha, boolean isGrayscale) throws IOException {
byte[] bytes = generatePngData(src, hasAlpha, isGrayscale);
return compressImage(bytes, false, Deflater.BEST_COMPRESSION);
}
private byte[] generatePngData(BufferedImage src, boolean hasAlpha, boolean isGrayscale) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
int width = src.getWidth();
int height = src.getHeight();
int[] row = Allocator.intArray(width);
for (int y = 0; y < height; y++) {
src.getRGB(0, y, width, 1, row, 0, width);
baos.write(FilterType.NONE.ordinal());
for (int x = 0; x < width; x++) {
final int argb = row[x];
final int alpha = 0xff & argb >> 24;
final int red = 0xff & argb >> 16;
final int green = 0xff & argb >> 8;
final int blue = 0xff & argb >> 0;
if (isGrayscale) {
final int gray = (red + green + blue) / 3;
baos.write(gray);
} else {
baos.write(red);
baos.write(green);
baos.write(blue);
}
if (hasAlpha) {
baos.write(alpha);
}
}
}
return baos.toByteArray();
}
private byte[] tryPalette(BufferedImage src, PaletteFactory paletteFactory, boolean hasAlpha) throws IOException {
ByteArrayOutputStream paletteOs = new ByteArrayOutputStream();
Palette palette;
final int maxColors = 256;
if (hasAlpha) {
palette = paletteFactory.makeQuantizedRgbaPalette(src, true, maxColors);
writeChunkPLTE(paletteOs, palette);
writeChunkTRNS(paletteOs, palette);
} else {
palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors);
writeChunkPLTE(paletteOs, palette);
}
byte[] bytes = generatePaletteData(src, palette);
paletteOs.write(compressImage(bytes, false, Deflater.BEST_COMPRESSION));
return paletteOs.toByteArray();
}
private byte[] generatePaletteData(BufferedImage src, Palette palette) throws IOException {
int width = src.getWidth();
int height = src.getHeight();
final ByteArrayOutputStream dataOs = new ByteArrayOutputStream();
final int[] row = Allocator.intArray(width);
for (int y = 0; y < height; y++) {
src.getRGB(0, y, width, 1, row, 0, width);
dataOs.write(FilterType.NONE.ordinal());
for (int x = 0; x < width; x++) {
final int argb = row[x];
final int index = palette.getPaletteIndex(argb);
dataOs.write(0xff & index);
}
}
return dataOs.toByteArray();
}
private byte[] compressImage(byte[] uncompressed, boolean useFiltered, int level) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final int chunkSize = 32 * 1024;
final Deflater deflater = new Deflater(level);
if (useFiltered) {
deflater.setStrategy(Deflater.FILTERED);
}
final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
for (int index = 0; index < uncompressed.length; index += chunkSize) {
final int end = Math.min(uncompressed.length, index + chunkSize);
final int length = end - index;
dos.write(uncompressed, index, length);
dos.flush();
baos.flush();
final byte[] compressed = baos.toByteArray();
baos.reset();
if (compressed.length > 0) {
writeChunkIDAT(output, compressed);
}
}
{
dos.finish();
final byte[] compressed = baos.toByteArray();
if (compressed.length > 0) {
writeChunkIDAT(output, compressed);
}
}
return output.toByteArray();
}
private void writeChunkIDAT(final OutputStream os, final byte[] bytes) throws IOException {
writeChunk(os, ChunkType.IDAT, bytes);
}
private void writeChunkIEND(final OutputStream os) throws IOException {
writeChunk(os, ChunkType.IEND, null);
}
private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException {
final byte[] bytes = Allocator.byteArray(palette.length());
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (0xff & palette.getEntry(i) >> 24);
}
writeChunk(os, ChunkType.tRNS, bytes);
}
private void writeChunkPLTE(final OutputStream os, final Palette palette) throws IOException {
final int length = palette.length();
final byte[] bytes = Allocator.byteArray(length * 3);
for (int i = 0; i < length; i++) {
final int rgb = palette.getEntry(i);
final int index = i * 3;
bytes[index + 0] = (byte) (0xff & rgb >> 16);
bytes[index + 1] = (byte) (0xff & rgb >> 8);
bytes[index + 2] = (byte) (0xff & rgb >> 0);
}
writeChunk(os, ChunkType.PLTE, bytes);
}
private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeInt(baos, value.width);
writeInt(baos, value.height);
baos.write(0xff & value.bitDepth);
baos.write(0xff & value.pngColorType.getValue());
baos.write(0xff & value.compressionMethod);
baos.write(0xff & value.filterMethod);
baos.write(0xff & value.interlaceMethod.ordinal());
writeChunk(os, ChunkType.IHDR, baos.toByteArray());
}
private void writeInt(final OutputStream os, final int value) throws IOException {
os.write(0xff & value >> 24);
os.write(0xff & value >> 16);
os.write(0xff & value >> 8);
os.write(0xff & value >> 0);
}
private void writeChunk(final OutputStream os, final ChunkType chunkType, final byte[] data) throws IOException {
final int dataLength = data == null ? 0 : data.length;
byte[] chunkTypeBytes = chunkType.name().getBytes(StandardCharsets.UTF_8);
writeInt(os, dataLength);
os.write(chunkTypeBytes);
if (data != null) {
os.write(data);
}
writeInt(os, 0); // crc
}
public enum PngColorType {
GREYSCALE(0, true, false, 1, new int[] { 1, 2, 4, 8, 16 }), TRUE_COLOR(2, false, false, 3, new int[] { 8, 16 }),
INDEXED_COLOR(3, false, false, 1, new int[] { 1, 2, 4, 8 }), GREYSCALE_WITH_ALPHA(4, true, true, 2, new int[] { 8, 16 }),
TRUE_COLOR_WITH_ALPHA(6, false, true, 4, new int[] { 8, 16 });
private final int value;
private final boolean greyscale;
private final boolean alpha;
private final int samplesPerPixel;
private final int[] allowedBitDepths;
PngColorType(final int value, final boolean greyscale, final boolean alpha, final int samplesPerPixel, final int[] allowedBitDepths) {
this.value = value;
this.greyscale = greyscale;
this.alpha = alpha;
this.samplesPerPixel = samplesPerPixel;
this.allowedBitDepths = allowedBitDepths;
}
int getSamplesPerPixel() {
return samplesPerPixel;
}
int getValue() {
return value;
}
boolean hasAlpha() {
return alpha;
}
boolean isBitDepthAllowed(final int bitDepth) {
return Arrays.binarySearch(allowedBitDepths, bitDepth) >= 0;
}
boolean isGreyscale() {
return greyscale;
}
}
enum FilterType {
NONE, SUB, UP, AVERAGE, PAETH
}
}