// Copyright 1996-1998 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OIGifImageProcessor.h"

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OWF/OWF.h>
#import <OIF/OIF.h>

#import "OIGifColorTable.h"

#if defined(DEBUG) || defined(NO_INLINE)
#define INLINE
#else
#define INLINE inline
#endif

RCS_ID("$Header: /Network/Developer/Source/CVS/OmniGroup/OmniComponents/ImageProcessors/gif/OIGifImageProcessor.m,v 1.21 1998/12/10 21:21:42 kc Exp $")

@interface OIGifImageProcessor (Private)
- (void)updateImage;
- (void)flushImage;
- (void)parseHeader;
- (void)parseImageHeader;
- (void)parseImage;
- (void)setAnimationFrameFromData:(NSMutableData *)imageData;
- (void)setTransparentColorIndex:(short)anIndex;
- (void)resetExtensionFlags;
- (void)processImageDescriptor;
- (void)processNetscape_2_0_Extension;
- (void)processApplicationExtension;
- (void)processExtension;
@end

typedef enum {
    EXTENSION_LABEL = 0x21,
    IMAGE_DESCRIPTOR_LABEL = 0x2c,
    TRAILER_LABEL = 0x3b,
} GifBlockLabelsEnum;

typedef enum {
    PLAIN_TEXT_EXTENSION = 0x1,
    GRAPHIC_CONTROL_EXTENSION = 0xf9,
    COMMENT_EXTENSION = 0xfe,
    GIF_APPLICATION_EXTENSION = 0xff,
} GifExtensionLabelsEnum;

typedef OIGifColor OIGifPixelData;


#define EXTRACT_BITS_LENGTH(value, begin, length) \
	(((value) >> (begin)) & ((1 << (length)) - 1))
#define EXTRACT_BITS_RANGE(value, begin, end) \
	(((value) >> (begin)) & ((1 << ((end) - (begin) + 1)) - 1))

@implementation OIGifImageProcessor

static OWContentType *animationContentType;
static NSString *OIGifBadHeaderError = @"OIGifBadHeaderError";

+ (void)initialize;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
	return;
    initialized = YES;

    animationContentType = [OWContentType contentTypeForString:@"omni/animation"];
}

- initWithPipeline:(OWPipeline *)aPipeline;
{
    if (![super initWithPipeline:aPipeline])
        return nil;

    [dataCursor setByteOrder:NS_LittleEndian];
    gamma = [[OFUserDefaults sharedUserDefaults] floatForKey:@"OIGammaCorrection"];

    return self;
}

- (void)dealloc;
{
    [globalColorTable release];
    [localColorTable release];
    [animation release];
    [animationFrame release];
    [savedImageData release];
    [super dealloc];
}

- (void)processBegin;
{
    OFUserDefaults *userDefaults;

    [super processBegin];
    
    userDefaults = [OFUserDefaults sharedUserDefaults];

    if ([pipeline contextObjectForKey:@"DisableIncrementalDisplay"])
	drawInterval = 1e+10;
    else
        drawInterval = [userDefaults floatForKey:@"OIImageProcessorDrawInterval"];
    OIImageProcessorCheckTimeEveryNRows = [userDefaults integerForKey:@"OIImageProcessorCheckTimeEveryNRows"];
    lastDrawTimeInterval = [NSDate timeIntervalSinceReferenceDate];
}

- (void)processEnd;
{
    if (needUpdate)
	[self updateImage];
    [animation endFrames];
    [super processEnd];
}

- (void)processAbort;
{
    [self updateImage];
    [animationFrame abortImage];
    [animation endFrames];
    [super processAbort];
}

- (void)process;
{
    BOOL badHeader = NO;
    BOOL endOfImages = NO;

    NS_DURING {
        [self parseHeader];
        [self resetExtensionFlags];
    } NS_HANDLER {
        if ([[localException name] isEqualToString:OIGifBadHeaderError])
            badHeader = YES;
        else
            [localException raise];
    } NS_ENDHANDLER;

    if (badHeader) {
        // let's guess what kind of image we have.
        [[dataCursor dataStream] setContentType:[OWUnknownDataStreamProcessor unknownContentType]];
        [pipeline addContent:[dataCursor dataStream]];
        [pipeline startProcessingContent];
        return;
    }
   
    while (!endOfImages) {
	GifBlockLabelsEnum          blockLabel;
	
	NS_DURING {
	    blockLabel = [dataCursor readByte];
	} NS_HANDLER {
	    if (localException == OWDataStreamCursor_UnderflowException && animationFrame)
		return;
	    else
		[localException raise];
	} NS_ENDHANDLER;
	switch (blockLabel) {
	case IMAGE_DESCRIPTOR_LABEL:
	    [self processImageDescriptor];
	    break;
	case EXTENSION_LABEL:
	    [self processExtension];
	    break;
	case TRAILER_LABEL:
	case 0:
	    endOfImages = YES;
	    break;
	default:
#ifdef DEBUG
	    NSLog(@"GIF has unrecognized separator (%X)", blockLabel);
#endif
	    endOfImages = YES;
	    break;
	}
    }
}



static INLINE unsigned long
readByte(byte *blockBuffer, byte *blockIndex, int *blockSize,
	 BOOL *endOfData, OWDataStreamCursor *dataCursor)
{
    if (*blockIndex < *blockSize)
        return blockBuffer[(*blockIndex)++];

    if (*endOfData)
	return 0;
	
    if ((*blockSize = [dataCursor readByte])) {
	*blockIndex = 0;
	[dataCursor readBytes:*blockSize intoBuffer:blockBuffer];
	return blockBuffer[(*blockIndex)++];
    } else {
        *endOfData = YES;
	return 0;
    }
}


static INLINE unsigned long int
readCode(unsigned long *raster, int codeSize, int *bitOffset, int readMask, byte *blockBuffer, byte *blockIndex, int *blockSize, BOOL *endOfData, OWDataStreamCursor *dataCursor)
{
    unsigned long int rawCode;

    rawCode = EXTRACT_BITS_LENGTH(*raster, *bitOffset, codeSize);

    *bitOffset += codeSize;
    while (*bitOffset / 8) {
	*raster = (*raster >> 8) | (readByte(blockBuffer, blockIndex, blockSize, endOfData, dataCursor) << 24);
	*bitOffset -= 8;
    }

    return rawCode;
}


static INLINE void
writePixel(OIGifPixelData *rawImage, OIGifPixelData *lastRawImage, unsigned int *lineStartIndex, unsigned int *lineOffsetIndex, byte pixelValue, OIGifImageProcessor *self, unsigned int *interlacePass)
{
    OIGifPixelData pixel;
    unsigned int width, screenWidth;
    OIGifPixelData *pixelPointer;
    BOOL isInterlaced;
    unsigned int pixelOffset, imageEndOffset, screenEndOffset;
    OIGifPixelData *imageEndPointer;

    pixel = OIGifColorTableColorAtIndex(self->localColorTable, pixelValue);
    isInterlaced = self->isInterlaced;
    width = self->width;
    screenWidth = self->screenWidth;
    imageEndOffset = (self->top + self->height) * screenWidth;
    screenEndOffset = self->screenWidth * self->screenHeight;

    if (imageEndOffset <= screenEndOffset)
        imageEndPointer = rawImage + imageEndOffset;
    else
        imageEndPointer = rawImage + screenEndOffset;

    if (*lineOffsetIndex == width) {
	*lineOffsetIndex = 0;

	if (!isInterlaced) {
	    *lineStartIndex += screenWidth;
	} else {
	    unsigned int imageStartOffset;

            switch (*interlacePass) {
                case 0:
                case 1:
                    *lineStartIndex += 8 * screenWidth;
                    break;
                case 2:
                    *lineStartIndex += 4 * screenWidth;
                    break;
                default:
                    *lineStartIndex += 2 * screenWidth;
                    break;
            }

	    imageStartOffset = self->top * screenWidth + self->left;
            while (*lineStartIndex >= imageEndOffset) {
                // Start the next pass
                (*interlacePass)++;

                // Position the line start to the first line of the new pass
                switch (*interlacePass) {
                    case 1:
                        *lineStartIndex = imageStartOffset + 4 * screenWidth;
                        break;
                    case 2:
                        *lineStartIndex = imageStartOffset + 2 * screenWidth;
                        break;
                    case 3:
                        *lineStartIndex = imageStartOffset + screenWidth;
                        break;
                    default:
                        [NSException raise:@"OIGifImageError" format:@"Corrupt GIF image"];
                        break;
                }
            }
	}
    }

    pixelOffset = *lineStartIndex + *lineOffsetIndex;
    pixelPointer = rawImage + pixelOffset;
    if (pixelPointer >= imageEndPointer)
        goto done;

    if (pixel.components.alpha == 0) {
	if (!lastRawImage) {
	    if (!isInterlaced) {
		goto done;
	    }
	    pixel.packed = 0;
	} else {
            pixel.packed = lastRawImage[pixelOffset].packed;
	}
    }
    pixelPointer->packed = pixel.packed;

#define PAD_INTERLACED_LINES
#ifdef PAD_INTERLACED_LINES
    if (isInterlaced) {
        switch (*interlacePass) {
            case 0:
                // +1
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +2
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +3
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +4
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +5
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +6
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +7
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;
                    break;

            case 1:
                // +1
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +2
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;

                // +3
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;
                    break;

            case 2:
                // +1
                pixelPointer += screenWidth;
                if (pixelPointer < imageEndPointer)
                    pixelPointer->packed = pixel.packed;
                    break;

            default:
                break;
        }
    }
#endif

done:
    (*lineOffsetIndex)++;
}

#define TABLE_SIZE 4096

static inline unsigned int safe_table_index(unsigned int index)
{
    if (index >= TABLE_SIZE)
        [NSException raise:@"OIGifImageError" format:@"Corrupt GIF image"];
    return index;
}

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];
    if (globalColorTable)
	[debugDictionary setObject:globalColorTable forKey:@"globalColorTable"];
    [debugDictionary setObject:isInterlaced ? @"YES" : @"NO" forKey:@"isInterlaced"];
    if (localColorTable)
	[debugDictionary setObject:localColorTable forKey:@"localColorTable"];
    if (animationFrame)
	[debugDictionary setObject:animationFrame forKey:@"animationFrame"];
    if (animation)
        [debugDictionary setObject:animation forKey:@"animation"];

    return debugDictionary;
}

@end

@implementation OIGifImageProcessor (Private)

- (void)updateImage;
{
    [animationFrame updateImage:[animationFrame image]];
    needUpdate = NO;
}

- (void)flushImage;
{
    NSTimeInterval nowTimeInterval;
    
    needUpdate = YES;

    nowTimeInterval = [NSDate timeIntervalSinceReferenceDate];
    if (nowTimeInterval - lastDrawTimeInterval <= drawInterval)
	return;
    [self updateImage];
}

- (void)parseHeader;
{
    char signature[3];
    char version[3];
    byte packedData;
    byte encodedGlobalColorTableSize;
    byte encodedAspectRatio;
    unsigned int globalColorTableSize;

    [dataCursor readBytes:3 intoBuffer:signature];
    if (strncmp(signature, "GIF", 3) != 0)
        [NSException raise:OIGifBadHeaderError format:@"File is not a GIF:  header is '%c%c%c', not 'GIF'", signature[0], signature[1], signature[2]];

    [dataCursor readBytes:3 intoBuffer:version];
    if (strncmp(version, "87a", 3) == 0)
        gifVersion = GIF87A;
    else if (strncmp(version, "89a", 3) == 0)
        gifVersion = GIF89A;
    else
        [NSException raise:@"OIGifImageError" format:@"GIF version '%c%c%c' not supported", version[0], version[1], version[2]];
	    
    screenWidth = [dataCursor readShort];
    screenHeight = [dataCursor readShort];

    packedData = [dataCursor readByte];
    encodedGlobalColorTableSize = EXTRACT_BITS_RANGE(packedData, 0, 2);
    isGlobalColorTableSorted = EXTRACT_BITS_RANGE(packedData, 3, 3);
    colorResolution = EXTRACT_BITS_RANGE(packedData, 4, 6);
    hasGlobalColorTable = EXTRACT_BITS_RANGE(packedData, 7, 7);

    backgroundColor = [dataCursor readByte];
    encodedAspectRatio = [dataCursor readByte];

    if (encodedAspectRatio)
	aspectRatio = ((double)encodedAspectRatio + 15.0) / 64.0;
    else
	aspectRatio = 1.0;
    
    globalBitsPerPixel = encodedGlobalColorTableSize + 1;
    globalColorTableSize = hasGlobalColorTable ? (1 << globalBitsPerPixel) : 0;
    
    globalColorTable = [[OIGifColorTable alloc] initFromDataStreamCursor:dataCursor size:globalColorTableSize gamma:gamma];
}

- (void)parseImageHeader;
{
    byte packedData;
    byte encodedLocalColorTableSize;
    unsigned int localColorTableSize;

    [localColorTable release];

    left = [dataCursor readShort];
    top = [dataCursor readShort];
    width = [dataCursor readShort];
    height = [dataCursor readShort];
    
    packedData = [dataCursor readByte];
    encodedLocalColorTableSize = EXTRACT_BITS_RANGE(packedData, 0, 2);
    isLocalColorTableSorted = EXTRACT_BITS_RANGE(packedData, 5, 5);
    isInterlaced = EXTRACT_BITS_RANGE(packedData, 6, 6);
    hasLocalColorTable = EXTRACT_BITS_RANGE(packedData, 7, 7);

    localBitsPerPixel = encodedLocalColorTableSize + 1;
    localColorTableSize = 1 << localBitsPerPixel;

    if (hasLocalColorTable) {
	localColorTable = [[OIGifColorTable alloc] initFromDataStreamCursor:dataCursor size:localColorTableSize gamma:gamma];
        if (hasTransparency)
	    [localColorTable setTransparentColorIndex:transparentIndex];
    } else {
	localColorTable = [globalColorTable retain];
	localBitsPerPixel = globalBitsPerPixel;
    }
}

- (void)parseImage;
{
    OIGifPixelData             *rawImage;
    OIGifPixelData             *lastRawImage;
    byte                        blockBuffer[257];
    byte                        blockIndex = 0;
    unsigned int                blockSize = 0;

    // The hash table used by the decompressor
    int                         prefix[TABLE_SIZE];
    int                         suffix[TABLE_SIZE];
    // An output array used by the decompressor
    int                         outCode[TABLE_SIZE + 1];
    // decompressor variables
    int                         npixels, maxpixels;
    int                         outCount = 0;	// Decompressor output 'stack count'
    int                         codeSize;	// Code size, read from GIF header
    int                         initialCodeSize; // Starting code size, used during Clear
    int                         code;	// Value returned by ReadCode
    int                         maxCode;	// limiting value for current code size
    int                         clearCode;	// GIF clear code
    int                         eofCode;	// GIF end-of-information code
    int                         curCode, oldCode, inCode;	// Decompressor variables
    int                         firstFree;	// First free code, generated per GIF spec
    int                         freeCode;	// Decompressor, next free slot in hash table
    int                         finChar;	// Decompressor variable
    int                         readMask;	// Code AND mask for current code size
    int				bitOffset;
    // wil's cool fifo
    unsigned long		raster;
    // position within image
    unsigned int                lineStartIndex, lineOffsetIndex;
    // interlacing variables
    unsigned int                interlacePass;
    //
    BOOL			endOfData = NO;
    // for redrawing half-done GIFs
    int				checkTimeEveryPixels;
    int				lastCheckPixel = 0;
    BOOL                        incrementalDraw = NO;
    NSMutableData              *imageData;

    if (!animation) {
	// first frame
        animation = [[OIAnimation alloc] initWithLoopCount:shouldLoop ? (loopCount == 0 ? OIAnimationInfiniteLoopCount : loopCount) : 1];
        [pipeline addContent:animation];
        [pipeline cacheContent];
        incrementalDraw = ![pipeline contextObjectForKey:@"DisableIncrementalDisplay"];
        [pipeline startProcessingContent];
    }

    if (savedImageData) {
	imageData = [savedImageData mutableCopy];
    } else {
	imageData = [[NSMutableData alloc] initWithLength:screenWidth * screenHeight * sizeof(OIGifPixelData)];
    }
    [self setAnimationFrameFromData:imageData];

    lastRawImage = savedImageData ? [savedImageData mutableBytes] : NULL;
    rawImage = [imageData mutableBytes];
    if (incrementalDraw) {
        [animation addFrame:animationFrame];
    }

    npixels = 0;
    maxpixels = width * height;
    codeSize = [dataCursor readByte];
    clearCode = (1 << codeSize);
    eofCode = clearCode + 1;
    freeCode = firstFree = clearCode + 2;

    checkTimeEveryPixels = width * OIImageProcessorCheckTimeEveryNRows;

    // The GIF spec has it that the code size used to compute the above values is the code size given in the file, but the code size used in compression/decompression is the code size given in the file plus one. (thus the ++).
    codeSize++;

    initialCodeSize = codeSize;
    maxCode = (1 << codeSize);
    readMask = maxCode - 1;
    finChar = 0;
    bitOffset = 0;
    blockBuffer[256] = 0;

    lineStartIndex = top * screenWidth + left;
    lineOffsetIndex = 0;
    interlacePass = 0;
        
    // Load up the raster with 4 bytes
    readCode(&raster, 32, &bitOffset, readMask, blockBuffer, &blockIndex, &blockSize, &endOfData, dataCursor);
    bitOffset = 0;
    
    // Let's rock
    code = readCode(&raster, codeSize, &bitOffset, readMask, blockBuffer, &blockIndex, &blockSize, &endOfData, dataCursor);
    oldCode = code;
    while (code != eofCode) {
	// Clear code sets everything back to its initial value, then reads the immediately subsequent code as uncompressed data.

	if (code == clearCode) {
	    codeSize = initialCodeSize;
	    maxCode = (1 << codeSize);
	    readMask = maxCode - 1;
	    freeCode = firstFree;
	    code = readCode(&raster, codeSize, &bitOffset, readMask, blockBuffer, &blockIndex, &blockSize, &endOfData, dataCursor);
	    curCode = oldCode = code;
	    finChar = curCode;
	    writePixel(rawImage, lastRawImage, &lineStartIndex, &lineOffsetIndex, finChar, self, &interlacePass);
	    npixels++;
	} else {
	    // If not a clear code, must be data: save same as curCode and inCode

	    // if we're at maxcode and didn't get a clear, stop loading
	    if (freeCode >= TABLE_SIZE) {
		// warning: freeCode overflow
		break;
	    }
	    curCode = inCode = code;

	    // If greater or equal to freeCode, not in the hash table yet; repeat the last character decoded

	    if (curCode >= freeCode) {
		curCode = oldCode;
		if (outCount > TABLE_SIZE) {
		    // NSLog(@"outCount1 blew up");
		    break;
		}
		outCode[outCount++] = finChar;
	    }
	    // Unless this code is raw data, pursue the chain pointed to by curCode through the hash table to its end; each code in the chain puts its associated output code on the output queue.

	    while (curCode >= firstFree) {
		outCode[safe_table_index(outCount++)] = suffix[safe_table_index(curCode)];
		curCode = prefix[curCode];
	    }

	    // The last code in the chain is treated as raw data.

	    finChar = curCode;
	    outCode[outCount++] = finChar;

	    // Now we put the data out to the Output routine. It's been stacked LIFO, so deal with it that way...

	    // safety thing: prevent exceeding range of 'pic8'
	    if (npixels + outCount > maxpixels)
		outCount = maxpixels - npixels;

	    while (outCount--) {
		writePixel(rawImage, lastRawImage, &lineStartIndex, &lineOffsetIndex, outCode[outCount], self, &interlacePass);
		npixels++;
	    }
	    outCount = 0;

	    // Build the hash table on-the-fly. No table is stored in the file.

	    prefix[freeCode] = oldCode;
	    suffix[freeCode] = finChar;
	    oldCode = inCode;

	    // Point to the next slot in the table.  If we exceed the current maxCode value, increment the code size unless it's already 12.  If it is, do nothing:  the next code decompressed better be CLEAR.

	    freeCode++;
	    if (freeCode >= maxCode) {
		if (codeSize < 12) {
		    codeSize++;
		    maxCode *= 2;
		    readMask = (1 << codeSize) - 1;
		}
	    }
	}
	code = readCode(&raster, codeSize, &bitOffset, readMask,
			blockBuffer, &blockIndex,
			&blockSize, &endOfData, dataCursor);
	if (npixels >= maxpixels)
	    break;
	    
	if (incrementalDraw &&
	    npixels - lastCheckPixel > checkTimeEveryPixels) {
	    lastCheckPixel = npixels;
	    [self flushImage]; // This will check the time since our last draw
	}
    }

    switch (disposalMethod) {
        default:
        case GDMNothing:
        case GDMLeaveAsIs:
            if (savedImageData)
                [savedImageData release];
            savedImageData = [imageData retain];
            break;
        case GDMPreviousImage:
            break;
        case GDMBackground:
            if (savedImageData)
                [savedImageData release];
            savedImageData = nil;
            break;
    }

    [imageData release];

    if (npixels != maxpixels) {
	// warning "This GIF file seems to be truncated.  Winging it."
	// force this to be the last image read
    }
    
    if (!incrementalDraw)
	[animation addFrame:animationFrame];

    [self flushImage];
}

- (void)setAnimationFrameFromData:(NSMutableData *)imageData;
{
    void *imageDataBytes;
    NSBitmapImageRep *imageRep;
    NSImage *newImage;
    unsigned char *dataPlanes[5] = {NULL};

    imageDataBytes = [imageData mutableBytes];
    dataPlanes[0] = imageDataBytes;
#warning Would it be better to use NSCalibratedRGBColorSpace?  Would it do some gamma correction for us?
    imageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:dataPlanes pixelsWide:screenWidth pixelsHigh:screenHeight bitsPerSample:8 samplesPerPixel:sizeof(OIGifPixelData) hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bytesPerRow:0 bitsPerPixel:0];
    newImage = [[NSImage alloc] init];
    [newImage setDataRetained:YES];
    [newImage setCachedSeparately:YES];
    [newImage addRepresentation:imageRep];
    [imageRep release];

    [animationFrame release];
    animationFrame = [[OIAnimationFrame alloc] initWithDelayInterval:0.01 * (double)delayTime];
    [animationFrame setSize:NSMakeSize(screenWidth, screenHeight)];
    [animationFrame updateImage:newImage];
    [newImage release];
    [animationFrame setSourceData:imageData];
}

- (void)setTransparentColorIndex:(short)anIndex;
{
    transparentIndex = anIndex;
    [globalColorTable setTransparentColorIndex:anIndex];
    [localColorTable setTransparentColorIndex:anIndex];
}

- (void)resetExtensionFlags;
{
    hasTransparency = NO;
    wantsUserInput = NO;
    disposalMethod = 0;
    delayTime = 0;
    [self setTransparentColorIndex:-1];
}

- (void)processImageDescriptor;
{
    [self parseImageHeader];
    if (screenWidth == 0)
        screenWidth = width;
    if (screenHeight == 0)
        screenHeight = height;
    [self parseImage];
    [self resetExtensionFlags];
}

- (void)processNetscape_2_0_Extension;
{
    unsigned int blockSize;
    byte code;

    while ((blockSize = [dataCursor readByte])) {
	code = [dataCursor readByte];
	switch (code) {
	case 1:
	    shouldLoop = YES;
	    loopCount = [dataCursor readShort];
	    break;
	default:
	    [dataCursor skipBytes:blockSize];
	    break;
	}
    }
}

const char netscapeChars[] = "NETSCAPE2.0";
const int netscapeCharCount = sizeof(netscapeChars)-1;

- (void)processApplicationExtension;
{
    unsigned int blockSize;

    if ([dataCursor peekByte] == netscapeCharCount) {
	char applicationHeader[netscapeCharCount];

	[dataCursor skipBytes:1];
	[dataCursor readBytes:netscapeCharCount intoBuffer:applicationHeader];
	if (strncmp(applicationHeader, netscapeChars, netscapeCharCount) == 0) {
	    [self processNetscape_2_0_Extension];
	    return;
	}
    }
    while ((blockSize = [dataCursor readByte]))
	[dataCursor skipBytes:blockSize];
}

- (void)processExtension;
{
    GifExtensionLabelsEnum extensionLabel;
    byte packedData;
    unsigned int blockSize;
    
    extensionLabel = [dataCursor readByte];
    switch (extensionLabel) {
        case GRAPHIC_CONTROL_EXTENSION:
            blockSize = [dataCursor readByte];
            packedData = [dataCursor readByte];
            hasTransparency = EXTRACT_BITS_RANGE(packedData, 0, 0);
            wantsUserInput = EXTRACT_BITS_RANGE(packedData, 1, 1);
            disposalMethod = EXTRACT_BITS_RANGE(packedData, 2, 4);
            delayTime = [dataCursor readShort];
            if (hasTransparency) {
                [self setTransparentColorIndex:[dataCursor readByte]];
            } else {
                [dataCursor skipBytes:1]; // ignore transparency value
                [self setTransparentColorIndex:-1];
            }
            // skip terminator
            [dataCursor skipBytes:1];
            break;
        case GIF_APPLICATION_EXTENSION:
            [self processApplicationExtension];
            break;
        case PLAIN_TEXT_EXTENSION:
        case COMMENT_EXTENSION:
        default:
            while ((blockSize = [dataCursor readByte]))
                [dataCursor skipBytes:blockSize];
            break;
    }
}

@end
