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.
now(void)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>
getPixel(T * buffer,int width,int x,int y)72 T* getPixel(T* buffer, int width, int x, int y) {
73 return buffer + (y * width + x);
74 }
75
76 } // namespace
77
transcode(const char * pathIn,const char * pathOut)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
resizeBoxFilter(GifFileType * gifIn,GifFileType * gifOut)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
readImage(GifFileType * gifIn,GifByteType * rasterBits)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
renderImage(GifFileType * gifIn,GifByteType * rasterBits,int imageIndex,int transparentColorIndex,ColorARGB * renderBuffer,ColorARGB bgColor,GifImageDesc prevImageDimens,int prevImageDisposalMode)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
fillRect(ColorARGB * renderBuffer,int imageWidth,int imageHeight,int left,int top,int width,int height,ColorARGB color)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
computeNewColorIndex(GifFileType * gifIn,int transparentColorIndex,ColorARGB * renderBuffer,int x,int y)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
computeAverage(ColorARGB c1,ColorARGB c2,ColorARGB c3,ColorARGB c4)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
findBestColor(ColorMapObject * colorMap,int transparentColorIndex,ColorARGB targetColor)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
computeDistance(ColorARGB c1,ColorARGB c2)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
getColorMap(GifFileType * gifIn)480 ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) {
481 if (gifIn->Image.ColorMap) {
482 return gifIn->Image.ColorMap;
483 }
484 return gifIn->SColorMap;
485 }
486
getColorARGB(ColorMapObject * colorMap,int transparentColorIndex,GifByteType colorIndex)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
gifColorToColorARGB(const GifColorType & color)495 ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) {
496 return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue);
497 }
498
~GifFilesCloser()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
setGifIn(GifFileType * gifIn)510 void GifFilesCloser::setGifIn(GifFileType* gifIn) {
511 ASSERT(mGifIn == NULL, "mGifIn is already set");
512 mGifIn = gifIn;
513 }
514
releaseGifIn()515 void GifFilesCloser::releaseGifIn() {
516 ASSERT(mGifIn != NULL, "mGifIn is already NULL");
517 mGifIn = NULL;
518 }
519
setGifOut(GifFileType * gifOut)520 void GifFilesCloser::setGifOut(GifFileType* gifOut) {
521 ASSERT(mGifOut == NULL, "mGifOut is already set");
522 mGifOut = gifOut;
523 }
524
releaseGifOut()525 void GifFilesCloser::releaseGifOut() {
526 ASSERT(mGifOut != NULL, "mGifOut is already NULL");
527 mGifOut = NULL;
528 }
529
530 // JNI stuff
531
transcode(JNIEnv * env,jobject clazz,jstring filePath,jstring outFilePath)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
registerNativeMethods(JNIEnv * env,const char * className,JNINativeMethod * gMethods,int numMethods)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
JNI_OnLoad(JavaVM * vm,void * reserved)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