GIFDecoder.java

Переключить прокрутку окна
Загрузить этот исходный код

/*
    Реализация спецификаций 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;
    }
}