/*
Реализация спецификаций 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.gif;
import java.io.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.graphics.*;
public final class GIFDecoder extends Object implements ImageDecoder
{
public static final long SIGNATURE = 0x474946L;
private static final int VERSION_87A = 0x383761;
private static final int VERSION_89A = 0x383961;
private static final int EXTENSION = 0x21;
private static final int GRAPHIC_CONTROL_EXTENSION = EXTENSION << 8 | 0xf9;
private static final int PLAIN_TEXT_EXTENSION = EXTENSION << 8 | 0x01;
private static final int APPLICATION_EXTENSION = EXTENSION << 8 | 0xff;
private static final int COMMENT_EXTENSION = EXTENSION << 8 | 0xfe;
private static final int IMAGE_DESCRIPTOR = 0x2c;
private static final int TRAILER = 0x3b;
private static void skipSubblocks(DataInputStream stream) throws IOException {
int blockSize;
do
{
stream.skip(blockSize = stream.readUnsignedByte()); /* игнорируем подблок данных */
} while(blockSize > 0);
}
private static byte[] decompress(DataInputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
int minimumCodeSize = stream.readUnsignedByte(); /* минимальный размер кода LZW */
if(minimumCodeSize < 1 || minimumCodeSize > 8)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
int initialCodeBits = minimumCodeSize <= 1 ? 3 : minimumCodeSize + 1;
int specialClearCode = 1 << minimumCodeSize;
int specialEndCode = specialClearCode + 1;
int initialArrayLength = specialClearCode;
int initialTableLength = specialEndCode + 1;
byte[] array = new byte[0x0200];
long[] table = new long[0x1000];
for(int i = initialArrayLength; i-- > 0; )
{
array[i] = (byte) i;
table[i] = 0x0000000100000000L | i;
}
int currentCodeBits = initialCodeBits;
int currentArrayLength = initialArrayLength;
int currentTableLength = initialTableLength;
int localCode = -1;
int incomingCode = 0;
int incomingPosition = 0;
int incomingLength = 0;
byte[] incoming = new byte[0xff];
for(; ; incomingCode = 0)
{
/* чтение очередного входящего кода */
for(int incomingReaded = 0; incomingReaded < currentCodeBits; )
{
int remainingToEndOfByte;
if(incomingPosition < incomingLength)
{
remainingToEndOfByte = 8 - (incomingPosition & 7);
} else
{
int blockSize = stream.readUnsignedByte(); /* размер подблока */
if(blockSize <= 0)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
stream.readFully(incoming, 0, blockSize); /* подблок сжатых данных */
incomingLength = blockSize << 3;
incomingPosition = 0;
remainingToEndOfByte = 8;
}
int remainingToEndOfCode = currentCodeBits - incomingReaded;
if(remainingToEndOfByte > remainingToEndOfCode) remainingToEndOfByte = remainingToEndOfCode;
incomingCode |= (incoming[incomingPosition >> 3] >> (incomingPosition & 7) & ((1 << remainingToEndOfByte) - 1)) << incomingReaded;
incomingPosition += remainingToEndOfByte;
incomingReaded += remainingToEndOfByte;
}
/* обработка специальных входящих кодов */
if(incomingCode == specialEndCode)
{
break;
}
if(incomingCode == specialClearCode)
{
localCode = -1;
currentCodeBits = initialCodeBits;
currentArrayLength = initialArrayLength;
currentTableLength = initialTableLength;
continue;
}
/* обработка обычных входящих кодов */
if(localCode < 0)
{
if(incomingCode >= specialClearCode)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
long incomItem = table[incomingCode];
int incomOffset = (int) incomItem;
int incomLength = (int) (incomItem >> 32);
localCode = incomingCode;
result.write(array, incomOffset, incomLength);
continue;
}
if(currentTableLength >= table.length)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
if(incomingCode >= currentTableLength)
{
long localItem = table[localCode];
int localOffset = (int) localItem;
int localLength = (int) (localItem >> 32);
int addedOffset = currentArrayLength;
int addedLength = localLength + 1;
int addedLimit = addedOffset + addedLength;
int newArrayLength = array.length;
if(addedLimit > newArrayLength)
{
if((newArrayLength <<= 1) < addedLimit) newArrayLength = addedLimit;
Array.copy(array, 0, array = new byte[newArrayLength], 0, currentArrayLength);
}
Array.copy(array, localOffset, array, addedOffset, localLength);
array[addedLimit - 1] = array[localOffset];
currentArrayLength = addedLimit;
table[currentTableLength] = (long) addedLength << 32 | (long) addedOffset;
localCode = currentTableLength++;
result.write(array, addedOffset, addedLength);
} else
{
long localItem = table[localCode];
long incomItem = table[incomingCode];
int incomOffset = (int) incomItem;
int incomLength = (int) (incomItem >> 32);
int localOffset = (int) localItem;
int localLength = (int) (localItem >> 32);
int addedOffset = currentArrayLength;
int addedLength = localLength + 1;
int addedLimit = addedOffset + addedLength;
int newArrayLength = array.length;
if(addedLimit > newArrayLength)
{
if((newArrayLength <<= 1) < addedLimit) newArrayLength = addedLimit;
Array.copy(array, 0, array = new byte[newArrayLength], 0, currentArrayLength);
}
Array.copy(array, localOffset, array, addedOffset, localLength);
array[addedLimit - 1] = array[incomOffset];
currentArrayLength = addedLimit;
table[currentTableLength++] = (long) addedLength << 32 | (long) addedOffset;
localCode = incomingCode;
result.write(array, incomOffset, incomLength);
}
/* повышение разрядности входящих кодов */
if((currentTableLength & -currentTableLength) == currentTableLength)
{
currentCodeBits++;
}
}
if(stream.readUnsignedByte() != 0) /* размер подблока */
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
return result.toByteArray();
}
private int width;
private int height;
private int[] pixels;
public GIFDecoder() {
}
public void loadFromInputStream(InputStream stream) throws IOException {
loadFromDataStream(new ExtendedDataInputStream(stream));
}
public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
boolean controlHandled = false;
boolean imageHandled = false;
int imageBackgroundColor = -1;
int imageWidth = 0;
int imageHeight = 0;
int imagePaletteLength = 0;
int[] imagePixels = null;
int[] imagePalette = null;
/* чтение входного потока данных */
{
/* header */
int version = stream.readUnsignedByte() << 16 | stream.readUnsignedShort(); /* версия */
if(version != VERSION_87A && version != VERSION_89A)
{
throw new UnsupportedDataException("GIFDecoder.loadFromInputStream: неподдерживаемые данные.");
}
/* logical screen descriptor */
{
imageWidth = stream.readUnsignedShortLE(); /* ширина */
imageHeight = stream.readUnsignedShortLE(); /* высота */
int packedFields = stream.readUnsignedByte(); /* упакованные поля */
int backgroundColorIndex = stream.readUnsignedByte(); /* индекс прозрачного цвета в глобальной палитре (imagePalette) */
stream.skip(1L); /* игнорируем отношение ширины к высоте пиксела */
if((long) imageWidth * (long) imageHeight > 0x00100000L)
{
throw new UnsupportedDataException("GIFDecoder.loadFromInputStream: неподдерживаемые данные.");
}
imagePixels = new int[imageWidth * imageHeight];
/* global color table */
if((packedFields & 0x80) != 0)
{
imagePalette = new int[imagePaletteLength = 2 << (packedFields & 0x07)];
for(int i = 0; i < imagePaletteLength; i++)
{
int rComponent = stream.readUnsignedByte(); /* значение красного канала компонента палитры */
int gComponent = stream.readUnsignedByte(); /* значение зелёного канала компонента палитры */
int bComponent = stream.readUnsignedByte(); /* значение синего канала компонента палитры */
imagePalette[i] = rComponent << 16 | gComponent << 8 | bComponent;
}
/* прозрачный цвет */
if(backgroundColorIndex < imagePaletteLength)
{
imageBackgroundColor = imagePalette[backgroundColorIndex];
for(int i = imagePaletteLength; i-- > 0; )
{
int color = imagePalette[i];
if(color != imageBackgroundColor) imagePalette[i] = color | 0xff000000;
}
}
}
}
int blockType;
do
{
label0: switch(blockType = stream.readUnsignedByte()) /* тип блока данных */
{
case TRAILER:
break;
case IMAGE_DESCRIPTOR:
if(controlHandled)
{
stream.skip(8L); /* игнорируем положение и размеры изображения */
int packedFields = stream.readUnsignedByte(); /* упакованные поля */
if((packedFields & 0x80) != 0)
{
stream.skip(3 * (2 << (packedFields & 0x07))); /* игнорируем локальную палитру */
}
stream.skip(1L); /* игнорируем минимальный размер кода LZW */
skipSubblocks(stream);
} else
{
/* image descriptor */
int x0 = stream.readUnsignedShortLE(); /* отступ слева области изображения */
int y0 = stream.readUnsignedShortLE(); /* отступ сверху области изображения */
int x1 = stream.readUnsignedShortLE() + x0; /* ширина области изображения */
int y1 = stream.readUnsignedShortLE() + y0; /* высота области изображения */
if(x1 - x0 == 0 || y1 - y0 == 0 || x1 > imageWidth || y1 > imageHeight)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
int delta = imageWidth - x1 + x0;
int packedFields = stream.readUnsignedByte(); /* упакованные поля */
int localPaletteLength = 0;
int[] localPalette = null;
/* local color table */
if((packedFields & 0x80) != 0)
{
localPalette = new int[localPaletteLength = 2 << (packedFields & 0x07)];
for(int i = 0; i < localPaletteLength; i++)
{
int rComponent = stream.readUnsignedByte(); /* значение красного канала компонента палитры */
int gComponent = stream.readUnsignedByte(); /* значение зелёного канала компонента палитры */
int bComponent = stream.readUnsignedByte(); /* значение синего канала компонента палитры */
int color = rComponent << 16 | gComponent << 8 | bComponent;
localPalette[i] = color == imageBackgroundColor ? color : color | 0xff000000;
}
}
/* table based image data */
int[] palette = localPalette == null ? imagePalette : localPalette;
if(palette == null)
{
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
byte[] pixels = decompress(stream);
if((packedFields & 0x40) == 0)
{
/* linear image */
for(int mask = palette.length - 1, length = pixels.length, x = x0, y = y0, offset = x + y * imageWidth, i = 0; y < y1 && i < length; offset++, i++)
{
int color = palette[pixels[i] & mask];
if((color & 0xff000000) != 0)
{
imagePixels[offset] = color;
}
if(++x >= x1)
{
x = x0;
y++;
offset += delta;
}
}
} else
{
/* interlaced image */
delta += 7 * imageWidth;
int mask = palette.length - 1;
int length = pixels.length;
int i = 0;
for(int x = x0, y = y0, offset = x + y * imageWidth; y < y1 && i < length; offset++, i++)
{
int color = palette[pixels[i] & mask];
if((color & 0xff000000) != 0)
{
imagePixels[offset] = color;
}
if(++x >= x1)
{
x = x0;
y += 8;
offset += delta;
}
}
for(int x = x0, y = y0 + 4, offset = x + y * imageWidth; y < y1 && i < length; offset++, i++)
{
int color = palette[pixels[i] & mask];
if((color & 0xff000000) != 0)
{
imagePixels[offset] = color;
}
if(++x >= x1)
{
x = x0;
y += 8;
offset += delta;
}
}
delta -= imageWidth << 2;
for(int x = x0, y = y0 + 2, offset = x + y * imageWidth; y < y1 && i < length; offset++, i++)
{
int color = palette[pixels[i] & mask];
if((color & 0xff000000) != 0)
{
imagePixels[offset] = color;
}
if(++x >= x1)
{
x = x0;
y += 4;
offset += delta;
}
}
delta -= imageWidth << 1;
for(int x = x0, y = y0 + 1, offset = x + y * imageWidth; y < y1 && i < length; offset++, i++)
{
int color = palette[pixels[i] & mask];
if((color & 0xff000000) != 0)
{
imagePixels[offset] = color;
}
if(++x >= x1)
{
x = x0;
y += 2;
offset += delta;
}
}
}
imageHandled = true;
}
break;
case EXTENSION:
switch(EXTENSION << 8 | stream.readUnsignedByte()) /* тип расширения */
{
case PLAIN_TEXT_EXTENSION:
if(stream.readUnsignedByte() != 12) /* размер блока */
{
break;
}
if(controlHandled)
{
stream.skip(12L); /* игнорируем блок расширения простого текста */
} else
{
int x0 = stream.readUnsignedShortLE(); /* отступ слева области текста */
int y0 = stream.readUnsignedShortLE(); /* отступ сверху области текста */
int x1 = stream.readUnsignedShortLE() + x0; /* ширина области текста */
int y1 = stream.readUnsignedShortLE() + y0; /* высота области текста */
stream.skip(3L); /* игнорируем ширину знакоместа, высоту знакоместа и индекс цвета текста */
int backgroundColorIndex = stream.readUnsignedByte(); /* индекс цвета фона */
int backgroundColor = imagePalette == null || backgroundColorIndex >= imagePaletteLength ? 0 : imagePalette[backgroundColorIndex];
if((backgroundColor & 0xff000000) != 0 && x0 < imageWidth && y0 < imageHeight)
{
if(x1 > imageWidth)
{
x1 = imageWidth;
}
if(y1 > imageHeight)
{
y1 = imageHeight;
}
for(int offset = x0 + y0 * imageWidth, length = x1 - x0, y = y0; y < y1; offset += imageWidth, y++)
{
Array.fill(imagePixels, offset, length, backgroundColor);
}
}
imageHandled = true;
}
skipSubblocks(stream);
break label0;
case GRAPHIC_CONTROL_EXTENSION:
if(stream.readUnsignedByte() != 4) /* размер блока */
{
break;
}
{
stream.skip(1L); /* игнорируем упакованные поля */
int delay = stream.readUnsignedShortLE(); /* задержка, ×0,01 секунды */
stream.skip(1L); /* игнорируем индекс прозрачного цвета */
if(imageHandled && delay > 0) controlHandled = true;
}
if(stream.readUnsignedByte() != 0) /* размер подблока */
{
break;
}
break label0;
case APPLICATION_EXTENSION:
if(stream.readUnsignedByte() != 11) /* размер блока */
{
break;
}
{
stream.skip(11L); /* игнорируем блок расширения приложения */
}
/* падение через */
case COMMENT_EXTENSION:
skipSubblocks(stream);
break label0;
default:
}
/* падение через */
default:
throw new InvalidDataFormatException("GIFDecoder.loadFromInputStream: неправильный формат данных.");
}
} while(blockType != TRAILER);
}
/* вывод результата */
width = imageWidth;
height = imageHeight;
pixels = imagePixels;
}
public void clear() {
width = 0;
height = 0;
pixels = null;
}
public boolean isEmpty() {
return (width | height) == 0 && pixels == null;
}
public boolean alphaSupported() {
return true;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int[] getPixels() {
int len;
int[] result;
if((result = pixels) == null) return null;
Array.copy(result, 0, result = new int[len = result.length], 0, len);
return result;
}
}