1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17#include <jni.h> 18#include <time.h> 19#include <stdio.h> 20#include <memory> 21#include <vector> 22 23#include <android/log.h> 24 25#include "GifTranscoder.h" 26 27#define SQUARE(a) (a)*(a) 28 29// GIF does not support partial transparency, so our alpha channels are always 0x0 or 0xff. 30static const ColorARGB TRANSPARENT = 0x0; 31 32#define ALPHA(color) (((color) >> 24) & 0xff) 33#define RED(color) (((color) >> 16) & 0xff) 34#define GREEN(color) (((color) >> 8) & 0xff) 35#define BLUE(color) (((color) >> 0) & 0xff) 36 37#define MAKE_COLOR_ARGB(a, r, g, b) \ 38 ((a) << 24 | (r) << 16 | (g) << 8 | (b)) 39 40#define MAX_COLOR_DISTANCE 255 * 255 * 255 41 42#define TAG "GifTranscoder.cpp" 43#define LOGD_ENABLED 0 44#if LOGD_ENABLED 45#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)) 46#else 47#define LOGD(...) ((void)0) 48#endif 49#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)) 50#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)) 51#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)) 52 53// This macro expects the assertion to pass, but logs a FATAL if not. 54#define ASSERT(cond, ...) \ 55 ( (__builtin_expect((cond) == 0, 0)) \ 56 ? ((void)__android_log_assert(#cond, TAG, ## __VA_ARGS__)) \ 57 : (void) 0 ) 58#define ASSERT_ENABLED 1 59 60namespace { 61 62// Current time in milliseconds since Unix epoch. 63double now(void) { 64 struct timespec res; 65 clock_gettime(CLOCK_REALTIME, &res); 66 return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6; 67} 68 69// Gets the pixel at position (x,y) from a buffer that uses row-major order to store an image with 70// the specified width. 71template <typename T> 72T* getPixel(T* buffer, int width, int x, int y) { 73 return buffer + (y * width + x); 74} 75 76} // namespace 77 78int GifTranscoder::transcode(const char* pathIn, const char* pathOut) { 79 int error; 80 double t0; 81 GifFileType* gifIn; 82 GifFileType* gifOut; 83 84 // Automatically closes the GIF files when this method returns 85 GifFilesCloser closer; 86 87 gifIn = DGifOpenFileName(pathIn, &error); 88 if (gifIn) { 89 closer.setGifIn(gifIn); 90 LOGD("Opened input GIF: %s", pathIn); 91 } else { 92 LOGE("Could not open input GIF: %s, error = %d", pathIn, error); 93 return GIF_ERROR; 94 } 95 96 gifOut = EGifOpenFileName(pathOut, false, &error); 97 if (gifOut) { 98 closer.setGifOut(gifOut); 99 LOGD("Opened output GIF: %s", pathOut); 100 } else { 101 LOGE("Could not open output GIF: %s, error = %d", pathOut, error); 102 return GIF_ERROR; 103 } 104 105 t0 = now(); 106 if (resizeBoxFilter(gifIn, gifOut)) { 107 LOGD("Resized GIF in %.2f ms", now() - t0); 108 } else { 109 LOGE("Could not resize GIF"); 110 return GIF_ERROR; 111 } 112 113 return GIF_OK; 114} 115 116bool GifTranscoder::resizeBoxFilter(GifFileType* gifIn, GifFileType* gifOut) { 117 ASSERT(gifIn != NULL, "gifIn cannot be NULL"); 118 ASSERT(gifOut != NULL, "gifOut cannot be NULL"); 119 120 if (gifIn->SWidth < 0 || gifIn->SHeight < 0) { 121 LOGE("Input GIF has invalid size: %d x %d", gifIn->SWidth, gifIn->SHeight); 122 return false; 123 } 124 125 // Output GIF will be 50% the size of the original. 126 if (EGifPutScreenDesc(gifOut, 127 gifIn->SWidth / 2, 128 gifIn->SHeight / 2, 129 gifIn->SColorResolution, 130 gifIn->SBackGroundColor, 131 gifIn->SColorMap) == GIF_ERROR) { 132 LOGE("Could not write screen descriptor"); 133 return false; 134 } 135 LOGD("Wrote screen descriptor"); 136 137 // Index of the current image. 138 int imageIndex = 0; 139 140 // Transparent color of the current image. 141 int transparentColor = NO_TRANSPARENT_COLOR; 142 143 // Buffer for reading raw images from the input GIF. 144 std::vector<GifByteType> srcBuffer(gifIn->SWidth * gifIn->SHeight); 145 146 // Buffer for rendering images from the input GIF. 147 std::unique_ptr<ColorARGB> renderBuffer(new ColorARGB[gifIn->SWidth * gifIn->SHeight]); 148 149 // Buffer for writing new images to output GIF (one row at a time). 150 std::unique_ptr<GifByteType> dstRowBuffer(new GifByteType[gifOut->SWidth]); 151 152 // Many GIFs use DISPOSE_DO_NOT to make images draw on top of previous images. They can also 153 // use DISPOSE_BACKGROUND to clear the last image region before drawing the next one. We need 154 // to keep track of the disposal mode as we go along to properly render the GIF. 155 int disposalMode = DISPOSAL_UNSPECIFIED; 156 int prevImageDisposalMode = DISPOSAL_UNSPECIFIED; 157 GifImageDesc prevImageDimens; 158 159 // Background color (applies to entire GIF). 160 ColorARGB bgColor = TRANSPARENT; 161 162 GifRecordType recordType; 163 do { 164 if (DGifGetRecordType(gifIn, &recordType) == GIF_ERROR) { 165 LOGE("Could not get record type"); 166 return false; 167 } 168 LOGD("Read record type: %d", recordType); 169 switch (recordType) { 170 case IMAGE_DESC_RECORD_TYPE: { 171 if (DGifGetImageDesc(gifIn) == GIF_ERROR) { 172 LOGE("Could not read image descriptor (%d)", imageIndex); 173 return false; 174 } 175 176 // Sanity-check the current image position. 177 if (gifIn->Image.Left < 0 || 178 gifIn->Image.Top < 0 || 179 gifIn->Image.Left + gifIn->Image.Width > gifIn->SWidth || 180 gifIn->Image.Top + gifIn->Image.Height > gifIn->SHeight) { 181 LOGE("GIF image extends beyond logical screen"); 182 return false; 183 } 184 185 // Write the new image descriptor. 186 if (EGifPutImageDesc(gifOut, 187 0, // Left 188 0, // Top 189 gifOut->SWidth, 190 gifOut->SHeight, 191 false, // Interlace 192 gifIn->Image.ColorMap) == GIF_ERROR) { 193 LOGE("Could not write image descriptor (%d)", imageIndex); 194 return false; 195 } 196 197 // Read the image from the input GIF. The buffer is already initialized to the 198 // size of the GIF, which is usually equal to the size of all the images inside it. 199 // If not, the call to resize below ensures that the buffer is the right size. 200 srcBuffer.resize(gifIn->Image.Width * gifIn->Image.Height); 201 if (readImage(gifIn, srcBuffer.data()) == false) { 202 LOGE("Could not read image data (%d)", imageIndex); 203 return false; 204 } 205 LOGD("Read image data (%d)", imageIndex); 206 // Render the image from the input GIF. 207 if (renderImage(gifIn, 208 srcBuffer.data(), 209 imageIndex, 210 transparentColor, 211 renderBuffer.get(), 212 bgColor, 213 prevImageDimens, 214 prevImageDisposalMode) == false) { 215 LOGE("Could not render %d", imageIndex); 216 return false; 217 } 218 LOGD("Rendered image (%d)", imageIndex); 219 220 // Generate the image in the output GIF. 221 for (int y = 0; y < gifOut->SHeight; y++) { 222 for (int x = 0; x < gifOut->SWidth; x++) { 223 const GifByteType dstColorIndex = computeNewColorIndex( 224 gifIn, transparentColor, renderBuffer.get(), x, y); 225 *(dstRowBuffer.get() + x) = dstColorIndex; 226 } 227 if (EGifPutLine(gifOut, dstRowBuffer.get(), gifOut->SWidth) == GIF_ERROR) { 228 LOGE("Could not write raster data (%d)", imageIndex); 229 return false; 230 } 231 } 232 LOGD("Wrote raster data (%d)", imageIndex); 233 234 // Save the disposal mode for rendering the next image. 235 // We only support DISPOSE_DO_NOT and DISPOSE_BACKGROUND. 236 prevImageDisposalMode = disposalMode; 237 if (prevImageDisposalMode == DISPOSAL_UNSPECIFIED) { 238 prevImageDisposalMode = DISPOSE_DO_NOT; 239 } else if (prevImageDisposalMode == DISPOSE_PREVIOUS) { 240 prevImageDisposalMode = DISPOSE_BACKGROUND; 241 } 242 if (prevImageDisposalMode == DISPOSE_BACKGROUND) { 243 prevImageDimens.Left = gifIn->Image.Left; 244 prevImageDimens.Top = gifIn->Image.Top; 245 prevImageDimens.Width = gifIn->Image.Width; 246 prevImageDimens.Height = gifIn->Image.Height; 247 } 248 249 if (gifOut->Image.ColorMap) { 250 GifFreeMapObject(gifOut->Image.ColorMap); 251 gifOut->Image.ColorMap = NULL; 252 } 253 254 imageIndex++; 255 } break; 256 case EXTENSION_RECORD_TYPE: { 257 int extCode; 258 GifByteType* ext; 259 if (DGifGetExtension(gifIn, &extCode, &ext) == GIF_ERROR) { 260 LOGE("Could not read extension block"); 261 return false; 262 } 263 LOGD("Read extension block, code: %d", extCode); 264 if (extCode == GRAPHICS_EXT_FUNC_CODE) { 265 GraphicsControlBlock gcb; 266 if (DGifExtensionToGCB(ext[0], ext + 1, &gcb) == GIF_ERROR) { 267 LOGE("Could not interpret GCB extension"); 268 return false; 269 } 270 transparentColor = gcb.TransparentColor; 271 272 // This logic for setting the background color based on the first GCB 273 // doesn't quite match the GIF spec, but empirically it seems to work and it 274 // matches what libframesequence (Rastermill) does. 275 if (imageIndex == 0 && gifIn->SColorMap) { 276 if (gcb.TransparentColor == NO_TRANSPARENT_COLOR) { 277 GifColorType bgColorIndex = 278 gifIn->SColorMap->Colors[gifIn->SBackGroundColor]; 279 bgColor = gifColorToColorARGB(bgColorIndex); 280 LOGD("Set background color based on first GCB"); 281 } 282 } 283 284 // Record the original disposal mode and then update it. 285 disposalMode = gcb.DisposalMode; 286 gcb.DisposalMode = DISPOSE_BACKGROUND; 287 EGifGCBToExtension(&gcb, ext + 1); 288 } 289 if (EGifPutExtensionLeader(gifOut, extCode) == GIF_ERROR) { 290 LOGE("Could not write extension leader"); 291 return false; 292 } 293 if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) { 294 LOGE("Could not write extension block"); 295 return false; 296 } 297 LOGD("Wrote extension block"); 298 while (ext != NULL) { 299 if (DGifGetExtensionNext(gifIn, &ext) == GIF_ERROR) { 300 LOGE("Could not read extension continuation"); 301 return false; 302 } 303 if (ext != NULL) { 304 LOGD("Read extension continuation"); 305 if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) { 306 LOGE("Could not write extension continuation"); 307 return false; 308 } 309 LOGD("Wrote extension continuation"); 310 } 311 } 312 if (EGifPutExtensionTrailer(gifOut) == GIF_ERROR) { 313 LOGE("Could not write extension trailer"); 314 return false; 315 } 316 } break; 317 } 318 319 } while (recordType != TERMINATE_RECORD_TYPE); 320 LOGD("No more records"); 321 322 return true; 323} 324 325bool GifTranscoder::readImage(GifFileType* gifIn, GifByteType* rasterBits) { 326 if (gifIn->Image.Interlace) { 327 int interlacedOffset[] = { 0, 4, 2, 1 }; 328 int interlacedJumps[] = { 8, 8, 4, 2 }; 329 330 // Need to perform 4 passes on the image 331 for (int i = 0; i < 4; i++) { 332 for (int j = interlacedOffset[i]; j < gifIn->Image.Height; j += interlacedJumps[i]) { 333 if (DGifGetLine(gifIn, 334 rasterBits + j * gifIn->Image.Width, 335 gifIn->Image.Width) == GIF_ERROR) { 336 LOGE("Could not read interlaced raster data"); 337 return false; 338 } 339 } 340 } 341 } else { 342 if (DGifGetLine(gifIn, rasterBits, gifIn->Image.Width * gifIn->Image.Height) == GIF_ERROR) { 343 LOGE("Could not read raster data"); 344 return false; 345 } 346 } 347 return true; 348} 349 350bool GifTranscoder::renderImage(GifFileType* gifIn, 351 GifByteType* rasterBits, 352 int imageIndex, 353 int transparentColorIndex, 354 ColorARGB* renderBuffer, 355 ColorARGB bgColor, 356 GifImageDesc prevImageDimens, 357 int prevImageDisposalMode) { 358 ASSERT(imageIndex < gifIn->ImageCount, 359 "Image index %d is out of bounds (count=%d)", imageIndex, gifIn->ImageCount); 360 361 ColorMapObject* colorMap = getColorMap(gifIn); 362 if (colorMap == NULL) { 363 LOGE("No GIF color map found"); 364 return false; 365 } 366 367 // Clear all or part of the background, before drawing the first image and maybe before drawing 368 // subsequent images (depending on the DisposalMode). 369 if (imageIndex == 0) { 370 fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight, 371 0, 0, gifIn->SWidth, gifIn->SHeight, bgColor); 372 } else if (prevImageDisposalMode == DISPOSE_BACKGROUND) { 373 fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight, 374 prevImageDimens.Left, prevImageDimens.Top, 375 prevImageDimens.Width, prevImageDimens.Height, TRANSPARENT); 376 } 377 378 // Paint this image onto the canvas 379 for (int y = 0; y < gifIn->Image.Height; y++) { 380 for (int x = 0; x < gifIn->Image.Width; x++) { 381 GifByteType colorIndex = *getPixel(rasterBits, gifIn->Image.Width, x, y); 382 383 // This image may be smaller than the GIF's "logical screen" 384 int renderX = x + gifIn->Image.Left; 385 int renderY = y + gifIn->Image.Top; 386 387 // Skip drawing transparent pixels if this image renders on top of the last one 388 if (imageIndex > 0 && prevImageDisposalMode == DISPOSE_DO_NOT && 389 colorIndex == transparentColorIndex) { 390 continue; 391 } 392 393 ColorARGB* renderPixel = getPixel(renderBuffer, gifIn->SWidth, renderX, renderY); 394 *renderPixel = getColorARGB(colorMap, transparentColorIndex, colorIndex); 395 } 396 } 397 return true; 398} 399 400void GifTranscoder::fillRect(ColorARGB* renderBuffer, 401 int imageWidth, 402 int imageHeight, 403 int left, 404 int top, 405 int width, 406 int height, 407 ColorARGB color) { 408 ASSERT(left + width <= imageWidth, "Rectangle is outside image bounds"); 409 ASSERT(top + height <= imageHeight, "Rectangle is outside image bounds"); 410 411 for (int y = 0; y < height; y++) { 412 for (int x = 0; x < width; x++) { 413 ColorARGB* renderPixel = getPixel(renderBuffer, imageWidth, x + left, y + top); 414 *renderPixel = color; 415 } 416 } 417} 418 419GifByteType GifTranscoder::computeNewColorIndex(GifFileType* gifIn, 420 int transparentColorIndex, 421 ColorARGB* renderBuffer, 422 int x, 423 int y) { 424 ColorMapObject* colorMap = getColorMap(gifIn); 425 426 // Compute the average color of 4 adjacent pixels from the input image. 427 ColorARGB c1 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2); 428 ColorARGB c2 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2); 429 ColorARGB c3 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2 + 1); 430 ColorARGB c4 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2 + 1); 431 ColorARGB avgColor = computeAverage(c1, c2, c3, c4); 432 433 // Search the color map for the best match. 434 return findBestColor(colorMap, transparentColorIndex, avgColor); 435} 436 437ColorARGB GifTranscoder::computeAverage(ColorARGB c1, ColorARGB c2, ColorARGB c3, ColorARGB c4) { 438 char avgAlpha = (char)(((int) ALPHA(c1) + (int) ALPHA(c2) + 439 (int) ALPHA(c3) + (int) ALPHA(c4)) / 4); 440 char avgRed = (char)(((int) RED(c1) + (int) RED(c2) + 441 (int) RED(c3) + (int) RED(c4)) / 4); 442 char avgGreen = (char)(((int) GREEN(c1) + (int) GREEN(c2) + 443 (int) GREEN(c3) + (int) GREEN(c4)) / 4); 444 char avgBlue = (char)(((int) BLUE(c1) + (int) BLUE(c2) + 445 (int) BLUE(c3) + (int) BLUE(c4)) / 4); 446 return MAKE_COLOR_ARGB(avgAlpha, avgRed, avgGreen, avgBlue); 447} 448 449GifByteType GifTranscoder::findBestColor(ColorMapObject* colorMap, int transparentColorIndex, 450 ColorARGB targetColor) { 451 // Return the transparent color if the average alpha is zero. 452 char alpha = ALPHA(targetColor); 453 if (alpha == 0 && transparentColorIndex != NO_TRANSPARENT_COLOR) { 454 return transparentColorIndex; 455 } 456 457 GifByteType closestColorIndex = 0; 458 int closestColorDistance = MAX_COLOR_DISTANCE; 459 for (int i = 0; i < colorMap->ColorCount; i++) { 460 // Skip the transparent color (we've already eliminated that option). 461 if (i == transparentColorIndex) { 462 continue; 463 } 464 ColorARGB indexedColor = gifColorToColorARGB(colorMap->Colors[i]); 465 int distance = computeDistance(targetColor, indexedColor); 466 if (distance < closestColorDistance) { 467 closestColorIndex = i; 468 closestColorDistance = distance; 469 } 470 } 471 return closestColorIndex; 472} 473 474int GifTranscoder::computeDistance(ColorARGB c1, ColorARGB c2) { 475 return SQUARE(RED(c1) - RED(c2)) + 476 SQUARE(GREEN(c1) - GREEN(c2)) + 477 SQUARE(BLUE(c1) - BLUE(c2)); 478} 479 480ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) { 481 if (gifIn->Image.ColorMap) { 482 return gifIn->Image.ColorMap; 483 } 484 return gifIn->SColorMap; 485} 486 487ColorARGB GifTranscoder::getColorARGB(ColorMapObject* colorMap, int transparentColorIndex, 488 GifByteType colorIndex) { 489 if (colorIndex == transparentColorIndex) { 490 return TRANSPARENT; 491 } 492 return gifColorToColorARGB(colorMap->Colors[colorIndex]); 493} 494 495ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) { 496 return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue); 497} 498 499GifFilesCloser::~GifFilesCloser() { 500 if (mGifIn) { 501 DGifCloseFile(mGifIn, NULL); 502 mGifIn = NULL; 503 } 504 if (mGifOut) { 505 EGifCloseFile(mGifOut, NULL); 506 mGifOut = NULL; 507 } 508} 509 510void GifFilesCloser::setGifIn(GifFileType* gifIn) { 511 ASSERT(mGifIn == NULL, "mGifIn is already set"); 512 mGifIn = gifIn; 513} 514 515void GifFilesCloser::releaseGifIn() { 516 ASSERT(mGifIn != NULL, "mGifIn is already NULL"); 517 mGifIn = NULL; 518} 519 520void GifFilesCloser::setGifOut(GifFileType* gifOut) { 521 ASSERT(mGifOut == NULL, "mGifOut is already set"); 522 mGifOut = gifOut; 523} 524 525void GifFilesCloser::releaseGifOut() { 526 ASSERT(mGifOut != NULL, "mGifOut is already NULL"); 527 mGifOut = NULL; 528} 529 530// JNI stuff 531 532jboolean transcode(JNIEnv* env, jobject clazz, jstring filePath, jstring outFilePath) { 533 const char* pathIn = env->GetStringUTFChars(filePath, JNI_FALSE); 534 const char* pathOut = env->GetStringUTFChars(outFilePath, JNI_FALSE); 535 536 GifTranscoder transcoder; 537 int gifCode = transcoder.transcode(pathIn, pathOut); 538 539 env->ReleaseStringUTFChars(filePath, pathIn); 540 env->ReleaseStringUTFChars(outFilePath, pathOut); 541 542 return (gifCode == GIF_OK); 543} 544 545const char *kClassPathName = "com/android/messaging/util/GifTranscoder"; 546 547JNINativeMethod kMethods[] = { 548 { "transcodeInternal", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)transcode }, 549}; 550 551int registerNativeMethods(JNIEnv* env, const char* className, 552 JNINativeMethod* gMethods, int numMethods) { 553 jclass clazz = env->FindClass(className); 554 if (clazz == NULL) { 555 return JNI_FALSE; 556 } 557 if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { 558 return JNI_FALSE; 559 } 560 return JNI_TRUE; 561} 562 563jint JNI_OnLoad(JavaVM* vm, void* reserved) { 564 JNIEnv* env; 565 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { 566 return -1; 567 } 568 if (!registerNativeMethods(env, kClassPathName, 569 kMethods, sizeof(kMethods) / sizeof(kMethods[0]))) { 570 return -1; 571 } 572 return JNI_VERSION_1_6; 573} 574