[PATCH v3 0/1] MR11149: windowscodecs: Decode GIF frames on demand.
The GIF decoder currently calls DGifSlurp() during initialization, which decodes and stores pixel data for every frame in the image. For animated GIFs with a large number of frames this can result in significant startup time and memory consumption, even when only a single frame is requested by the application. Change the decoder to scan GIF headers during initialization and defer frame pixel decoding until CopyPixels() is called for a specific frame. This avoids eagerly decoding unused frames while preserving existing decoder behavior. Signed-off-by: chenzhengyong chenzhengyong@uniontech.com tested on deepin v25 & UOS V20. -- v3: windowscodecs: Decode GIF frames on demand. https://gitlab.winehq.org/wine/wine/-/merge_requests/11149
From: chenzhengyong <chenzhengyong@uniontech.com> The GIF decoder currently calls DGifSlurp() during initialization, which decodes and stores pixel data for every frame in the image. For animated GIFs with a large number of frames this can result in significant startup time and memory consumption, even when only a single frame is requested by the application. Change the decoder to scan GIF headers during initialization and defer frame pixel decoding until CopyPixels() is called for a specific frame. This avoids eagerly decoding unused frames while preserving existing decoder behavior. Signed-off-by: chenzhengyong <chenzhengyong@uniontech.com> --- dlls/windowscodecs/gifformat.c | 40 ++++++- dlls/windowscodecs/ungif.c | 185 ++++++++++++++++++++++++++++++++- dlls/windowscodecs/ungif.h | 8 ++ 3 files changed, 226 insertions(+), 7 deletions(-) diff --git a/dlls/windowscodecs/gifformat.c b/dlls/windowscodecs/gifformat.c index 287a49f83e9..7672b9c80d0 100644 --- a/dlls/windowscodecs/gifformat.c +++ b/dlls/windowscodecs/gifformat.c @@ -37,6 +37,7 @@ struct gif_decoder { struct decoder decoder; GifFileType *gif; + IStream *stream; /* kept to allow on-demand frame decoding */ }; static inline struct gif_decoder *impl_from_decoder(struct decoder *iface) @@ -646,6 +647,13 @@ static int _gif_inputfunc(GifFileType *gif, GifByteType *data, int len) { return bytesread; } +static void gif_seek_stream(GifFileType *gif, int offset) +{ + LARGE_INTEGER seek; + seek.QuadPart = offset; + IStream_Seek((IStream *)gif->UserData, seek, STREAM_SEEK_SET, NULL); +} + static HRESULT CDECL gif_decoder_initialize(struct decoder *iface, IStream *stream, struct decoder_stat *st) { struct gif_decoder *decoder = impl_from_decoder(iface); @@ -656,17 +664,31 @@ static HRESULT CDECL gif_decoder_initialize(struct decoder *iface, IStream *stre seek.QuadPart = 0; IStream_Seek(stream, seek, STREAM_SEEK_SET, NULL); + /* keep stream for on-demand frame decoding */ + IStream_AddRef(stream); + decoder->stream = stream; + /* read all data from the stream */ decoder->gif = DGifOpen((void *)stream, _gif_inputfunc); if (!decoder->gif) + { + IStream_Release(stream); + decoder->stream = NULL; return E_FAIL; + } + + /* Set up seek function for on-demand frame decoding */ + decoder->gif->seekFunc = gif_seek_stream; - ret = DGifSlurp(decoder->gif); + /* Only scan frame headers, skip pixel data for on-demand decoding */ + ret = DGifSlurpHeaders(decoder->gif); if (ret == GIF_ERROR) + { + DGifCloseFile(decoder->gif); + IStream_Release(stream); + decoder->stream = NULL; return E_FAIL; - - /* make sure we don't use the stream after this method returns */ - decoder->gif->UserData = NULL; + } st->flags = WICBitmapDecoderCapabilityCanDecodeAllImages | WICBitmapDecoderCapabilityCanDecodeSomeImages | @@ -759,6 +781,14 @@ static HRESULT CDECL gif_decoder_copy_pixels(struct decoder *iface, UINT frame, image = &decoder->gif->SavedImages[frame]; + /* Decode this frame's pixel data on demand */ + if (!image->RasterBits) + { + decoder->gif->UserData = (void *)decoder->stream; + if (DGifDecodeFrame(decoder->gif, frame) == GIF_ERROR) + return E_FAIL; + } + if (image->ImageDesc.Interlace) return copy_interlaced_pixels(image->RasterBits, image->ImageDesc.Width, image->ImageDesc.Height, image->ImageDesc.Width, prc, stride, buffersize, buffer); @@ -871,6 +901,8 @@ static void CDECL gif_decoder_destroy(struct decoder *iface) { struct gif_decoder *decoder = impl_from_decoder(iface); + if (decoder->stream) + IStream_Release(decoder->stream); DGifCloseFile(decoder->gif); free(decoder); } diff --git a/dlls/windowscodecs/ungif.c b/dlls/windowscodecs/ungif.c index aa0552c04ce..c994bad6bcb 100644 --- a/dlls/windowscodecs/ungif.c +++ b/dlls/windowscodecs/ungif.c @@ -452,9 +452,7 @@ DGifGetImageDesc(GifFileType * GifFile) { Private->PixelCount = GifFile->Image.Width * GifFile->Image.Height; - DGifSetupDecompress(GifFile); /* Reset decompress algorithm parameters. */ - - return GIF_OK; + return DGifSetupDecompress(GifFile); /* Reset decompress algorithm parameters. */ } /****************************************************************************** @@ -590,6 +588,24 @@ DGifGetCodeNext(GifFileType * GifFile, return GIF_OK; } +/****************************************************************************** + * Skip the LZW image sub-blocks without decoding any pixels. + * Called after DGifSetupDecompress has read the code size byte. + *****************************************************************************/ +static int +DGifSkipImageData(GifFileType *GifFile) +{ + GifByteType *CodeBlock; + do { + if (DGifGetCodeNext(GifFile, &CodeBlock) == GIF_ERROR) + { + WARN("GIF is not properly terminated\n"); + break; + } + } while (CodeBlock != NULL); + return GIF_OK; +} + /****************************************************************************** * Setup the LZ decompression for this image: *****************************************************************************/ @@ -891,7 +907,11 @@ DGifSlurp(GifFileType * GifFile) { } if (DGifGetLine(GifFile, sp->RasterBits, ImageSize) == GIF_ERROR) + { + free(sp->RasterBits); + sp->RasterBits = NULL; return (GIF_ERROR); + } if (temp_save.ExtensionBlocks) { sp->Extensions.ExtensionBlocks = temp_save.ExtensionBlocks; sp->Extensions.ExtensionBlockCount = temp_save.ExtensionBlockCount; @@ -975,6 +995,165 @@ DGifSlurp(GifFileType * GifFile) { return (GIF_OK); } +/****************************************************************************** + * Like DGifSlurp() but skips pixel data of each frame. + * Only reads frame headers (ImageDesc, extension blocks) so it is fast. + * Use DGifDecodeFrame() to decode individual frames on demand. + *****************************************************************************/ +int +DGifSlurpHeaders(GifFileType *GifFile) +{ + GifRecordType RecordType; + SavedImage *sp; + GifByteType *ExtData; + Extensions temp_save; + + temp_save.ExtensionBlocks = NULL; + temp_save.ExtensionBlockCount = 0; + + do { + if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) + return GIF_ERROR; + + switch (RecordType) { + case IMAGE_DESC_RECORD_TYPE: + if (DGifGetImageDesc(GifFile) == GIF_ERROR) + return GIF_ERROR; + + sp = &GifFile->SavedImages[GifFile->ImageCount - 1]; + /* GetFileOffset is past the code size byte read by DGifSetupDecompress + * inside DGifGetImageDesc, so subtract 1 to point at the code size byte + * itself, so DGifDecodeFrame can re-read it correctly after seeking. */ + sp->LZWDataOffset = GetFileOffset(GifFile) - 1; + + /* Skip LZW image sub-blocks without decoding */ + if (DGifSkipImageData(GifFile) == GIF_ERROR) + return GIF_ERROR; + + /* Attach pending extension to this frame */ + if (temp_save.ExtensionBlocks) { + sp->Extensions.ExtensionBlocks = temp_save.ExtensionBlocks; + sp->Extensions.ExtensionBlockCount = temp_save.ExtensionBlockCount; + temp_save.ExtensionBlocks = NULL; + temp_save.ExtensionBlockCount = 0; + sp->Extensions.Function = sp->Extensions.ExtensionBlocks[0].Function; + } + break; + + case EXTENSION_RECORD_TYPE: + { + int Function; + Extensions *Extensions; + + if (DGifGetExtension(GifFile, &Function, &ExtData) == GIF_ERROR) + return GIF_ERROR; + + if (GifFile->ImageCount || Function == GRAPHICS_EXT_FUNC_CODE) + Extensions = &temp_save; + else + Extensions = &GifFile->Extensions; + + Extensions->Function = Function; + + if (ExtData) + { + if (AddExtensionBlock(Extensions, ExtData[0], &ExtData[1]) == GIF_ERROR) + return GIF_ERROR; + } + else + { + if (AddExtensionBlock(Extensions, 0, NULL) == GIF_ERROR) + return GIF_ERROR; + } + + while (ExtData != NULL) { + int Len; + GifByteType *Data; + + if (DGifGetExtensionNext(GifFile, &ExtData) == GIF_ERROR) + return GIF_ERROR; + + if (ExtData) + { + Len = ExtData[0]; + Data = &ExtData[1]; + } + else + { + Len = 0; + Data = NULL; + } + + if (AppendExtensionBlock(Extensions, Len, Data) == GIF_ERROR) + return GIF_ERROR; + } + break; + } + + case TERMINATE_RECORD_TYPE: + break; + + default: + break; + } + } while (RecordType != TERMINATE_RECORD_TYPE); + + if (temp_save.ExtensionBlocks) + FreeExtension(&temp_save); + + return GIF_OK; +} + +/****************************************************************************** + * Decode a specific frame on demand. + * Requires DGifSlurpHeaders() to have been called first to populate SavedImages. + * On success the frame's RasterBits are allocated and filled; subsequent calls + * return immediately if the frame is already decoded. + *****************************************************************************/ +int +DGifDecodeFrame(GifFileType *GifFile, int frame) +{ + GifFilePrivateType *Private = GifFile->Private; + SavedImage *sp; + int ImageSize; + + if (frame < 0 || frame >= GifFile->ImageCount) + return GIF_ERROR; + + sp = &GifFile->SavedImages[frame]; + + /* Already decoded */ + if (sp->RasterBits) + return GIF_OK; + + /* Seek to this frame's LZW data in the stream */ + if (GifFile->seekFunc) + { + ((SeekFunc)GifFile->seekFunc)(GifFile, sp->LZWDataOffset); + /* Reset the decoder's byte offset counter so subsequent READ() calls + * start from the correct position */ + Private->Offset = sp->LZWDataOffset; + } + + ImageSize = sp->ImageDesc.Width * sp->ImageDesc.Height; + + /* Re-initialize LZW decompress state (reads the code size byte) */ + Private->PixelCount = ImageSize; + DGifSetupDecompress(GifFile); + + sp->RasterBits = malloc(ImageSize * sizeof(GifPixelType)); + if (!sp->RasterBits) + return GIF_ERROR; + + if (DGifGetLine(GifFile, sp->RasterBits, ImageSize) != GIF_OK) + { + free(sp->RasterBits); + sp->RasterBits = NULL; + return GIF_ERROR; + } + return GIF_OK; +} + /****************************************************************************** * GifFileType constructor with user supplied input function (TVT) *****************************************************************************/ diff --git a/dlls/windowscodecs/ungif.h b/dlls/windowscodecs/ungif.h index c402109bc67..4c91c4c4881 100644 --- a/dlls/windowscodecs/ungif.h +++ b/dlls/windowscodecs/ungif.h @@ -124,6 +124,7 @@ typedef struct GifFileType { struct SavedImage *SavedImages; /* Use this to accumulate file state */ void *UserData; /* hook to attach user data (TVT) */ void *Private; /* Don't mess with this! */ + void *seekFunc; /* SeekFunc: seek to absolute byte offset for on-demand decoding */ } GifFileType; typedef enum { @@ -137,6 +138,9 @@ typedef enum { /* func type to read gif data from arbitrary sources (TVT) */ typedef int (*InputFunc) (GifFileType *, GifByteType *, int); +/* func type to seek to an absolute byte offset in the GIF data source */ +typedef void (*SeekFunc) (GifFileType *, int offset); + /* GIF89 extension function codes */ #define COMMENT_EXT_FUNC_CODE 0xfe /* comment */ @@ -171,8 +175,12 @@ int DGifCloseFile(GifFileType * GifFile); typedef struct SavedImage { GifImageDesc ImageDesc; int ImageDescOffset; + int LZWDataOffset; /* byte offset to this frame's LZW sub-blocks in the stream */ unsigned char *RasterBits; Extensions Extensions; } SavedImage; +int DGifSlurpHeaders(GifFileType *GifFile); /* scan frame headers only, skip pixel data */ +int DGifDecodeFrame(GifFileType *GifFile, int frame); /* on-demand decode of a specific frame */ + #endif /* _UNGIF_H_ */ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11149
On Mon Jun 15 20:06:23 2026 +0000, Esme Povirk wrote:
We should set `decoder->stream` to NULL when releasing it, so we don't release it a second time when the object is freed. Yes, there was indeed an oversight here. I’ve fixed it and also corrected several other issues.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11149#note_143209
On Mon Jun 15 20:06:07 2026 +0000, Esme Povirk wrote:
So if I understand this, we're still reading all the image data, but then we're discarding it? No, the decoder does NOT load all pixel data into memory. The stream position is simply moved forward; the pixel data remains in the stream.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11149#note_143215
On Tue Jun 16 03:08:42 2026 +0000, zhengyong chen wrote:
No, the decoder does NOT load all pixel data into memory. The stream position is simply moved forward; the pixel data remains in the stream. DGifGetCodeNext calls READ, though, rather than SeekFunc, which I would expect to read the (compressed) data.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11149#note_143251
On Tue Jun 16 13:38:19 2026 +0000, Esme Povirk wrote:
DGifGetCodeNext calls READ, though, rather than SeekFunc, which I would expect to read the (compressed) data. You are right。But it does not load all pixel data into persistent memory. It reads compressed sub-blocks through a small fixed buffer(256 bytes) to advance the stream, but discards the data immediately after each sub-block.
The READ call advances the stream position (which is necessary to reach subsequent frames), but the actual memory footprint stays constant at 256 bytes — compared to the old approach which would allocate width × height bytes per frame and keep them all. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11149#note_143307
On Wed Jun 17 00:58:57 2026 +0000, zhengyong chen wrote:
You are right。But it does not load all pixel data into persistent memory. It reads compressed sub-blocks through a small fixed buffer(256 bytes) to advance the stream, but discards the data immediately after each sub-block. The READ call advances the stream position (which is necessary to reach subsequent frames), but the actual memory footprint stays constant at 256 bytes — compared to the old approach which would allocate width × height bytes per frame and keep them all. You might wonder why we don't simply use seekFunc to skip over the LZW image sub-blocks. The reason is that GIF is a purely sequential format with no index table. Each frame's LZW compressed data consists of a chain of sub-blocks (length prefix + data), terminated by a single 0x00 byte. Without reading through the sub-block chain, there is no way to know where the current frame's pixel data ends. The only way to determine frame boundaries is to read sequentially through the sub-blocks until the terminator is reached.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11149#note_143310
participants (3)
-
chenzhengyong -
Esme Povirk (@madewokherd) -
zhengyong chen (@chenzhengyong)