diff options
Diffstat (limited to 'jni/GifTranscoder.cpp')
-rw-r--r-- | jni/GifTranscoder.cpp | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/jni/GifTranscoder.cpp b/jni/GifTranscoder.cpp new file mode 100644 index 0000000..44fa30c --- /dev/null +++ b/jni/GifTranscoder.cpp @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <jni.h> +#include <time.h> +#include <stdio.h> +#include <memory> +#include <vector> + +#include <android/log.h> + +#include "GifTranscoder.h" + +#define SQUARE(a) (a)*(a) + +// GIF does not support partial transparency, so our alpha channels are always 0x0 or 0xff. +static const ColorARGB TRANSPARENT = 0x0; + +#define ALPHA(color) (((color) >> 24) & 0xff) +#define RED(color) (((color) >> 16) & 0xff) +#define GREEN(color) (((color) >> 8) & 0xff) +#define BLUE(color) (((color) >> 0) & 0xff) + +#define MAKE_COLOR_ARGB(a, r, g, b) \ + ((a) << 24 | (r) << 16 | (g) << 8 | (b)) + +#define MAX_COLOR_DISTANCE 255 * 255 * 255 + +#define TAG "GifTranscoder.cpp" +#define LOGD_ENABLED 0 +#if LOGD_ENABLED +#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)) +#else +#define LOGD(...) ((void)0) +#endif +#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)) +#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)) +#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)) + +// This macro expects the assertion to pass, but logs a FATAL if not. +#define ASSERT(cond, ...) \ + ( (__builtin_expect((cond) == 0, 0)) \ + ? ((void)__android_log_assert(#cond, TAG, ## __VA_ARGS__)) \ + : (void) 0 ) +#define ASSERT_ENABLED 1 + +namespace { + +// Current time in milliseconds since Unix epoch. +double now(void) { + struct timespec res; + clock_gettime(CLOCK_REALTIME, &res); + return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6; +} + +// Gets the pixel at position (x,y) from a buffer that uses row-major order to store an image with +// the specified width. +template <typename T> +T* getPixel(T* buffer, int width, int x, int y) { + return buffer + (y * width + x); +} + +} // namespace + +int GifTranscoder::transcode(const char* pathIn, const char* pathOut) { + int error; + double t0; + GifFileType* gifIn; + GifFileType* gifOut; + + // Automatically closes the GIF files when this method returns + GifFilesCloser closer; + + gifIn = DGifOpenFileName(pathIn, &error); + if (gifIn) { + closer.setGifIn(gifIn); + LOGD("Opened input GIF: %s", pathIn); + } else { + LOGE("Could not open input GIF: %s, error = %d", pathIn, error); + return GIF_ERROR; + } + + gifOut = EGifOpenFileName(pathOut, false, &error); + if (gifOut) { + closer.setGifOut(gifOut); + LOGD("Opened output GIF: %s", pathOut); + } else { + LOGE("Could not open output GIF: %s, error = %d", pathOut, error); + return GIF_ERROR; + } + + t0 = now(); + if (resizeBoxFilter(gifIn, gifOut)) { + LOGD("Resized GIF in %.2f ms", now() - t0); + } else { + LOGE("Could not resize GIF"); + return GIF_ERROR; + } + + return GIF_OK; +} + +bool GifTranscoder::resizeBoxFilter(GifFileType* gifIn, GifFileType* gifOut) { + ASSERT(gifIn != NULL, "gifIn cannot be NULL"); + ASSERT(gifOut != NULL, "gifOut cannot be NULL"); + + if (gifIn->SWidth < 0 || gifIn->SHeight < 0) { + LOGE("Input GIF has invalid size: %d x %d", gifIn->SWidth, gifIn->SHeight); + return false; + } + + // Output GIF will be 50% the size of the original. + if (EGifPutScreenDesc(gifOut, + gifIn->SWidth / 2, + gifIn->SHeight / 2, + gifIn->SColorResolution, + gifIn->SBackGroundColor, + gifIn->SColorMap) == GIF_ERROR) { + LOGE("Could not write screen descriptor"); + return false; + } + LOGD("Wrote screen descriptor"); + + // Index of the current image. + int imageIndex = 0; + + // Transparent color of the current image. + int transparentColor = NO_TRANSPARENT_COLOR; + + // Buffer for reading raw images from the input GIF. + std::vector<GifByteType> srcBuffer(gifIn->SWidth * gifIn->SHeight); + + // Buffer for rendering images from the input GIF. + std::unique_ptr<ColorARGB> renderBuffer(new ColorARGB[gifIn->SWidth * gifIn->SHeight]); + + // Buffer for writing new images to output GIF (one row at a time). + std::unique_ptr<GifByteType> dstRowBuffer(new GifByteType[gifOut->SWidth]); + + // Many GIFs use DISPOSE_DO_NOT to make images draw on top of previous images. They can also + // use DISPOSE_BACKGROUND to clear the last image region before drawing the next one. We need + // to keep track of the disposal mode as we go along to properly render the GIF. + int disposalMode = DISPOSAL_UNSPECIFIED; + int prevImageDisposalMode = DISPOSAL_UNSPECIFIED; + GifImageDesc prevImageDimens; + + // Background color (applies to entire GIF). + ColorARGB bgColor = TRANSPARENT; + + GifRecordType recordType; + do { + if (DGifGetRecordType(gifIn, &recordType) == GIF_ERROR) { + LOGE("Could not get record type"); + return false; + } + LOGD("Read record type: %d", recordType); + switch (recordType) { + case IMAGE_DESC_RECORD_TYPE: { + if (DGifGetImageDesc(gifIn) == GIF_ERROR) { + LOGE("Could not read image descriptor (%d)", imageIndex); + return false; + } + + // Sanity-check the current image position. + if (gifIn->Image.Left < 0 || + gifIn->Image.Top < 0 || + gifIn->Image.Left + gifIn->Image.Width > gifIn->SWidth || + gifIn->Image.Top + gifIn->Image.Height > gifIn->SHeight) { + LOGE("GIF image extends beyond logical screen"); + return false; + } + + // Write the new image descriptor. + if (EGifPutImageDesc(gifOut, + 0, // Left + 0, // Top + gifOut->SWidth, + gifOut->SHeight, + false, // Interlace + gifIn->Image.ColorMap) == GIF_ERROR) { + LOGE("Could not write image descriptor (%d)", imageIndex); + return false; + } + + // Read the image from the input GIF. The buffer is already initialized to the + // size of the GIF, which is usually equal to the size of all the images inside it. + // If not, the call to resize below ensures that the buffer is the right size. + srcBuffer.resize(gifIn->Image.Width * gifIn->Image.Height); + if (readImage(gifIn, srcBuffer.data()) == false) { + LOGE("Could not read image data (%d)", imageIndex); + return false; + } + LOGD("Read image data (%d)", imageIndex); + // Render the image from the input GIF. + if (renderImage(gifIn, + srcBuffer.data(), + imageIndex, + transparentColor, + renderBuffer.get(), + bgColor, + prevImageDimens, + prevImageDisposalMode) == false) { + LOGE("Could not render %d", imageIndex); + return false; + } + LOGD("Rendered image (%d)", imageIndex); + + // Generate the image in the output GIF. + for (int y = 0; y < gifOut->SHeight; y++) { + for (int x = 0; x < gifOut->SWidth; x++) { + const GifByteType dstColorIndex = computeNewColorIndex( + gifIn, transparentColor, renderBuffer.get(), x, y); + *(dstRowBuffer.get() + x) = dstColorIndex; + } + if (EGifPutLine(gifOut, dstRowBuffer.get(), gifOut->SWidth) == GIF_ERROR) { + LOGE("Could not write raster data (%d)", imageIndex); + return false; + } + } + LOGD("Wrote raster data (%d)", imageIndex); + + // Save the disposal mode for rendering the next image. + // We only support DISPOSE_DO_NOT and DISPOSE_BACKGROUND. + prevImageDisposalMode = disposalMode; + if (prevImageDisposalMode == DISPOSAL_UNSPECIFIED) { + prevImageDisposalMode = DISPOSE_DO_NOT; + } else if (prevImageDisposalMode == DISPOSE_PREVIOUS) { + prevImageDisposalMode = DISPOSE_BACKGROUND; + } + if (prevImageDisposalMode == DISPOSE_BACKGROUND) { + prevImageDimens.Left = gifIn->Image.Left; + prevImageDimens.Top = gifIn->Image.Top; + prevImageDimens.Width = gifIn->Image.Width; + prevImageDimens.Height = gifIn->Image.Height; + } + + if (gifOut->Image.ColorMap) { + GifFreeMapObject(gifOut->Image.ColorMap); + gifOut->Image.ColorMap = NULL; + } + + imageIndex++; + } break; + case EXTENSION_RECORD_TYPE: { + int extCode; + GifByteType* ext; + if (DGifGetExtension(gifIn, &extCode, &ext) == GIF_ERROR) { + LOGE("Could not read extension block"); + return false; + } + LOGD("Read extension block, code: %d", extCode); + if (extCode == GRAPHICS_EXT_FUNC_CODE) { + GraphicsControlBlock gcb; + if (DGifExtensionToGCB(ext[0], ext + 1, &gcb) == GIF_ERROR) { + LOGE("Could not interpret GCB extension"); + return false; + } + transparentColor = gcb.TransparentColor; + + // This logic for setting the background color based on the first GCB + // doesn't quite match the GIF spec, but empirically it seems to work and it + // matches what libframesequence (Rastermill) does. + if (imageIndex == 0 && gifIn->SColorMap) { + if (gcb.TransparentColor == NO_TRANSPARENT_COLOR) { + GifColorType bgColorIndex = + gifIn->SColorMap->Colors[gifIn->SBackGroundColor]; + bgColor = gifColorToColorARGB(bgColorIndex); + LOGD("Set background color based on first GCB"); + } + } + + // Record the original disposal mode and then update it. + disposalMode = gcb.DisposalMode; + gcb.DisposalMode = DISPOSE_BACKGROUND; + EGifGCBToExtension(&gcb, ext + 1); + } + if (EGifPutExtensionLeader(gifOut, extCode) == GIF_ERROR) { + LOGE("Could not write extension leader"); + return false; + } + if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) { + LOGE("Could not write extension block"); + return false; + } + LOGD("Wrote extension block"); + while (ext != NULL) { + if (DGifGetExtensionNext(gifIn, &ext) == GIF_ERROR) { + LOGE("Could not read extension continuation"); + return false; + } + if (ext != NULL) { + LOGD("Read extension continuation"); + if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) { + LOGE("Could not write extension continuation"); + return false; + } + LOGD("Wrote extension continuation"); + } + } + if (EGifPutExtensionTrailer(gifOut) == GIF_ERROR) { + LOGE("Could not write extension trailer"); + return false; + } + } break; + } + + } while (recordType != TERMINATE_RECORD_TYPE); + LOGD("No more records"); + + return true; +} + +bool GifTranscoder::readImage(GifFileType* gifIn, GifByteType* rasterBits) { + if (gifIn->Image.Interlace) { + int interlacedOffset[] = { 0, 4, 2, 1 }; + int interlacedJumps[] = { 8, 8, 4, 2 }; + + // Need to perform 4 passes on the image + for (int i = 0; i < 4; i++) { + for (int j = interlacedOffset[i]; j < gifIn->Image.Height; j += interlacedJumps[i]) { + if (DGifGetLine(gifIn, + rasterBits + j * gifIn->Image.Width, + gifIn->Image.Width) == GIF_ERROR) { + LOGE("Could not read interlaced raster data"); + return false; + } + } + } + } else { + if (DGifGetLine(gifIn, rasterBits, gifIn->Image.Width * gifIn->Image.Height) == GIF_ERROR) { + LOGE("Could not read raster data"); + return false; + } + } + return true; +} + +bool GifTranscoder::renderImage(GifFileType* gifIn, + GifByteType* rasterBits, + int imageIndex, + int transparentColorIndex, + ColorARGB* renderBuffer, + ColorARGB bgColor, + GifImageDesc prevImageDimens, + int prevImageDisposalMode) { + ASSERT(imageIndex < gifIn->ImageCount, + "Image index %d is out of bounds (count=%d)", imageIndex, gifIn->ImageCount); + + ColorMapObject* colorMap = getColorMap(gifIn); + if (colorMap == NULL) { + LOGE("No GIF color map found"); + return false; + } + + // Clear all or part of the background, before drawing the first image and maybe before drawing + // subsequent images (depending on the DisposalMode). + if (imageIndex == 0) { + fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight, + 0, 0, gifIn->SWidth, gifIn->SHeight, bgColor); + } else if (prevImageDisposalMode == DISPOSE_BACKGROUND) { + fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight, + prevImageDimens.Left, prevImageDimens.Top, + prevImageDimens.Width, prevImageDimens.Height, TRANSPARENT); + } + + // Paint this image onto the canvas + for (int y = 0; y < gifIn->Image.Height; y++) { + for (int x = 0; x < gifIn->Image.Width; x++) { + GifByteType colorIndex = *getPixel(rasterBits, gifIn->Image.Width, x, y); + + // This image may be smaller than the GIF's "logical screen" + int renderX = x + gifIn->Image.Left; + int renderY = y + gifIn->Image.Top; + + // Skip drawing transparent pixels if this image renders on top of the last one + if (imageIndex > 0 && prevImageDisposalMode == DISPOSE_DO_NOT && + colorIndex == transparentColorIndex) { + continue; + } + + ColorARGB* renderPixel = getPixel(renderBuffer, gifIn->SWidth, renderX, renderY); + *renderPixel = getColorARGB(colorMap, transparentColorIndex, colorIndex); + } + } + return true; +} + +void GifTranscoder::fillRect(ColorARGB* renderBuffer, + int imageWidth, + int imageHeight, + int left, + int top, + int width, + int height, + ColorARGB color) { + ASSERT(left + width <= imageWidth, "Rectangle is outside image bounds"); + ASSERT(top + height <= imageHeight, "Rectangle is outside image bounds"); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorARGB* renderPixel = getPixel(renderBuffer, imageWidth, x + left, y + top); + *renderPixel = color; + } + } +} + +GifByteType GifTranscoder::computeNewColorIndex(GifFileType* gifIn, + int transparentColorIndex, + ColorARGB* renderBuffer, + int x, + int y) { + ColorMapObject* colorMap = getColorMap(gifIn); + + // Compute the average color of 4 adjacent pixels from the input image. + ColorARGB c1 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2); + ColorARGB c2 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2); + ColorARGB c3 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2 + 1); + ColorARGB c4 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2 + 1); + ColorARGB avgColor = computeAverage(c1, c2, c3, c4); + + // Search the color map for the best match. + return findBestColor(colorMap, transparentColorIndex, avgColor); +} + +ColorARGB GifTranscoder::computeAverage(ColorARGB c1, ColorARGB c2, ColorARGB c3, ColorARGB c4) { + char avgAlpha = (char)(((int) ALPHA(c1) + (int) ALPHA(c2) + + (int) ALPHA(c3) + (int) ALPHA(c4)) / 4); + char avgRed = (char)(((int) RED(c1) + (int) RED(c2) + + (int) RED(c3) + (int) RED(c4)) / 4); + char avgGreen = (char)(((int) GREEN(c1) + (int) GREEN(c2) + + (int) GREEN(c3) + (int) GREEN(c4)) / 4); + char avgBlue = (char)(((int) BLUE(c1) + (int) BLUE(c2) + + (int) BLUE(c3) + (int) BLUE(c4)) / 4); + return MAKE_COLOR_ARGB(avgAlpha, avgRed, avgGreen, avgBlue); +} + +GifByteType GifTranscoder::findBestColor(ColorMapObject* colorMap, int transparentColorIndex, + ColorARGB targetColor) { + // Return the transparent color if the average alpha is zero. + char alpha = ALPHA(targetColor); + if (alpha == 0 && transparentColorIndex != NO_TRANSPARENT_COLOR) { + return transparentColorIndex; + } + + GifByteType closestColorIndex = 0; + int closestColorDistance = MAX_COLOR_DISTANCE; + for (int i = 0; i < colorMap->ColorCount; i++) { + // Skip the transparent color (we've already eliminated that option). + if (i == transparentColorIndex) { + continue; + } + ColorARGB indexedColor = gifColorToColorARGB(colorMap->Colors[i]); + int distance = computeDistance(targetColor, indexedColor); + if (distance < closestColorDistance) { + closestColorIndex = i; + closestColorDistance = distance; + } + } + return closestColorIndex; +} + +int GifTranscoder::computeDistance(ColorARGB c1, ColorARGB c2) { + return SQUARE(RED(c1) - RED(c2)) + + SQUARE(GREEN(c1) - GREEN(c2)) + + SQUARE(BLUE(c1) - BLUE(c2)); +} + +ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) { + if (gifIn->Image.ColorMap) { + return gifIn->Image.ColorMap; + } + return gifIn->SColorMap; +} + +ColorARGB GifTranscoder::getColorARGB(ColorMapObject* colorMap, int transparentColorIndex, + GifByteType colorIndex) { + if (colorIndex == transparentColorIndex) { + return TRANSPARENT; + } + return gifColorToColorARGB(colorMap->Colors[colorIndex]); +} + +ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) { + return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue); +} + +GifFilesCloser::~GifFilesCloser() { + if (mGifIn) { + DGifCloseFile(mGifIn); + mGifIn = NULL; + } + if (mGifOut) { + EGifCloseFile(mGifOut); + mGifOut = NULL; + } +} + +void GifFilesCloser::setGifIn(GifFileType* gifIn) { + ASSERT(mGifIn == NULL, "mGifIn is already set"); + mGifIn = gifIn; +} + +void GifFilesCloser::releaseGifIn() { + ASSERT(mGifIn != NULL, "mGifIn is already NULL"); + mGifIn = NULL; +} + +void GifFilesCloser::setGifOut(GifFileType* gifOut) { + ASSERT(mGifOut == NULL, "mGifOut is already set"); + mGifOut = gifOut; +} + +void GifFilesCloser::releaseGifOut() { + ASSERT(mGifOut != NULL, "mGifOut is already NULL"); + mGifOut = NULL; +} + +// JNI stuff + +jboolean transcode(JNIEnv* env, jobject clazz, jstring filePath, jstring outFilePath) { + const char* pathIn = env->GetStringUTFChars(filePath, JNI_FALSE); + const char* pathOut = env->GetStringUTFChars(outFilePath, JNI_FALSE); + + GifTranscoder transcoder; + int gifCode = transcoder.transcode(pathIn, pathOut); + + env->ReleaseStringUTFChars(filePath, pathIn); + env->ReleaseStringUTFChars(outFilePath, pathOut); + + return (gifCode == GIF_OK); +} + +const char *kClassPathName = "com/android/messaging/util/GifTranscoder"; + +JNINativeMethod kMethods[] = { + { "transcodeInternal", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)transcode }, +}; + +int registerNativeMethods(JNIEnv* env, const char* className, + JNINativeMethod* gMethods, int numMethods) { + jclass clazz = env->FindClass(className); + if (clazz == NULL) { + return JNI_FALSE; + } + if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { + return JNI_FALSE; + } + return JNI_TRUE; +} + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + if (!registerNativeMethods(env, kClassPathName, + kMethods, sizeof(kMethods) / sizeof(kMethods[0]))) { + return -1; + } + return JNI_VERSION_1_6; +} |