/*
Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
и других спецификаций для функционирования компактных приложений на языке
Java (мидлетов) в среде программного обеспечения Малик Эмулятор.
Copyright © 2016–2017, 2019–2023, 2025 Малик Разработчик
Это свободная программа: вы можете перераспространять ее и/или изменять
ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
в каком она была опубликована Фондом свободного программного обеспечения;
либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.
Эта программа распространяется в надежде, что она будет полезной,
но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
общественной лицензии GNU.
Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
вместе с этой программой. Если это не так, см.
<https://www.gnu.org/licenses/>.
*/
package malik.emulator.fileformats.graphics.png;
import java.io.*;
import malik.emulator.compression.zlib.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.graphics.*;
public final class PNGEncoder extends Object implements ImageEncoder
{
public static final long SIGNATURE = 0x89504e470d0a1a0aL;
private static final int IHDR = 0x49484452; /* IHDR */
private static final int PLTE = 0x504c5445; /* PLTE */
private static final int TRNS = 0x74524e53; /* tRNS */
private static final int TEXT = 0x74455874; /* tEXt */
private static final int IDAT = 0x49444154; /* IDAT */
private static final int IEND = 0x49454e44; /* IEND */
private static void writeChunk(DataOutputStream stream, ByteArrayOutputStream chunk, Checksum32 checksum) throws IOException {
int chunkBytesLength;
byte[] chunkBytes;
chunkBytesLength = (chunkBytes = chunk.toByteArray()).length;
checksum.reset();
checksum.update(chunkBytes, 0, chunkBytesLength);
stream.writeInt(chunkBytesLength - 4);
stream.write(chunkBytes);
stream.writeInt(checksum.value());
}
private static void writeTag(DataOutputStream stream, ByteArrayOutputStream chunkStream, DataOutputStream chunkData, Checksum32 checksum, String tag, String value) throws IOException {
chunkStream.reset();
chunkData.writeInt(TEXT);
chunkData.write(tag.getBytes());
chunkData.write(0);
chunkData.write(value.getBytes());
writeChunk(stream, chunkStream, checksum);
}
private boolean alpha;
private int level;
private int width;
private int height;
private int[] pixels;
public PNGEncoder() {
this.level = 7;
}
public PNGEncoder(int compressionLevel) {
this.level = compressionLevel < 0 ? 0 : compressionLevel > 9 ? 9 : compressionLevel;
}
public void saveToOutputStream(OutputStream stream) throws IOException {
saveToDataStream(new ExtendedDataOutputStream(stream));
}
public void saveToDataStream(ExtendedDataOutputStream stream) throws IOException {
boolean useAlpha = alpha;
boolean usePalette;
int compressionLevel = level;
int lineSize;
int bitDepth;
int pixelType;
int components;
int imagePixelsLength;
int imagePaletteLength;
int imageWidth = width;
int imageHeight = height;
byte[] rawPixels;
int[] imagePixels = pixels;
int[] imagePalette;
Checksum32 checksum;
DataOutputStream chunkData;
ByteArrayOutputStream chunkStream;
if(imagePixels == null)
{
throw new EmptyAdapterException("PNGEncoder.saveToOutputStream: кодер не содержит данные.");
}
/* палитра */
imagePixelsLength = imagePixels.length;
imagePaletteLength = 0;
imagePalette = new int[0x0100];
usePalette = true;
for(int i = 0; i < imagePixelsLength; i++)
{
int pixel = useAlpha ? imagePixels[i] : (imagePixels[i] &= 0x00ffffff);
if(imagePaletteLength <= 0 || Array.findb(imagePalette, imagePaletteLength - 1, pixel) < 0)
{
if(imagePaletteLength >= 0x0100)
{
imagePaletteLength = 0;
imagePalette = null;
usePalette = false;
break;
}
imagePalette[imagePaletteLength++] = pixel;
}
}
/* тип пикселов (2=RGB, 3=P, 6=RGBA) */
if(usePalette)
{
bitDepth = imagePaletteLength <= 2 ? 1 : imagePaletteLength <= 4 ? 2 : imagePaletteLength <= 16 ? 4 : 8;
pixelType = 3;
components = 1;
if(useAlpha)
{
useAlpha = false;
for(int i = imagePaletteLength; i-- > 0; )
{
if((imagePalette[i] >>> 24) < 0xff)
{
useAlpha = true;
break;
}
}
}
} else
{
bitDepth = 8;
if(!useAlpha)
{
pixelType = 2;
components = 3;
} else
{
useAlpha = false;
for(int i = imagePixelsLength; i-- > 0; )
{
if((imagePixels[i] >>> 24) < 0xff)
{
useAlpha = true;
break;
}
}
if(useAlpha)
{
pixelType = 6;
components = 4;
} else
{
pixelType = 2;
components = 3;
}
}
}
/* кодирование */
lineSize = bitDepth < 8 ? (imageWidth * bitDepth + 7) >> 3 : imageWidth * components;
rawPixels = new byte[imageHeight * (1 + lineSize)];
for(int imageOffset = 0, rawOffset = 0, y = 0; y < imageHeight; y++)
{
rawPixels[rawOffset++] = (byte) 1;
for(int subOffset = rawOffset, x = 0; x < imageWidth; x++)
{
int shift;
int pixel = imagePixels[imageOffset++];
switch(pixelType)
{
default:
break;
case 2:
rawPixels[subOffset++] = (byte) (pixel >> 16);
rawPixels[subOffset++] = (byte) (pixel >> 8);
rawPixels[subOffset++] = (byte) pixel;
break;
case 3:
pixel = Array.findf(imagePalette, 0, pixel);
switch(bitDepth)
{
case 1:
shift = (x & 7) ^ 7;
rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x01 << shift) | pixel << shift);
if(shift == 0) subOffset++;
break;
case 2:
shift = ((x & 3) ^ 3) << 1;
rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x03 << shift) | pixel << shift);
if(shift == 0) subOffset++;
break;
case 4:
shift = ((x & 1) ^ 1) << 2;
rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x0f << shift) | pixel << shift);
if(shift == 0) subOffset++;
break;
case 8:
rawPixels[subOffset++] = (byte) pixel;
break;
}
break;
case 6:
rawPixels[subOffset++] = (byte) (pixel >> 16);
rawPixels[subOffset++] = (byte) (pixel >> 8);
rawPixels[subOffset++] = (byte) pixel;
rawPixels[subOffset++] = (byte) (pixel >> 24);
break;
}
}
for(int lim = rawOffset + components, j = (rawOffset += lineSize); j-- > lim; ) rawPixels[j] = (byte) (rawPixels[j] - rawPixels[j - components]);
}
rawPixels = Zlib.compress(rawPixels, compressionLevel);
/* запись данных */
checksum = new CRC32();
chunkData = new DataOutputStream(chunkStream = new ByteArrayOutputStream(16));
stream.writeLong(SIGNATURE);
/* кусок – заголовок */
chunkStream.reset();
chunkData.writeInt(IHDR);
chunkData.writeInt(imageWidth); /* ширина */
chunkData.writeInt(imageHeight); /* высота */
chunkData.writeByte(bitDepth); /* глубина цвета */
chunkData.writeByte(pixelType); /* тип пикселов */
chunkData.writeByte(0); /* алгоритм сжатия */
chunkData.writeByte(0); /* алгоритм фильтрации */
chunkData.writeByte(0); /* алгоритм отображения */
writeChunk(stream, chunkStream, checksum);
if(usePalette)
{
/* кусок – палитра */
chunkStream.reset();
chunkData.writeInt(PLTE);
for(int i = 0; i < imagePaletteLength; i++)
{
int colour = imagePalette[i];
chunkData.writeByte(colour >> 16);
chunkData.writeByte(colour >> 8);
chunkData.writeByte(colour);
}
writeChunk(stream, chunkStream, checksum);
if(useAlpha)
{
int len = imagePaletteLength;
for(int i = imagePaletteLength; i-- > 0; )
{
if((imagePalette[i] >>> 24) < 0xff)
{
len = i + 1;
break;
}
}
/* кусок – палитра, альфа-канал */
chunkStream.reset();
chunkData.writeInt(TRNS);
for(int i = 0; i < len; i++) chunkData.writeByte(imagePalette[i] >> 24);
writeChunk(stream, chunkStream, checksum);
}
}
/* кусок – таг «Программное обеспечение» */
writeTag(stream, chunkStream, chunkData, checksum, "Software", "Malik Emulator https://malik-elaborarer.ru/emulator/");
{
int rawLength = rawPixels.length;
int chunksCount = (rawLength + (-rawLength & 0xffff)) >> 16;
int lim = (rawLength & 0xffff) <= 0 ? chunksCount : chunksCount - 1;
for(int i = 0; i < chunksCount; i++)
{
/* кусок – данные */
chunkStream.reset();
chunkData.writeInt(IDAT);
chunkData.write(rawPixels, i << 16, i < lim ? 0x00010000 : rawLength & 0xffff);
writeChunk(stream, chunkStream, checksum);
}
}
/* кусок – конец */
chunkStream.reset();
chunkData.writeInt(IEND);
writeChunk(stream, chunkStream, checksum);
}
public void clear() {
alpha = false;
width = 0;
height = 0;
pixels = null;
}
public void setPixels(boolean alpha, int width, int height, int[] pixels) throws InvalidDataFormatException, UnsupportedDataException {
int iarea;
long area;
if(pixels == null)
{
throw new NullPointerException("PNGEncoder.setPixels: аргумент pixels равен нулевой ссылке.");
}
if(width < 1 || height < 1)
{
throw new InvalidDataFormatException("PNGEncoder.setPixels: размеры могут быть только положительными.");
}
if((long) pixels.length < (area = (long) width * (long) height))
{
throw new InvalidDataFormatException("PNGEncoder.setPixels: длина аргумента pixels не может быть меньше произведения размеров.");
}
if(area > 0x00100000L)
{
throw new UnsupportedDataException("PNGEncoder.setPixels: размеры слишком велики.");
}
Array.copy(pixels, 0, pixels = new int[iarea = (int) area], 0, iarea);
this.alpha = alpha;
this.width = width;
this.height = height;
this.pixels = pixels;
}
public boolean alphaSupported() {
return true;
}
public boolean isEmpty() {
return pixels == null;
}
public void setCompressionLevel(int compressionLevel) {
this.level = compressionLevel < 0 ? 0 : compressionLevel > 9 ? 9 : compressionLevel;
}
public int getCompressionLevel() {
return level;
}
}