You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
584 lines
22 KiB
584 lines
22 KiB
/*
|
|
* 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;
|
|
}
|
|
|
|
// 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, NULL);
|
|
mGifIn = NULL;
|
|
}
|
|
if (mGifOut) {
|
|
EGifCloseFile(mGifOut, NULL);
|
|
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;
|
|
}
|