All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.graphics.image.gif.GifDecoder Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.graphics.image.gif;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;

/**
 * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.
 * @version 1.01 July 2001
 */
public class GifDecoder
{
    /**
     * File read status: No errors.
     */
    public static final int STATUS_OK = 0;

    /**
     * File read status: Error decoding file (may be partially decoded)
     */
    public static final int STATUS_FORMAT_ERROR = 1;

    /**
     * File read status: Unable to open source.
     */
    public static final int STATUS_OPEN_ERROR = 2;

    private BufferedInputStream in;
    private int status;

    private int width;            // full image width
    private int height;           // full image height
    private boolean gctFlag;      // global color table used
    private int gctSize;          // size of global color table
    private int loopCount = 1;    // iterations; 0 = repeat forever

    private int[] gct;            // global color table
    private int[] lct;            // local color table
    private int[] act;            // active color table

    private int bgIndex;          // background color index
    private int bgColor;          // background color
    private int lastBgColor;      // previous bg color
    private int pixelAspect;      // pixel aspect ratio (yes it is used!)

    private boolean lctFlag;      // local color table flag
    private boolean interlace;    // interlace flag
    private int lctSize;          // local color table size

    private int ix, iy, iw, ih;   // current image rectangle
    private Rectangle lastRect;   // last image rect
    private BufferedImage currentBufImg;  // current frame
    private BufferedImage prevBufImg;  // previous frame

    private final byte[] currentDataBlock = new byte[ 256 ];  // current data block
    private int blockSize = 0;    // block size

    // last graphic control extension info
    private int dispose = 0;   // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
    private int lastDispose = 0;
    private boolean transparency = false;   // use transparent color
    private int delay = 0;        // delay in milliseconds
    private int transIndex;       // transparent color index

    private static final int MaxStackSize = 4096;   // max decoder pixel stack size

    // LZW decoder working arrays
    private short[] prefix;
    private byte[] suffix;
    private byte[] pixelStack;
    private byte[] pixels;

    // frames read from current file
    private ArrayList frames;
    private int frameCount;

    /**
     * Returns pixel aspect ratio.
     *
     * @return pixel aspect ratio
     */
    public int getPixelAspect ()
    {
        return pixelAspect;
    }

    /**
     * Gets display duration for specified frame.
     *
     * @param n int index of frame
     * @return delay in milliseconds
     */
    public int getDelay ( final int n )
    {
        delay = -1;
        if ( n >= 0 && n < frameCount )
        {
            delay = ( ( GifFrame ) frames.get ( n ) ).delay;
        }
        return delay;
    }

    /**
     * Gets the image contents of frame n.
     *
     * @param n frame number
     * @return {@link BufferedImage} representation of frame, or {@code null} if n is invalid
     */
    public BufferedImage getFrame ( final int n )
    {
        BufferedImage bi = null;
        if ( n >= 0 && n < frameCount )
        {
            bi = ( ( GifFrame ) frames.get ( n ) ).bufferedImage;
        }
        return bi;
    }

    /**
     * Gets the number of frames read from file.
     *
     * @return frame count
     */
    public int getFrameCount ()
    {
        return frameCount;
    }

    /**
     * Gets the first (or only) image read.
     *
     * @return BufferedImage containing first frame, or null if none.
     */
    public BufferedImage getCurrentBufImg ()
    {
        return getFrame ( 0 );
    }

    /**
     * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitely.
     *
     * @return iteration count if one was specified, else 1.
     */
    public int getLoopCount ()
    {
        return loopCount;
    }

    /**
     * @param is An inputStream with gif animation
     * @return status code (0=no error)
     */
    public int read ( final BufferedInputStream is )
    {
        init ();
        if ( is != null )
        {
            in = is;
            readHeader ();
            if ( !err () )
            {
                readContents ();
                if ( frameCount < 0 )
                {
                    status = STATUS_FORMAT_ERROR;
                }
            }
            try
            {
                is.close ();
            }
            catch ( final IOException ignored )
            {
                //
            }
        }
        else
        {
            status = STATUS_OPEN_ERROR;
        }
        return status;
    }

    /**
     * Reads GIF file from specified source (file or URL string)
     *
     * @param name String containing source
     * @return read status code (0 = no errors)
     */
    public int read ( String name )
    {
        status = STATUS_OK;
        try
        {
            name = name.trim ();
            if ( name.indexOf ( "://" ) > 0 )
            {
                final URL url = new URL ( name );
                in = new BufferedInputStream ( url.openStream () );
            }
            else
            {
                in = new BufferedInputStream ( new FileInputStream ( name ) );
            }
            status = read ( in );
        }
        catch ( final IOException e )
        {
            status = STATUS_OPEN_ERROR;
        }

        return status;
    }

    /**
     * Decodes LZW image data into pixel array. Adapted from John Cristy's ImageMagick.
     */
    protected void decodeImageData ()
    {
        final int NullCode = -1;
        final int npix = iw * ih;
        int available;
        final int clear;
        int code_mask;
        int code_size;
        final int end_of_information;
        int in_code;
        int old_code;
        int bits;
        int code;
        int count;
        int i;
        int datum;
        final int data_size;
        int first;
        int top;
        int bi;
        int pi;

        if ( pixels == null || pixels.length < npix )
        {
            // allocate new pixel array
            pixels = new byte[ npix ];
        }

        if ( prefix == null )
        {
            prefix = new short[ MaxStackSize ];
        }
        if ( suffix == null )
        {
            suffix = new byte[ MaxStackSize ];
        }
        if ( pixelStack == null )
        {
            pixelStack = new byte[ MaxStackSize + 1 ];
        }

        //  Initialize GIF data stream decoder.
        data_size = read ();
        clear = 1 << data_size;
        end_of_information = clear + 1;
        available = clear + 2;
        old_code = NullCode;
        code_size = data_size + 1;
        code_mask = ( 1 << code_size ) - 1;
        for ( code = 0; code < clear; code++ )
        {
            prefix[ code ] = 0;
            suffix[ code ] = ( byte ) code;
        }

        //  Decode GIF pixel stream.
        datum = bits = count = first = top = pi = bi = 0;

        for ( i = 0; i < npix; )
        {
            if ( top == 0 )
            {
                if ( bits < code_size )
                {
                    //  Load bytes until there are enough bits for a code.
                    if ( count == 0 )
                    {
                        // Read a new data block.
                        count = readBlock ();
                        if ( count <= 0 )
                        {
                            break;
                        }
                        bi = 0;
                    }
                    datum += ( ( int ) currentDataBlock[ bi ] & 0xff ) << bits;
                    bits += 8;
                    bi++;
                    count--;
                    continue;
                }

                //  Get the next code.
                code = datum & code_mask;
                datum >>= code_size;
                bits -= code_size;

                //  Interpret the code
                if ( code > available || code == end_of_information )
                {
                    break;
                }
                if ( code == clear )
                {
                    //  Reset decoder.
                    code_size = data_size + 1;
                    code_mask = ( 1 << code_size ) - 1;
                    available = clear + 2;
                    old_code = NullCode;
                    continue;
                }
                if ( old_code == NullCode )
                {
                    pixelStack[ top++ ] = suffix[ code ];
                    old_code = code;
                    first = code;
                    continue;
                }
                in_code = code;
                if ( code == available )
                {
                    pixelStack[ top++ ] = ( byte ) first;
                    code = old_code;
                }
                while ( code > clear )
                {
                    pixelStack[ top++ ] = suffix[ code ];
                    code = prefix[ code ];
                }
                first = ( int ) suffix[ code ] & 0xff;

                //  Add a new string to the string table,

                if ( available >= MaxStackSize )
                {
                    break;
                }
                pixelStack[ top++ ] = ( byte ) first;
                prefix[ available ] = ( short ) old_code;
                suffix[ available ] = ( byte ) first;
                available++;
                if ( ( available & code_mask ) == 0 && available < MaxStackSize )
                {
                    code_size++;
                    code_mask += available;
                }
                old_code = in_code;
            }

            //  Pop a pixel off the pixel stack.

            top--;
            pixels[ pi++ ] = pixelStack[ top ];
            i++;
        }

        for ( i = pi; i < npix; i++ )
        {
            pixels[ i ] = 0;  // clear missing pixels
        }
    }

    /**
     * Returns whether or not an error was encountered during reading/decoding.
     *
     * @return {@code true} if an error was encountered during reading/decoding, {@code false} otherwise
     */
    protected boolean err ()
    {
        return status != STATUS_OK;
    }

    /**
     * Initializes or re-initializes reader
     */
    protected void init ()
    {
        status = STATUS_OK;
        frameCount = 0;
        frames = new ArrayList ();
        gct = null;
        lct = null;
    }

    /**
     * Reads a single byte from the input stream.
     *
     * @return single byte from the input stream
     */
    protected int read ()
    {
        int curByte = 0;
        try
        {
            curByte = in.read ();
        }
        catch ( final IOException e )
        {
            status = STATUS_FORMAT_ERROR;
        }
        return curByte;
    }

    /**
     * Reads next variable length block from input.
     *
     * @return number of bytes stored in "buffer"
     */
    protected int readBlock ()
    {
        blockSize = read ();
        int n = 0;
        if ( blockSize > 0 )
        {
            try
            {
                int count;
                while ( n < blockSize )
                {
                    count = in.read ( currentDataBlock, n, blockSize - n );
                    if ( count == -1 )
                    {
                        break;
                    }
                    n += count;
                }
            }
            catch ( final IOException ignored )
            {
                //
            }

            if ( n < blockSize )
            {
                status = STATUS_FORMAT_ERROR;
            }
        }
        return n;
    }

    /**
     * Reads color table as 256 RGB integer values
     *
     * @param ncolors int number of colors to read
     * @return int array containing 256 colors (packed ARGB with full alpha)
     */
    protected int[] readColorTable ( final int ncolors )
    {
        final int nbytes = 3 * ncolors;
        int[] tab = null;
        final byte[] c = new byte[ nbytes ];
        int n = 0;
        try
        {
            n = in.read ( c );
        }
        catch ( final IOException ignored )
        {
            //
        }
        if ( n < nbytes )
        {
            status = STATUS_FORMAT_ERROR;
        }
        else
        {
            tab = new int[ 256 ];  // max size to avoid bounds checks
            int i = 0;
            int j = 0;
            while ( i < ncolors )
            {
                final int r = ( int ) c[ j++ ] & 0xff;
                final int g = ( int ) c[ j++ ] & 0xff;
                final int b = ( int ) c[ j++ ] & 0xff;
                tab[ i++ ] = 0xff000000 | r << 16 | g << 8 | b;
            }
        }
        return tab;
    }

    /**
     * Main file parser.  Reads GIF content blocks.
     */
    protected void readContents ()
    {
        // read GIF file content blocks
        boolean done = false;
        while ( !( done || err () ) )
        {
            int code = read ();
            switch ( code )
            {

                case 0x2C:
                    // image separator
                    readImage ();
                    break;

                case 0x21:
                    // extension
                    code = read ();
                    switch ( code )
                    {

                        case 0xf9:
                            // graphics control extension
                            readGraphicControlExt ();
                            break;

                        case 0xff:
                            // application extension
                            readBlock ();
                            final StringBuilder app = new StringBuilder ();
                            for ( int i = 0; i < 11; i++ )
                            {
                                app.append ( ( char ) currentDataBlock[ i ] );
                            }
                            if ( app.toString ().equals ( "NETSCAPE2.0" ) )
                            {
                                readNetscapeExt ();
                            }
                            else
                            {
                                // don't care
                                skip ();
                            }
                            break;

                        default:
                            // uninteresting extension
                            skip ();
                    }
                    break;

                case 0x3b:
                    // terminator
                    done = true;
                    break;

                default:
                    status = STATUS_FORMAT_ERROR;
            }
        }
    }

    /**
     * Reads Graphics Control Extension values
     */
    protected void readGraphicControlExt ()
    {
        read ();    // block size
        final int packed = read ();   // packed fields
        dispose = ( packed & 0x1c ) >> 2;   // disposal method
        if ( dispose == 0 )
        {
            dispose = 1;   // elect to keep old image if discretionary
        }
        transparency = ( packed & 1 ) != 0;
        delay = readShort () * 10;   // delay in milliseconds
        transIndex = read ();        // transparent color index
        read ();                     // block terminator
    }

    /**
     * Reads GIF file header information.
     */
    @SuppressWarnings ( "MethodWithMultipleReturnPoints" )
    protected void readHeader ()
    {
        final StringBuilder id = new StringBuilder ();
        for ( int i = 0; i < 6; i++ )
        {
            id.append ( ( char ) read () );
        }
        if ( !id.toString ().startsWith ( "GIF" ) )
        {
            status = STATUS_FORMAT_ERROR;
            return;
        }

        readLSD ();
        if ( gctFlag && !err () )
        {
            gct = readColorTable ( gctSize );
            bgColor = gct[ bgIndex ];
        }
    }

    /**
     * Reads next frame image
     */
    @SuppressWarnings ( "MethodWithMultipleReturnPoints" )
    protected void readImage ()
    {
        ix = readShort ();    // (sub)image position & size
        iy = readShort ();
        iw = readShort ();
        ih = readShort ();

        final int packed = read ();
        lctFlag = ( packed & 0x80 ) != 0;     // 1 - local color table flag
        interlace = ( packed & 0x40 ) != 0;   // 2 - interlace flag
        // 3 - sort flag
        // 4-5 - reserved
        lctSize = 2 << ( packed & 7 );        // 6-8 - local color table size

        if ( lctFlag )
        {
            lct = readColorTable ( lctSize );   // read table
            act = lct;    // make local table active
        }
        else
        {
            act = gct;    // make global table active
            if ( bgIndex == transIndex )
            {
                bgColor = 0;
            }
        }
        int save = 0;
        if ( transparency )
        {
            save = act[ transIndex ];
            act[ transIndex ] = 0;    // set transparent color if specified
        }

        if ( act == null )
        {
            status = STATUS_FORMAT_ERROR;   // no color table defined
        }

        if ( err () )
        {
            return;
        }

        decodeImageData ();   // decode pixel data
        skip ();

        if ( err () )
        {
            return;
        }

        frameCount++;

        // create new image to receive frame data
        currentBufImg = new BufferedImage ( width, height, BufferedImage.TYPE_INT_ARGB_PRE );

        setPixels ();   // transfer pixel data to image

        frames.add ( new GifFrame ( currentBufImg, delay ) );   // add image to frame list

        if ( transparency )
        {
            act[ transIndex ] = save;
        }
        resetFrame ();
    }

    /**
     * Reads Logical Screen Descriptor
     */
    protected void readLSD ()
    {
        // logical screen size
        width = readShort ();
        height = readShort ();

        // packed fields
        final int packed = read ();
        gctFlag = ( packed & 0x80 ) != 0;      // 1   : global color table flag
        // 2-4 : color resolution
        // 5   : gct sort flag
        gctSize = 2 << ( packed & 7 );      // 6-8 : gct size

        bgIndex = read ();        // background color index
        pixelAspect = read ();    // pixel aspect ratio
    }

    /**
     * Reads Netscape extension to obtain iteration count
     */
    protected void readNetscapeExt ()
    {
        do
        {
            readBlock ();
            if ( currentDataBlock[ 0 ] == 1 )
            {
                // loop count sub-block
                final int b1 = ( int ) currentDataBlock[ 1 ] & 0xff;
                final int b2 = ( int ) currentDataBlock[ 2 ] & 0xff;
                loopCount = b2 << 8 | b1;
            }
        }
        while ( blockSize > 0 && !err () );
    }

    /**
     * Reads next 16-bit value, LSB first.
     *
     * @return next 16-bit value, LSB first
     */
    protected int readShort ()
    {
        // read 16-bit value, LSB first
        return read () | read () << 8;
    }

    /**
     * Resets frame state for reading next image.
     */
    protected void resetFrame ()
    {
        lastDispose = dispose;
        lastRect = new Rectangle ( ix, iy, iw, ih );
        prevBufImg = currentBufImg;
        lastBgColor = bgColor;
        // int dispose = 0;
        //boolean transparency = false;
        //int delay = 0;
        lct = null;
    }

    /**
     * Creates new frame image from current data (and previous frames as specified by their
     * disposition codes).
     */
    protected void setPixels ()
    {
        // expose destination image's pixels as int array
        final int[] dest = ( ( DataBufferInt ) currentBufImg.getRaster ().getDataBuffer () ).getData ();

        // fill in starting image contents based on last image's dispose code
        if ( lastDispose > 0 )
        {
            if ( lastDispose == 3 )
            {
                // use image before last
                final int n = frameCount - 2;
                if ( n > 0 )
                {
                    prevBufImg = getFrame ( n - 1 );
                }
                else
                {
                    prevBufImg = null;
                }
            }

            if ( prevBufImg != null )
            {
                final int[] prev = ( ( DataBufferInt ) prevBufImg.getRaster ().getDataBuffer () ).getData ();
                System.arraycopy ( prev, 0, dest, 0, width * height );  // copy pixels

                if ( lastDispose == 2 )
                {
                    // fill last image rect area with background color
                    final Graphics2D g = currentBufImg.createGraphics ();
                    final Color c;
                    if ( transparency )
                    {
                        c = new Color ( 0, 0, 0, 0 );       // assume background is transparent
                    }
                    else
                    {
                        c = new Color ( lastBgColor );   // use given background color
                    }
                    g.setPaint ( c );
                    g.setComposite ( AlphaComposite.Src );      // replace area
                    g.fill ( lastRect );
                    g.dispose ();
                }
            }
        }

        // copy each source line to the appropriate place in the destination
        int pass = 1;
        int inc = 8;
        int iline = 0;
        for ( int i = 0; i < ih; i++ )
        {
            int line = i;
            if ( interlace )
            {
                if ( iline >= ih )
                {
                    pass++;
                    switch ( pass )
                    {
                        case 2:
                            iline = 4;
                            break;
                        case 3:
                            iline = 2;
                            inc = 4;
                            break;
                        case 4:
                            iline = 1;
                            inc = 2;
                    }
                }
                line = iline;
                iline += inc;
            }
            line += iy;
            if ( line < height )
            {
                final int k = line * width;
                int dx = k + ix;          // start of line in dest
                int dlim = dx + iw;       // end of dest line
                if ( k + width < dlim )
                {
                    dlim = k + width;      // past dest edge
                }
                int sx = i * iw;          // start of line in source
                while ( dx < dlim )
                {
                    // map color and insert in destination
                    final int index = ( int ) pixels[ sx++ ] & 0xff;
                    final int c = act[ index ];
                    if ( c != 0 )
                    {
                        dest[ dx ] = c;
                    }
                    dx++;
                }
            }
        }
    }

    /**
     * Skips variable length blocks up to and including next zero length block.
     */
    protected void skip ()
    {
        do
        {
            readBlock ();
        }
        while ( blockSize > 0 && !err () );
    }

    /**
     * Returns decoder status.
     *
     * @return decoder status
     */
    public int getStatus ()
    {
        return status;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy