Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom Fresco decoder for binary XML #46711

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3443,6 +3443,12 @@ public final class com/facebook/react/modules/fresco/SystraceRequestListener : c
public fun requiresExtraMap (Ljava/lang/String;)Z
}

public final class com/facebook/react/modules/fresco/XmlFormat {
public static final field INSTANCE Lcom/facebook/react/modules/fresco/XmlFormat;
public final fun addDecodingCapability (Lcom/facebook/imagepipeline/decoder/ImageDecoderConfig$Builder;Landroid/content/Context;)Lcom/facebook/imagepipeline/decoder/ImageDecoderConfig$Builder;
public final fun getDrawableFactory ()Lcom/facebook/imagepipeline/drawable/DrawableFactory;
}

public final class com/facebook/react/modules/i18nmanager/I18nManagerModule : com/facebook/fbreact/specs/NativeI18nManagerSpec {
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
public fun allowRTL (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
package com.facebook.react.modules.fresco

import com.facebook.common.logging.FLog
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory.newBuilder
import com.facebook.imagepipeline.core.DownsampleMode
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.common.ReactConstants
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.modules.common.ModuleDataCleaner
import com.facebook.react.modules.network.ForwardingCookieHandler
Expand Down Expand Up @@ -75,10 +78,16 @@ constructor(
val reactContext = reactApplicationContext
reactContext.addLifecycleEventListener(this)
if (!hasBeenInitialized()) {
if (config == null) {
config = getDefaultConfig(reactContext)
val pipelineConfig = config ?: getDefaultConfig(reactContext)
val draweeConfigBuilder = DraweeConfig.newBuilder()
if (ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) {
draweeConfigBuilder.addCustomDrawableFactory(XmlFormat.getDrawableFactory())
}
Fresco.initialize(reactContext.applicationContext, config)
Fresco.initialize(
reactContext.applicationContext,
pipelineConfig,
draweeConfigBuilder.build(),
)
hasBeenInitialized = true
} else if (config != null) {
FLog.w(
Expand Down Expand Up @@ -149,13 +158,20 @@ constructor(
requestListeners.add(SystraceRequestListener())
val client = OkHttpClientProvider.createClient()

// Add support for XML drawable images
val decoderConfigBuilder = ImageDecoderConfig.Builder()
if (ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) {
XmlFormat.addDecodingCapability(decoderConfigBuilder, context)
}

// make sure to forward cookies for any requests via the okHttpClient
// so that image requests to endpoints that use cookies still work
val container = OkHttpCompat.getCookieJarContainer(client)
val handler = ForwardingCookieHandler(context)
container.setCookieJar(JavaNetCookieJar(handler))
return newBuilder(context.applicationContext, client)
.setNetworkFetcher(ReactOkHttpNetworkFetcher(client))
.setImageDecoderConfig(decoderConfigBuilder.build())
.setDownsampleMode(DownsampleMode.AUTO)
.setRequestListeners(requestListeners)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.modules.fresco

import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import com.facebook.common.logging.FLog
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormat.FormatChecker
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.DefaultCloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo

public object XmlFormat {

public fun addDecodingCapability(
builder: ImageDecoderConfig.Builder,
context: Context,
): ImageDecoderConfig.Builder {
return builder.addDecodingCapability(
XmlFormat.FORMAT,
XmlFormat.XmlFormatChecker(),
XmlFormat.XmlFormatDecoder(context),
)
}

public fun getDrawableFactory(): DrawableFactory {
return XmlDrawableFactory()
}

private val FORMAT: ImageFormat = ImageFormat("XML", "xml")
private const val TAG: String = "XmlFormat"
/**
* These are the first 4 bytes of a binary XML file. We can only support binary XML files and not
* raw XML files because Android explicitly disallows raw XML files when inflating drawables.
* Binary XML files are created at build time by Android's AAPT.
*
* @see
* https://developer.android.com/reference/android/view/LayoutInflater#inflate(org.xmlpull.v1.XmlPullParser,%20android.view.ViewGroup)
*/
private val BINARY_XML_HEADER: ByteArray =
byteArrayOf(
3.toByte(),
0.toByte(),
8.toByte(),
0.toByte(),
)

private class XmlFormatChecker : FormatChecker {
override val headerSize: Int = BINARY_XML_HEADER.size

override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat {
return when {
headerSize < BINARY_XML_HEADER.size -> ImageFormat.UNKNOWN
ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER) -> FORMAT
else -> ImageFormat.UNKNOWN
}
}
}

private class CloseableXmlImage(val name: String, val drawable: Drawable) :
DefaultCloseableImage() {
private var closed = false

override fun getSizeInBytes(): Int {
return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel
}

override fun close() {
closed = true
}

override fun isClosed(): Boolean {
return closed
}

override fun getWidth(): Int {
return drawable.intrinsicWidth.takeIf { it >= 0 } ?: 0
}

override fun getHeight(): Int {
return drawable.intrinsicHeight.takeIf { it >= 0 } ?: 0
}
}

private class XmlFormatDecoder(private val context: Context) : ImageDecoder {
override fun decode(
encodedImage: EncodedImage,
length: Int,
qualityInfo: QualityInfo,
options: ImageDecodeOptions
): CloseableImage? {
return try {
val xmlResourceName = encodedImage.source ?: error("No source in encoded image")
// Use insecure URI parser since we do not care about the validity of the URI
val xmlResource = Uri.parse(xmlResourceName)
// Only support binary XML files from resources, not assets or raw files
val xmlResourceId = parseImageSourceResourceId(xmlResource)
// Use application context to avoid leaking the activity
val drawable = context.applicationContext.resources.getDrawable(xmlResourceId, null)
CloseableXmlImage(xmlResourceName, drawable)
} catch (error: Throwable) {
FLog.e(TAG, "Cannot decode xml ${error}", error)
null
}
}

/**
* This parsing implementation is only designed to work with URI's that have been generated by
* the ResourceDrawableIdHelper that ImageSource uses. It will ignore package names and schemes
* in its quest to extract a basic integer resource ID.
*
* ResourceDrawableIdHelper generates URIs in the format of res:/[resourceId]
*
* @throws IllegalStateException if the resource ID cannot be parsed from the provided uri
*/
private fun parseImageSourceResourceId(xmlResource: Uri): Int {
return xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id")
}
}

private class XmlDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
return image is CloseableXmlImage
}

override fun createDrawable(image: CloseableImage): Drawable? {
return (image as CloseableXmlImage).drawable
}
}
}
Loading