#undef LOG_TAG #define LOG_TAG "BitmapFactory" #include "BitmapFactory.h" #include "CreateJavaOutputStreamAdaptor.h" #include "FrontBufferedStream.h" #include "GraphicsJNI.h" #include "MimeType.h" #include "NinePatchPeeker.h" #include "SkAndroidCodec.h" #include "SkCanvas.h" #include "SkMath.h" #include "SkPixelRef.h" #include "SkStream.h" #include "SkString.h" #include "SkUtils.h" #include "Utils.h" #include #include #include #include #include #include #include #include #include jfieldID gOptions_justBoundsFieldID; jfieldID gOptions_sampleSizeFieldID; jfieldID gOptions_configFieldID; jfieldID gOptions_colorSpaceFieldID; jfieldID gOptions_premultipliedFieldID; jfieldID gOptions_mutableFieldID; jfieldID gOptions_ditherFieldID; jfieldID gOptions_preferQualityOverSpeedFieldID; jfieldID gOptions_scaledFieldID; jfieldID gOptions_densityFieldID; jfieldID gOptions_screenDensityFieldID; jfieldID gOptions_targetDensityFieldID; jfieldID gOptions_widthFieldID; jfieldID gOptions_heightFieldID; jfieldID gOptions_mimeFieldID; jfieldID gOptions_outConfigFieldID; jfieldID gOptions_outColorSpaceFieldID; jfieldID gOptions_mCancelID; jfieldID gOptions_bitmapFieldID; jfieldID gBitmap_ninePatchInsetsFieldID; jclass gBitmapConfig_class; jmethodID gBitmapConfig_nativeToConfigMethodID; using namespace android; const char* getMimeType(SkEncodedImageFormat format) { switch (format) { case SkEncodedImageFormat::kBMP: return "image/bmp"; case SkEncodedImageFormat::kGIF: return "image/gif"; case SkEncodedImageFormat::kICO: return "image/x-ico"; case SkEncodedImageFormat::kJPEG: return "image/jpeg"; case SkEncodedImageFormat::kPNG: return "image/png"; case SkEncodedImageFormat::kWEBP: return "image/webp"; case SkEncodedImageFormat::kHEIF: return "image/heif"; case SkEncodedImageFormat::kAVIF: return "image/avif"; case SkEncodedImageFormat::kWBMP: return "image/vnd.wap.wbmp"; case SkEncodedImageFormat::kDNG: return "image/x-adobe-dng"; default: return nullptr; } } jstring getMimeTypeAsJavaString(JNIEnv* env, SkEncodedImageFormat format) { jstring jstr = nullptr; const char* mimeType = getMimeType(format); if (mimeType) { // NOTE: Caller should env->ExceptionCheck() for OOM // (can't check for nullptr as it's a valid return value) jstr = env->NewStringUTF(mimeType); } return jstr; } class ScaleCheckingAllocator : public SkBitmap::HeapAllocator { public: ScaleCheckingAllocator(float scale, int size) : mScale(scale), mSize(size) { } virtual bool allocPixelRef(SkBitmap* bitmap) { // accounts for scale in final allocation, using eventual size and config const int bytesPerPixel = SkColorTypeBytesPerPixel(bitmap->colorType()); const int requestedSize = bytesPerPixel * int(bitmap->width() * mScale + 0.5f) * int(bitmap->height() * mScale + 0.5f); if (requestedSize > mSize) { ALOGW("bitmap for alloc reuse (%d bytes) can't fit scaled bitmap (%d bytes)", mSize, requestedSize); return false; } return SkBitmap::HeapAllocator::allocPixelRef(bitmap); } private: const float mScale; const int mSize; }; class RecyclingPixelAllocator : public SkBitmap::Allocator { public: RecyclingPixelAllocator(android::Bitmap* bitmap, unsigned int size) : mBitmap(bitmap), mSize(size) { } ~RecyclingPixelAllocator() { } virtual bool allocPixelRef(SkBitmap* bitmap) { const SkImageInfo& info = bitmap->info(); if (info.colorType() == kUnknown_SkColorType) { ALOGW("unable to reuse a bitmap as the target has an unknown bitmap configuration"); return false; } const size_t size = info.computeByteSize(bitmap->rowBytes()); if (size > SK_MaxS32) { ALOGW("bitmap is too large"); return false; } if (size > mSize) { ALOGW("bitmap marked for reuse (%u bytes) can't fit new bitmap " "(%zu bytes)", mSize, size); return false; } mBitmap->reconfigure(info, bitmap->rowBytes()); bitmap->setPixelRef(sk_ref_sp(mBitmap), 0, 0); return true; } private: android::Bitmap* const mBitmap; const unsigned int mSize; }; // Necessary for decodes when the native decoder cannot scale to appropriately match the sampleSize // (for example, RAW). If the sampleSize divides evenly into the dimension, we require that the // scale matches exactly. If sampleSize does not divide evenly, we allow the decoder to choose how // best to round. static bool needsFineScale(const int fullSize, const int decodedSize, const int sampleSize) { if (fullSize % sampleSize == 0 && fullSize / sampleSize != decodedSize) { return true; } else if ((fullSize / sampleSize + 1) != decodedSize && (fullSize / sampleSize) != decodedSize) { return true; } return false; } static bool needsFineScale(const SkISize fullSize, const SkISize decodedSize, const int sampleSize) { return needsFineScale(fullSize.width(), decodedSize.width(), sampleSize) || needsFineScale(fullSize.height(), decodedSize.height(), sampleSize); } static jobject doDecode(JNIEnv* env, std::unique_ptr stream, jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) { // Set default values for the options parameters. int sampleSize = 1; bool onlyDecodeSize = false; SkColorType prefColorType = kN32_SkColorType; bool isHardware = false; bool isMutable = false; float scale = 1.0f; bool requireUnpremultiplied = false; jobject javaBitmap = NULL; sk_sp prefColorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle); // Update with options supplied by the client. if (options != NULL) { sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); // Correct a non-positive sampleSize. sampleSize defaults to zero within the // options object, which is strange. if (sampleSize <= 0) { sampleSize = 1; } if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) { onlyDecodeSize = true; } // initialize these, in case we fail later on env->SetIntField(options, gOptions_widthFieldID, -1); env->SetIntField(options, gOptions_heightFieldID, -1); env->SetObjectField(options, gOptions_mimeFieldID, 0); env->SetObjectField(options, gOptions_outConfigFieldID, 0); env->SetObjectField(options, gOptions_outColorSpaceFieldID, 0); jobject jconfig = env->GetObjectField(options, gOptions_configFieldID); prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig); isHardware = GraphicsJNI::isHardwareConfig(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID); javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } if (isMutable && isHardware) { doThrowIAE(env, "Bitmaps with Config.HARDWARE are always immutable"); return nullObjectReturn("Cannot create mutable hardware bitmap"); } // Create the codec. NinePatchPeeker peeker; std::unique_ptr codec; { SkCodec::Result result; std::unique_ptr c = SkCodec::MakeFromStream(std::move(stream), &result, &peeker); if (!c) { SkString msg; msg.printf("Failed to create image decoder with message '%s'", SkCodec::ResultToString(result)); return nullObjectReturn(msg.c_str()); } codec = SkAndroidCodec::MakeFromCodec(std::move(c)); if (!codec) { return nullObjectReturn("SkAndroidCodec::MakeFromCodec returned null"); } } // Do not allow ninepatch decodes to 565. In the past, decodes to 565 // would dither, and we do not want to pre-dither ninepatches, since we // know that they will be stretched. We no longer dither 565 decodes, // but we continue to prevent ninepatches from decoding to 565, in order // to maintain the old behavior. if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) { prefColorType = kN32_SkColorType; } // Determine the output size. SkISize size = codec->getSampledDimensions(sampleSize); int scaledWidth = size.width(); int scaledHeight = size.height(); bool willScale = false; // Apply a fine scaling step if necessary. if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) { willScale = true; scaledWidth = codec->getInfo().width() / sampleSize; scaledHeight = codec->getInfo().height() / sampleSize; } // Set the decode colorType SkColorType decodeColorType = codec->computeOutputColorType(prefColorType); if (decodeColorType == kRGBA_F16_SkColorType && isHardware && !uirenderer::HardwareBitmapUploader::hasFP16Support()) { decodeColorType = kN32_SkColorType; } sk_sp decodeColorSpace = codec->computeOutputColorSpace( decodeColorType, prefColorSpace); // Set the options and return if the client only wants the size. if (options != NULL) { jstring mimeType = getMimeTypeAsJavaString(env, codec->getEncodedFormat()); if (env->ExceptionCheck()) { return nullObjectReturn("OOM in getMimeTypeAsJavaString()"); } env->SetIntField(options, gOptions_widthFieldID, scaledWidth); env->SetIntField(options, gOptions_heightFieldID, scaledHeight); env->SetObjectField(options, gOptions_mimeFieldID, mimeType); jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(decodeColorType); if (isHardware) { configID = GraphicsJNI::kHardware_LegacyBitmapConfig; } jobject config = env->CallStaticObjectMethod(gBitmapConfig_class, gBitmapConfig_nativeToConfigMethodID, configID); env->SetObjectField(options, gOptions_outConfigFieldID, config); env->SetObjectField(options, gOptions_outColorSpaceFieldID, GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType)); if (onlyDecodeSize) { return nullptr; } } // Scale is necessary due to density differences. if (scale != 1.0f) { willScale = true; scaledWidth = static_cast(scaledWidth * scale + 0.5f); scaledHeight = static_cast(scaledHeight * scale + 0.5f); } android::Bitmap* reuseBitmap = nullptr; unsigned int existingBufferSize = 0; if (javaBitmap != nullptr) { reuseBitmap = &bitmap::toBitmap(inBitmapHandle); if (reuseBitmap->isImmutable()) { ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); javaBitmap = nullptr; reuseBitmap = nullptr; } else { existingBufferSize = reuseBitmap->getAllocationByteCount(); } } HeapAllocator defaultAllocator; RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize); ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize); SkBitmap::HeapAllocator heapAllocator; SkBitmap::Allocator* decodeAllocator; if (javaBitmap != nullptr && willScale) { // This will allocate pixels using a HeapAllocator, since there will be an extra // scaling step that copies these pixels into Java memory. This allocator // also checks that the recycled javaBitmap is large enough. decodeAllocator = &scaleCheckingAllocator; } else if (javaBitmap != nullptr) { decodeAllocator = &recyclingAllocator; } else if (willScale || isHardware) { // This will allocate pixels using a HeapAllocator, // for scale case: there will be an extra scaling step. // for hardware case: there will be extra swizzling & upload to gralloc step. decodeAllocator = &heapAllocator; } else { decodeAllocator = &defaultAllocator; } SkAlphaType alphaType = codec->computeOutputAlphaType(requireUnpremultiplied); const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace); SkImageInfo bitmapInfo = decodeInfo; if (decodeColorType == kGray_8_SkColorType) { // The legacy implementation of BitmapFactory used kAlpha8 for // grayscale images (before kGray8 existed). While the codec // recognizes kGray8, we need to decode into a kAlpha8 bitmap // in order to avoid a behavior change. bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType).makeAlphaType(kPremul_SkAlphaType); } SkBitmap decodingBitmap; if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator)) { // SkAndroidCodec should recommend a valid SkImageInfo, so setInfo() // should only only fail if the calculated value for rowBytes is too // large. // tryAllocPixels() can fail due to OOM on the Java heap, OOM on the // native heap, or the recycled javaBitmap being too small to reuse. return nullptr; } // Use SkAndroidCodec to perform the decode. SkAndroidCodec::AndroidOptions codecOptions; codecOptions.fZeroInitialized = decodeAllocator == &defaultAllocator ? SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized; codecOptions.fSampleSize = sampleSize; SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(), decodingBitmap.rowBytes(), &codecOptions); switch (result) { case SkCodec::kSuccess: case SkCodec::kIncompleteInput: break; default: return nullObjectReturn("codec->getAndroidPixels() failed."); } // This is weird so let me explain: we could use the scale parameter // directly, but for historical reasons this is how the corresponding // Dalvik code has always behaved. We simply recreate the behavior here. // The result is slightly different from simply using scale because of // the 0.5f rounding bias applied when computing the target image size const float scaleX = scaledWidth / float(decodingBitmap.width()); const float scaleY = scaledHeight / float(decodingBitmap.height()); jbyteArray ninePatchChunk = NULL; if (peeker.mPatch != NULL) { if (willScale) { peeker.scale(scaleX, scaleY, scaledWidth, scaledHeight); } size_t ninePatchArraySize = peeker.mPatch->serializedSize(); ninePatchChunk = env->NewByteArray(ninePatchArraySize); if (ninePatchChunk == NULL) { return nullObjectReturn("ninePatchChunk == null"); } jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL); if (array == NULL) { return nullObjectReturn("primitive array == null"); } memcpy(array, peeker.mPatch, peeker.mPatchSize); env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0); } jobject ninePatchInsets = NULL; if (peeker.mHasInsets) { ninePatchInsets = peeker.createNinePatchInsets(env, scale); if (ninePatchInsets == NULL) { return nullObjectReturn("nine patch insets == null"); } if (javaBitmap != NULL) { env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets); } } SkBitmap outputBitmap; if (willScale) { // Set the allocator for the outputBitmap. SkBitmap::Allocator* outputAllocator; if (javaBitmap != nullptr) { outputAllocator = &recyclingAllocator; } else { outputAllocator = &defaultAllocator; } SkColorType scaledColorType = decodingBitmap.colorType(); // FIXME: If the alphaType is kUnpremul and the image has alpha, the // colors may not be correct, since Skia does not yet support drawing // to/from unpremultiplied bitmaps. outputBitmap.setInfo( bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType)); if (!outputBitmap.tryAllocPixels(outputAllocator)) { // This should only fail on OOM. The recyclingAllocator should have // enough memory since we check this before decoding using the // scaleCheckingAllocator. return nullObjectReturn("allocation failed for scaled bitmap"); } SkPaint paint; // kSrc_Mode instructs us to overwrite the uninitialized pixels in // outputBitmap. Otherwise we would blend by default, which is not // what we want. paint.setBlendMode(SkBlendMode::kSrc); SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy); canvas.scale(scaleX, scaleY); decodingBitmap.setImmutable(); // so .asImage() doesn't make a copy canvas.drawImage(decodingBitmap.asImage(), 0.0f, 0.0f, SkSamplingOptions(SkFilterMode::kLinear), &paint); } else { outputBitmap.swap(decodingBitmap); } if (padding) { peeker.getPadding(env, padding); } // If we get here, the outputBitmap should have an installed pixelref. if (outputBitmap.pixelRef() == NULL) { return nullObjectReturn("Got null SkPixelRef"); } if (!isMutable && javaBitmap == NULL) { // promise we will never change our pixels (great for sharing and pictures) outputBitmap.setImmutable(); } bool isPremultiplied = !requireUnpremultiplied; if (javaBitmap != nullptr) { bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied); outputBitmap.notifyPixelsChanged(); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } int bitmapCreateFlags = 0x0; if (isMutable) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Mutable; if (isPremultiplied) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Premultiplied; if (isHardware) { sk_sp hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap); if (!hardwareBitmap.get()) { return nullObjectReturn("Failed to allocate a hardware bitmap"); } return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } // now create the java bitmap return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) { jobject bitmap = NULL; std::unique_ptr stream(CreateJavaInputStreamAdaptor(env, is, storage)); if (stream.get()) { std::unique_ptr bufferedStream(skia::FrontBufferedStream::Make( std::move(stream), SkCodec::MinBufferedBytesNeeded())); SkASSERT(bufferedStream.get() != NULL); bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle, colorSpaceHandle); } return bitmap; } static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fileDescriptor, jobject padding, jobject bitmapFactoryOptions, jlong inBitmapHandle, jlong colorSpaceHandle) { #ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC return nullObjectReturn("Not supported on Windows"); #else NPE_CHECK_RETURN_ZERO(env, fileDescriptor); int descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor); struct stat fdStat; if (fstat(descriptor, &fdStat) == -1) { doThrowIOE(env, "broken file descriptor"); return nullObjectReturn("fstat return -1"); } // Restore the descriptor's offset on exiting this function. Even though // we dup the descriptor, both the original and dup refer to the same open // file description and changes to the file offset in one impact the other. AutoFDSeek autoRestore(descriptor); // Duplicate the descriptor here to prevent leaking memory. A leak occurs // if we only close the file descriptor and not the file object it is used to // create. If we don't explicitly clean up the file (which in turn closes the // descriptor) the buffers allocated internally by fseek will be leaked. int dupDescriptor = fcntl(descriptor, F_DUPFD_CLOEXEC, 0); FILE* file = fdopen(dupDescriptor, "r"); if (file == NULL) { // cleanup the duplicated descriptor since it will not be closed when the // file is cleaned up (fclose). close(dupDescriptor); return nullObjectReturn("Could not open file"); } std::unique_ptr fileStream(new SkFILEStream(file)); // If there is no offset for the file descriptor, we use SkFILEStream directly. if (::lseek(descriptor, 0, SEEK_CUR) == 0) { assert(isSeekable(dupDescriptor)); return doDecode(env, std::move(fileStream), padding, bitmapFactoryOptions, inBitmapHandle, colorSpaceHandle); } // Use a buffered stream. Although an SkFILEStream can be rewound, this // ensures that SkImageDecoder::Factory never rewinds beyond the // current position of the file descriptor. std::unique_ptr stream(skia::FrontBufferedStream::Make( std::move(fileStream), SkCodec::MinBufferedBytesNeeded())); return doDecode(env, std::move(stream), padding, bitmapFactoryOptions, inBitmapHandle, colorSpaceHandle); #endif } static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset, jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) { Asset* asset = reinterpret_cast(native_asset); // since we know we'll be done with the asset when we return, we can // just use a simple wrapper return doDecode(env, std::make_unique(asset), padding, options, inBitmapHandle, colorSpaceHandle); } static jobject nativeDecodeByteArray(JNIEnv* env, jobject, jbyteArray byteArray, jint offset, jint length, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) { AutoJavaByteArray ar(env, byteArray); return doDecode(env, std::make_unique(ar.ptr() + offset, length, false), nullptr, options, inBitmapHandle, colorSpaceHandle); } static jboolean nativeIsSeekable(JNIEnv* env, jobject, jobject fileDescriptor) { jint descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor); return isSeekable(descriptor) ? JNI_TRUE : JNI_FALSE; } /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gMethods[] = { { "nativeDecodeStream", "(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;", (void*)nativeDecodeStream }, { "nativeDecodeFileDescriptor", "(Ljava/io/FileDescriptor;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;", (void*)nativeDecodeFileDescriptor }, { "nativeDecodeAsset", "(JLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;", (void*)nativeDecodeAsset }, { "nativeDecodeByteArray", "([BIILandroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;", (void*)nativeDecodeByteArray }, { "nativeIsSeekable", "(Ljava/io/FileDescriptor;)Z", (void*)nativeIsSeekable }, }; int register_android_graphics_BitmapFactory(JNIEnv* env) { jclass options_class = FindClassOrDie(env, "android/graphics/BitmapFactory$Options"); gOptions_bitmapFieldID = GetFieldIDOrDie(env, options_class, "inBitmap", "Landroid/graphics/Bitmap;"); gOptions_justBoundsFieldID = GetFieldIDOrDie(env, options_class, "inJustDecodeBounds", "Z"); gOptions_sampleSizeFieldID = GetFieldIDOrDie(env, options_class, "inSampleSize", "I"); gOptions_configFieldID = GetFieldIDOrDie(env, options_class, "inPreferredConfig", "Landroid/graphics/Bitmap$Config;"); gOptions_colorSpaceFieldID = GetFieldIDOrDie(env, options_class, "inPreferredColorSpace", "Landroid/graphics/ColorSpace;"); gOptions_premultipliedFieldID = GetFieldIDOrDie(env, options_class, "inPremultiplied", "Z"); gOptions_mutableFieldID = GetFieldIDOrDie(env, options_class, "inMutable", "Z"); gOptions_ditherFieldID = GetFieldIDOrDie(env, options_class, "inDither", "Z"); gOptions_preferQualityOverSpeedFieldID = GetFieldIDOrDie(env, options_class, "inPreferQualityOverSpeed", "Z"); gOptions_scaledFieldID = GetFieldIDOrDie(env, options_class, "inScaled", "Z"); gOptions_densityFieldID = GetFieldIDOrDie(env, options_class, "inDensity", "I"); gOptions_screenDensityFieldID = GetFieldIDOrDie(env, options_class, "inScreenDensity", "I"); gOptions_targetDensityFieldID = GetFieldIDOrDie(env, options_class, "inTargetDensity", "I"); gOptions_widthFieldID = GetFieldIDOrDie(env, options_class, "outWidth", "I"); gOptions_heightFieldID = GetFieldIDOrDie(env, options_class, "outHeight", "I"); gOptions_mimeFieldID = GetFieldIDOrDie(env, options_class, "outMimeType", "Ljava/lang/String;"); gOptions_outConfigFieldID = GetFieldIDOrDie(env, options_class, "outConfig", "Landroid/graphics/Bitmap$Config;"); gOptions_outColorSpaceFieldID = GetFieldIDOrDie(env, options_class, "outColorSpace", "Landroid/graphics/ColorSpace;"); gOptions_mCancelID = GetFieldIDOrDie(env, options_class, "mCancel", "Z"); jclass bitmap_class = FindClassOrDie(env, "android/graphics/Bitmap"); gBitmap_ninePatchInsetsFieldID = GetFieldIDOrDie(env, bitmap_class, "mNinePatchInsets", "Landroid/graphics/NinePatch$InsetStruct;"); gBitmapConfig_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap$Config")); gBitmapConfig_nativeToConfigMethodID = GetStaticMethodIDOrDie(env, gBitmapConfig_class, "nativeToConfig", "(I)Landroid/graphics/Bitmap$Config;"); return android::RegisterMethodsOrDie(env, "android/graphics/BitmapFactory", gMethods, NELEM(gMethods)); }