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

Test binary XML image formats #2800

Closed
wants to merge 4 commits 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
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ public boolean supportsImageType(CloseableImage image) {
}

@Override
@Nullable
public Drawable createDrawable(CloseableImage image) {
if (!supportsImageType(image)) {
return null;
}
CloseableAnimatedImage closeable = ((CloseableAnimatedImage) image);
AnimatedImage animatedImage = closeable.getImage();
AnimationBackend animationBackend =
Expand All @@ -134,8 +138,12 @@ public Drawable createDrawable(CloseableImage image) {
}

@Override
@Nullable
public Drawable createDrawable(
Resources resources, CloseableImage closeableImage, ImageOptions imageOptions) {
if (!supportsImageType(closeableImage)) {
return null;
}
CloseableAnimatedImage closeable = ((CloseableAnimatedImage) closeableImage);
AnimatedImage animatedImage = closeable.getImage();
AnimationBackend animationBackend = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ class DefaultImageFormatChecker : FormatChecker {
GIF_HEADER_LENGTH,
BMP_HEADER_LENGTH,
ICO_HEADER_LENGTH,
HEIF_HEADER_LENGTH)
HEIF_HEADER_LENGTH,
BINARY_XML_HEADER_LENGTH,
)
.maxOrNull())

/**
Expand Down Expand Up @@ -61,6 +63,9 @@ class DefaultImageFormatChecker : FormatChecker {
if (isHeifHeader(headerBytes, headerSize)) {
return DefaultImageFormats.HEIF
}
if (isBinaryXmlHeader(headerBytes, headerSize)) {
return DefaultImageFormats.BINARY_XML
}
return if (isDngHeader(headerBytes, headerSize)) {
DefaultImageFormats.DNG
} else {
Expand Down Expand Up @@ -277,5 +282,27 @@ class DefaultImageFormatChecker : FormatChecker {
headerSize >= DNG_HEADER_LENGTH &&
(ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, DNG_HEADER_II) ||
ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, DNG_HEADER_MM))

/**
* 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 const val BINARY_XML_HEADER_LENGTH: Int = 4

private fun isBinaryXmlHeader(headerBytes: ByteArray, headerSize: Int): Boolean {
return headerSize >= BINARY_XML_HEADER_LENGTH &&
ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object DefaultImageFormats {
@JvmField val WEBP_ANIMATED: ImageFormat = ImageFormat("WEBP_ANIMATED", "webp")
@JvmField val HEIF: ImageFormat = ImageFormat("HEIF", "heif")
@JvmField val DNG: ImageFormat = ImageFormat("DNG", "dng")
@JvmField val BINARY_XML: ImageFormat = ImageFormat("BINARY_XML", "xml")

/**
* Check if the given image format is a WebP image format (static or animated).
Expand Down Expand Up @@ -67,5 +68,6 @@ object DefaultImageFormats {
WEBP_EXTENDED_WITH_ALPHA,
WEBP_ANIMATED,
HEIF,
BINARY_XML,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ class ImageFormatCheckerTest constructor() {
singleImageTypeTest(getName("heifs/1.heif"), DefaultImageFormats.HEIF)
}

@Test
fun testXmlVectorDrawable() {
singleImageTypeTest(
getName("xmls/compiled/vector_drawable.xml"), DefaultImageFormats.BINARY_XML)
}

@Test
fun testXmlLayerListDrawable() {
singleImageTypeTest(getName("xmls/compiled/layer_list.xml"), DefaultImageFormats.BINARY_XML)
}

@Test
fun testXmlLevelListDrawable() {
singleImageTypeTest(getName("xmls/compiled/level_list.xml"), DefaultImageFormats.BINARY_XML)
}

@Test
fun testXmlStateListDrawable() {
singleImageTypeTest(getName("xmls/compiled/state_list.xml"), DefaultImageFormats.BINARY_XML)
}

private fun singleImageTypeTest(resourceNames: List<String>, expectedImageType: ImageFormat) {
for (name: String in resourceNames) {
val resourceStream = getResourceStream(name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.facebook.imageformat.xmls"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# XMLs

## Context

Android compiles raw XML to binary XML during app build as layout inflation at runtime does not support parsing raw XML files. As a result, Fresco only supports loading binary XML files since it relies on the Android to perform layout inflation. Therefore, we need to mock the build step that performs this compilation with the Android packaging tool. This directory houses raw, uncompiled files and their compiled counterparts in respective directories so we can test against them.

## How to add/update assets

These instructions are for POSIX devices. If you are on Windows, you will have to manually run these steps or use Android Studio to generate an APK

1. Install Android's command-line tools by [following these instructions](https://developer.android.com/tools). Make sure you have `$ANDROID_HOME` set in your PATH by running `echo $ANDROID_HOME`
1. Install the latest packages of Android's build tools and platform. For example, you can run `sdkmanager "build-tools;34.0.0"` or `sdkmanager "platforms;android-33"`. [More instructions for sdkmanager can be found here](https://developer.android.com/tools/sdkmanager)
1. Run `./convert.sh` to automatically compile and extract resources into the `compiled` folder. It will use the latest available version of your build-tools and platforms.
1. That's it!
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/sh
# 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.

if [ -z "$ANDROID_HOME" ]; then
echo "Environment variable ANDROID_HOME is not set"
exit 1
fi

SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
cd "$SCRIPT_DIR" || exit

# Use the latest available AAPT binary
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools"
LATEST_TOOLS_DIR=$(find "$BUILD_TOOLS_DIR" -maxdepth 1 -type d -print | sort -rn --key=4.1 | head -1)

if [ ! -d "$LATEST_TOOLS_DIR" ]; then
echo "Could not find build tools in $BUILD_TOOLS_DIR"
exit 1
fi

# Use the latest available Android version
ANDROID_PLATFORMS_DIR="$ANDROID_HOME/platforms"
LATEST_PLATFORM_DIR=$(find "$ANDROID_PLATFORMS_DIR" -maxdepth 1 -type d -print | sort -rn --key=4.1 | head -1)

if [ ! -d "$LATEST_PLATFORM_DIR" ]; then
echo "Could not find any platforms in $ANDROID_PLATFORMS_DIR"
exit 1
fi

# Define a temporary directory for the APK
TMP_DIR=$(mktemp -d)
APK_OUTPUT="$TMP_DIR/app.apk"

# Build an APK with our raw resources
"$LATEST_TOOLS_DIR/aapt" package -f -m -M AndroidManifest.xml -S raw -0 "" -I "$LATEST_PLATFORM_DIR/android.jar" -F "$APK_OUTPUT" || exit 1

# Unzip all APK artifacts
cd "$TMP_DIR" || exit
unzip -q "app.apk"

# Copy all compiled resources from the base drawable folder
cd "$SCRIPT_DIR" || exit
rm -rf compiled
mkdir compiled
cp -R "$TMP_DIR/res/drawable/" ./compiled

# Remove the temporary folder
rm -rf "$TMP_DIR"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/vector_drawable"
android:gravity="center"
android:top="100dp"
android:left="10dp"
/>
<item
android:drawable="@drawable/vector_drawable"
android:gravity="center"
android:top="5dp"
android:left="30dp"
/>
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/vector_drawable"
android:minLevel="1"
/>
<item
android:drawable="@drawable/vector_drawable"
android:maxLevel="1"
android:minLevel="0"
/>
</level-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/layer_list" android:state_checked="false" android:state_pressed="false" />
<item android:drawable="@drawable/level_list" android:state_checked="true" android:state_pressed="false" />
</selector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#a4c639"
android:viewportWidth="24"
android:viewportHeight="24"
>
<path
android:fillColor="#000"
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"
/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public class DefaultImageDecoder implements ImageDecoder {
return decodeGif(encodedImage, length, qualityInfo, options);
} else if (imageFormat == DefaultImageFormats.WEBP_ANIMATED) {
return decodeAnimatedWebp(encodedImage, length, qualityInfo, options);
} else if (imageFormat == DefaultImageFormats.BINARY_XML) {
throw new DecodeException("unsupported image format", encodedImage);
} else if (imageFormat == ImageFormat.UNKNOWN) {
throw new DecodeException("unknown image format", encodedImage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class KFrescoController(
drawableFactory?.createDrawable(r, a, b)?.let { createDrawableModel(it, b) }
}
}
?: run {
FLog.e(TAG, "Could not create Drawable for CloseableImage: $b")
null
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.facebook.fresco.vito.options.ImageOptionsDrawableFactory
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage

class DrawableFactoryWrapper(private val drawableFactory: DrawableFactory) :
class DrawableFactoryWrapper private constructor(private val drawableFactory: DrawableFactory) :
ImageOptionsDrawableFactory {
override fun createDrawable(
resources: Resources,
Expand All @@ -30,4 +30,15 @@ class DrawableFactoryWrapper(private val drawableFactory: DrawableFactory) :
} else {
null
}

companion object {
@JvmStatic
fun wrap(drawableFactory: DrawableFactory): ImageOptionsDrawableFactory {
return if (drawableFactory is ImageOptionsDrawableFactory) {
drawableFactory
} else {
DrawableFactoryWrapper(drawableFactory)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,8 @@ class DefaultFrescoVitoProvider(
return if (animatedDrawableFactory == null) {
bitmapFactory
} else {
if (animatedDrawableFactory is ImageOptionsDrawableFactory) {
ArrayVitoDrawableFactory(bitmapFactory, animatedDrawableFactory)
} else {
ArrayVitoDrawableFactory(bitmapFactory, DrawableFactoryWrapper(animatedDrawableFactory))
}
ArrayVitoDrawableFactory(
bitmapFactory, DrawableFactoryWrapper.wrap(animatedDrawableFactory))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ class KFrescoVitoProvider(
override fun getConfig(): FrescoVitoConfig = vitoConfig

private fun getFactory(): ImageOptionsDrawableFactory? {
return ImagePipelineFactory.getInstance().getAnimatedDrawableFactory(null)?.let {
DrawableFactoryWrapper(it)
}
return ImagePipelineFactory.getInstance()
.getAnimatedDrawableFactory(null)
?.let(DrawableFactoryWrapper::wrap)
}
}