/* * 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 #include #include #include #include #include #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 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 srcBuffer(gifIn->SWidth * gifIn->SHeight); // Buffer for rendering images from the input GIF. std::unique_ptr renderBuffer(new ColorARGB[gifIn->SWidth * gifIn->SHeight]); // Buffer for writing new images to output GIF (one row at a time). std::unique_ptr 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) { if (gifIn->SBackGroundColor < 0 || gifIn->SBackGroundColor >= gifIn->SColorMap->ColorCount) { LOGE("SBackGroundColor overflow"); return false; } 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); if (colorIndex >= colorMap->ColorCount) { LOGE("Color Index %d is out of bounds (count=%d)", colorIndex, colorMap->ColorCount); return false; } // 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(&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; }