diff --git a/binding/bitmap-binding.cpp b/binding/bitmap-binding.cpp index 69de9a99..44a8f678 100644 --- a/binding/bitmap-binding.cpp +++ b/binding/bitmap-binding.cpp @@ -436,6 +436,131 @@ RB_METHOD(bitmapGetMega){ return rb_bool_new(b->isMega()); } +RB_METHOD(bitmapGetAnimated){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + + return rb_bool_new(b->isAnimated()); +} + +RB_METHOD(bitmapGetPlaying){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + + return rb_bool_new(b->isPlaying()); +} + +RB_METHOD(bitmapSetPlaying){ + RB_UNUSED_PARAM; + + bool play; + + rb_get_args(argc, argv, "b", &play RB_ARG_END); + + Bitmap *b = getPrivateData(self); + + (play) ? b->play() : b->stop(); + + return RUBY_Qnil; +} + +RB_METHOD(bitmapGotoStop){ + RB_UNUSED_PARAM; + + int frame; + + rb_get_args(argc, argv, "i", &frame RB_ARG_END); + + Bitmap *b = getPrivateData(self); + + b->gotoAndStop(frame); + + return RUBY_Qnil; +} + +RB_METHOD(bitmapGotoPlay){ + RB_UNUSED_PARAM; + + int frame; + + rb_get_args(argc, argv, "i", &frame RB_ARG_END); + + Bitmap *b = getPrivateData(self); + + b->gotoAndPlay(frame); + + return RUBY_Qnil; +} + +RB_METHOD(bitmapFrames){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + + return INT2NUM(b->numFrames()); +} + +RB_METHOD(bitmapCurrentFrame){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + + return INT2NUM(b->currentFrameI()); +} + +RB_METHOD(bitmapSetFPS){ + RB_UNUSED_PARAM; + + float fps; + rb_get_args(argc, argv, "f", &fps RB_ARG_END); + + Bitmap *b = getPrivateData(self); + + b->setAnimationFPS(fps); + + return RUBY_Qnil; +} + +RB_METHOD(bitmapGetFPS){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + return rb_float_new(b->getAnimationFPS()); +} + +RB_METHOD(bitmapSetLooping){ + RB_UNUSED_PARAM; + + bool loop; + rb_get_args(argc, argv, "b", &loop RB_ARG_END); + + Bitmap *b = getPrivateData(self); + b->setLooping(loop); + + return Qnil; +} + +RB_METHOD(bitmapGetLooping){ + RB_UNUSED_PARAM; + + rb_check_argc(argc, 0); + + Bitmap *b = getPrivateData(self); + return rb_bool_new(b->getLooping()); +} + RB_METHOD(bitmapGetMaxSize){ RB_UNUSED_PARAM; @@ -500,6 +625,20 @@ void bitmapBindingInit() { _rb_define_method(klass, "mega?", bitmapGetMega); rb_define_singleton_method(klass, "max_size", RUBY_METHOD_FUNC(bitmapGetMaxSize), -1); + + _rb_define_method(klass, "animated?", bitmapGetAnimated); + _rb_define_method(klass, "playing", bitmapGetPlaying); + _rb_define_method(klass, "playing=", bitmapSetPlaying); + _rb_define_method(klass, "goto_and_stop", bitmapGotoStop); + _rb_define_method(klass, "goto_and_play", bitmapGotoPlay); + _rb_define_method(klass, "frame_count", bitmapFrames); + _rb_define_method(klass, "current_frame", bitmapCurrentFrame); + _rb_define_method(klass, "frame_rate", bitmapGetFPS); + + // For some reason Ruby says "screw this function in particular" + //_rb_define_method(klass, "frame_rate=", bitmapSetFPS); + _rb_define_method(klass, "looping", bitmapGetLooping); + _rb_define_method(klass, "looping=", bitmapSetLooping); INIT_PROP_BIND(Bitmap, Font, "font"); } diff --git a/macos/mkxp-z.xcodeproj/project.pbxproj b/macos/mkxp-z.xcodeproj/project.pbxproj index e74c81fe..fc111a9f 100644 --- a/macos/mkxp-z.xcodeproj/project.pbxproj +++ b/macos/mkxp-z.xcodeproj/project.pbxproj @@ -286,6 +286,14 @@ 3B97F77625E6182100A569B5 /* libSDL2_sound.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B97F77525E6182100A569B5 /* libSDL2_sound.a */; }; 3B97F77725E6182100A569B5 /* libSDL2_sound.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B97F77525E6182100A569B5 /* libSDL2_sound.a */; }; 3BA08E9B256638C900449CFF /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BD2B46925651C1B003DAD8A /* AudioToolbox.framework */; }; + 3BA69454263DAB53004194EB /* libnsgif.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944E263DAB53004194EB /* libnsgif.c */; }; + 3BA69455263DAB53004194EB /* libnsgif.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944E263DAB53004194EB /* libnsgif.c */; }; + 3BA69456263DAB53004194EB /* libnsgif.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944E263DAB53004194EB /* libnsgif.c */; }; + 3BA69457263DAB53004194EB /* libnsgif.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944E263DAB53004194EB /* libnsgif.c */; }; + 3BA69458263DAB53004194EB /* lzw.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944F263DAB53004194EB /* lzw.c */; }; + 3BA69459263DAB53004194EB /* lzw.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944F263DAB53004194EB /* lzw.c */; }; + 3BA6945A263DAB53004194EB /* lzw.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944F263DAB53004194EB /* lzw.c */; }; + 3BA6945B263DAB53004194EB /* lzw.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BA6944F263DAB53004194EB /* lzw.c */; }; 3BC65CCD2584EDC60063AFF1 /* tilemapvx.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3B10ED7D2568E95D00372D13 /* tilemapvx.cpp */; }; 3BC65CCF2584EDC60063AFF1 /* rgssad.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3B10ED382568E95D00372D13 /* rgssad.cpp */; }; 3BC65CD02584EDC60063AFF1 /* input.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3B10ED462568E95D00372D13 /* input.cpp */; }; @@ -972,6 +980,11 @@ 3B97F77525E6182100A569B5 /* libSDL2_sound.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libSDL2_sound.a; path = "Dependencies/build-macosx-x86_64/lib/libSDL2_sound.a"; sourceTree = ""; }; 3BA08EA4256641ED00449CFF /* Assets.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Assets.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 3BA08EA6256641EE00449CFF /* Assets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Assets.plist; sourceTree = ""; }; + 3BA6944E263DAB53004194EB /* libnsgif.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = libnsgif.c; sourceTree = ""; }; + 3BA6944F263DAB53004194EB /* lzw.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = lzw.c; sourceTree = ""; }; + 3BA69451263DAB53004194EB /* log.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = log.h; sourceTree = ""; }; + 3BA69452263DAB53004194EB /* lzw.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lzw.h; sourceTree = ""; }; + 3BA69453263DAB53004194EB /* libnsgif.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = libnsgif.h; sourceTree = ""; }; 3BC65D442584EDC60063AFF1 /* Z.arm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Z.arm.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3BC65D4A2584EED10063AFF1 /* libvorbis.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvorbis.a; path = "Dependencies/build-macosx-arm64/lib/libvorbis.a"; sourceTree = ""; }; 3BC65D4B2584EED10063AFF1 /* libpixman-1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libpixman-1.a"; path = "Dependencies/build-macosx-arm64/lib/libpixman-1.a"; sourceTree = ""; }; @@ -1407,6 +1420,7 @@ 3B10ED6F2568E95D00372D13 /* display */ = { isa = PBXGroup; children = ( + 3BA6944D263DAB53004194EB /* libnsgif */, 3B10ED7E2568E95D00372D13 /* gl */, 3B10ED702568E95D00372D13 /* tilemap.h */, 3B10ED712568E95D00372D13 /* tilemap-common.h */, @@ -1605,6 +1619,26 @@ path = misc; sourceTree = ""; }; + 3BA6944D263DAB53004194EB /* libnsgif */ = { + isa = PBXGroup; + children = ( + 3BA6944E263DAB53004194EB /* libnsgif.c */, + 3BA6944F263DAB53004194EB /* lzw.c */, + 3BA69450263DAB53004194EB /* utils */, + 3BA69452263DAB53004194EB /* lzw.h */, + 3BA69453263DAB53004194EB /* libnsgif.h */, + ); + path = libnsgif; + sourceTree = ""; + }; + 3BA69450263DAB53004194EB /* utils */ = { + isa = PBXGroup; + children = ( + 3BA69451263DAB53004194EB /* log.h */, + ); + path = utils; + sourceTree = ""; + }; 3BC65D492584EE690063AFF1 /* ARM64 */ = { isa = PBXGroup; children = ( @@ -2041,6 +2075,7 @@ 3B1C239425A19C600075EF5D /* etc.cpp in Sources */, 3B1C239525A19C600075EF5D /* shader.cpp in Sources */, 3B1C239625A19C600075EF5D /* tilemap.cpp in Sources */, + 3BA6945B263DAB53004194EB /* lzw.c in Sources */, 3B1C239825A19C600075EF5D /* window.cpp in Sources */, 3B1C239A25A19C600075EF5D /* input-binding.cpp in Sources */, 3B1C239B25A19C600075EF5D /* keybindings.cpp in Sources */, @@ -2054,6 +2089,7 @@ 3B1C23A525A19C600075EF5D /* tilemapvx-binding.cpp in Sources */, 3B1C23A625A19C600075EF5D /* window-binding.cpp in Sources */, 3B1C23A725A19C600075EF5D /* midisource.cpp in Sources */, + 3BA69457263DAB53004194EB /* libnsgif.c in Sources */, 3B1C23A825A19C600075EF5D /* graphics-binding.cpp in Sources */, 3B1C23A925A19C600075EF5D /* plane.cpp in Sources */, 3B1C23AA25A19C600075EF5D /* tilequad.cpp in Sources */, @@ -2093,6 +2129,7 @@ 3B522DDE259C1E53003301C4 /* http-binding.cpp in Sources */, 3BC65CCD2584EDC60063AFF1 /* tilemapvx.cpp in Sources */, 3BC65CCF2584EDC60063AFF1 /* rgssad.cpp in Sources */, + 3BA6945A263DAB53004194EB /* lzw.c in Sources */, 3BC65CD02584EDC60063AFF1 /* input.cpp in Sources */, 3BC65CD12584EDC60063AFF1 /* tilemap-binding.cpp in Sources */, 3BC65CD22584EDC60063AFF1 /* audio.cpp in Sources */, @@ -2105,6 +2142,7 @@ 3BC65CD82584EDC60063AFF1 /* bitmap-binding.cpp in Sources */, 3BC65CD92584EDC60063AFF1 /* vorbissource.cpp in Sources */, 3BC65CDB2584EDC60063AFF1 /* filesystem-binding.cpp in Sources */, + 3BA69456263DAB53004194EB /* libnsgif.c in Sources */, 3BC65CDD2584EDC60063AFF1 /* glstate.cpp in Sources */, 3BC65CDE2584EDC60063AFF1 /* gl-fun.cpp in Sources */, 3BC65CDF2584EDC60063AFF1 /* sprite-binding.cpp in Sources */, @@ -2169,6 +2207,7 @@ 3B522DDC259C1E53003301C4 /* http-binding.cpp in Sources */, 3BC65D8E2584F3AD0063AFF1 /* tilemapvx.cpp in Sources */, 3BC65D902584F3AD0063AFF1 /* rgssad.cpp in Sources */, + 3BA69458263DAB53004194EB /* lzw.c in Sources */, 3BC65D912584F3AD0063AFF1 /* input.cpp in Sources */, 3BC65D922584F3AD0063AFF1 /* tilemap-binding.cpp in Sources */, 3BC65D932584F3AD0063AFF1 /* audio.cpp in Sources */, @@ -2181,6 +2220,7 @@ 3BC65D992584F3AD0063AFF1 /* bitmap-binding.cpp in Sources */, 3BC65D9A2584F3AD0063AFF1 /* vorbissource.cpp in Sources */, 3BC65D9C2584F3AD0063AFF1 /* filesystem-binding.cpp in Sources */, + 3BA69454263DAB53004194EB /* libnsgif.c in Sources */, 3BC65D9E2584F3AD0063AFF1 /* glstate.cpp in Sources */, 3BC65D9F2584F3AD0063AFF1 /* gl-fun.cpp in Sources */, 3BC65DA02584F3AD0063AFF1 /* sprite-binding.cpp in Sources */, @@ -2245,6 +2285,7 @@ 3B522DDD259C1E53003301C4 /* http-binding.cpp in Sources */, 3B10EDC22568E95E00372D13 /* tilemapvx.cpp in Sources */, 3B10EDA72568E95E00372D13 /* rgssad.cpp in Sources */, + 3BA69459263DAB53004194EB /* lzw.c in Sources */, 3B10EDA82568E95E00372D13 /* input.cpp in Sources */, 3B10EE022568E96A00372D13 /* tilemap-binding.cpp in Sources */, 3B10EDB72568E95E00372D13 /* audio.cpp in Sources */, @@ -2257,6 +2298,7 @@ 3B10EDFF2568E96A00372D13 /* bitmap-binding.cpp in Sources */, 3B10EDBA2568E95E00372D13 /* vorbissource.cpp in Sources */, 3B10EDF62568E96A00372D13 /* filesystem-binding.cpp in Sources */, + 3BA69455263DAB53004194EB /* libnsgif.c in Sources */, 3B10EDC92568E95E00372D13 /* glstate.cpp in Sources */, 3B10EDCC2568E95E00372D13 /* gl-fun.cpp in Sources */, 3B10EDFB2568E96A00372D13 /* sprite-binding.cpp in Sources */, diff --git a/src/display/bitmap.cpp b/src/display/bitmap.cpp index abce1656..b830702c 100644 --- a/src/display/bitmap.cpp +++ b/src/display/bitmap.cpp @@ -43,14 +43,40 @@ #include "filesystem.h" #include "font.h" #include "eventthread.h" +#include "graphics.h" +#include "system.h" +#include "util/util.h" + +#include "debugwriter.h" + +#include +#include + +extern "C" { +#include "libnsgif/libnsgif.h" +} #define GUARD_MEGA \ { \ if (p->megaSurface) \ throw Exception(Exception::MKXPError, \ - "Operation not supported for mega surfaces"); \ + "Operation not supported for mega surfaces / animated bitmaps"); \ } +#define GUARD_ANIMATED \ +{ \ + if (p->animation.enabled) \ + throw Exception(Exception::MKXPError, \ + "Operation not supported for animated bitmaps"); \ +} + +#define GUARD_UNANIMATED \ +{ \ + if (!p->animation.enabled) \ + throw Exception(Exception::MKXPError, \ + "Operation not supported for static bitmaps"); \ +} + #define OUTLINE_SIZE 1 /* Normalize (= ensure width and @@ -74,10 +100,97 @@ static IntRect normalizedRect(const IntRect &rect) return norm; } + +// libnsgif loading callbacks, taken pretty much straight from their tests + +static void *gif_bitmap_create(int width, int height) +{ + /* ensure a stupidly large bitmap is not created */ + return calloc(width * height, 4); +} + + +static void gif_bitmap_set_opaque(void *bitmap, bool opaque) +{ + (void) opaque; /* unused */ + (void) bitmap; /* unused */ + assert(bitmap); +} + + +static bool gif_bitmap_test_opaque(void *bitmap) +{ + (void) bitmap; /* unused */ + assert(bitmap); + return false; +} + + +static unsigned char *gif_bitmap_get_buffer(void *bitmap) +{ + assert(bitmap); + return (unsigned char *)bitmap; +} + + +static void gif_bitmap_destroy(void *bitmap) +{ + assert(bitmap); + free(bitmap); +} + + +static void gif_bitmap_modified(void *bitmap) +{ + (void) bitmap; /* unused */ + assert(bitmap); + return; +} + +// -------------------- + struct BitmapPrivate { Bitmap *self; + struct { + int width; + int height; + + bool enabled; + bool playing; + bool loop; + std::vector frames; + float fps; + int lastFrame; + unsigned long long startTime; + + inline int currentFrameI() { + if (!playing) return lastFrame; + int i = lastFrame + ((shState->runTime() - startTime) / ((1 / fps) * 1000000)); + int r = (loop) ? fmod(i, frames.size()) : (i > frames.size() - 1) ? frames.size() - 1 : i; + return r; + } + + TEXFBO ¤tFrame() { + return frames[currentFrameI()]; + } + + void play() { + playing = true; + startTime = shState->runTime(); + } + + void stop() { + lastFrame = currentFrameI(); + playing = false; + } + + void seek(int frame) { + lastFrame = clamp(frame, 0, (int)frames.size()); + } + } animation; + TEXFBO gl; Font *font; @@ -108,6 +221,15 @@ struct BitmapPrivate surface(0) { format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888); + + animation.width = -1; + animation.height = -1; + animation.enabled = false; + animation.playing = false; + animation.loop = true; + animation.startTime = 0; + animation.fps = -1; + animation.lastFrame = 0; font = &shState->defaultFont(); pixman_region_init(&tainted); @@ -168,13 +290,19 @@ struct BitmapPrivate void bindTexture(ShaderBase &shader) { + if (animation.enabled) { + TEXFBO cframe = animation.currentFrame(); + TEX::bind(cframe.tex); + shader.setTexSize(Vec2i(cframe.width, cframe.height)); + return; + } TEX::bind(gl.tex); shader.setTexSize(Vec2i(gl.width, gl.height)); } void bindFBO() { - FBO::bind(gl.fbo); + FBO::bind((animation.enabled) ? animation.currentFrame().fbo : gl.fbo); } void pushSetViewport(ShaderBase &shader) const @@ -235,16 +363,90 @@ struct BitmapPrivate struct BitmapOpenHandler : FileSystem::OpenHandler { - SDL_Surface *surf; + std::vector surfaces; + float animation_rate; + + // Filled if errors from GIF reading are needed + std::string error; BitmapOpenHandler() - : surf(0) + : animation_rate(-1) {} bool tryRead(SDL_RWops &ops, const char *ext) { - surf = IMG_LoadTyped_RW(&ops, 1, ext); - return surf != 0; + if (IMG_isGIF(&ops)) { + // Use libnsgif to initialise the gif data + gif_animation gif {}; + + gif_bitmap_callback_vt gif_bitmap_callbacks = { + gif_bitmap_create, + gif_bitmap_destroy, + gif_bitmap_get_buffer, + gif_bitmap_set_opaque, + gif_bitmap_test_opaque, + gif_bitmap_modified + }; + + gif_create(&gif, &gif_bitmap_callbacks); + + size_t data_size = ops.size(&ops); + + auto data = new unsigned char[data_size]; + ops.seek(&ops, 0, RW_SEEK_SET); + ops.read(&ops, data, data_size, 1); + + int status; + do { + status = gif_initialise(&gif, data_size, data); + if (status != GIF_OK && status != GIF_WORKING) { + gif_finalise(&gif); + delete data; + error = "Failed to initialize GIF (Error " + std::to_string(status) + ")"; + return false; + } + } while (status != GIF_OK); + + int image_width = -1; + int image_height = -1; + + // Read every frame + for (int i = 0; i < gif.frame_count; i++) { + int status = gif_decode_frame(&gif, i); + if (status != GIF_OK && status != GIF_WORKING) { + error = "Failed to read GIF frame " + std::to_string(i + 1) + " (Error " + std::to_string(status) + ")"; + for (SDL_Surface *s : surfaces) + SDL_FreeSurface(s); + break; + } + + if (image_width == -1 || !image_height == -1) { + image_width = gif.width; + image_height = gif.height; + } + else if (gif.width != image_width || gif.height != image_height) { + error = "Failed to read GIF (Varying frame size)"; + for (SDL_Surface *s : surfaces) + SDL_FreeSurface(s); + break; + } + + if (animation_rate == -1 && gif.frames[gif.decoded_frame].frame_delay) { + animation_rate = 1 / ((float)gif.frames[gif.decoded_frame].frame_delay / 100); + } + + SDL_Surface *s = SDL_CreateRGBSurfaceWithFormat(0, gif.width, gif.height, 32, SDL_PIXELFORMAT_ABGR8888); + SDL_SetSurfaceBlendMode(s, SDL_BLENDMODE_NONE); + memcpy(s->pixels, gif.frame_image, gif.width * gif.height * 4); + surfaces.push_back(s); + } + + gif_finalise(&gif); + delete data; + } else { + surfaces.push_back(IMG_LoadTyped_RW(&ops, 1, ext)); + } + return (surfaces.size() > 0 && error.empty()); } }; @@ -252,11 +454,59 @@ Bitmap::Bitmap(const char *filename) { BitmapOpenHandler handler; shState->fileSystem().openRead(handler, filename); - SDL_Surface *imgSurf = handler.surf; + + if (!handler.error.empty()) { + // Not loaded with SDL, but I want it to be caught with the same exception type + throw Exception(Exception::SDLError, "Error loading image '%s': %s", filename, handler.error.c_str()); + } + else if (handler.surfaces.size() < 1) { + throw Exception(Exception::SDLError, "Error loading image '%s': %s", + filename, SDL_GetError()); + } + + if (handler.surfaces.size() > 1) { + p = new BitmapPrivate(this); + p->animation.enabled = true; + p->animation.width = handler.surfaces[0]->w; + p->animation.height = handler.surfaces[0]->h; + + if (p->animation.width >= glState.caps.maxTexSize || p->animation.height > glState.caps.maxTexSize) + { + throw new Exception(Exception::MKXPError, "Animation too large (%ix%i, max %ix%i)", + p->animation.width, p->animation.height, glState.caps.maxTexSize, glState.caps.maxTexSize); + } + + p->animation.fps = (handler.animation_rate == -1) ? shState->graphics().getFrameRate() : handler.animation_rate; + + for (SDL_Surface* s : handler.surfaces) + { + TEXFBO texfbo; + try { + texfbo = shState->texPool().request(p->animation.width, p->animation.height); + } + catch (const Exception &e) + { + for (SDL_Surface *s : handler.surfaces) + SDL_FreeSurface(s); + + throw e; + } + TEX::bind(texfbo.tex); + TEX::uploadImage(p->animation.width, p->animation.height, s->pixels, GL_RGBA); + + p->animation.frames.push_back(texfbo); + } + + for (SDL_Surface *s : handler.surfaces) + SDL_FreeSurface(s); + + p->addTaintedArea(rect()); + p->animation.play(); + return; + } - if (!imgSurf) - throw Exception(Exception::SDLError, "Error loading image '%s': %s", - filename, SDL_GetError()); + SDL_Surface *imgSurf = handler.surfaces[0]; + p->ensureFormat(imgSurf, SDL_PIXELFORMAT_ABGR8888); @@ -356,6 +606,7 @@ Bitmap::Bitmap(void *pixeldata, int width, int height) Bitmap::Bitmap(const Bitmap &other) { other.ensureNonMega(); + other.ensureNonAnimated(); p = new BitmapPrivate(this); @@ -375,6 +626,9 @@ int Bitmap::width() const if (p->megaSurface) return p->megaSurface->w; + + if (p->animation.enabled) + return p->animation.width; return p->gl.width; } @@ -385,6 +639,9 @@ int Bitmap::height() const if (p->megaSurface) return p->megaSurface->h; + + if (p->animation.enabled) + return p->animation.height; return p->gl.height; } @@ -392,7 +649,13 @@ int Bitmap::height() const bool Bitmap::isMega() const{ guardDisposed(); - return p->megaSurface; + return p->megaSurface || p->animation.enabled; +} + +bool Bitmap::isAnimated() const { + guardDisposed(); + + return p->animation.enabled; } IntRect Bitmap::rect() const @@ -431,7 +694,10 @@ void Bitmap::stretchBlt(const IntRect &destRect, { guardDisposed(); - GUARD_MEGA; + // Don't need this, right? This function is fine with megasurfaces it seems + //GUARD_MEGA; + + GUARD_ANIMATED; if (source.isDisposed()) return; @@ -581,6 +847,7 @@ void Bitmap::fillRect(const IntRect &rect, const Vec4 &color) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; p->fillRect(rect, color); @@ -609,6 +876,7 @@ void Bitmap::gradientFillRect(const IntRect &rect, guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; SimpleColorShader &shader = shState->shaders().simpleColor; shader.bind(); @@ -655,6 +923,7 @@ void Bitmap::clearRect(const IntRect &rect) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; p->fillRect(rect, Vec4()); @@ -666,6 +935,7 @@ void Bitmap::blur() guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; Quad &quad = shState->gpQuad(); FloatRect rect(0, 0, width(), height()); @@ -711,6 +981,7 @@ void Bitmap::radialBlur(int angle, int divisions) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; angle = clamp(angle, 0, 359); divisions = clamp(divisions, 2, 100); @@ -806,6 +1077,7 @@ void Bitmap::clear() guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; p->bindFBO(); @@ -833,6 +1105,7 @@ Color Bitmap::getPixel(int x, int y) const guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; if (x < 0 || y < 0 || x >= width() || y >= height()) return Vec4(); @@ -863,6 +1136,7 @@ void Bitmap::setPixel(int x, int y, const Color &color) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; uint8_t pixel[] = { @@ -896,6 +1170,7 @@ bool Bitmap::getRaw(void *output, int output_size) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; FBO::bind(p->gl.fbo); glReadPixels(0,0,width(),height(),GL_RGBA,GL_UNSIGNED_BYTE,output); @@ -910,6 +1185,7 @@ void Bitmap::replaceRaw(void *pixel_data, int size) if (size != w*h*4) return; GUARD_MEGA; + GUARD_ANIMATED; TEX::bind(p->gl.tex); TEX::uploadImage(w, h, pixel_data, GL_RGBA); @@ -923,6 +1199,7 @@ void Bitmap::saveToFile(const char *filename) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; SDL_Surface *surf = SDL_CreateRGBSurface(0, width(), height(),p->format->BitsPerPixel, p->format->Rmask,p->format->Gmask,p->format->Bmask,p->format->Amask); @@ -972,6 +1249,7 @@ void Bitmap::hueChange(int hue) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; if ((hue % 360) == 0) return; @@ -1119,6 +1397,7 @@ void Bitmap::drawText(const IntRect &rect, const char *str, int align) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; std::string fixed = fixupString(str); str = fixed.c_str(); @@ -1380,6 +1659,7 @@ IntRect Bitmap::textSize(const char *str) guardDisposed(); GUARD_MEGA; + GUARD_ANIMATED; TTF_Font *font = p->font->getSdlFont(); @@ -1415,7 +1695,7 @@ void Bitmap::setInitFont(Font *value) TEXFBO &Bitmap::getGLTypes() { - return p->gl; + return (p->animation.enabled) ? p->animation.currentFrame() : p->gl; } SDL_Surface *Bitmap::megaSurface() const @@ -1431,6 +1711,94 @@ void Bitmap::ensureNonMega() const GUARD_MEGA; } +void Bitmap::ensureNonAnimated() const +{ + if (isDisposed()) + return; + + GUARD_ANIMATED; +} + +void Bitmap::stop() +{ + GUARD_UNANIMATED; + if (!p->animation.playing) return; + + p->animation.stop(); +} + +void Bitmap::play() +{ + GUARD_UNANIMATED; + if (p->animation.playing) return; + p->animation.play(); +} + +bool Bitmap::isPlaying() +{ + GUARD_UNANIMATED; + return (p->animation.playing); +} + +void Bitmap::gotoAndStop(int frame) +{ + GUARD_UNANIMATED; + + p->animation.stop(); + p->animation.seek(frame); +} +void Bitmap::gotoAndPlay(int frame) +{ + GUARD_UNANIMATED; + + p->animation.stop(); + p->animation.seek(frame); + p->animation.play(); +} + +int Bitmap::numFrames() +{ + GUARD_UNANIMATED; + return p->animation.frames.size(); +} + +int Bitmap::currentFrameI() const +{ + GUARD_UNANIMATED; + return p->animation.currentFrameI(); +} + +void Bitmap::setAnimationFPS(float FPS) +{ + GUARD_UNANIMATED; + + bool restart = p->animation.playing; + p->animation.stop(); + p->animation.fps = FPS; + if (restart) p->animation.play(); +} + +float Bitmap::getAnimationFPS() +{ + GUARD_UNANIMATED; + + return p->animation.fps; +} + +void Bitmap::setLooping(bool loop) +{ + GUARD_UNANIMATED; + + p->animation.loop = loop; +} + +bool Bitmap::getLooping() +{ + GUARD_UNANIMATED; + + return p->animation.loop; +} + void Bitmap::bindTex(ShaderBase &shader) { p->bindTexture(shader); @@ -1449,6 +1817,10 @@ void Bitmap::releaseResources() { if (p->megaSurface) SDL_FreeSurface(p->megaSurface); + else if (p->animation.enabled) { + for (TEXFBO &tex : p->animation.frames) + shState->texPool().release(tex); + } else shState->texPool().release(p->gl); diff --git a/src/display/bitmap.h b/src/display/bitmap.h index 9c2ffea4..9b35c02d 100644 --- a/src/display/bitmap.h +++ b/src/display/bitmap.h @@ -48,6 +48,7 @@ public: int width() const; int height() const; bool isMega() const; + bool isAnimated() const; IntRect rect() const; @@ -116,6 +117,22 @@ public: TEXFBO &getGLTypes(); SDL_Surface *megaSurface() const; void ensureNonMega() const; + void ensureNonAnimated() const; + + // GIF functions + void stop(); + void play(); + bool isPlaying(); + void gotoAndStop(int frame); + void gotoAndPlay(int frame); + int numFrames(); + int currentFrameI() const; + + void setAnimationFPS(float FPS); + float getAnimationFPS(); + + void setLooping(bool loop); + bool getLooping(); /* Binds the backing texture and sets the correct * texture size uniform in shader */ diff --git a/src/display/libnsgif/libnsgif.c b/src/display/libnsgif/libnsgif.c new file mode 100644 index 00000000..479d161e --- /dev/null +++ b/src/display/libnsgif/libnsgif.c @@ -0,0 +1,1228 @@ +/* + * Copyright 2004 Richard Wilson + * Copyright 2008 Sean Fox + * + * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +#include +#include +#include +#include +#include +#include +#include "libnsgif.h" +#include "utils/log.h" + +#include "lzw.h" + +/** + * + * \file + * \brief GIF image decoder + * + * The GIF format is thoroughly documented; a full description can be found at + * http://www.w3.org/Graphics/GIF/spec-gif89a.txt + * + * \todo Plain text and comment extensions should be implemented. + */ + + +/** Maximum colour table size */ +#define GIF_MAX_COLOURS 256 + +/** Internal flag that the colour table needs to be processed */ +#define GIF_PROCESS_COLOURS 0xaa000000 + +/** Internal flag that a frame is invalid/unprocessed */ +#define GIF_INVALID_FRAME -1 + +/** Transparent colour */ +#define GIF_TRANSPARENT_COLOUR 0x00 + +/* GIF Flags */ +#define GIF_FRAME_COMBINE 1 +#define GIF_FRAME_CLEAR 2 +#define GIF_FRAME_RESTORE 3 +#define GIF_FRAME_QUIRKS_RESTORE 4 + +#define GIF_IMAGE_SEPARATOR 0x2c +#define GIF_INTERLACE_MASK 0x40 +#define GIF_COLOUR_TABLE_MASK 0x80 +#define GIF_COLOUR_TABLE_SIZE_MASK 0x07 +#define GIF_EXTENSION_INTRODUCER 0x21 +#define GIF_EXTENSION_GRAPHIC_CONTROL 0xf9 +#define GIF_DISPOSAL_MASK 0x1c +#define GIF_TRANSPARENCY_MASK 0x01 +#define GIF_EXTENSION_COMMENT 0xfe +#define GIF_EXTENSION_PLAIN_TEXT 0x01 +#define GIF_EXTENSION_APPLICATION 0xff +#define GIF_BLOCK_TERMINATOR 0x00 +#define GIF_TRAILER 0x3b + +/** standard GIF header size */ +#define GIF_STANDARD_HEADER_SIZE 13 + + +/** + * Updates the sprite memory size + * + * \param gif The animation context + * \param width The width of the sprite + * \param height The height of the sprite + * \return GIF_INSUFFICIENT_MEMORY for a memory error GIF_OK for success + */ +static gif_result +gif_initialise_sprite(gif_animation *gif, + unsigned int width, + unsigned int height) +{ + unsigned int max_width; + unsigned int max_height; + struct bitmap *buffer; + + /* Check if we've changed */ + if ((width <= gif->width) && (height <= gif->height)) { + return GIF_OK; + } + + /* Get our maximum values */ + max_width = (width > gif->width) ? width : gif->width; + max_height = (height > gif->height) ? height : gif->height; + + /* Allocate some more memory */ + assert(gif->bitmap_callbacks.bitmap_create); + buffer = gif->bitmap_callbacks.bitmap_create(max_width, max_height); + if (buffer == NULL) { + return GIF_INSUFFICIENT_MEMORY; + } + + assert(gif->bitmap_callbacks.bitmap_destroy); + gif->bitmap_callbacks.bitmap_destroy(gif->frame_image); + gif->frame_image = buffer; + gif->width = max_width; + gif->height = max_height; + + /* Invalidate our currently decoded image */ + gif->decoded_frame = GIF_INVALID_FRAME; + return GIF_OK; +} + + +/** + * Attempts to initialise the frame's extensions + * + * \param gif The animation context + * \param frame The frame number + * @return GIF_INSUFFICIENT_FRAME_DATA for insufficient data to complete the + * frame GIF_OK for successful initialisation. + */ +static gif_result +gif_initialise_frame_extensions(gif_animation *gif, const int frame) +{ + unsigned char *gif_data, *gif_end; + int gif_bytes; + unsigned int block_size; + + /* Get our buffer position etc. */ + gif_data = (unsigned char *)(gif->gif_data + gif->buffer_position); + gif_end = (unsigned char *)(gif->gif_data + gif->buffer_size); + + /* Initialise the extensions */ + while (gif_data < gif_end && gif_data[0] == GIF_EXTENSION_INTRODUCER) { + ++gif_data; + if ((gif_bytes = (gif_end - gif_data)) < 1) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + /* Switch on extension label */ + switch (gif_data[0]) { + case GIF_EXTENSION_GRAPHIC_CONTROL: + /* 6-byte Graphic Control Extension is: + * + * +0 CHAR Graphic Control Label + * +1 CHAR Block Size + * +2 CHAR __Packed Fields__ + * 3BITS Reserved + * 3BITS Disposal Method + * 1BIT User Input Flag + * 1BIT Transparent Color Flag + * +3 SHORT Delay Time + * +5 CHAR Transparent Color Index + */ + if (gif_bytes < 6) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + gif->frames[frame].frame_delay = gif_data[3] | (gif_data[4] << 8); + if (gif_data[2] & GIF_TRANSPARENCY_MASK) { + gif->frames[frame].transparency = true; + gif->frames[frame].transparency_index = gif_data[5]; + } + gif->frames[frame].disposal_method = ((gif_data[2] & GIF_DISPOSAL_MASK) >> 2); + /* I have encountered documentation and GIFs in the + * wild that use 0x04 to restore the previous frame, + * rather than the officially documented 0x03. I + * believe some (older?) software may even actually + * export this way. We handle this as a type of + * "quirks" mode. + */ + if (gif->frames[frame].disposal_method == GIF_FRAME_QUIRKS_RESTORE) { + gif->frames[frame].disposal_method = GIF_FRAME_RESTORE; + } + gif_data += (2 + gif_data[1]); + break; + + case GIF_EXTENSION_APPLICATION: + /* 14-byte+ Application Extension is: + * + * +0 CHAR Application Extension Label + * +1 CHAR Block Size + * +2 8CHARS Application Identifier + * +10 3CHARS Appl. Authentication Code + * +13 1-256 Application Data (Data sub-blocks) + */ + if (gif_bytes < 17) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + if ((gif_data[1] == 0x0b) && + (strncmp((const char *) gif_data + 2, + "NETSCAPE2.0", 11) == 0) && + (gif_data[13] == 0x03) && + (gif_data[14] == 0x01)) { + gif->loop_count = gif_data[15] | (gif_data[16] << 8); + } + gif_data += (2 + gif_data[1]); + break; + + case GIF_EXTENSION_COMMENT: + /* Move the pointer to the first data sub-block Skip 1 + * byte for the extension label + */ + ++gif_data; + break; + + default: + /* Move the pointer to the first data sub-block Skip 2 + * bytes for the extension label and size fields Skip + * the extension size itself + */ + if (gif_bytes < 2) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + gif_data += (2 + gif_data[1]); + } + + /* Repeatedly skip blocks until we get a zero block or run out + * of data This data is ignored by this gif decoder + */ + gif_bytes = (gif_end - gif_data); + block_size = 0; + while (gif_data < gif_end && gif_data[0] != GIF_BLOCK_TERMINATOR) { + block_size = gif_data[0] + 1; + if ((gif_bytes -= block_size) < 0) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + gif_data += block_size; + } + ++gif_data; + } + + /* Set buffer position and return */ + gif->buffer_position = (gif_data - gif->gif_data); + return GIF_OK; +} + + +/** + * Attempts to initialise the next frame + * + * \param gif The animation context + * \return error code + * - GIF_INSUFFICIENT_DATA for insufficient data to do anything + * - GIF_FRAME_DATA_ERROR for GIF frame data error + * - GIF_INSUFFICIENT_MEMORY for insufficient memory to process + * - GIF_INSUFFICIENT_FRAME_DATA for insufficient data to complete the frame + * - GIF_DATA_ERROR for GIF error (invalid frame header) + * - GIF_OK for successful decoding + * - GIF_WORKING for successful decoding if more frames are expected +*/ +static gif_result gif_initialise_frame(gif_animation *gif) +{ + int frame; + gif_frame *temp_buf; + + unsigned char *gif_data, *gif_end; + int gif_bytes; + unsigned int flags = 0; + unsigned int width, height, offset_x, offset_y; + unsigned int block_size, colour_table_size; + bool first_image = true; + gif_result return_value; + + /* Get the frame to decode and our data position */ + frame = gif->frame_count; + + /* Get our buffer position etc. */ + gif_data = (unsigned char *)(gif->gif_data + gif->buffer_position); + gif_end = (unsigned char *)(gif->gif_data + gif->buffer_size); + gif_bytes = (gif_end - gif_data); + + /* Check if we've finished */ + if ((gif_bytes > 0) && (gif_data[0] == GIF_TRAILER)) { + return GIF_OK; + } + + /* Check if there is enough data remaining. The shortest block of data + * is a 4-byte comment extension + 1-byte block terminator + 1-byte gif + * trailer + */ + if (gif_bytes < 6) { + return GIF_INSUFFICIENT_DATA; + } + + /* We could theoretically get some junk data that gives us millions of + * frames, so we ensure that we don't have a silly number + */ + if (frame > 4096) { + return GIF_FRAME_DATA_ERROR; + } + + /* Get some memory to store our pointers in etc. */ + if ((int)gif->frame_holders <= frame) { + /* Allocate more memory */ + temp_buf = (gif_frame *)realloc(gif->frames, (frame + 1) * sizeof(gif_frame)); + if (temp_buf == NULL) { + return GIF_INSUFFICIENT_MEMORY; + } + gif->frames = temp_buf; + gif->frame_holders = frame + 1; + } + + /* Store our frame pointer. We would do it when allocating except we + * start off with one frame allocated so we can always use realloc. + */ + gif->frames[frame].frame_pointer = gif->buffer_position; + gif->frames[frame].display = false; + gif->frames[frame].virgin = true; + gif->frames[frame].disposal_method = 0; + gif->frames[frame].transparency = false; + gif->frames[frame].frame_delay = 100; + gif->frames[frame].redraw_required = false; + + /* Invalidate any previous decoding we have of this frame */ + if (gif->decoded_frame == frame) { + gif->decoded_frame = GIF_INVALID_FRAME; + } + + /* We pretend to initialise the frames, but really we just skip over + * all the data contained within. This is all basically a cut down + * version of gif_decode_frame that doesn't have any of the LZW bits in + * it. + */ + + /* Initialise any extensions */ + gif->buffer_position = gif_data - gif->gif_data; + return_value = gif_initialise_frame_extensions(gif, frame); + if (return_value != GIF_OK) { + return return_value; + } + gif_data = (gif->gif_data + gif->buffer_position); + gif_bytes = (gif_end - gif_data); + + /* Check if we've finished */ + if ((gif_bytes = (gif_end - gif_data)) < 1) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + if (gif_data[0] == GIF_TRAILER) { + gif->buffer_position = (gif_data - gif->gif_data); + gif->frame_count = frame + 1; + return GIF_OK; + } + + /* If we're not done, there should be an image descriptor */ + if (gif_data[0] != GIF_IMAGE_SEPARATOR) { + return GIF_FRAME_DATA_ERROR; + } + + /* Do some simple boundary checking */ + if (gif_bytes < 10) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + offset_x = gif_data[1] | (gif_data[2] << 8); + offset_y = gif_data[3] | (gif_data[4] << 8); + width = gif_data[5] | (gif_data[6] << 8); + height = gif_data[7] | (gif_data[8] << 8); + + /* Set up the redraw characteristics. We have to check for extending + * the area due to multi-image frames. + */ + if (!first_image) { + if (gif->frames[frame].redraw_x > offset_x) { + gif->frames[frame].redraw_width += (gif->frames[frame].redraw_x - offset_x); + gif->frames[frame].redraw_x = offset_x; + } + + if (gif->frames[frame].redraw_y > offset_y) { + gif->frames[frame].redraw_height += (gif->frames[frame].redraw_y - offset_y); + gif->frames[frame].redraw_y = offset_y; + } + + if ((offset_x + width) > (gif->frames[frame].redraw_x + gif->frames[frame].redraw_width)) { + gif->frames[frame].redraw_width = (offset_x + width) - gif->frames[frame].redraw_x; + } + + if ((offset_y + height) > (gif->frames[frame].redraw_y + gif->frames[frame].redraw_height)) { + gif->frames[frame].redraw_height = (offset_y + height) - gif->frames[frame].redraw_y; + } + } else { + first_image = false; + gif->frames[frame].redraw_x = offset_x; + gif->frames[frame].redraw_y = offset_y; + gif->frames[frame].redraw_width = width; + gif->frames[frame].redraw_height = height; + } + + /* if we are clearing the background then we need to redraw enough to + * cover the previous frame too + */ + gif->frames[frame].redraw_required = ((gif->frames[frame].disposal_method == GIF_FRAME_CLEAR) || + (gif->frames[frame].disposal_method == GIF_FRAME_RESTORE)); + + /* Boundary checking - shouldn't ever happen except with junk data */ + if (gif_initialise_sprite(gif, (offset_x + width), (offset_y + height))) { + return GIF_INSUFFICIENT_MEMORY; + } + + /* Decode the flags */ + flags = gif_data[9]; + colour_table_size = 2 << (flags & GIF_COLOUR_TABLE_SIZE_MASK); + + /* Move our data onwards and remember we've got a bit of this frame */ + gif_data += 10; + gif_bytes = (gif_end - gif_data); + gif->frame_count_partial = frame + 1; + + /* Skip the local colour table */ + if (flags & GIF_COLOUR_TABLE_MASK) { + gif_data += 3 * colour_table_size; + if ((gif_bytes = (gif_end - gif_data)) < 0) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + } + + /* Ensure we have a correct code size */ + if (gif_bytes < 1) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + if (gif_data[0] > LZW_CODE_MAX) { + return GIF_DATA_ERROR; + } + + /* Move our pointer to the actual image data */ + gif_data++; + --gif_bytes; + + /* Repeatedly skip blocks until we get a zero block or run out of data + * These blocks of image data are processed later by gif_decode_frame() + */ + block_size = 0; + while (block_size != 1) { + if (gif_bytes < 1) return GIF_INSUFFICIENT_FRAME_DATA; + block_size = gif_data[0] + 1; + /* Check if the frame data runs off the end of the file */ + if ((int)(gif_bytes - block_size) < 0) { + /* Try to recover by signaling the end of the gif. + * Once we get garbage data, there is no logical way to + * determine where the next frame is. It's probably + * better to partially load the gif than not at all. + */ + if (gif_bytes >= 2) { + gif_data[0] = 0; + gif_data[1] = GIF_TRAILER; + gif_bytes = 1; + ++gif_data; + break; + } else { + return GIF_INSUFFICIENT_FRAME_DATA; + } + } else { + gif_bytes -= block_size; + gif_data += block_size; + } + } + + /* Add the frame and set the display flag */ + gif->buffer_position = gif_data - gif->gif_data; + gif->frame_count = frame + 1; + gif->frames[frame].display = true; + + /* Check if we've finished */ + if (gif_bytes < 1) { + return GIF_INSUFFICIENT_FRAME_DATA; + } else { + if (gif_data[0] == GIF_TRAILER) { + return GIF_OK; + } + } + return GIF_WORKING; +} + + + + +/** + * Skips the frame's extensions (which have been previously initialised) + * + * \param gif The animation context + * \return GIF_INSUFFICIENT_FRAME_DATA for insufficient data to complete the + * frame GIF_OK for successful decoding + */ +static gif_result gif_skip_frame_extensions(gif_animation *gif) +{ + unsigned char *gif_data, *gif_end; + int gif_bytes; + unsigned int block_size; + + /* Get our buffer position etc. */ + gif_data = (unsigned char *)(gif->gif_data + gif->buffer_position); + gif_end = (unsigned char *)(gif->gif_data + gif->buffer_size); + gif_bytes = (gif_end - gif_data); + + /* Skip the extensions */ + while (gif_data < gif_end && gif_data[0] == GIF_EXTENSION_INTRODUCER) { + ++gif_data; + if (gif_data >= gif_end) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + /* Switch on extension label */ + switch(gif_data[0]) { + case GIF_EXTENSION_COMMENT: + /* Move the pointer to the first data sub-block + * 1 byte for the extension label + */ + ++gif_data; + break; + + default: + /* Move the pointer to the first data sub-block 2 bytes + * for the extension label and size fields Skip the + * extension size itself + */ + if (gif_data + 1 >= gif_end) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + gif_data += (2 + gif_data[1]); + } + + /* Repeatedly skip blocks until we get a zero block or run out + * of data This data is ignored by this gif decoder + */ + gif_bytes = (gif_end - gif_data); + block_size = 0; + while (gif_data < gif_end && gif_data[0] != GIF_BLOCK_TERMINATOR) { + block_size = gif_data[0] + 1; + if ((gif_bytes -= block_size) < 0) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + gif_data += block_size; + } + ++gif_data; + } + + /* Set buffer position and return */ + gif->buffer_position = (gif_data - gif->gif_data); + return GIF_OK; +} + +static unsigned int gif_interlaced_line(int height, int y) { + if ((y << 3) < height) { + return (y << 3); + } + y -= ((height + 7) >> 3); + if ((y << 3) < (height - 4)) { + return (y << 3) + 4; + } + y -= ((height + 3) >> 3); + if ((y << 2) < (height - 2)) { + return (y << 2) + 2; + } + y -= ((height + 1) >> 2); + return (y << 1) + 1; +} + + +static gif_result gif_error_from_lzw(lzw_result l_res) +{ + static const gif_result g_res[] = { + [LZW_OK] = GIF_OK, + [LZW_OK_EOD] = GIF_END_OF_FRAME, + [LZW_NO_MEM] = GIF_INSUFFICIENT_MEMORY, + [LZW_NO_DATA] = GIF_INSUFFICIENT_FRAME_DATA, + [LZW_EOI_CODE] = GIF_FRAME_DATA_ERROR, + [LZW_BAD_ICODE] = GIF_FRAME_DATA_ERROR, + [LZW_BAD_CODE] = GIF_FRAME_DATA_ERROR, + }; + return g_res[l_res]; +} + +static void gif__record_previous_frame(gif_animation *gif) +{ + bool need_alloc = gif->prev_frame == NULL; + const uint32_t *frame_data; + uint32_t *prev_frame; + + if (gif->decoded_frame == GIF_INVALID_FRAME || + gif->decoded_frame == gif->prev_index) { + /* No frame to copy, or already have this frame recorded. */ + return; + } + + assert(gif->bitmap_callbacks.bitmap_get_buffer); + frame_data = (void *)gif->bitmap_callbacks.bitmap_get_buffer(gif->frame_image); + if (!frame_data) { + return; + } + + if (gif->prev_frame != NULL && + gif->width * gif->height > gif->prev_width * gif->prev_height) { + need_alloc = true; + } + + if (need_alloc) { + prev_frame = realloc(gif->prev_frame, + gif->width * gif->height * 4); + if (prev_frame == NULL) { + return; + } + } else { + prev_frame = gif->prev_frame; + } + + memcpy(prev_frame, frame_data, gif->width * gif->height * 4); + + gif->prev_frame = prev_frame; + gif->prev_width = gif->width; + gif->prev_height = gif->height; + gif->prev_index = gif->decoded_frame; +} + +static gif_result gif__recover_previous_frame(const gif_animation *gif) +{ + const uint32_t *prev_frame = gif->prev_frame; + unsigned height = gif->height < gif->prev_height ? gif->height : gif->prev_height; + unsigned width = gif->width < gif->prev_width ? gif->width : gif->prev_width; + uint32_t *frame_data; + + if (prev_frame == NULL) { + return GIF_FRAME_DATA_ERROR; + } + + assert(gif->bitmap_callbacks.bitmap_get_buffer); + frame_data = (void *)gif->bitmap_callbacks.bitmap_get_buffer(gif->frame_image); + if (!frame_data) { + return GIF_INSUFFICIENT_MEMORY; + } + + for (unsigned y = 0; y < height; y++) { + memcpy(frame_data, prev_frame, width * 4); + + frame_data += gif->width; + prev_frame += gif->prev_width; + } + + return GIF_OK; +} + +/** + * decode a gif frame + * + * \param gif gif animation context. + * \param frame The frame number to decode. + * \param clear_image flag for image data being cleared instead of plotted. + */ +static gif_result +gif_internal_decode_frame(gif_animation *gif, + unsigned int frame, + bool clear_image) +{ + gif_result err; + unsigned int index = 0; + unsigned char *gif_data, *gif_end; + int gif_bytes; + unsigned int width, height, offset_x, offset_y; + unsigned int flags, colour_table_size, interlace; + unsigned int *colour_table; + unsigned int *frame_data = 0; // Set to 0 for no warnings + unsigned int *frame_scanline; + unsigned int save_buffer_position; + unsigned int return_value = 0; + unsigned int x, y, decode_y, burst_bytes; + register unsigned char colour; + + /* Ensure this frame is supposed to be decoded */ + if (gif->frames[frame].display == false) { + return GIF_OK; + } + + /* Ensure the frame is in range to decode */ + if (frame > gif->frame_count_partial) { + return GIF_INSUFFICIENT_DATA; + } + + /* done if frame is already decoded */ + if ((!clear_image) && + ((int)frame == gif->decoded_frame)) { + return GIF_OK; + } + + if (gif->frames[frame].disposal_method == GIF_FRAME_RESTORE) { + /* Store the previous frame for later restoration */ + gif__record_previous_frame(gif); + } + + /* Get the start of our frame data and the end of the GIF data */ + gif_data = gif->gif_data + gif->frames[frame].frame_pointer; + gif_end = gif->gif_data + gif->buffer_size; + gif_bytes = (gif_end - gif_data); + + /* + * Ensure there is a minimal amount of data to proceed. The shortest + * block of data is a 10-byte image descriptor + 1-byte gif trailer + */ + if (gif_bytes < 12) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + /* Save the buffer position */ + save_buffer_position = gif->buffer_position; + gif->buffer_position = gif_data - gif->gif_data; + + /* Skip any extensions because they have allready been processed */ + if ((return_value = gif_skip_frame_extensions(gif)) != GIF_OK) { + goto gif_decode_frame_exit; + } + gif_data = (gif->gif_data + gif->buffer_position); + gif_bytes = (gif_end - gif_data); + + /* Ensure we have enough data for the 10-byte image descriptor + 1-byte + * gif trailer + */ + if (gif_bytes < 12) { + return_value = GIF_INSUFFICIENT_FRAME_DATA; + goto gif_decode_frame_exit; + } + + /* 10-byte Image Descriptor is: + * + * +0 CHAR Image Separator (0x2c) + * +1 SHORT Image Left Position + * +3 SHORT Image Top Position + * +5 SHORT Width + * +7 SHORT Height + * +9 CHAR __Packed Fields__ + * 1BIT Local Colour Table Flag + * 1BIT Interlace Flag + * 1BIT Sort Flag + * 2BITS Reserved + * 3BITS Size of Local Colour Table + */ + if (gif_data[0] != GIF_IMAGE_SEPARATOR) { + return_value = GIF_DATA_ERROR; + goto gif_decode_frame_exit; + } + offset_x = gif_data[1] | (gif_data[2] << 8); + offset_y = gif_data[3] | (gif_data[4] << 8); + width = gif_data[5] | (gif_data[6] << 8); + height = gif_data[7] | (gif_data[8] << 8); + + /* Boundary checking - shouldn't ever happen except unless the data has + * been modified since initialisation. + */ + if ((offset_x + width > gif->width) || + (offset_y + height > gif->height)) { + return_value = GIF_DATA_ERROR; + goto gif_decode_frame_exit; + } + + /* Decode the flags */ + flags = gif_data[9]; + colour_table_size = 2 << (flags & GIF_COLOUR_TABLE_SIZE_MASK); + interlace = flags & GIF_INTERLACE_MASK; + + /* Advance data pointer to next block either colour table or image + * data. + */ + gif_data += 10; + gif_bytes = (gif_end - gif_data); + + /* Set up the colour table */ + if (flags & GIF_COLOUR_TABLE_MASK) { + if (gif_bytes < (int)(3 * colour_table_size)) { + return_value = GIF_INSUFFICIENT_FRAME_DATA; + goto gif_decode_frame_exit; + } + colour_table = gif->local_colour_table; + if (!clear_image) { + for (index = 0; index < colour_table_size; index++) { + /* Gif colour map contents are r,g,b. + * + * We want to pack them bytewise into the + * colour table, such that the red component + * is in byte 0 and the alpha component is in + * byte 3. + */ + unsigned char *entry = + (unsigned char *) &colour_table[index]; + + entry[0] = gif_data[0]; /* r */ + entry[1] = gif_data[1]; /* g */ + entry[2] = gif_data[2]; /* b */ + entry[3] = 0xff; /* a */ + + gif_data += 3; + } + } else { + gif_data += 3 * colour_table_size; + } + gif_bytes = (gif_end - gif_data); + } else { + colour_table = gif->global_colour_table; + } + + /* Ensure sufficient data remains */ + if (gif_bytes < 1) { + return_value = GIF_INSUFFICIENT_FRAME_DATA; + goto gif_decode_frame_exit; + } + + /* check for an end marker */ + if (gif_data[0] == GIF_TRAILER) { + return_value = GIF_OK; + goto gif_decode_frame_exit; + } + + /* Get the frame data */ + assert(gif->bitmap_callbacks.bitmap_get_buffer); + frame_data = (void *)gif->bitmap_callbacks.bitmap_get_buffer(gif->frame_image); + if (!frame_data) { + return GIF_INSUFFICIENT_MEMORY; + } + + /* If we are clearing the image we just clear, if not decode */ + if (!clear_image) { + lzw_result res; + const uint8_t *stack_base; + const uint8_t *stack_pos; + + /* Ensure we have enough data for a 1-byte LZW code size + + * 1-byte gif trailer + */ + if (gif_bytes < 2) { + return_value = GIF_INSUFFICIENT_FRAME_DATA; + goto gif_decode_frame_exit; + } + + /* If we only have a 1-byte LZW code size + 1-byte gif trailer, + * we're finished + */ + if ((gif_bytes == 2) && (gif_data[1] == GIF_TRAILER)) { + return_value = GIF_OK; + goto gif_decode_frame_exit; + } + + /* If the previous frame's disposal method requires we restore + * the background colour or this is the first frame, clear + * the frame data + */ + if ((frame == 0) || (gif->decoded_frame == GIF_INVALID_FRAME)) { + memset((char*)frame_data, + GIF_TRANSPARENT_COLOUR, + gif->width * gif->height * sizeof(int)); + gif->decoded_frame = frame; + /* The line below would fill the image with its + * background color, but because GIFs support + * transparency we likely wouldn't want to do that. */ + /* memset((char*)frame_data, colour_table[gif->background_index], gif->width * gif->height * sizeof(int)); */ + } else if ((frame != 0) && + (gif->frames[frame - 1].disposal_method == GIF_FRAME_CLEAR)) { + return_value = gif_internal_decode_frame(gif, + (frame - 1), + true); + if (return_value != GIF_OK) { + goto gif_decode_frame_exit; + } + + } else if ((frame != 0) && + (gif->frames[frame - 1].disposal_method == GIF_FRAME_RESTORE)) { + /* + * If the previous frame's disposal method requires we + * restore the previous image, restore our saved image. + */ + err = gif__recover_previous_frame(gif); + if (err != GIF_OK) { + /* see notes above on transparency + * vs. background color + */ + memset((char*)frame_data, + GIF_TRANSPARENT_COLOUR, + gif->width * gif->height * sizeof(int)); + } + } + gif->decoded_frame = frame; + gif->buffer_position = (gif_data - gif->gif_data) + 1; + + /* Initialise the LZW decoding */ + res = lzw_decode_init(gif->lzw_ctx, gif->gif_data, + gif->buffer_size, gif->buffer_position, + gif_data[0], &stack_base, &stack_pos); + if (res != LZW_OK) { + return gif_error_from_lzw(res); + } + + /* Decompress the data */ + for (y = 0; y < height; y++) { + if (interlace) { + decode_y = gif_interlaced_line(height, y) + offset_y; + } else { + decode_y = y + offset_y; + } + frame_scanline = frame_data + offset_x + (decode_y * gif->width); + + /* Rather than decoding pixel by pixel, we try to burst + * out streams of data to remove the need for end-of + * data checks every pixel. + */ + x = width; + while (x > 0) { + burst_bytes = (stack_pos - stack_base); + if (burst_bytes > 0) { + if (burst_bytes > x) { + burst_bytes = x; + } + x -= burst_bytes; + while (burst_bytes-- > 0) { + colour = *--stack_pos; + if (((gif->frames[frame].transparency) && + (colour != gif->frames[frame].transparency_index)) || + (!gif->frames[frame].transparency)) { + *frame_scanline = colour_table[colour]; + } + frame_scanline++; + } + } else { + res = lzw_decode(gif->lzw_ctx, &stack_pos); + if (res != LZW_OK) { + /* Unexpected end of frame, try to recover */ + if (res == LZW_OK_EOD) { + return_value = GIF_OK; + } else { + return_value = gif_error_from_lzw(res); + } + goto gif_decode_frame_exit; + } + } + } + } + } else { + /* Clear our frame */ + if (gif->frames[frame].disposal_method == GIF_FRAME_CLEAR) { + for (y = 0; y < height; y++) { + frame_scanline = frame_data + offset_x + ((offset_y + y) * gif->width); + if (gif->frames[frame].transparency) { + memset(frame_scanline, + GIF_TRANSPARENT_COLOUR, + width * 4); + } else { + memset(frame_scanline, + colour_table[gif->background_index], + width * 4); + } + } + } + } +gif_decode_frame_exit: + + /* Check if we should test for optimisation */ + if (gif->frames[frame].virgin) { + if (gif->bitmap_callbacks.bitmap_test_opaque) { + gif->frames[frame].opaque = gif->bitmap_callbacks.bitmap_test_opaque(gif->frame_image); + } else { + gif->frames[frame].opaque = false; + } + gif->frames[frame].virgin = false; + } + + if (gif->bitmap_callbacks.bitmap_set_opaque) { + gif->bitmap_callbacks.bitmap_set_opaque(gif->frame_image, gif->frames[frame].opaque); + } + + if (gif->bitmap_callbacks.bitmap_modified) { + gif->bitmap_callbacks.bitmap_modified(gif->frame_image); + } + + /* Restore the buffer position */ + gif->buffer_position = save_buffer_position; + + return return_value; +} + + +/* exported function documented in libnsgif.h */ +void gif_create(gif_animation *gif, gif_bitmap_callback_vt *bitmap_callbacks) +{ + memset(gif, 0, sizeof(gif_animation)); + gif->bitmap_callbacks = *bitmap_callbacks; + gif->decoded_frame = GIF_INVALID_FRAME; + gif->prev_index = GIF_INVALID_FRAME; +} + + +/* exported function documented in libnsgif.h */ +gif_result gif_initialise(gif_animation *gif, size_t size, unsigned char *data) +{ + unsigned char *gif_data; + unsigned int index; + gif_result return_value; + + /* Initialize values */ + gif->buffer_size = size; + gif->gif_data = data; + + if (gif->lzw_ctx == NULL) { + lzw_result res = lzw_context_create( + (struct lzw_ctx **)&gif->lzw_ctx); + if (res != LZW_OK) { + return gif_error_from_lzw(res); + } + } + + /* Check for sufficient data to be a GIF (6-byte header + 7-byte + * logical screen descriptor) + */ + if (gif->buffer_size < GIF_STANDARD_HEADER_SIZE) { + return GIF_INSUFFICIENT_DATA; + } + + /* Get our current processing position */ + gif_data = gif->gif_data + gif->buffer_position; + + /* See if we should initialise the GIF */ + if (gif->buffer_position == 0) { + /* We want everything to be NULL before we start so we've no + * chance of freeing bad pointers (paranoia) + */ + gif->frame_image = NULL; + gif->frames = NULL; + gif->local_colour_table = NULL; + gif->global_colour_table = NULL; + + /* The caller may have been lazy and not reset any values */ + gif->frame_count = 0; + gif->frame_count_partial = 0; + gif->decoded_frame = GIF_INVALID_FRAME; + + /* 6-byte GIF file header is: + * + * +0 3CHARS Signature ('GIF') + * +3 3CHARS Version ('87a' or '89a') + */ + if (strncmp((const char *) gif_data, "GIF", 3) != 0) { + return GIF_DATA_ERROR; + } + gif_data += 3; + + /* Ensure GIF reports version 87a or 89a */ + /* + if ((strncmp(gif_data, "87a", 3) != 0) && + (strncmp(gif_data, "89a", 3) != 0)) + LOG(("Unknown GIF format - proceeding anyway")); + */ + gif_data += 3; + + /* 7-byte Logical Screen Descriptor is: + * + * +0 SHORT Logical Screen Width + * +2 SHORT Logical Screen Height + * +4 CHAR __Packed Fields__ + * 1BIT Global Colour Table Flag + * 3BITS Colour Resolution + * 1BIT Sort Flag + * 3BITS Size of Global Colour Table + * +5 CHAR Background Colour Index + * +6 CHAR Pixel Aspect Ratio + */ + gif->width = gif_data[0] | (gif_data[1] << 8); + gif->height = gif_data[2] | (gif_data[3] << 8); + gif->global_colours = (gif_data[4] & GIF_COLOUR_TABLE_MASK); + gif->colour_table_size = (2 << (gif_data[4] & GIF_COLOUR_TABLE_SIZE_MASK)); + gif->background_index = gif_data[5]; + gif->aspect_ratio = gif_data[6]; + gif->loop_count = 1; + gif_data += 7; + + /* Some broken GIFs report the size as the screen size they + * were created in. As such, we detect for the common cases and + * set the sizes as 0 if they are found which results in the + * GIF being the maximum size of the frames. + */ + if (((gif->width == 640) && (gif->height == 480)) || + ((gif->width == 640) && (gif->height == 512)) || + ((gif->width == 800) && (gif->height == 600)) || + ((gif->width == 1024) && (gif->height == 768)) || + ((gif->width == 1280) && (gif->height == 1024)) || + ((gif->width == 1600) && (gif->height == 1200)) || + ((gif->width == 0) || (gif->height == 0)) || + ((gif->width > 2048) || (gif->height > 2048))) { + gif->width = 1; + gif->height = 1; + } + + /* Allocate some data irrespective of whether we've got any + * colour tables. We always get the maximum size in case a GIF + * is lying to us. It's far better to give the wrong colours + * than to trample over some memory somewhere. + */ + gif->global_colour_table = calloc(GIF_MAX_COLOURS, sizeof(unsigned int)); + gif->local_colour_table = calloc(GIF_MAX_COLOURS, sizeof(unsigned int)); + if ((gif->global_colour_table == NULL) || + (gif->local_colour_table == NULL)) { + gif_finalise(gif); + return GIF_INSUFFICIENT_MEMORY; + } + + /* Set the first colour to a value that will never occur in + * reality so we know if we've processed it + */ + gif->global_colour_table[0] = GIF_PROCESS_COLOURS; + + /* Check if the GIF has no frame data (13-byte header + 1-byte + * termination block) Although generally useless, the GIF + * specification does not expressly prohibit this + */ + if (gif->buffer_size == (GIF_STANDARD_HEADER_SIZE + 1)) { + if (gif_data[0] == GIF_TRAILER) { + return GIF_OK; + } else { + return GIF_INSUFFICIENT_DATA; + } + } + + /* Initialise enough workspace for a frame */ + if ((gif->frames = (gif_frame *)malloc(sizeof(gif_frame))) == NULL) { + gif_finalise(gif); + return GIF_INSUFFICIENT_MEMORY; + } + gif->frame_holders = 1; + + /* Initialise the bitmap header */ + assert(gif->bitmap_callbacks.bitmap_create); + gif->frame_image = gif->bitmap_callbacks.bitmap_create(gif->width, gif->height); + if (gif->frame_image == NULL) { + gif_finalise(gif); + return GIF_INSUFFICIENT_MEMORY; + } + + /* Remember we've done this now */ + gif->buffer_position = gif_data - gif->gif_data; + } + + /* Do the colour map if we haven't already. As the top byte is always + * 0xff or 0x00 depending on the transparency we know if it's been + * filled in. + */ + if (gif->global_colour_table[0] == GIF_PROCESS_COLOURS) { + /* Check for a global colour map signified by bit 7 */ + if (gif->global_colours) { + if (gif->buffer_size < (gif->colour_table_size * 3 + GIF_STANDARD_HEADER_SIZE)) { + return GIF_INSUFFICIENT_DATA; + } + for (index = 0; index < gif->colour_table_size; index++) { + /* Gif colour map contents are r,g,b. + * + * We want to pack them bytewise into the + * colour table, such that the red component + * is in byte 0 and the alpha component is in + * byte 3. + */ + unsigned char *entry = (unsigned char *) &gif-> + global_colour_table[index]; + + entry[0] = gif_data[0]; /* r */ + entry[1] = gif_data[1]; /* g */ + entry[2] = gif_data[2]; /* b */ + entry[3] = 0xff; /* a */ + + gif_data += 3; + } + gif->buffer_position = (gif_data - gif->gif_data); + } else { + /* Create a default colour table with the first two + * colours as black and white + */ + unsigned int *entry = gif->global_colour_table; + + entry[0] = 0x00000000; + /* Force Alpha channel to opaque */ + ((unsigned char *) entry)[3] = 0xff; + + entry[1] = 0xffffffff; + } + } + + /* Repeatedly try to initialise frames */ + while ((return_value = gif_initialise_frame(gif)) == GIF_WORKING); + + /* If there was a memory error tell the caller */ + if ((return_value == GIF_INSUFFICIENT_MEMORY) || + (return_value == GIF_DATA_ERROR)) { + return return_value; + } + + /* If we didn't have some frames then a GIF_INSUFFICIENT_DATA becomes a + * GIF_INSUFFICIENT_FRAME_DATA + */ + if ((return_value == GIF_INSUFFICIENT_DATA) && + (gif->frame_count_partial > 0)) { + return GIF_INSUFFICIENT_FRAME_DATA; + } + + /* Return how many we got */ + return return_value; +} + + +/* exported function documented in libnsgif.h */ +gif_result gif_decode_frame(gif_animation *gif, unsigned int frame) +{ + return gif_internal_decode_frame(gif, frame, false); +} + + +/* exported function documented in libnsgif.h */ +void gif_finalise(gif_animation *gif) +{ + /* Release all our memory blocks */ + if (gif->frame_image) { + assert(gif->bitmap_callbacks.bitmap_destroy); + gif->bitmap_callbacks.bitmap_destroy(gif->frame_image); + } + + gif->frame_image = NULL; + free(gif->frames); + gif->frames = NULL; + free(gif->local_colour_table); + gif->local_colour_table = NULL; + free(gif->global_colour_table); + gif->global_colour_table = NULL; + + free(gif->prev_frame); + gif->prev_frame = NULL; + + lzw_context_destroy(gif->lzw_ctx); + gif->lzw_ctx = NULL; +} diff --git a/src/display/libnsgif/libnsgif.h b/src/display/libnsgif/libnsgif.h new file mode 100644 index 00000000..50dc6883 --- /dev/null +++ b/src/display/libnsgif/libnsgif.h @@ -0,0 +1,192 @@ +/* + * Copyright 2004 Richard Wilson + * Copyright 2008 Sean Fox + * + * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +/** + * \file + * Interface to progressive animated GIF file decoding. + */ + +#ifndef _LIBNSGIF_H_ +#define _LIBNSGIF_H_ + +#include +#include + +/* Error return values */ +typedef enum { + GIF_WORKING = 1, + GIF_OK = 0, + GIF_INSUFFICIENT_FRAME_DATA = -1, + GIF_FRAME_DATA_ERROR = -2, + GIF_INSUFFICIENT_DATA = -3, + GIF_DATA_ERROR = -4, + GIF_INSUFFICIENT_MEMORY = -5, + GIF_FRAME_NO_DISPLAY = -6, + GIF_END_OF_FRAME = -7 +} gif_result; + +/** GIF frame data */ +typedef struct gif_frame { + /** whether the frame should be displayed/animated */ + bool display; + /** delay (in cs) before animating the frame */ + unsigned int frame_delay; + + /* Internal members are listed below */ + + /** offset (in bytes) to the GIF frame data */ + unsigned int frame_pointer; + /** whether the frame has previously been used */ + bool virgin; + /** whether the frame is totally opaque */ + bool opaque; + /** whether a forcable screen redraw is required */ + bool redraw_required; + /** how the previous frame should be disposed; affects plotting */ + unsigned char disposal_method; + /** whether we acknoledge transparency */ + bool transparency; + /** the index designating a transparent pixel */ + unsigned char transparency_index; + /** x co-ordinate of redraw rectangle */ + unsigned int redraw_x; + /** y co-ordinate of redraw rectangle */ + unsigned int redraw_y; + /** width of redraw rectangle */ + unsigned int redraw_width; + /** height of redraw rectangle */ + unsigned int redraw_height; +} gif_frame; + +/* API for Bitmap callbacks */ +typedef void* (*gif_bitmap_cb_create)(int width, int height); +typedef void (*gif_bitmap_cb_destroy)(void *bitmap); +typedef unsigned char* (*gif_bitmap_cb_get_buffer)(void *bitmap); +typedef void (*gif_bitmap_cb_set_opaque)(void *bitmap, bool opaque); +typedef bool (*gif_bitmap_cb_test_opaque)(void *bitmap); +typedef void (*gif_bitmap_cb_modified)(void *bitmap); + +/** Bitmap callbacks function table */ +typedef struct gif_bitmap_callback_vt { + /** Create a bitmap. */ + gif_bitmap_cb_create bitmap_create; + /** Free a bitmap. */ + gif_bitmap_cb_destroy bitmap_destroy; + /** Return a pointer to the pixel data in a bitmap. */ + gif_bitmap_cb_get_buffer bitmap_get_buffer; + + /* Members below are optional */ + + /** Sets whether a bitmap should be plotted opaque. */ + gif_bitmap_cb_set_opaque bitmap_set_opaque; + /** Tests whether a bitmap has an opaque alpha channel. */ + gif_bitmap_cb_test_opaque bitmap_test_opaque; + /** The bitmap image has changed, so flush any persistant cache. */ + gif_bitmap_cb_modified bitmap_modified; +} gif_bitmap_callback_vt; + +/** GIF animation data */ +typedef struct gif_animation { + /** LZW decode context */ + void *lzw_ctx; + /** callbacks for bitmap functions */ + gif_bitmap_callback_vt bitmap_callbacks; + /** pointer to GIF data */ + unsigned char *gif_data; + /** width of GIF (may increase during decoding) */ + unsigned int width; + /** heigth of GIF (may increase during decoding) */ + unsigned int height; + /** number of frames decoded */ + unsigned int frame_count; + /** number of frames partially decoded */ + unsigned int frame_count_partial; + /** decoded frames */ + gif_frame *frames; + /** current frame decoded to bitmap */ + int decoded_frame; + /** currently decoded image; stored as bitmap from bitmap_create callback */ + void *frame_image; + /** number of times to loop animation */ + int loop_count; + + /* Internal members are listed below */ + + /** current index into GIF data */ + unsigned int buffer_position; + /** total number of bytes of GIF data available */ + unsigned int buffer_size; + /** current number of frame holders */ + unsigned int frame_holders; + /** index in the colour table for the background colour */ + unsigned int background_index; + /** image aspect ratio (ignored) */ + unsigned int aspect_ratio; + /** size of colour table (in entries) */ + unsigned int colour_table_size; + /** whether the GIF has a global colour table */ + bool global_colours; + /** global colour table */ + unsigned int *global_colour_table; + /** local colour table */ + unsigned int *local_colour_table; + + /** previous frame for GIF_FRAME_RESTORE */ + void *prev_frame; + /** previous frame index */ + int prev_index; + /** previous frame width */ + unsigned prev_width; + /** previous frame height */ + unsigned prev_height; +} gif_animation; + +/** + * Initialises necessary gif_animation members. + */ +void gif_create(gif_animation *gif, gif_bitmap_callback_vt *bitmap_callbacks); + +/** + * Initialises any workspace held by the animation and attempts to decode + * any information that hasn't already been decoded. + * If an error occurs, all previously decoded frames are retained. + * + * @return Error return value. + * - GIF_FRAME_DATA_ERROR for GIF frame data error + * - GIF_INSUFFICIENT_FRAME_DATA for insufficient data to process + * any more frames + * - GIF_INSUFFICIENT_MEMORY for memory error + * - GIF_DATA_ERROR for GIF error + * - GIF_INSUFFICIENT_DATA for insufficient data to do anything + * - GIF_OK for successful decoding + * - GIF_WORKING for successful decoding if more frames are expected + */ +gif_result gif_initialise(gif_animation *gif, size_t size, unsigned char *data); + +/** + * Decodes a GIF frame. + * + * @return Error return value. If a frame does not contain any image data, + * GIF_OK is returned and gif->current_error is set to + * GIF_FRAME_NO_DISPLAY + * - GIF_FRAME_DATA_ERROR for GIF frame data error + * - GIF_INSUFFICIENT_FRAME_DATA for insufficient data to complete the frame + * - GIF_DATA_ERROR for GIF error (invalid frame header) + * - GIF_INSUFFICIENT_DATA for insufficient data to do anything + * - GIF_INSUFFICIENT_MEMORY for insufficient memory to process + * - GIF_OK for successful decoding + */ +gif_result gif_decode_frame(gif_animation *gif, unsigned int frame); + +/** + * Releases any workspace held by a gif + */ +void gif_finalise(gif_animation *gif); + +#endif diff --git a/src/display/libnsgif/lzw.c b/src/display/libnsgif/lzw.c new file mode 100644 index 00000000..31cf7d4e --- /dev/null +++ b/src/display/libnsgif/lzw.c @@ -0,0 +1,377 @@ +/* + * This file is part of NetSurf's LibNSGIF, http://www.netsurf-browser.org/ + * Licensed under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright 2017 Michael Drake + */ + +#include +#include +#include +#include + +#include "lzw.h" + +/** + * \file + * \brief LZW decompression (implementation) + * + * Decoder for GIF LZW data. + */ + + +/** + * Context for reading LZW data. + * + * LZW data is split over multiple sub-blocks. Each sub-block has a + * byte at the start, which says the sub-block size, and then the data. + * Zero-size sub-blocks have no data, and the biggest sub-block size is + * 255, which means there are 255 bytes of data following the sub-block + * size entry. + * + * Note that an individual LZW code can be split over up to three sub-blocks. + */ +struct lzw_read_ctx { + const uint8_t *data; /**< Pointer to start of input data */ + uint32_t data_len; /**< Input data length */ + uint32_t data_sb_next; /**< Offset to sub-block size */ + + const uint8_t *sb_data; /**< Pointer to current sub-block in data */ + uint32_t sb_bit; /**< Current bit offset in sub-block */ + uint32_t sb_bit_count; /**< Bit count in sub-block */ +}; + +/** + * LZW dictionary entry. + * + * Records in the dictionary are composed of 1 or more entries. + * Entries point to previous entries which can be followed to compose + * the complete record. To compose the record in reverse order, take + * the `last_value` from each entry, and move to the previous entry. + * If the previous_entry's index is < the current clear_code, then it + * is the last entry in the record. + */ +struct lzw_dictionary_entry { + uint8_t last_value; /**< Last value for record ending at entry. */ + uint8_t first_value; /**< First value for entry's record. */ + uint16_t previous_entry; /**< Offset in dictionary to previous entry. */ +}; + +/** + * LZW decompression context. + */ +struct lzw_ctx { + /** Input reading context */ + struct lzw_read_ctx input; + + uint32_t previous_code; /**< Code read from input previously. */ + uint32_t previous_code_first; /**< First value of previous code. */ + + uint32_t initial_code_size; /**< Starting LZW code size. */ + uint32_t current_code_size; /**< Current LZW code size. */ + uint32_t current_code_size_max; /**< Max code value for current size. */ + + uint32_t clear_code; /**< Special Clear code value */ + uint32_t eoi_code; /**< Special End of Information code value */ + + uint32_t current_entry; /**< Next position in table to fill. */ + + /** Output value stack. */ + uint8_t stack_base[1 << LZW_CODE_MAX]; + + /** LZW decode dictionary. Generated during decode. */ + struct lzw_dictionary_entry table[1 << LZW_CODE_MAX]; +}; + + +/* Exported function, documented in lzw.h */ +lzw_result lzw_context_create(struct lzw_ctx **ctx) +{ + struct lzw_ctx *c = malloc(sizeof(*c)); + if (c == NULL) { + return LZW_NO_MEM; + } + + *ctx = c; + return LZW_OK; +} + + +/* Exported function, documented in lzw.h */ +void lzw_context_destroy(struct lzw_ctx *ctx) +{ + free(ctx); +} + + +/** + * Advance the context to the next sub-block in the input data. + * + * \param[in] ctx LZW reading context, updated on success. + * \return LZW_OK or LZW_OK_EOD on success, appropriate error otherwise. + */ +static lzw_result lzw__block_advance(struct lzw_read_ctx *ctx) +{ + uint32_t block_size; + uint32_t next_block_pos = ctx->data_sb_next; + const uint8_t *data_next = ctx->data + next_block_pos; + + if (next_block_pos >= ctx->data_len) { + return LZW_NO_DATA; + } + + block_size = *data_next; + + if ((next_block_pos + block_size) >= ctx->data_len) { + return LZW_NO_DATA; + } + + ctx->sb_bit = 0; + ctx->sb_bit_count = block_size * 8; + + if (block_size == 0) { + ctx->data_sb_next += 1; + return LZW_OK_EOD; + } + + ctx->sb_data = data_next + 1; + ctx->data_sb_next += block_size + 1; + + return LZW_OK; +} + + +/** + * Get the next LZW code of given size from the raw input data. + * + * Reads codes from the input data stream coping with GIF data sub-blocks. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] code_size Size of LZW code to get from data. + * \param[out] code_out Returns an LZW code on success. + * \return LZW_OK or LZW_OK_EOD on success, appropriate error otherwise. + */ +static inline lzw_result lzw__next_code( + struct lzw_read_ctx *ctx, + uint8_t code_size, + uint32_t *code_out) +{ + uint32_t code = 0; + uint8_t current_bit = ctx->sb_bit & 0x7; + uint8_t byte_advance = (current_bit + code_size) >> 3; + + assert(byte_advance <= 2); + + if (ctx->sb_bit + code_size <= ctx->sb_bit_count) { + /* Fast path: code fully inside this sub-block */ + const uint8_t *data = ctx->sb_data + (ctx->sb_bit >> 3); + switch (byte_advance) { + case 2: code |= data[2] << 16; /* Fall through */ + case 1: code |= data[1] << 8; /* Fall through */ + case 0: code |= data[0] << 0; + } + ctx->sb_bit += code_size; + } else { + /* Slow path: code spans sub-blocks */ + uint8_t byte = 0; + uint8_t bits_remaining_0 = (code_size < (8 - current_bit)) ? + code_size : (8 - current_bit); + uint8_t bits_remaining_1 = code_size - bits_remaining_0; + uint8_t bits_used[3] = { + [0] = bits_remaining_0, + [1] = bits_remaining_1 < 8 ? bits_remaining_1 : 8, + [2] = bits_remaining_1 - 8, + }; + + while (true) { + const uint8_t *data = ctx->sb_data; + lzw_result res; + + /* Get any data from end of this sub-block */ + while (byte <= byte_advance && + ctx->sb_bit < ctx->sb_bit_count) { + code |= data[ctx->sb_bit >> 3] << (byte << 3); + ctx->sb_bit += bits_used[byte]; + byte++; + } + + /* Check if we have all we need */ + if (byte > byte_advance) { + break; + } + + /* Move to next sub-block */ + res = lzw__block_advance(ctx); + if (res != LZW_OK) { + return res; + } + } + } + + *code_out = (code >> current_bit) & ((1 << code_size) - 1); + return LZW_OK; +} + + +/** + * Clear LZW code dictionary. + * + * \param[in] ctx LZW reading context, updated. + * \param[out] stack_pos_out Returns current stack position. + * \return LZW_OK or error code. + */ +static lzw_result lzw__clear_codes( + struct lzw_ctx *ctx, + const uint8_t ** const stack_pos_out) +{ + uint32_t code; + uint8_t *stack_pos; + + /* Reset dictionary building context */ + ctx->current_code_size = ctx->initial_code_size + 1; + ctx->current_code_size_max = (1 << ctx->current_code_size) - 1;; + ctx->current_entry = (1 << ctx->initial_code_size) + 2; + + /* There might be a sequence of clear codes, so process them all */ + do { + lzw_result res = lzw__next_code(&ctx->input, + ctx->current_code_size, &code); + if (res != LZW_OK) { + return res; + } + } while (code == ctx->clear_code); + + /* The initial code must be from the initial dictionary. */ + if (code > ctx->clear_code) { + return LZW_BAD_ICODE; + } + + /* Record this initial code as "previous" code, needed during decode. */ + ctx->previous_code = code; + ctx->previous_code_first = code; + + /* Reset the stack, and add first non-clear code added as first item. */ + stack_pos = ctx->stack_base; + *stack_pos++ = code; + + *stack_pos_out = stack_pos; + return LZW_OK; +} + + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode_init( + struct lzw_ctx *ctx, + const uint8_t *compressed_data, + uint32_t compressed_data_len, + uint32_t compressed_data_pos, + uint8_t code_size, + const uint8_t ** const stack_base_out, + const uint8_t ** const stack_pos_out) +{ + struct lzw_dictionary_entry *table = ctx->table; + + /* Initialise the input reading context */ + ctx->input.data = compressed_data; + ctx->input.data_len = compressed_data_len; + ctx->input.data_sb_next = compressed_data_pos; + + ctx->input.sb_bit = 0; + ctx->input.sb_bit_count = 0; + + /* Initialise the dictionary building context */ + ctx->initial_code_size = code_size; + + ctx->clear_code = (1 << code_size) + 0; + ctx->eoi_code = (1 << code_size) + 1; + + /* Initialise the standard dictionary entries */ + for (uint32_t i = 0; i < ctx->clear_code; ++i) { + table[i].first_value = i; + table[i].last_value = i; + } + + *stack_base_out = ctx->stack_base; + return lzw__clear_codes(ctx, stack_pos_out); +} + + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode(struct lzw_ctx *ctx, + const uint8_t ** const stack_pos_out) +{ + lzw_result res; + uint32_t code_new; + uint32_t code_out; + uint8_t last_value; + uint8_t *stack_pos = ctx->stack_base; + uint32_t clear_code = ctx->clear_code; + uint32_t current_entry = ctx->current_entry; + struct lzw_dictionary_entry * const table = ctx->table; + + /* Get a new code from the input */ + res = lzw__next_code(&ctx->input, ctx->current_code_size, &code_new); + if (res != LZW_OK) { + return res; + } + + /* Handle the new code */ + if (code_new == clear_code) { + /* Got Clear code */ + return lzw__clear_codes(ctx, stack_pos_out); + + } else if (code_new == ctx->eoi_code) { + /* Got End of Information code */ + return LZW_EOI_CODE; + + } else if (code_new > current_entry) { + /* Code is invalid */ + return LZW_BAD_CODE; + + } else if (code_new < current_entry) { + /* Code is in table */ + code_out = code_new; + last_value = table[code_new].first_value; + } else { + /* Code not in table */ + *stack_pos++ = ctx->previous_code_first; + code_out = ctx->previous_code; + last_value = ctx->previous_code_first; + } + + /* Add to the dictionary, only if there's space */ + if (current_entry < (1 << LZW_CODE_MAX)) { + struct lzw_dictionary_entry *entry = table + current_entry; + entry->last_value = last_value; + entry->first_value = ctx->previous_code_first; + entry->previous_entry = ctx->previous_code; + ctx->current_entry++; + } + + /* Ensure code size is increased, if needed. */ + if (current_entry == ctx->current_code_size_max) { + if (ctx->current_code_size < LZW_CODE_MAX) { + ctx->current_code_size++; + ctx->current_code_size_max = + (1 << ctx->current_code_size) - 1; + } + } + + /* Store details of this code as "previous code" to the context. */ + ctx->previous_code_first = table[code_new].first_value; + ctx->previous_code = code_new; + + /* Put rest of data for this code on output stack. + * Note, in the case of "code not in table", the last entry of the + * current code has already been placed on the stack above. */ + while (code_out > clear_code) { + struct lzw_dictionary_entry *entry = table + code_out; + *stack_pos++ = entry->last_value; + code_out = entry->previous_entry; + } + *stack_pos++ = table[code_out].last_value; + + *stack_pos_out = stack_pos; + return LZW_OK; +} diff --git a/src/display/libnsgif/lzw.h b/src/display/libnsgif/lzw.h new file mode 100644 index 00000000..385b4255 --- /dev/null +++ b/src/display/libnsgif/lzw.h @@ -0,0 +1,105 @@ +/* + * This file is part of NetSurf's LibNSGIF, http://www.netsurf-browser.org/ + * Licensed under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright 2017 Michael Drake + */ + +#ifndef LZW_H_ +#define LZW_H_ + +/** + * \file + * \brief LZW decompression (interface) + * + * Decoder for GIF LZW data. + */ + + +/** Maximum LZW code size in bits */ +#define LZW_CODE_MAX 12 + + +/* Declare lzw internal context structure */ +struct lzw_ctx; + + +/** LZW decoding response codes */ +typedef enum lzw_result { + LZW_OK, /**< Success */ + LZW_OK_EOD, /**< Success; reached zero-length sub-block */ + LZW_NO_MEM, /**< Error: Out of memory */ + LZW_NO_DATA, /**< Error: Out of data */ + LZW_EOI_CODE, /**< Error: End of Information code */ + LZW_BAD_ICODE, /**< Error: Bad initial LZW code */ + LZW_BAD_CODE, /**< Error: Bad LZW code */ +} lzw_result; + + +/** + * Create an LZW decompression context. + * + * \param[out] ctx Returns an LZW decompression context. Caller owned, + * free with lzw_context_destroy(). + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_context_create( + struct lzw_ctx **ctx); + +/** + * Destroy an LZW decompression context. + * + * \param[in] ctx The LZW decompression context to destroy. + */ +void lzw_context_destroy( + struct lzw_ctx *ctx); + +/** + * Initialise an LZW decompression context for decoding. + * + * Caller owns neither `stack_base_out` or `stack_pos_out`. + * + * \param[in] ctx The LZW decompression context to initialise. + * \param[in] compressed_data The compressed data. + * \param[in] compressed_data_len Byte length of compressed data. + * \param[in] compressed_data_pos Start position in data. Must be position + * of a size byte at sub-block start. + * \param[in] code_size The initial LZW code size to use. + * \param[out] stack_base_out Returns base of decompressed data stack. + * \param[out] stack_pos_out Returns current stack position. + * There are `stack_pos_out - stack_base_out` + * current stack entries. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode_init( + struct lzw_ctx *ctx, + const uint8_t *compressed_data, + uint32_t compressed_data_len, + uint32_t compressed_data_pos, + uint8_t code_size, + const uint8_t ** const stack_base_out, + const uint8_t ** const stack_pos_out); + +/** + * Fill the LZW stack with decompressed data + * + * Ensure anything on the stack is used before calling this, as anything + * on the stack before this call will be trampled. + * + * Caller does not own `stack_pos_out`. + * + * \param[in] ctx LZW reading context, updated. + * \param[out] stack_pos_out Returns current stack position. + * Use with `stack_base_out` value from previous + * lzw_decode_init() call. + * There are `stack_pos_out - stack_base_out` + * current stack entries. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode( + struct lzw_ctx *ctx, + const uint8_t ** const stack_pos_out); + + +#endif diff --git a/src/display/libnsgif/utils/log.h b/src/display/libnsgif/utils/log.h new file mode 100644 index 00000000..1413374c --- /dev/null +++ b/src/display/libnsgif/utils/log.h @@ -0,0 +1,21 @@ +/* + * Copyright 2003 James Bursa + * Copyright 2004 John Tytgat + * + * This file is part of NetSurf, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +#include + +#ifndef _LIBNSGIF_LOG_H_ +#define _LIBNSGIF_LOG_H_ + +#ifdef NDEBUG +# define LOG(x) ((void) 0) +#else +# define LOG(x) do { fprintf(stderr, x), fputc('\n', stderr); } while (0) +#endif /* NDEBUG */ + +#endif /* _LIBNSGIF_LOG_H_ */ diff --git a/src/display/sprite.cpp b/src/display/sprite.cpp index 3391af8c..991d62f9 100644 --- a/src/display/sprite.cpp +++ b/src/display/sprite.cpp @@ -575,7 +575,7 @@ void Sprite::draw() p->bitmap->bindTex(*base); - if (p->wave.active) + if (p->wave.active) p->wave.qArray.draw(); else p->quad.draw(); diff --git a/src/display/tilemap.cpp b/src/display/tilemap.cpp index 35e9938a..1c8a5d30 100644 --- a/src/display/tilemap.cpp +++ b/src/display/tilemap.cpp @@ -521,7 +521,8 @@ struct TilemapPrivate /* Assembles atlas from tileset and autotile bitmaps */ void buildAtlas() { - updateAutotileInfo(); + updateAutotileInfo(); + tileset->ensureNonAnimated(); TileAtlas::BlitVec blits = TileAtlas::calcBlits(atlas.efTilesetH, atlas.size); @@ -542,6 +543,7 @@ struct TilemapPrivate { const uint8_t atInd = atlas.usableATs[i]; Bitmap *autotile = autotiles[atInd]; + autotile->ensureNonAnimated(); int atW = autotile->width(); int atH = autotile->height(); diff --git a/src/meson.build b/src/meson.build index f2d2e3e8..9d7175c8 100755 --- a/src/meson.build +++ b/src/meson.build @@ -68,7 +68,7 @@ global_args += '-DMKXPZ_ALCDEVICE=' + alcdev_struct global_include_dirs += include_directories('.', 'audio', 'crypto', - 'display', 'display/gl', + 'display', 'display/gl', 'display/libnsgif', 'display/libnsgif/utils', 'etc', 'filesystem', 'filesystem/ghc', 'input', @@ -130,6 +130,9 @@ main_source = files( 'display/window.cpp', 'display/windowvx.cpp', + 'display/libnsgif/libnsgif.c', + 'display/libnsgif/lzw.c', + 'display/gl/gl-debug.cpp', 'display/gl/gl-fun.cpp', 'display/gl/gl-meta.cpp',