Home | History | Annotate | Download | only in jni
      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.
     30 static 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 
     60 namespace {
     61 
     62 // Current time in milliseconds since Unix epoch.
     63 double 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.
     71 template <typename T>
     72 T* getPixel(T* buffer, int width, int x, int y) {
     73     return buffer + (y * width + x);
     74 }
     75 
     76 } // namespace
     77 
     78 int 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 
    116 bool 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 
    325 bool 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 
    350 bool 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 
    400 void 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 
    419 GifByteType 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 
    437 ColorARGB 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 
    449 GifByteType 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 
    474 int 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 
    480 ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) {
    481     if (gifIn->Image.ColorMap) {
    482         return gifIn->Image.ColorMap;
    483     }
    484     return gifIn->SColorMap;
    485 }
    486 
    487 ColorARGB 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 
    495 ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) {
    496     return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue);
    497 }
    498 
    499 GifFilesCloser::~GifFilesCloser() {
    500     if (mGifIn) {
    501         DGifCloseFile(mGifIn);
    502         mGifIn = NULL;
    503     }
    504     if (mGifOut) {
    505         EGifCloseFile(mGifOut);
    506         mGifOut = NULL;
    507     }
    508 }
    509 
    510 void GifFilesCloser::setGifIn(GifFileType* gifIn) {
    511     ASSERT(mGifIn == NULL, "mGifIn is already set");
    512     mGifIn = gifIn;
    513 }
    514 
    515 void GifFilesCloser::releaseGifIn() {
    516     ASSERT(mGifIn != NULL, "mGifIn is already NULL");
    517     mGifIn = NULL;
    518 }
    519 
    520 void GifFilesCloser::setGifOut(GifFileType* gifOut) {
    521     ASSERT(mGifOut == NULL, "mGifOut is already set");
    522     mGifOut = gifOut;
    523 }
    524 
    525 void GifFilesCloser::releaseGifOut() {
    526     ASSERT(mGifOut != NULL, "mGifOut is already NULL");
    527     mGifOut = NULL;
    528 }
    529 
    530 // JNI stuff
    531 
    532 jboolean 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 
    545 const char *kClassPathName = "com/android/messaging/util/GifTranscoder";
    546 
    547 JNINativeMethod kMethods[] = {
    548         { "transcodeInternal", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)transcode },
    549 };
    550 
    551 int 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 
    563 jint 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