From fbcb28e9230a4affea9ebff46c0290211f7a2599 Mon Sep 17 00:00:00 2001 From: XiaoMoMi <972454774@qq.com> Date: Mon, 27 Oct 2025 03:03:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9png=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common-files/src/main/resources/config.yml | 7 +- .../core/pack/AbstractPackManager.java | 4 +- .../craftengine/core/util/PngOptimizer.java | 521 ++++++++++++++++++ .../craftengine/core/util/PngWriter.java | 336 ----------- 4 files changed, 526 insertions(+), 342 deletions(-) create mode 100644 core/src/main/java/net/momirealms/craftengine/core/util/PngOptimizer.java delete mode 100644 core/src/main/java/net/momirealms/craftengine/core/util/PngWriter.java diff --git a/common-files/src/main/resources/config.yml b/common-files/src/main/resources/config.yml index 16309a4c4..c2316a34b 100644 --- a/common-files/src/main/resources/config.yml +++ b/common-files/src/main/resources/config.yml @@ -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 diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java index 962c8a4d9..2938b920f 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java @@ -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(); } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/PngOptimizer.java b/core/src/main/java/net/momirealms/craftengine/core/util/PngOptimizer.java new file mode 100644 index 000000000..027d73696 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/util/PngOptimizer.java @@ -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 ope = new HashMap<>(); + Map 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 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 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 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 colorToIndex; // 颜色到索引的映射 + + public ExactOpaquePalette(final Map 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 colorToIndex; // 颜色到索引的映射 + + public ExactTransparentPalette(final Map opaque, final Map transparent) { + // 分别处理透明色和不透明色 + List transparentList = transparent.entrySet().stream() + .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) // 按频次降序 + .map(Map.Entry::getKey) + .toList(); + + List opaqueList = opaque.entrySet().stream() + .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) // 按频次降序 + .map(Map.Entry::getKey) + .toList(); + + // 合并:透明色在前,不透明色在后 + List 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 opaque, Map transparent, boolean isGrayscale) { + + public int uniqueColorCount() { + return this.opaque.size() + this.transparent.size(); + } + + public boolean hasAlpha() { + return !this.transparent.isEmpty(); + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/PngWriter.java b/core/src/main/java/net/momirealms/craftengine/core/util/PngWriter.java deleted file mode 100644 index e2b9780a4..000000000 --- a/core/src/main/java/net/momirealms/craftengine/core/util/PngWriter.java +++ /dev/null @@ -1,336 +0,0 @@ -/** - * This file is based on the work from the Apache Commons Imaging project. - *

- * Original source: https://github.com/apache/commons-imaging/blob/master/src/main/java/org/apache/commons/imaging/formats/png/PngWriter.java - *

- * Modifications have been made to the original code. - *

- * 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 - *

- * 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 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 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 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 - } -}