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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user